From 4dc10f19147f2590ef020408fcb3839ccd0d88bb Mon Sep 17 00:00:00 2001 From: Lukas Wuttke Date: Tue, 14 Jul 2026 14:29:39 +0200 Subject: [PATCH 1/2] =?UTF-8?q?ci:=20goldens-drift=20job=20=E2=80=94=20val?= =?UTF-8?q?idator=20goldens=20vs=20data-ingestors=20@=20pin=20(cli#287)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New dedicated workflow (goldens-drift.yml): checks out tracebloc/data-ingestors at the pinned ref (scripts/.data-ingestors-ref), installs its runtime requirements, and runs scripts/sync-validator-goldens.sh --check — failing any PR whose committed parity goldens have drifted from the REAL validators at the pin. Closes the wired-into-no-CI hole: until now the goldens were only regenerated by hand, so a corpus/generator/pin change could land with stale goldens and CI stayed green. Triggers: PRs touching internal/push/**, the pin, or the sync harness; weekly Monday cron as a backstop; manual dispatch. Cross-repo access: data-ingestors is currently PUBLIC (verified via gh), so the default workflow token suffices — the cli#62 precedent (install-path-persist.yml) also checks out a sibling repo with no secret. The job still prefers an org secret CROSS_REPO_READ_TOKEN when present and skips-with-warning (never hard-fails) if the repo becomes unreadable, so a future privatization flips to 'create the secret' instead of redding every open PR. Deps: pip install -r data-ingestors/requirements.txt, not just pandas+pillow — the generator's import chain also needs sqlalchemy, tenacity, ijson et al (verified in a clean venv; pandas+pillow alone fails on import). requirements.txt is all light pure-python wheels. Co-Authored-By: Claude Fable 5 --- .github/workflows/goldens-drift.yml | 120 ++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 .github/workflows/goldens-drift.yml diff --git a/.github/workflows/goldens-drift.yml b/.github/workflows/goldens-drift.yml new file mode 100644 index 0000000..18fd424 --- /dev/null +++ b/.github/workflows/goldens-drift.yml @@ -0,0 +1,120 @@ +name: Goldens drift + +# Validator-goldens drift check (cli#287, epic backend#1106 WS-C.2). +# +# internal/push/testdata/parity/goldens.json pins the verdicts (and value-level +# reads) of the REAL data-ingestors validators over the parity corpus, and +# parity_golden_test.go asserts the CLI's local preflight agrees case-by-case. +# Until this job, the goldens were regenerated only by hand — a change to the +# corpus, the generator, or the pin could land with stale goldens and CI would +# stay green (the wired-into-no-CI hole). +# +# This job checks out tracebloc/data-ingestors at the PINNED ref +# (scripts/.data-ingestors-ref — never a floating branch, so an unrelated +# upstream commit can't red every open CLI PR, backend#1009), runs the real +# validators over the corpus, and fails when the committed goldens diverge. +# +# Cross-repo access: data-ingestors is currently PUBLIC, so the default +# workflow token is enough (same as the install-path-persist.yml precedent, +# cli#62, which checks out tracebloc/client with no extra secret). If the repo +# is ever made private, create an org-level secret CROSS_REPO_READ_TOKEN (a +# fine-grained PAT with contents:read on tracebloc/data-ingestors) — this +# workflow already prefers it when present, and skips WITH A WARNING (rather +# than hard-failing every PR) when the repo is unreachable with the tokens +# available. +# +# Runs on PRs that touch the mirrored surface (internal/push/**, the pin, the +# sync harness) + a weekly cron as a backstop for drift introduced outside +# those paths + manual dispatch. + +on: + pull_request: + paths: + - 'internal/push/**' + - 'scripts/.data-ingestors-ref' + - 'scripts/sync-validator-goldens.sh' + - 'scripts/gen-validator-goldens.py' + - '.github/workflows/goldens-drift.yml' + schedule: + - cron: '30 5 * * 1' # weekly, Monday 05:30 UTC — offset from stale-backlog's Monday 00:00 + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: goldens-drift-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + goldens-drift: + timeout-minutes: 15 + name: Validator goldens vs data-ingestors @ pin + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Resolve the data-ingestors pin + id: pin + # First non-comment, non-blank line of the ref file — the same rule + # scripts/sync-schema.sh applies. Fail loudly on an empty/missing ref: + # a silently-defaulted ref would check against the wrong upstream. + run: | + ref="$(grep -vE '^[[:space:]]*(#|$)' scripts/.data-ingestors-ref | head -1 | tr -d '[:space:]')" + if [ -z "$ref" ]; then + echo "::error::scripts/.data-ingestors-ref contains no ref" + exit 1 + fi + echo "ref=$ref" >> "$GITHUB_OUTPUT" + + - name: Probe data-ingestors readability + id: access + # data-ingestors is public today, so this always succeeds with the + # default token. The probe exists for the day it goes private: without + # a CROSS_REPO_READ_TOKEN secret the checkout would hard-fail every PR + # touching internal/push/** — instead, skip the check with a loud + # warning telling the admin exactly what to create. + env: + CROSS_REPO_READ_TOKEN: ${{ secrets.CROSS_REPO_READ_TOKEN }} + FALLBACK_TOKEN: ${{ github.token }} + run: | + token="${CROSS_REPO_READ_TOKEN:-$FALLBACK_TOKEN}" + if git ls-remote "https://x-access-token:${token}@github.com/tracebloc/data-ingestors.git" HEAD >/dev/null 2>&1; then + echo "ok=true" >> "$GITHUB_OUTPUT" + else + echo "ok=false" >> "$GITHUB_OUTPUT" + echo "::warning::tracebloc/data-ingestors is not readable with the available tokens — goldens-drift check SKIPPED. If the repo has been made private, create the org secret CROSS_REPO_READ_TOKEN (fine-grained PAT, contents:read on tracebloc/data-ingestors)." + fi + + - name: Checkout tracebloc/data-ingestors @ pin + if: steps.access.outputs.ok == 'true' + uses: actions/checkout@v4 + with: + repository: tracebloc/data-ingestors + ref: ${{ steps.pin.outputs.ref }} + path: data-ingestors + # Prefer the cross-repo secret when it exists (needed only if the + # repo goes private); the default token suffices while it's public. + token: ${{ secrets.CROSS_REPO_READ_TOKEN || github.token }} + + - name: Set up Python + if: steps.access.outputs.ok == 'true' + uses: actions/setup-python@v5 + with: + python-version: '3.11' # matches data-ingestors' own CI + cache: pip + cache-dependency-path: data-ingestors/requirements.txt + + - name: Install the ingestor's runtime dependencies + if: steps.access.outputs.ok == 'true' + # The generator drives the REAL validators + read path, whose import + # chain needs more than pandas+Pillow (sqlalchemy, tenacity, ijson, …). + # Install the ingestor's own requirements.txt — all light, pure-python + # wheels — so an upstream dependency change can't break this job. + run: pip install -r data-ingestors/requirements.txt + + - name: scripts/sync-validator-goldens.sh --check + if: steps.access.outputs.ok == 'true' + env: + DATA_INGESTORS_DIR: ${{ github.workspace }}/data-ingestors + run: ./scripts/sync-validator-goldens.sh --check From e190f21569873e6a65312d79b39743817bc965f8 Mon Sep 17 00:00:00 2001 From: Lukas Wuttke Date: Tue, 14 Jul 2026 16:39:05 +0200 Subject: [PATCH 2/2] ci: goldens-drift probe falls back to github.token on invalid PAT (cli#287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A set-but-invalid CROSS_REPO_READ_TOKEN (expired / wrong scope) made the readability probe pick only the PAT, fail, and skip the whole drift check while data-ingestors is still public and the default token would work. Probe now tries the PAT, and if it fails falls back to github.token before marking access failed — only skip-with-warning when BOTH fail. It emits a `use_pat` selector so the checkout uses exactly the token the probe validated, keeping the two in lockstep. Still read-only, least-privilege, skip-not-hard-fail. Co-Authored-By: Claude Fable 5 --- .github/workflows/goldens-drift.yml | 42 +++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/.github/workflows/goldens-drift.yml b/.github/workflows/goldens-drift.yml index 18fd424..53837b0 100644 --- a/.github/workflows/goldens-drift.yml +++ b/.github/workflows/goldens-drift.yml @@ -74,13 +74,43 @@ jobs: # a CROSS_REPO_READ_TOKEN secret the checkout would hard-fail every PR # touching internal/push/** — instead, skip the check with a loud # warning telling the admin exactly what to create. + # + # Prefer the least-privilege PAT when it's set AND actually works, but + # a set-but-invalid PAT (expired / wrong scope) must NOT strand the + # check: fall back to the default token before giving up, so a broken + # secret can't silently skip drift while the repo is still public. Only + # skip-with-warning when BOTH tokens fail. The `use_pat` output tells + # the checkout which token the probe validated, keeping the two in sync. env: CROSS_REPO_READ_TOKEN: ${{ secrets.CROSS_REPO_READ_TOKEN }} FALLBACK_TOKEN: ${{ github.token }} run: | - token="${CROSS_REPO_READ_TOKEN:-$FALLBACK_TOKEN}" - if git ls-remote "https://x-access-token:${token}@github.com/tracebloc/data-ingestors.git" HEAD >/dev/null 2>&1; then + probe() { + # Read-only reachability check — no clone, no working-tree writes. + git ls-remote "https://x-access-token:${1}@github.com/tracebloc/data-ingestors.git" HEAD >/dev/null 2>&1 + } + + ok=false + use_pat=false + + if [ -n "$CROSS_REPO_READ_TOKEN" ]; then + if probe "$CROSS_REPO_READ_TOKEN"; then + ok=true + use_pat=true + else + echo "::warning::CROSS_REPO_READ_TOKEN is set but tracebloc/data-ingestors is not readable with it (expired or wrong scope) — falling back to the default workflow token." + fi + fi + + if [ "$ok" != "true" ]; then + if probe "$FALLBACK_TOKEN"; then + ok=true + fi + fi + + if [ "$ok" = "true" ]; then echo "ok=true" >> "$GITHUB_OUTPUT" + echo "use_pat=$use_pat" >> "$GITHUB_OUTPUT" else echo "ok=false" >> "$GITHUB_OUTPUT" echo "::warning::tracebloc/data-ingestors is not readable with the available tokens — goldens-drift check SKIPPED. If the repo has been made private, create the org secret CROSS_REPO_READ_TOKEN (fine-grained PAT, contents:read on tracebloc/data-ingestors)." @@ -93,9 +123,11 @@ jobs: repository: tracebloc/data-ingestors ref: ${{ steps.pin.outputs.ref }} path: data-ingestors - # Prefer the cross-repo secret when it exists (needed only if the - # repo goes private); the default token suffices while it's public. - token: ${{ secrets.CROSS_REPO_READ_TOKEN || github.token }} + # Use exactly the token the probe validated: the PAT only when the + # probe confirmed it works (use_pat), else the default token (which + # the probe then confirmed). This keeps checkout in lockstep with the + # probe so a set-but-invalid PAT can't hard-fail a reachable repo. + token: ${{ steps.access.outputs.use_pat == 'true' && secrets.CROSS_REPO_READ_TOKEN || github.token }} - name: Set up Python if: steps.access.outputs.ok == 'true'