Skip to content

Commit 7b7677f

Browse files
Lockdown round 2: PGP verification, hardened runners, retire risky workflows (#25)
* Lockdown round 2: PGP verification, hardened runners, retire risky workflows Builds on the lockdown landed in #19. Four independent improvements: 1. Maven PGP verification scaffold (closes Finding 5 path). - pom.xml: opt-in `verify-pgp` profile wires org.simplify4u.plugins:pgpverify-maven-plugin:1.18.3 with strict settings (failNoSignature, failWeakSignature, failNoKey, verifyPomFiles, verifyPlugins, verifyPluginDependencies). - .maven-keys.list: keysmap header with format docs, sentinels, refresh procedure, and a trust-anchor URL per direct dep. - scripts/security/maven-pgp-{bootstrap,verify}: helpers in the pin-gh-actions style. - .github/workflows/verify-maven-pgp.yml: CI gate triggered on edits to pom.xml / keysmap / helper scripts. Migration to enforced (follow-up PR): populate keysmap via bootstrap, cross-check fingerprints, flip profile to activeByDefault, add workflow to required status checks. 2. Hardened runner group on every job in every workflow. `runs-on: larger` → `runs-on: { group: databricks-protected-runner-group, labels: linux-ubuntu-latest }`. Trust-boundary now an org-controlled property; Labs lockdown policy. 3. Retire the workflow_run Pwn-Request shape (closes Finding 1). Delete .github/workflows/doc-tests.yml in full. Repo now contains zero workflow_run triggers — cannot be re-introduced by toggling an `if:`. CODEOWNERS forces any new workflow through review. 4. Retire disabled release workflows (closes Finding 8). Delete publish-maven.yml and release.yml. Re-introducing publishing requires a deliberate new PR rather than flipping `if: false`. Net: workflow surface area shrinks ~300 lines, four findings closed or on-track, no findings worsened. Full diff analysis at prompts/security/2026-05-05-supply-chain-security-analysis-diff.md. Co-authored-by: Isaac * Lockdown round 3: JFrog OIDC, DBR 17.3 pins, local-act validation, real CI fixes Round 3 builds on the lockdown landed in PR #25 by closing the remaining HIGH finding from review (JFrog OIDC for pip/Maven), aligning every pinned package version against DBR 17.3 LTS, and adding a local CI dry-run scaffold that has already paid for itself by surfacing three real workflow bugs. 1. JFrog OIDC for pip + Maven + npm. - New composite action .github/actions/jfrog-auth/ (ported from UCX). Auto-detects mvn/pip3/npm/sbt/coursier/uv/bun on PATH and configures each via ~/.m2/settings.xml, PIP_CONFIG_FILE, ~/.npmrc, NETRC. - Wired into scala_build + python_build composites (after setup-python so mvn + pip3 are detectable), and directly into verify-maven-pgp.yml (defense-in-depth: artifacts via JFrog mirror, sigs verified by pgpverify-maven-plugin) and deploy-docs.yml (npm). - Added permissions: id-token: write to all 8 jobs that pip/mvn/npm install. Verified pom.xml has no resolution-time <repositories> so <mirrorOf>*</mirrorOf> is safe. 2. Mixed runner strategy (fixes wrong-org regression from PR #25). - PR #25 used databricks-protected-runner-group (databricks org, not registered for databrickslabs). Replaced repo-wide: - Heavy jobs (mvn install + GDAL + pytest): larger-runners / larger - Light jobs (artifact handling, dep resolution, npm only): databrickslabs-protected-runner-group / linux-ubuntu-latest - Per-job justification comments added so the heavy/light split is self-explanatory. - .github/CODECOV.md and .github/docs/ci-runner-and-cache.md updated to describe the mixed strategy. 3. DBR 17.3 LTS package alignment across CI / Dockerfile / cluster init / lint venv / notebook tests. Source: https://docs.databricks.com/aws/en/release-notes/runtime/17.3lts - DBR-aligned: pip 25.0.1, setuptools 74.0.0, wheel 0.45.1, cython 3.0.12, numpy 2.1.3, pandas 2.2.3, requests 2.32.3, pyspark 4.0.0, ipython 8.30.0, ipykernel 6.29.5, ipywidgets 7.8.1, jupyterlab 4.3.4, jupyter_server 2.14.1, jupyter_core 5.7.2, jupyter_client 8.6.3, tornado 6.4.2, nbformat 5.10.4, nbconvert 7.16.4. - Not in DBR, pinned to PyPI stable: build 1.4.4, pytest-cov 7.1.0, geopandas 1.1.3 (pandas 2.2.x compatible), keplergl 0.3.2 (last stable compatible with ipywidgets 7.x line). - Intentional divergences: pytest 8.4.2 (CI matrix; we don't run tests in DBR), pyzmq 25.1.1 (Dockerfile only; ZMQ workaround). 4. SECURITY.md at repo root, modeled on UCX's template (Beta-aware). 5. New gbx:versions:audit Cursor command — inventories every place a pinned package version lives so future DBR LTS bumps run end-to-end: read the audit, ask Claude to bump given the new release notes URL, re-run the audit to verify nothing drifted. 6. Local-act CI dry-run scaffold (scripts/ci-local/, .cursor/commands/ gbx-ci-act). Lets us iterate on workflow changes locally before re-enabling GHA. Real .github/ tree is never modified; the scaffold uses a workspace mirror at .cache/act-workspace/ with stub overlays for jfrog-auth (no OIDC locally) and upload_artifacts (act's bundled protobuf doesn't grok upload-artifact@v4's mime_type field). Runner image is catthehacker/ubuntu:runner-24.04 with corp pip/maven/npm proxies pre-baked. End-to-end validated: - verify-maven-pgp.yml: runs cleanly through to the pgp-verify step; fails by design on empty .maven-keys.list (the correct scaffold signal). Confirms the verify-maven-pgp workflow itself is wired correctly — when GHA is re-enabled and PR #25 merges to master, the workflow will fire on the merge commit (it touches pom.xml + workflow + scripts/security/maven-pgp-*) and fail with the expected wall of "Unsigned artifact ... in keys map" errors, turning green only after the follow-up keysmap-population PR. - build_scala.yml: 774/774 tests pass via local act (47 min under Rosetta amd64 emulation on Apple Silicon). - build_python.yml: 101/101 tests pass via local act (~10 min); job exits clean. 7. Real CI bugs surfaced + fixed in this commit: a. pom.xml: pgpverify-maven-plugin 1.18.3 → 1.19.1. Version 1.18.3 doesn't exist on Maven Central (maven org/simplify4u/plugins jumps 1.18.2 → 1.19.0). Would have failed real CI as "Could not find artifact" on first run. b. pom.xml: pinned <scalacPluginVersion>2.3.0</scalacPluginVersion> in both scoverage-maven-plugin blocks. scoverage-maven-plugin 2.1.5 defaults scalacPluginVersion to 2.5.2, which is NOT published on Maven Central for scala_2.13.12 (only up through 2.3.0 exists). Would have failed real CI as "Could not find artifact org.scoverage:scalac-scoverage-plugin_2.13.12:jar:2.5.2". c. scala_build/action.yml: replaced sed | head -1 with awk in the coverage-summary step. With GHA's default bash -eo pipefail, head closing the pipe makes sed exit 141 (SIGPIPE), failing the step even on successful test runs. Net for PR #25 reviewers: the verify-maven-pgp workflow has been verified to run end-to-end via local act; once GHA is re-enabled, expect it to fail by design on empty keysmap, not on infrastructure (JFrog auth, mvn dep resolution, pgpverify plugin loading) — those are all proven working locally with the new pom.xml + jfrog-auth setup. Files in this commit: 19 modified + 16 new. Co-authored-by: Isaac * Lockdown round 4: PR #25 security review — close every actionable point Address reviewer feedback on PR #25 (security/repo-lockdown-2). Maps each point of the feedback to either a concrete code change, a documented follow-up, or an explicit pushback with evidence. Major changes ------------- * Sweep `master` -> `main` after Labs admin renamed the default branch: workflow triggers (deploy-docs, codeql-analysis, verify-maven-pgp, build_main doc-inventory gating), badge URLs in README/CODECOV.md, documentation links across docs/**/*.mdx, and scripts/security/README.md. * `mvn -C` (strict-checksums) on every CI Maven invocation (scala_build/action.yml, build_main.yml doc tests, maven-pgp-verify). Failures now abort instead of warn on artifact checksum mismatch. * Maven PGP verify gates every build. `.maven-keys.list` populated from 0 to 1055 entries: 26 dev-bootstrap noSig + 249 signed-key entries + 666 act-surfaced version-specific noSig sentinels + supporting docs. Wired `scripts/security/maven-pgp-verify` as the FIRST Maven step in scala_build/action.yml so the gate runs before any compile, test, or install. Block clearly marked "TRUST STATUS: SNAPSHOT, NOT CROSS-CHECKED" and `scripts/security/README.md` documents the path to flipping `verify-pgp` to `<activeByDefault>true</activeByDefault>` once the fingerprints are cross-checked against project KEYS files. * ubuntugis PPA versions exact-pinned to `3.11.4+dfsg-1~noble0` (libgdal-dev/gdal-bin/python3-gdal) in scala_build, python_build, and scripts/geobrix-gdal-init.sh. PPA still floats but CI fails loudly on PPA drift instead of silently rolling forward. * `scripts/ci-local/Dockerfile.gha-runner` pinned to `catthehacker/ubuntu@sha256:5b8d4550...` (main Dockerfile was already digest-pinned). * Azul JDK key fetch hardened from TOFU to fingerprint pin `27BC0C8CB3D81623F59BDADCB1998361219BD9C9` in scripts/docker/Dockerfile. Build aborts if upstream serves a different key. * Corp proxy URLs scrubbed from all public files (pypi-proxy/maven-proxy/npm-proxy.dev.databricks.com). All registry URLs now build-arg injected from host env (PIP_INDEX_URL / MAVEN_MIRROR_URL / NPM_REGISTRY_URL); defaults to public registries. `scripts/docker/build_smart.sh` auto-forwards from host env. * SECURITY.md documents the `runtime` GitHub Environment requirement that repo admins must configure (deployment-branches: main only, required reviewers, REPO_ACCESS_TOKEN scope). Python supply chain (mitigating LiteLLM-class attacks at install time) ---------------------------------------------------------------------- * CI install path now hash-pinned via uv-generated lockfile. `python/geobrix/requirements-ci.{in,txt}` — 401-line lockfile with sha256 hashes on every transitive dep. scala_build/python_build use `pip install --require-hashes` instead of loose `pip install foo==X`. * Dev container Dockerfile hash-pinned. Replaced 4 ad-hoc `pip install` blocks with a single `pip install --require-hashes --ignore-installed -r requirements-dev-container.txt`. New files: - `python/geobrix/requirements-dev-container.{in,txt}` (2039-line lock, ~110 packages) - `.dockerignore` (build context shifted to project root) - `scripts/docker/build_smart.sh` updated for new context * `notebooks/tests/requirements.txt` hash-pinned via new `.in` source + 470-line lockfile. * Cursor commands (`gbx-test-{notebooks,sql-docs,python-docs}.sh`) use `pip install --no-deps` for the local geobrix package so transitive deps come exclusively from the hash-pinned lockfile in the image. Explicitly NOT touched (per maintainer review) ---------------------------------------------- * `%pip install` cells inside notebooks/examples/**/*.ipynb (customer content). * Code examples inside docs/docs/**/*.mdx (illustrative for customers). * `databricks.jfrog.io` references in jfrog-auth action — that's the public Labs CI mirror, ported verbatim from databrickslabs/ucx, not a corp internal. Local validation ---------------- * Native scala: 774 tests / 63 suites / 0 failed / 3:22 wall on rebuilt hash-pinned image. * Native python: 101 passed / 0 failed / 2 deselected / 1:34 wall. * Hash-pinned dev container Docker build: 3:13 wall, 130+ packages all sha256-verified. * Act `verify-maven-pgp`: BUILD SUCCESS against populated keysmap. * Act `build_python` (full e2e): every step succeeded, inline pgpverify gate fired at 1:41, 101 python tests passed. Co-authored-by: Isaac * Lockdown round 5: PR #25 follow-up — supply-chain blind spots + reviewer hygiene Close the four remaining items from the PR #25 review: 1. Hash-verify Python tarball download in the local-act runner image. scripts/ci-local/Dockerfile.gha-runner now pins PYTHON_VERSION, PYTHON_TARBALL_TAG, and PYTHON_SHA256 as build args and pipes the curl through `sha256sum -c -` before unpacking — previously the tarball was trusted blindly on a developer machine. 2. Hash-verify the Jupyter / pyzmq / testbook installs in the dev container. Those packages were all already present in requirements-dev-container.txt, but a follow-up `pip install --upgrade` block in scripts/docker/Dockerfile was bypassing --require-hashes and silently undoing the lockfile guarantee. Removed the redundant block; also hash-pinned the pip bootstrap step (chicken-and-egg). 3. Pin the ubuntugis PPA signing key by fingerprint instead of TOFU. Vendored the official Launchpad signing key at scripts/security/keys/ubuntugis-ppa.asc plus a helper script scripts/security/apt-add-ubuntugis-ppa that refuses the PPA unless the key's fingerprint matches the expected value (sourced from Launchpad's signing_key_fingerprint API). Both CI composite actions now call the helper; scripts/geobrix-gdal-init.sh embeds the key + fingerprint check inline so it stays self-contained as a cluster init script. 4. Clarify the GDAL --require-hashes exemption. GDAL's Python binding version is dynamic (must match the apt/source-built libgdal we already pin and verify) and OSGeo doesn't publish per-version PyPI hashes. Comments in scripts/docker/Dockerfile and both CI composite actions now spell out the reasoning; added --no-binary :all: so the binding always source-builds against verified libgdal headers instead of accepting a pre-built manylinux wheel. Plus reviewer-flagged hygiene: - Scrubbed every internal policy reference and operational-process link (env-var-based proxy mechanism is documented neutrally; the env vars themselves stay since they're a perfectly normal pip pattern). - Replaced the docs.google.com policy link in scripts/security/README.md with a plain-text summary. Two latent bugs caught during local-act validation, both on the public-contributor path that the docs claim to support but had never been exercised end-to-end: - scripts/ci-local/run-act.sh: BUILD_ARGS[@]: unbound variable on bash 3.2 (macOS) when no proxy env vars are set. Used the ${BUILD_ARGS[@]+...} idiom to handle empty arrays under set -u. - scripts/{ci-local/,docker/}Dockerfile: PIP_INDEX_URL defaulted to empty string, which causes pip to construct URLs with no host. Made https://pypi.org/simple/ the explicit default so falling back to public PyPI actually works. Verification: - gbx:test:scala: 774 passed, 0 failed (3:19) - gbx:test:python: 101 passed, 0 failed (1:15) - gbx:ci:act -W .github/workflows/build_python.yml: Job succeeded Runner image rebuilt with Python SHA-256 verify; ubuntugis fingerprint pin cleared on both composite invocations; --no-binary :all: GDAL sdist built against PPA-installed libgdal headers; --require-hashes requirements-ci.txt closure installed cleanly. Co-authored-by: Isaac * docs: add Security page; align installation with init-script lockdown Add docs/docs/security.mdx as a user-facing chapter covering (a) what we do upstream to harden the GeoBrix supply chain — third-party Action SHA pinning, PGP-verified Maven dependencies, hash-pinned Python lockfiles, fingerprint-pinned ubuntugis PPA + pinned GDAL package version with source-only Python install, hardened ephemeral CI runners, JFrog OIDC for pip/Maven/npm, gated deploy environment — and (b) what users can do on top: use the init script verbatim, stage release artifacts in a Volume they own, pin the GeoBrix version, restrict the GDAL driver surface for untrusted inputs, and report vulnerabilities privately. Wire the page into docs/sidebars.js between Developers and Known Limitations. Tighten docs/docs/installation.mdx to match the post-lockdown init script: - Narrow Prerequisites from "DBR 17.1 or later" to DBR 17.3 LTS. The init script's pinned GDAL_PPA_VERSION targets the Ubuntu 24.04 noble base; older or non-LTS DBRs use different bases and the pin either fails or silently falls back to an unpinned GDAL. - Add a "what the script does" callout above the rendered init script block, linking to the Security page for the why. - Rewrite the GDAL Library Issues troubleshooting section. The old text described pre-lockdown PPA float ("from time-to-time might update GDAL version"), which is no longer the risk model. New text covers fingerprint mismatch (UBUNTUGIS_FPR), retired pinned package, DBR base image mismatch, and the existing general-checks fallback. The init script content itself is auto-rendered via raw-loader, so no changes were needed to keep that section current. Co-authored-by: Isaac --------- Co-authored-by: Michael Johns <user.name>
2 parents ab812cb + 14e1023 commit 7b7677f

66 files changed

Lines changed: 6660 additions & 457 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/agents/docker.md

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,90 @@ gbx:docker:rebuild --start
470470
- **Mounted to**: `/root/geobrix/scripts/docker/m2/`
471471
- **Purpose**: Persist Maven dependencies between container restarts
472472

473+
## Registry proxies (optional)
474+
475+
The dev container is built to route through whatever registry URLs the host
476+
environment supplies — useful if you sit behind a network that blocks public
477+
PyPI / Maven Central, or if you want a single team-wide pin set:
478+
479+
| Tool | Source of URL | Configured by |
480+
|---|---|---|
481+
| pip | host env `PIP_INDEX_URL` forwarded as `--build-arg` | `Dockerfile` writes `/etc/pip.conf` + sets `PIP_INDEX_URL` env only when set; `build_smart.sh` auto-forwards |
482+
| Maven | `scripts/docker/m2/settings.xml` `<mirror>` block (gitignored, host-local) | `docker_maven_setup.sh` |
483+
484+
Export `PIP_INDEX_URL` before building (or `build_smart.sh` picks it up
485+
automatically). Leave it unset and the build uses public PyPI.
486+
487+
If a `pip install` step in the Dockerfile fails with `Connection refused` or
488+
`Could not find a version` listing only old releases, your proxy is either
489+
unreachable or has an embargo on recently-published versions — fall back to
490+
the prior stable release of the offending package.
491+
492+
## Local GitHub Actions dry-runs with `act`
493+
494+
Separate from the dev container (`geobrix-dev`), there's a second Docker image
495+
purpose-built for **local CI validation**`geobrix-ci-runner:local`. It's
496+
shaped like a GitHub-hosted runner (`catthehacker/ubuntu:runner-24.04`,
497+
digest-pinned). pip/Maven/npm registry URLs are build-arg injected from the
498+
host env (`PIP_INDEX_URL`, `MAVEN_MIRROR_URL`, `NPM_REGISTRY_URL`) — set them
499+
to a private proxy if your network requires it; leave them unset to use
500+
public registries.
501+
502+
### When to use
503+
504+
- After editing any `.github/workflows/*.yml` or `.github/actions/*/action.yml`
505+
- Before push, to catch typos, action SHA pin breakage, step ordering issues
506+
- To debug a CI failure that doesn't reproduce locally
507+
508+
### Quickstart
509+
510+
```bash
511+
brew install act # one-time
512+
gbx:ci:act -l # list jobs across workflows
513+
gbx:ci:act -W .github/workflows/build_main.yml -j build # run one job
514+
gbx:ci:act push # simulate a push event
515+
```
516+
517+
First run builds the runner image (~5 min, cached after).
518+
519+
### How real `.github/` stays untouched
520+
521+
`act` parses workflow + composite-action YAML on the host filesystem *before*
522+
any container starts, so we need the overlay on disk — not just inside the
523+
container. `scripts/ci-local/run-act.sh` regenerates a mirror at
524+
`.cache/act-workspace/` (gitignored) on every run:
525+
526+
- `.github/` is freshly copied (~100 KB, ~50 ms) with the jfrog-auth stub
527+
overlaid on top.
528+
- Every other top-level entry (`pom.xml`, `src/`, `scripts/`, `.git`, …) is
529+
symlinked back to the real project, so workflow content is identical.
530+
- `act --bind` runs from inside the mirror; `actions/checkout` becomes a
531+
no-op (uses the bind-mounted workspace as-is).
532+
533+
The real `.github/` tree on disk is never modified — only the mirror's copy
534+
is. JFrog OIDC can't run locally (no real GitHub OIDC issuer); pip / Maven /
535+
npm fall back to whatever proxies were build-arg-injected into the runner
536+
image (public registries by default).
537+
538+
### Files
539+
540+
| Path | Purpose |
541+
|---|---|
542+
| `scripts/ci-local/Dockerfile.gha-runner` | Runner image build |
543+
| `scripts/ci-local/{pip.conf,maven-settings.xml,npmrc}` | Proxy configs baked into the image |
544+
| `scripts/ci-local/jfrog-auth-stub/action.yml` | No-op overlay |
545+
| `scripts/ci-local/run-act.sh` | act invocation with overlay mount |
546+
| `scripts/ci-local/README.md` | Detailed mechanics + caveats |
547+
| `.cursor/commands/gbx-ci-act.{sh,md}` | Cursor command wrapper |
548+
549+
### Caveats
550+
551+
- **JFrog OIDC**: mocked locally (stub action). Real OIDC exchange runs only in CI.
552+
- **`runs-on: larger-runners`**: treated as a label alias; you don't actually get a "larger" machine — just whatever Docker resources are available.
553+
- **Real GitHub event payloads**: `act` mocks `head_sha`, `head_ref`, etc.
554+
- **Secrets**: only `GITHUB_TOKEN` is provided (auto-mocked); workflows fall back via the `REPO_ACCESS_TOKEN || GITHUB_TOKEN` pattern. `CODECOV_TOKEN` is missing but the upload step has `fail_ci_if_error: false`.
555+
- **Org-level runner-group policy**: not simulated (which is fine — local runs use a local Docker container regardless).
556+
473557
### Settings File
474558
- **Location**: `scripts/docker/m2/settings.xml`
475559
- **Key settings**:
@@ -483,9 +567,18 @@ gbx:docker:rebuild --start
483567
## Environment Variables
484568

485569
### Key Variables in Container
570+
Pinned in `scripts/docker/Dockerfile` (DBR 17.3 LTS aligned). Run
571+
`gbx:versions:audit` to see all of them. The most load-bearing:
572+
486573
```bash
487-
SPARK_VERSION=3.5.3 # Spark version
488-
GDAL_VERSION=3.10.0 # GDAL version
574+
SPARK_VERSION=4.0.0 # Spark version (DBR 17.3 LTS)
575+
NUMPY_VERSION=2.1.3 # NumPy 2.x (DBR 17.3 LTS)
576+
PANDAS_VERSION=2.2.3 # pandas (DBR 17.3 LTS)
577+
PIP_VERSION=25.0.1 # pip (DBR 17.3 LTS)
578+
SETUPTOOLS_VERSION=74.0.0 # setuptools (DBR 17.3 LTS)
579+
WHEEL_VERSION=0.45.1 # wheel (DBR 17.3 LTS)
580+
# GDAL is NOT in DBR; built from ubuntugis PPA.
581+
# Python bindings auto-detect via `gdal-config --version` (currently 3.11.4).
489582
JUPYTER_PLATFORM_DIRS=1 # Suppress Jupyter warnings
490583
```
491584

.cursor/commands/gbx-ci-act.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Run GitHub Actions locally with act
2+
3+
Validate `.github/workflows/*.yml` changes before pushing by running them locally via [act](https://github.com/nektos/act). Catches ~80% of CI bugs (typos, missing env, action SHA pin issues, step ordering, composite-action structure errors) without a push-and-iterate loop.
4+
5+
## Usage
6+
7+
```bash
8+
bash .cursor/commands/gbx-ci-act.sh [act-arguments...]
9+
```
10+
11+
## Examples
12+
13+
```bash
14+
# List jobs across all workflows
15+
gbx:ci:act -l
16+
17+
# Run one job from one workflow
18+
gbx:ci:act -W .github/workflows/build_main.yml -j build
19+
20+
# Simulate a push event (runs all workflows triggered on push)
21+
gbx:ci:act push
22+
23+
# Simulate a PR event
24+
gbx:ci:act pull_request
25+
26+
# Pass any other arg through to act
27+
gbx:ci:act --help
28+
```
29+
30+
## First-time setup
31+
32+
```bash
33+
brew install act
34+
```
35+
36+
The first invocation builds `geobrix-ci-runner:local` (~5 min). Subsequent runs reuse the image.
37+
38+
## What's wired
39+
40+
- **Runner image**: `catthehacker/ubuntu:runner-24.04` (digest-pinned). pip/Maven/npm registry URLs are build-arg injected from the host env (`PIP_INDEX_URL`, `MAVEN_MIRROR_URL`, `NPM_REGISTRY_URL`); set them to a private proxy if your network requires it, otherwise leave unset to use public registries.
41+
- **Platform map**: `ubuntu-latest`, `ubuntu-24.04`, `ubuntu-22.04`, `larger`, `linux-ubuntu-latest``geobrix-ci-runner:local`
42+
- **JFrog auth stub**: Real `.github/actions/jfrog-auth/action.yml` is bind-mounted over inside the act container with a no-op stub. Real `.github/` on disk is never modified.
43+
44+
## Coverage
45+
46+
| Catches | Doesn't catch |
47+
|---|---|
48+
| YAML syntax errors | JFrog OIDC token exchange (mocked) |
49+
| Action SHA pin issues | `larger-runners` actually being larger |
50+
| Step ordering | Real GitHub event payloads (head_sha, head_ref) |
51+
| Composite action structure | Real secrets (CODECOV_TOKEN, REPO_ACCESS_TOKEN) |
52+
| pip/Maven/npm install correctness | Org-level runner-group access policy |
53+
| Matrix expansion | Protected env / branch protection gating |
54+
| Conditional `if:` evaluation | |
55+
56+
For the gaps, push to a draft PR and let real CI exercise them.
57+
58+
## Notes
59+
60+
- The runner image is roughly 2 GB; cached locally after first build
61+
- `act` reuses the image across runs; rebuild via `docker rmi geobrix-ci-runner:local && gbx:ci:act -l`
62+
- Full mechanics in `scripts/ci-local/README.md`; runbook context in `.cursor/agents/docker.md`

.cursor/commands/gbx-ci-act.sh

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/bin/bash
2+
# gbx:ci:act - Run GitHub Actions workflows locally via `act`, against a
3+
# project-shaped runner image. The real .github/ tree is NEVER modified —
4+
# the jfrog-auth composite is bind-mounted with a no-op stub at runtime.
5+
6+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
7+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
8+
9+
source "$SCRIPT_DIR/common.sh"
10+
11+
show_help() {
12+
show_banner "Local CI dry-run with act"
13+
echo -e "${CYAN}Usage:${NC}"
14+
echo -e " ${GREEN}gbx:ci:act${NC} ${YELLOW}[act-arguments...]${NC}"
15+
echo ""
16+
echo -e "${CYAN}Common invocations:${NC}"
17+
echo -e " ${GREEN}gbx:ci:act -l${NC} List jobs"
18+
echo -e " ${GREEN}gbx:ci:act -W .github/workflows/build_main.yml -j build${NC} Run a specific job"
19+
echo -e " ${GREEN}gbx:ci:act push${NC} Simulate a push event"
20+
echo -e " ${GREEN}gbx:ci:act pull_request${NC} Simulate a PR event"
21+
echo -e " ${GREEN}gbx:ci:act --help${NC} Pass-through to act --help"
22+
echo ""
23+
echo -e "${CYAN}First run (one-time, ~5 min):${NC}"
24+
echo -e " Builds ${YELLOW}geobrix-ci-runner:local${NC} Docker image."
25+
echo -e " Requires act: ${YELLOW}brew install act${NC}"
26+
echo ""
27+
echo -e "${CYAN}What's pre-baked into the runner image:${NC}"
28+
echo -e " • pip / Maven / npm registry URLs are build-arg injected from your env"
29+
echo -e " (${YELLOW}PIP_INDEX_URL${NC} / ${YELLOW}MAVEN_MIRROR_URL${NC} / ${YELLOW}NPM_REGISTRY_URL${NC});"
30+
echo -e " set them to a private proxy if your network requires it, or leave unset"
31+
echo -e " to default to public registries."
32+
echo ""
33+
echo -e "${CYAN}Caveats:${NC}"
34+
echo -e " • JFrog OIDC is mocked (no real token); pip/maven/npm flow via the registry baked in at build time"
35+
echo -e "${YELLOW}runs-on: larger-runners${NC} is treated as a label only — no actual"
36+
echo -e " larger machine; uses your local Docker resources"
37+
echo -e " • Real .github/ files are never modified — see scripts/ci-local/README.md"
38+
echo ""
39+
echo -e "${CYAN}Iteration loop:${NC}"
40+
echo -e " 1. Edit a workflow in .github/workflows/"
41+
echo -e " 2. ${GREEN}gbx:ci:act -W <workflow> -j <job>${NC}"
42+
echo -e " 3. When clean, push and let real CI exercise OIDC + larger runners"
43+
echo ""
44+
}
45+
46+
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
47+
show_help
48+
exit 0
49+
fi
50+
51+
show_banner "Local CI dry-run with act"
52+
exec bash "$PROJECT_ROOT/scripts/ci-local/run-act.sh" "$@"

.cursor/commands/gbx-lint-python.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ run_fix_host() {
7979
if [ ! -d "$venv_dir" ] || [ ! -x "$venv_dir/bin/isort" ] || [ ! -x "$venv_dir/bin/black" ] || [ ! -x "$venv_dir/bin/flake8" ]; then
8080
echo -e "${CYAN}Creating venv at ${YELLOW}$venv_dir${NC} and installing dev deps..."
8181
python3 -m venv "$venv_dir" || { echo -e "${RED}Failed to create venv.${NC}"; exit 1; }
82-
"$venv_dir/bin/pip" install -q --upgrade pip
82+
# Pin bootstrap to DBR 17.3 LTS — keep in sync with .github/actions/{scala,python}_build/action.yml,
83+
# scripts/docker/Dockerfile, scripts/geobrix-gdal-init.sh.
84+
"$venv_dir/bin/pip" install -q --upgrade pip==25.0.1 setuptools==74.0.0 wheel==0.45.1
8385
(cd "$PY_DIR" && "$venv_dir/bin/pip" install -q -e ".[dev]") || { echo -e "${RED}Failed to install python/geobrix[dev].${NC}"; exit 1; }
8486
echo -e "${GREEN}Venv ready.${NC}"
8587
fi

.cursor/commands/gbx-test-notebooks.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,11 @@ ${INCLUDE_INTEGRATION:+export GBX_NOTEBOOK_INCLUDE_INTEGRATION=1}
145145
${ALLOW_ABSOLUTE_READS:+export GBX_NOTEBOOK_ALLOW_ABSOLUTE_READS=1}
146146
${ALLOW_ABSOLUTE_WRITES:+export GBX_NOTEBOOK_ALLOW_ABSOLUTE_WRITES=1}
147147
cd /root/geobrix
148-
pip install -e /root/geobrix/python/geobrix --break-system-packages -q 2>/dev/null || true
149-
pip install nbformat nbconvert --break-system-packages -q 2>/dev/null || true
148+
# Install the local geobrix package (no deps — runtime deps are already in
149+
# the hash-pinned dev container lockfile). nbformat / nbconvert are also
150+
# pre-installed there; no ad-hoc pip install needed (would defeat the
151+
# supply-chain hardening).
152+
pip install --no-deps -e /root/geobrix/python/geobrix --break-system-packages -q 2>/dev/null || true
150153
python3 /root/geobrix/notebooks/tests/run_notebooks_cell_by_cell.py ${PATH_ARG:+"$PATH_ARG"}
151154
"
152155

.cursor/commands/gbx-test-python-docs.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ if [ \"$SKIP_BUILD\" != 'true' ]; then
192192
cd /root/geobrix/python/geobrix && python3 -m build && cd /root/geobrix
193193
echo ''
194194
echo 'Installing Python package (editable)...'
195-
pip install -e /root/geobrix/python/geobrix --break-system-packages -q
195+
pip install --no-deps -e /root/geobrix/python/geobrix --break-system-packages -q
196196
echo ''
197197
fi
198198

.cursor/commands/gbx-test-sql-docs.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ if [ \"$SKIP_BUILD\" != 'true' ]; then
118118
echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
119119
mvn package -DskipTests -q
120120
cd /root/geobrix/python/geobrix && python3 -m build && cd /root/geobrix
121-
pip install -e /root/geobrix/python/geobrix --break-system-packages -q
121+
pip install --no-deps -e /root/geobrix/python/geobrix --break-system-packages -q
122122
echo ''
123123
fi
124124
echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Audit pinned Python tooling versions
2+
3+
Inventories every place a pinned package version lives across CI, the dev container Dockerfile, the Databricks cluster init script, and the lint venv setup. Use it to **detect drift** between locations, and as the entry point for **bumping pins to a new DBR LTS**.
4+
5+
## Usage
6+
7+
```bash
8+
bash .cursor/commands/gbx-versions-audit.sh [OPTIONS]
9+
```
10+
11+
## Options
12+
13+
- `--package <name>` — Show only one package (e.g. `pip`, `numpy`, `pyspark`).
14+
- `--log <path>` — Write output to log file.
15+
- `--help` — Display help.
16+
17+
## Examples
18+
19+
```bash
20+
# Full audit (all tracked packages, all locations)
21+
gbx:versions:audit
22+
23+
# Just numpy
24+
gbx:versions:audit --package numpy
25+
26+
# Just pyspark — useful when bumping Spark
27+
gbx:versions:audit --package pyspark
28+
```
29+
30+
## What's tracked
31+
32+
Source of truth (priority order):
33+
34+
1. **DBR LTS release notes** — for any package shipped with the runtime:
35+
- Core build/runtime: `pip`, `setuptools`, `wheel`, `cython`, `numpy`, `pandas`, `pyspark`/`spark`, `requests`, `python`.
36+
- Jupyter / notebook stack (dev container): `ipython`, `ipykernel`, `ipywidgets`, `jupyterlab`, `jupyter_server`, `jupyter_core`, `jupyter_client`, `tornado`, `nbformat`, `nbconvert`.
37+
2. **Current PyPI stable** — for tools NOT in DBR: `build`, `pytest-cov`, `geopandas` (pinned to be compatible with DBR's pandas 2.2.x), `keplergl` (last stable; requires `ipywidgets >= 7`).
38+
3. **CI matrix** — for tools intentionally divergent from DBR: `pytest 8.4.2` (kept ahead of DBR 8.3.5 because tests don't run in DBR).
39+
4. **System apt + ubuntugis PPA** — for GDAL native + Python bindings; auto-detected from `gdal-config --version`.
40+
5. **Intentional DBR divergence**`pyzmq 25.1.1` (Dockerfile only). DBR has 26.2.0 but our Jupyter ZMQ workaround requires the older pin. Re-evaluate on next DBR LTS bump.
41+
42+
## Files audited
43+
44+
- `.github/actions/scala_build/action.yml`
45+
- `.github/actions/python_build/action.yml`
46+
- `.github/workflows/build_*.yml` and `codecov-*.yml` (matrix values)
47+
- `scripts/docker/Dockerfile`
48+
- `scripts/geobrix-gdal-init.sh`
49+
- `.cursor/commands/gbx-lint-python.sh`
50+
- `python/geobrix/pyproject.toml` (dev/test extras — currently unpinned)
51+
- `notebooks/tests/requirements.txt` (lower bounds — not pinned)
52+
53+
If a new file pins versions, add it to the `FILES` array in `gbx-versions-audit.sh`.
54+
55+
## Workflow: bumping to a new DBR LTS
56+
57+
1. Get the DBR LTS release-notes URL (e.g. `https://docs.databricks.com/aws/en/release-notes/runtime/18.3lts`).
58+
2. Run `gbx:versions:audit` and capture the output.
59+
3. Ask Claude:
60+
> "Bump pinned versions to DBR 18.3 LTS from `<URL>`. Here's the current audit: `<paste>`. Favor DBR versions where listed; pin packages not in DBR (`build`, `pytest-cov`) to current PyPI stable; keep `pytest` at the current CI matrix value (we don't run tests in DBR)."
61+
4. Re-run `gbx:versions:audit` to verify everything moved together.
62+
5. Commit: `chore(deps): align pins to DBR 18.3 LTS`.
63+
64+
## Notes
65+
66+
- The audit is **read-only** — never modifies files. All edits happen via Claude after you review the audit.
67+
- Pyproject.toml dev/test extras (`isort`, `black`, `flake8`, `build`, `pytest`, etc.) are intentionally unpinned to allow `pip install -e .[dev]` to resolve cleanly. The hard pins live in CI/Docker/init script — those are the trust boundaries.
68+
- The audit prints a "Files audited" section at the end with ``/`` markers — a `` means a tracked file is missing (e.g. renamed/deleted). Update `FILES` in the script to fix.

0 commit comments

Comments
 (0)