diff --git a/.github/workflows/goldens-drift.yml b/.github/workflows/goldens-drift.yml new file mode 100644 index 0000000..53837b0 --- /dev/null +++ b/.github/workflows/goldens-drift.yml @@ -0,0 +1,152 @@ +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. + # + # 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: | + 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)." + 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 + # 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' + 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