Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 215 additions & 0 deletions .github/workflows/head-drift-canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
name: data-ingestors HEAD-drift canary

# Weekly advisory canary (cli#289, epic backend#1106 WS-C.4). NEVER blocking:
# cron + manual dispatch only, no PR trigger, and the job succeeds even when
# drift is found — the signal is a single tracking issue, not a red build.
#
# The pin (scripts/.data-ingestors-ref) keeps PR CI deterministic, but it also
# means upstream can walk away from the mirror silently. This canary looks at
# data-ingestors' develop HEAD (NOT the pin) and raises a hand when the pin has
# gone stale in either of two ways:
#
# 1. GOLDENS DRIFT AT HEAD — the committed parity goldens no longer match
# what the REAL validators at HEAD produce (a verdict/value the CLI's
# preflight would disagree with once the pin is bumped).
# 2. MIRRORED-SOURCE DRIFT — any upstream source file the CLI's preflight
# mirrors changed between the pin and HEAD, even if no golden flips.
# This catches the di#365 class: a new/extended upstream validator with
# no corpus case yet, invisible to the goldens check by definition.
#
# On either signal it opens (or comments on) ONE tracking issue, keyed by the
# `head-drift-canary` label — never a second open issue, never a hard failure.
# The fix is always the same playbook: bump the pin (cli#286 shape) — edit
# scripts/.data-ingestors-ref, re-run both sync scripts, adopt semantics,
# commit goldens in the same diff.
#
# Cross-repo access: data-ingestors is currently public, so the default token
# suffices; an org secret CROSS_REPO_READ_TOKEN is preferred when present
# (only needed if the repo ever goes private). A checkout failure reds this
# cron run — acceptable for an advisory canary, it blocks nothing.

on:
schedule:
- cron: '0 6 * * 1' # weekly, Monday 06:00 UTC — after goldens-drift's 05:30
workflow_dispatch:

permissions:
contents: read
issues: write

concurrency:
group: head-drift-canary
cancel-in-progress: false

jobs:
canary:
timeout-minutes: 20
name: Pin vs HEAD drift (advisory)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Resolve the data-ingestors pin
id: pin
# First non-comment, non-blank line — the same rule sync-schema.sh uses.
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: Checkout tracebloc/data-ingestors @ develop HEAD
uses: actions/checkout@v4
with:
repository: tracebloc/data-ingestors
ref: develop
path: data-ingestors
# Full history so the pin SHA is present for the pin-vs-HEAD diff.
fetch-depth: 0
token: ${{ secrets.CROSS_REPO_READ_TOKEN || github.token }}

- name: Diff the mirrored validator surface, pin vs HEAD
id: srcdiff
env:
PIN: ${{ steps.pin.outputs.ref }}
# Content comparison via git (equivalent to hashing each file at both
# refs, minus the plumbing). The path list is the upstream surface
# internal/push + internal/schema mirror — keep it in sync with the
# file references in those packages' doc comments:
# validators/ every preflight verdict previewed (or skipped)
# modalities/ factories, specs, layout traits, link columns
# utils/coercion.py NA_SENTINELS + bool vocabulary
# utils/columns.py resolve_column (header resolve rule)
# utils/constants.py media-extension sets
# utils/validators_mapping.py which validators run per category
# ingestors/preflight.py check_csv_encoding
# ingestors/csv_ingestor.py read dialect / BOM / na_values
# ingestors/record_processor.py schema-filter (kept columns)
# ingestors/base.py label resolution + options threading
# file_transfer.py _find_src / _has_extension (CrossCheckLabels)
# schema_inference.py tabular type-inference parity
# database.py table-name/identifier rules
# schema/ ingest.v1.json + layout.v1.json (embedded)
run: |
cd data-ingestors
echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
if ! git cat-file -e "${PIN}^{commit}" 2>/dev/null; then
# A rewritten/garbage pin is itself report-worthy drift.
echo "pin_reachable=false" >> "$GITHUB_OUTPUT"
{
echo "changed<<CHANGED_EOF"
echo "CHANGED_EOF"
} >> "$GITHUB_OUTPUT"
exit 0
fi
echo "pin_reachable=true" >> "$GITHUB_OUTPUT"
changed="$(git diff --name-only "$PIN" HEAD -- \
tracebloc_ingestor/validators \
tracebloc_ingestor/modalities \
tracebloc_ingestor/utils/coercion.py \
tracebloc_ingestor/utils/columns.py \
tracebloc_ingestor/utils/constants.py \
tracebloc_ingestor/utils/validators_mapping.py \
tracebloc_ingestor/ingestors/preflight.py \
tracebloc_ingestor/ingestors/csv_ingestor.py \
tracebloc_ingestor/ingestors/record_processor.py \
tracebloc_ingestor/ingestors/base.py \
tracebloc_ingestor/file_transfer.py \
tracebloc_ingestor/schema_inference.py \
tracebloc_ingestor/database.py \
tracebloc_ingestor/schema)"
{
echo "changed<<CHANGED_EOF"
echo "$changed"
echo "CHANGED_EOF"
} >> "$GITHUB_OUTPUT"

- name: Set up Python
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
run: pip install -r data-ingestors/requirements.txt

- name: Goldens check against HEAD (advisory)
id: goldens
# Same harness the goldens-drift job runs against the PIN, pointed at
# HEAD instead. Failure here must not red the run — capture the verdict
# and let the reporting step raise the issue.
env:
DATA_INGESTORS_DIR: ${{ github.workspace }}/data-ingestors
run: |
if ./scripts/sync-validator-goldens.sh --check >goldens-out.txt 2>&1; then
echo "drift=false" >> "$GITHUB_OUTPUT"
echo "goldens check vs HEAD: in sync"
else
echo "drift=true" >> "$GITHUB_OUTPUT"
echo "goldens check vs HEAD: DRIFT"
fi
tail -n 20 goldens-out.txt || true

- name: Open or update the tracking issue
if: >-
steps.goldens.outputs.drift == 'true' ||
steps.srcdiff.outputs.changed != '' ||
steps.srcdiff.outputs.pin_reachable == 'false'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PIN: ${{ steps.pin.outputs.ref }}
HEAD_SHA: ${{ steps.srcdiff.outputs.head_sha }}
PIN_REACHABLE: ${{ steps.srcdiff.outputs.pin_reachable }}
GOLDENS_DRIFT: ${{ steps.goldens.outputs.drift }}
CHANGED: ${{ steps.srcdiff.outputs.changed }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
# shellcheck disable=SC2016 # single-quoted printf formats carry markdown backticks, not expansions
{
printf '<!-- head-drift-canary -->\n'
printf '**Canary run:** %s\n\n' "$RUN_URL"
printf '| | |\n|---|---|\n'
printf '| pin (`scripts/.data-ingestors-ref`) | `%s` |\n' "$PIN"
printf '| data-ingestors develop HEAD | `%s` |\n' "$HEAD_SHA"
printf '| goldens check vs HEAD | %s |\n' "$([ "$GOLDENS_DRIFT" = "true" ] && echo 'DRIFT — verdicts/values changed' || echo 'in sync')"
printf '| pin reachable from HEAD | %s |\n\n' "${PIN_REACHABLE:-unknown}"
if [ "$PIN_REACHABLE" = "false" ]; then
printf '**The pinned SHA is not reachable in the upstream clone** — upstream history was rewritten or the ref file is wrong. Fix the pin first.\n\n'
fi
if [ -n "$CHANGED" ]; then
printf '**Mirrored validator source files changed between pin and HEAD** (may include verdict-neutral changes with no corpus case yet — the di#365 class):\n\n```\n'
printf '%s\n' "$CHANGED"
printf '```\n\n'
fi
printf '**Playbook** (pin-bump doctrine, backend#1009 / cli#286 shape): bump `scripts/.data-ingestors-ref` to the new HEAD, run `scripts/sync-schema.sh` + `scripts/sync-validator-goldens.sh`, adopt/audit the intervening upstream changes in the `internal/push` mirror, and ship the regenerated goldens in the same PR.\n\n'
printf '_Advisory only — this canary never blocks CI. Close this issue when the pin bump lands._\n'
} > body.md
gh label create head-drift-canary --repo "$REPO" --force \
--description "auto-filed by head-drift-canary.yml (cli#289)" --color D93F0B
existing="$(gh issue list --repo "$REPO" --label head-drift-canary --state open \
--json number --jq '.[0].number // empty')"
if [ -n "$existing" ]; then
echo "updating existing tracking issue #$existing"
gh issue comment "$existing" --repo "$REPO" --body-file body.md
else
echo "opening new tracking issue"
gh issue create --repo "$REPO" \
--title "data-ingestors HEAD has drifted from the CLI's pin (canary)" \
--body-file body.md \
--label head-drift-canary
fi

- name: All clear
if: >-
steps.goldens.outputs.drift != 'true' &&
steps.srcdiff.outputs.changed == '' &&
steps.srcdiff.outputs.pin_reachable == 'true'
env:
PIN: ${{ steps.pin.outputs.ref }}
HEAD_SHA: ${{ steps.srcdiff.outputs.head_sha }}
run: |
echo "::notice::No HEAD drift — pin $PIN, HEAD $HEAD_SHA: goldens in sync, mirrored surface unchanged."
Loading