From cc097868ab0b865d1fc4eca94fa3185c22ca18b2 Mon Sep 17 00:00:00 2001 From: Arturo Peroni Date: Thu, 27 Aug 2026 16:44:51 +0200 Subject: [PATCH 1/2] fix(sync): watch the data-ingestors pin for contract-version drift (backend#2704) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `scripts/sync-schema.sh --check` verifies the vendored contract matches the PINNED SHA — it answers "does my copy match what I pinned?", never "is what I pinned still current?". So the pin silently went a whole contract VERSION behind (layout v2 vs data-ingestors@develop v3, di#535) while every drift gate stayed green. The pin is deliberate and stays (backend#1009: a floating ref reds every open CLI PR on unrelated upstream commits). Add a watcher for the pin itself: - scripts/check-pin-version.sh resolves the pin and data-ingestors' current default branch, fetches layout.v1.json at both, and compares the top-level "version" — VERSION, not commit distance (180 unrelated commits are fine). FAILS CLOSED: absent/malformed pin, moved path, unreachable raw URL, non-JSON, or missing "version" all exit non-zero. Exit 0 in sync, 1 drift, 2 can't-eval. - .github/workflows/pin-version-drift.yml runs it weekly (Mon 06:30 UTC, after head-drift-canary) + workflow_dispatch. No pull_request trigger — gating PRs on live upstream is the backend#1009 failure. The pinned PR comparison is unchanged. - scripts/tests/pin-version-verify.sh mutation-proves it hermetically (drift reddens; each cannot-evaluate reddens), wired into build.yml alongside the other fail-closed harnesses. - BUGBOT.md: record the new scheduled-only, never-PR-gating workflow. Co-Authored-By: Claude Opus 4.8 --- .cursor/BUGBOT.md | 4 + .github/workflows/build.yml | 11 ++ .github/workflows/pin-version-drift.yml | 61 +++++++++ scripts/check-pin-version.sh | 163 ++++++++++++++++++++++++ scripts/tests/pin-version-verify.sh | 114 +++++++++++++++++ 5 files changed, 353 insertions(+) create mode 100644 .github/workflows/pin-version-drift.yml create mode 100755 scripts/check-pin-version.sh create mode 100755 scripts/tests/pin-version-verify.sh diff --git a/.cursor/BUGBOT.md b/.cursor/BUGBOT.md index 86525c2..de4a745 100644 --- a/.cursor/BUGBOT.md +++ b/.cursor/BUGBOT.md @@ -152,6 +152,10 @@ Two things make this repo unusual and should shape every finding: - `test/integration/*` uses 30s–5min timeouts because it drives a real cluster — not the production timeout convention. - `mutation.yml` and `head-drift-canary.yml` are advisory and never gate a merge. + `pin-version-drift.yml` is scheduled-only (no `pull_request` trigger) and fails CLOSED on + data-ingestors contract-version drift — a red scheduled run is the signal, by design; it still + never gates a PR merge, and gating PRs on live upstream is the backend#1009 failure it avoids + (backend#2704). - No `vendor/` directory — the module cache is used on purpose. - `// style-guard: allow` is a defined escape hatch but is currently used nowhere; if one appears, it is a novel exception worth scrutiny rather than an accepted pattern. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 405a368..7265b0e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,6 +87,10 @@ jobs: bash -n scripts/tests/install-verify.sh shellcheck --shell=bash --severity=error scripts/tests/install-ps1-verify.sh bash -n scripts/tests/install-ps1-verify.sh + shellcheck --shell=bash --severity=error scripts/check-pin-version.sh + shellcheck --shell=bash --severity=error scripts/tests/pin-version-verify.sh + bash -n scripts/check-pin-version.sh + bash -n scripts/tests/pin-version-verify.sh # format.sh's own fail-closed properties. Formatters are stubbed, so this is # hermetic and needs no Go toolchain — which is why it lives in this job # rather than Lint. It exists because the first cut of format.sh reported @@ -108,6 +112,13 @@ jobs: # since "cannot tell" is not evidence that verification is mandatory. - name: Verification harness — Windows (mandatory cosign / fail-closed) run: bash scripts/tests/install-ps1-verify.sh + # check-pin-version.sh's fail-closed properties: contract-version DRIFT + # reddens (exit 1) and every "cannot evaluate" reddens (exit 2). Fetches + # are file:// URLs and the pin/branch are injected, so this is hermetic + # and needs no network — the live pin-vs-HEAD comparison is the scheduled + # pin-version-drift.yml, which must never gate a PR (backend#2704 / #1009). + - name: Pin-version watcher harness (drift reddens / fail-closed) + run: bash scripts/tests/pin-version-verify.sh test: timeout-minutes: 15 diff --git a/.github/workflows/pin-version-drift.yml b/.github/workflows/pin-version-drift.yml new file mode 100644 index 0000000..8b6980e --- /dev/null +++ b/.github/workflows/pin-version-drift.yml @@ -0,0 +1,61 @@ +name: Contract-version drift (pin freshness) + +# Watches the data-ingestors pin ITSELF for contract-version staleness +# (backend#2704). scripts/sync-schema.sh --check (build.yml) answers "does my +# vendored copy match the SHA I pinned?"; it never answers "is the SHA I pinned +# still current?" So the pin sat a whole contract VERSION behind (layout v2 vs +# data-ingestors@develop v3) while every drift gate stayed green. +# +# This job resolves scripts/.data-ingestors-ref, resolves data-ingestors' +# current default branch, and compares the CONTRACT VERSION — the top-level +# "version" in tracebloc_ingestor/schema/layout.v1.json — at the pin vs at HEAD. +# Version, NOT commit distance: 180 unrelated upstream commits are fine, a +# version bump is the finding (scripts/check-pin-version.sh). +# +# SCHEDULED, never a PR gate. Gating open PRs on live upstream is precisely the +# backend#1009 failure this repo removed by pinning — an unrelated upstream +# bump would red every open PR. So there is no `pull_request` trigger here; the +# checker's own logic is guarded on PRs by its hermetic unit test +# (scripts/tests/pin-version-verify.sh, run in build.yml), which touches no +# network. The pinned PR comparison (sync-schema.sh --check) is left unchanged. +# +# FAIL CLOSED. A red run is the signal: version drift (exit 1) AND any +# "cannot evaluate" — absent/malformed pin, moved path, unreachable raw URL, +# non-JSON, missing "version" (exit 2) — both fail the job. "Cannot tell" is a +# finding, never a green pass. +# +# Complements head-drift-canary.yml (weekly, advisory, never-blocking, opens a +# tracking issue on ANY mirrored-source change). This one is narrower and +# harder: a fail-closed gate on the semantic contract VERSION specifically. +# +# data-ingestors is public, so github.token suffices for the `gh api` +# default-branch lookup and the raw fetches. If it ever goes private, add a +# read token (CROSS_REPO_READ_TOKEN) the way head-drift-canary.yml does. + +on: + schedule: + - cron: "30 6 * * 1" # weekly Monday 06:30 UTC — after head-drift-canary (06:00) + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pin-version-drift + cancel-in-progress: false + +jobs: + contract-version: + timeout-minutes: 10 + name: Pinned contract version vs HEAD (fail-closed) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Compare pinned contract version against data-ingestors HEAD + # GH_TOKEN lets `gh api` resolve the current default branch. python3 and + # curl are preinstalled on ubuntu-latest. Non-zero exit (drift OR + # cannot-evaluate) reds this run by design — that IS the finding. + env: + GH_TOKEN: ${{ github.token }} + run: ./scripts/check-pin-version.sh diff --git a/scripts/check-pin-version.sh b/scripts/check-pin-version.sh new file mode 100755 index 0000000..6398a39 --- /dev/null +++ b/scripts/check-pin-version.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# ============================================================================= +# check-pin-version.sh — is the pinned data-ingestors contract still the +# CURRENT contract version? (backend#2704) +# +# scripts/sync-schema.sh --check answers one question well: "does my vendored +# copy match the SHA I pinned?" It never answers the second: "is the SHA I +# pinned still current?" So the pin can sit six weeks / a whole contract +# VERSION behind while every drift gate stays green — which is exactly what +# happened (pin on layout contract v2, data-ingestors@develop on v3). +# +# The pin is deliberate and must stay (backend#1009: a floating ref reds every +# open CLI PR on unrelated upstream commits). This script does NOT float it. It +# is the watcher for the pin ITSELF: resolve the pin, resolve data-ingestors' +# current default branch, fetch the dataset-layout contract at BOTH refs, and +# compare the CONTRACT VERSION — the top-level "version" string in +# tracebloc_ingestor/schema/layout.v1.json. +# +# VERSION, not commit distance. 180 unrelated upstream commits that never touch +# the version are fine and stay green; a single version bump is the finding. +# This is why it is a SCHEDULED job, never a PR gate: gating PRs on live +# upstream is the backend#1009 failure this repo already removed. +# +# FAIL CLOSED. "Cannot tell" is a finding, never a pass — the guard already has +# one shape of "cannot tell reads as fine" (the pinned check going stale) and +# must not gain a second. An absent/malformed pin, a moved path, an unreachable +# raw URL, a non-JSON body, or a missing "version" all exit non-zero. +# +# Exit codes: +# 0 IN SYNC — the pin is on the current contract version. +# 1 DRIFT — the pin's contract version differs from HEAD's. Re-pin +# (scripts/.data-ingestors-ref) and re-run scripts/sync-schema.sh. +# 2 FAIL CLOSED — could not evaluate (bad/absent pin, moved path, unreachable +# URL, non-JSON, missing version). Never reported as agreement. +# +# Env knobs (defaults are the production values; overrides exist for the +# hermetic test harness, scripts/tests/pin-version-verify.sh): +# REF_FILE pin file (default: scripts/.data-ingestors-ref) +# DATA_INGESTORS_REF override the pin ref (default: from REF_FILE) +# DATA_INGESTORS_DEFAULT_BRANCH override the upstream default branch +# (default: resolved via `gh api`) +# PIN_LAYOUT_URL / HEAD_LAYOUT_URL full URL overrides for the two fetches +# (default: built from the refs above) +# ============================================================================= +set -uo pipefail + +# Where the vendored layout contract lives upstream — keep in lockstep with the +# UPSTREAM_BASE/layout.v1.json path in scripts/sync-schema.sh. +readonly UPSTREAM_BASE="https://raw.githubusercontent.com/tracebloc/data-ingestors" +readonly LAYOUT_SUBPATH="tracebloc_ingestor/schema/layout.v1.json" + +REF_FILE="${REF_FILE:-$(cd "$(dirname "$0")" && pwd)/.data-ingestors-ref}" + +die_closed() { echo "FAIL CLOSED: $*" >&2; exit 2; } + +# A ref is interpolated into a raw.githubusercontent URL, so validate its shape +# before use — same guard as sync-schema.sh / chart-drift.yml: alnum start, then +# alnum . _ - / and no ".." component. Blocks path-traversal / extra segments. +valid_ref_shape() { + grep -qE '^[A-Za-z0-9][A-Za-z0-9._/-]*$' <<<"$1" && ! grep -q '\.\.' <<<"$1" +} + +# Resolve the pin: first non-comment, non-blank line of REF_FILE (the rule +# sync-schema.sh uses), overridable via DATA_INGESTORS_REF. +resolve_pin() { + if [[ -n "${DATA_INGESTORS_REF:-}" ]]; then + printf '%s' "$DATA_INGESTORS_REF"; return 0 + fi + [[ -f "$REF_FILE" ]] || return 1 + grep -vE '^[[:space:]]*(#|$)' "$REF_FILE" 2>/dev/null | head -1 | tr -d '[:space:]' +} + +# Resolve data-ingestors' CURRENT default branch, overridable for tests. Uses +# `gh api` (needs GH_TOKEN in CI); a public repo's metadata reads with the +# default workflow token. +resolve_default_branch() { + if [[ -n "${DATA_INGESTORS_DEFAULT_BRANCH:-}" ]]; then + printf '%s' "$DATA_INGESTORS_DEFAULT_BRANCH"; return 0 + fi + local b + b="$(gh api repos/tracebloc/data-ingestors --jq .default_branch 2>/dev/null)" + # gh emits nothing on failure and the literal "null" when the field is absent + # (a 200 with an unexpected body). Treat both as unresolved so main's -n check + # fail-closes on the real cause instead of curling a `.../null/...` URL. + [[ -n "$b" && "$b" != "null" ]] || return 1 + printf '%s' "$b" +} + +# fetch_version