Skip to content

Check that examples still converge to the repos they serve - #9

Merged
hseshadr merged 6 commits into
mainfrom
fix/examples-are-not-verified
Jul 31, 2026
Merged

Check that examples still converge to the repos they serve#9
hseshadr merged 6 commits into
mainfrom
fix/examples-are-not-verified

Conversation

@hseshadr

Copy link
Copy Markdown
Owner

Why

ci built a detector proving consumers diverge from it, and never built the mirror proving its own examples converge back to them. examples/ — the surface every convergence PR starts by copying — was audited by actionlint and zizmor only. Both check YAML shape and workflow security; neither resolves a repo-relative path inside somebody else's repository.

So eight broken references shipped green, including one that leaked a production signing seed.

Here is the whole gap in one run, on this branch, with a single reference deliberately broken:

--- actionlint (examples) ---
--- zizmor (examples, online audits on) ---
No findings to report. Good job! (2 ignored, 19 suppressed)
--- example fidelity (references resolve in the consumer repo) ---
Example fidelity: 170 resolved, 2 MISSING, 0 UNVERIFIABLE across 7 consumer repo(s).

Broken references (the example is red as drafted — fix the example, not the consumer):
  examples/edge-reco/ci.yml              file           frontend/.node-version
      -> does not resolve in edge-reco (... @ origin/main (committed 2026-07-31))
::error::2 example reference(s) do not resolve in the repository the example is written for.

P0 — the seed that outlived its shred

examples/aml-filter/deploy.yml decoded an Ed25519 production signing seed to /tmp and shreded it on the last line of the same run: block — after a verify step documented to abort the deploy on failure. The failure mode the design expects is exactly the one that skipped the shred, leaving the seed on the runner.

Fixed:

Property Before After
Scrub last line of the signing block its own step, if: always()
Location /tmp/signing.key $RUNNER_TEMP — outside the checkout, never a deploy source
Permissions default umask 077
Size check none 32-byte assertion
Method shred -u shred -f -n 3 -z --remove

On whether the seed should be on disk at all: @amlfilter/publisher takes --key <path> and exposes no stdin or env interface (verified — git grep over frontend/packages/amlfilter-publisher finds no other form). An example that piped the key in would not drive the tool this portfolio actually ships, which is the defect this PR exists to remove. So the file stays and the handling is hardened.

The live aml-filter/deploy.yml already had all of this (Scrub the key / if: always(), L281-283). The example had silently shipped weaker than the code it claimed to represent — the same pattern as the fork-PR guard in ci-v2.0.3.

The new guard

tests/example-fidelity.sh + tests/lib/example-references.rb resolve every path, package script, node script, poe task, brick ref, and brick input name an example uses, against the consumer's committed default branch.

  • Three statuses, because two would lie. UNVERIFIABLE is never counted as a pass and never as a failure.
  • Reads a git ref, never the working tree — an uncommitted local edit cannot invent a false alarm. The ref and its commit date are printed, so a stale clone discloses itself.
  • Coverage floors: a run that inspected nothing exits 2, not 0.
  • Wired into tests/lint-examples.sh, so CI runs it. In CI it shallow-clones the seven consumers (26s, all public).

It found 8 defects — 3 more than the manual audit:

Example Broken reference
edge-reco/ci.yml x2, deploy.yml frontend/.node-version (repo has .nvmrc)
aml-filter/ci.yml frontend/public/models, fetch-weights.mjs
aml-filter/deploy.yml x3 build:bundle / sign:bundle / verify:bundle do not exist

It also caught its own false alarm during development: -F is pnpm's short --filter, so pnpm -C frontend -F frontend run build:pages resolves by package name, not by the -C directory. Reading it as a directory flagged a script that exists. Fixed, and pinned in both directions.

Everything else

  • Re-pinned all 40 first-party refs bc68fde (v2.0.3) -> 2a575cd (v3.0.0), version comments included. No # ci-vX.Y.Z comment now disagrees with the SHA it annotates. v2.0.3's cloudflare-pages-deploy.yml lacks the event == 'push' fork-PR guard all three live deploys carry — adopting these examples would have downgraded protection against the bug class that leaked prod signing keys to fork PRs on 2026-07-25.
  • Classifier false positive fixeddetect_secret_scan matched raw run: text and fired on a shell comment. Comments are now stripped quote-aware, so a # inside a string cannot hide a real command either (a false negative is the worse failure). Live count corrected 30 -> 29.
  • Four false allowlist reasons corrected, not one. See below.
  • Three silent skips closed. See below.
  • Ruby pinned (.ruby-version 3.4.10 + ruby/setup-ruby) — 1,335 lines of gate logic ran on whatever the runner shipped.

Silent-skip audit (repo-wide)

Location Shape Action
consumer-drift.sh inspected 0 repos -> 0 across 0 repos, exit 0 Fixed — exit 2 on total miss, warn on partial
consumer-drift.yml no token -> ::notice:: + exit 0; daily run reported SUCCESS having looked at nothing Fixed — scheduled run fails; PR run warns into the step summary
security-audit.yml both audit toggles default false -> two skipped jobs, green check, nothing audited Fixed — unconditional configured job refuses the no-op; generalised as validate_no_vacuous_success
classify-workflow.rb, scan-*.rb rescue StandardError; next silently skips an unparseable file Reported, not changedvalidate_yaml fails first on any malformed file in scope, so it is covered by a different check
apply-security-headers.sh exit 0 when an app-owned _headers exists Reported, not changed — intentional, disclosed by ::notice::, and behaviourally tested
*-publish.yml run-gate defaults true; a caller can disable the gate Reported, not changed — documented input, safe default

The existing case asserting "an unreachable repository is skipped, not failed" encoded the first defect for the zero-repo case. It is inverted and labelled loudly; its real intent (one unreachable repo must not fail the build) is preserved as a separate case.

False allowlist reasons — 4, not 1

Claim Truth
almamesh's gate "spans Python + browser and has never been split" False. test.yml has five jobs; backend is an exact python-gate.yml fit (uv sync --locked --extra dev -> uv run poe gate)
"edgeproc-core has no converged example yet" x3 False. examples/shared-libs-python/ is its example set — same GitHub repo id 1124367465, renamed. git mv'd to examples/edgeproc-core/

Composites: the contradiction, settled with data

Neither prior pass was right. Measured across all 7 consumers — 6 call-sites total, split evenly, all inside publish workflows:

Call-sites Repos
setup-python-uv 3 assay, edge-proc, edgeproc-core
ts-publish.yml 3 assay x2, privacy-core
setup-pnpm, setup-playwright, pages-deploy-dist, restore-model-cache 0
the other 6 reusable workflows 0

Composites are not categorically the adopted shape — 4 of 5 have zero adopters. The one that is adopted got there by necessity: cross-repo PyPI publishing fails invalid-publisher because Trusted Publishing matches job_workflow_ref, so the job must be inlined and the composite is the only shareable unit left.

The real argument for composites is different and stronger: composite adoption preserves required-status-check names; reusable-workflow adoption always renames them (gate -> gate / gate), which strands every open PR on a repo with branch protection. That is why the gitleaks and dependency-audit composites are still worth writing — 14 of the 29 findings are those two controls running as a step inside an existing job, which only a composite can converge without a settings change.

Deliberate split — what is NOT here

Two requested items are new published interface, which needs a ci-v3.1.0 tag before any consumer can pin it. Bundling them would double this diff and mix "fix what is broken" with "design new inputs":

PR B — feat/deploy-lane-inputs:

  1. pages-deploy-dist: commit-hash / commit-dirty inputs. edge-reco's CF-API identity check cannot pass without --commit-hash, so converging today deletes it.
  2. cloudflare-pages-deploy.yml: post-deploy-run (env-routed via TRUSTED_COMMAND, exactly like the existing pre-build-runrun: ${{ inputs.… }} would fail this repo's own scan-run-interpolation.rb), plus a concurrency-group override (aml-filter deliberately shares deploy-aml-filter-com between two workflows).
  3. The two new composites, gitleaks and dependency-audit.
  4. A bug found while scoping it: the workflow's SHA expression must be github.event.workflow_run.head_sha || github.sha, not || github.ref — on workflow_dispatch github.ref is refs/heads/main, a valid checkout ref but not a SHA.

Design note for PR B: only edge-reco can adopt the reusable workflow; aml-filter and almamesh keep their post-deploy steps in-job and need the composite inputs only. Their verifications read $GITHUB_ENV values and build-tree files a downstream job cannot see, so verification must stay in-job — workflow outputs: are a complement, not a substitute.

Known follow-up (not in scope)

1,335 lines of gate logic are Ruby, with no Gemfile and no dependency manifest. This PR pins the interpreter (.ruby-version 3.4.10); the language question is tracked separately and deliberately untouched here.

Evidence

Claim Proof
Guard catches the real defects 8 MISSING on unmodified examples/, pasted above
Guard passes after the fixes 172 resolved, 0 MISSING, 0 UNVERIFIABLE across 7 consumer repo(s)
Guard can fail on demand example-fidelity-cases.sh: 20 cases, every property in both polarities, incl. floors and the 3-status contract
Wiring is real lint-examples.sh red run above — actionlint + zizmor clean, fidelity red
Vacuous-success guard can fail deleting configured from a copy -> guard exits 1; real file -> exits 0
Classifier fix consumer-drift-cases.sh red first (expected [], got [dependency-audit DRIFT, secret-scan DRIFT]), then green
Live drift count 29 hand-rolled control(s) across 7 repo(s); 29 allowlisted; 0 new
No lying pins every one of 40 refs is 2a575cd… # ci-v3.0.0; grep for a mismatch returns nothing
Local gate security-policy.sh OK, lineage-guard-cases.sh OK, consumer-drift-cases.sh OK, example-fidelity-cases.sh OK, shellcheck clean, actionlint + zizmor clean

Unverified, stated plainly

  • No test executes a wrangler pages deploy or a registry upload. The three remote-mutating bricks are asserted structurally (validate_publish_provenance, validate_trusted_command_contracts, validate_pages_headers — which does really execute apply-security-headers.sh), never behaviourally. Inherent to publishing, but a stated gap.
  • No consumer has run any of the corrected examples. They are proven to resolve, not proven to go green in a real run. The first true proof is the edge-proc adoption PR, which this PR unblocks.
  • The Ruby is pinned to 3.4.10 in CI but local runs used the system Ruby 2.6 — CI is the first execution on 3.4.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CRTjUqZ8AcsC5srjBMMbc9

hseshadr and others added 6 commits July 31, 2026 10:33
This repository built a detector proving CONSUMERS diverge from what it
publishes, and never built the mirror proving its own EXAMPLES converge
back to them. examples/ was audited by actionlint and zizmor only —
syntax and workflow security. Neither resolves a repo-relative path
inside somebody else's repository, so both stayed green on eight broken
references, including one that leaked a production signing seed.

SECURITY (P0). examples/aml-filter/deploy.yml decoded an Ed25519
production seed to /tmp and shredded it on the LAST line of the same
`run:` block, after a verify step documented to abort on failure. The
failure mode the design expects is exactly the one that skipped the
shred. The scrub is now its own step with `if: always()`, the key moved
to $RUNNER_TEMP (outside the checkout, so a dist-dir mistake cannot
publish it) and is written under `umask 077` with a 32-byte assertion.
The key stays a file because @amlfilter/publisher takes `--key <path>`
and offers no stdin or env interface; an example that piped it in would
not drive the tool the repo ships. The live aml-filter workflow already
had all of this — the example had silently shipped weaker.

NEW GUARD. tests/example-fidelity.sh + tests/lib/example-references.rb
resolve every path, package script, node script, poe task, brick ref and
brick INPUT NAME an example uses against the consumer's committed
default branch. Three statuses, because two would lie: UNVERIFIABLE is
never counted as a pass and never as a failure. References resolve
against a git ref, never the working tree, so uncommitted local edits
cannot invent a false alarm. Coverage floors make a run that inspected
nothing exit 2, not 0. Wired into tests/lint-examples.sh, so CI runs it.

Caught 8 real defects, 3 more than the manual audit found:
  edge-reco ci.yml x2 + deploy.yml  frontend/.node-version (it has .nvmrc)
  aml-filter ci.yml                 frontend/public/models, fetch-weights.mjs
  aml-filter deploy.yml x3          build:/sign:/verify:bundle do not exist

RE-PIN. All 40 first-party refs moved bc68fde (ci-v2.0.3) -> 2a575cd
(ci-v3.0.0), version comments included. v2.0.3's
cloudflare-pages-deploy.yml lacks the `event == 'push'` fork-PR guard
that all three live deploys carry, so adopting these examples would have
downgraded protection against the class of bug that leaked prod signing
keys to fork PRs on 2026-07-25. No `# ci-vX.Y.Z` comment now disagrees
with the SHA it annotates.

CLASSIFIER FALSE POSITIVE. detect_secret_scan matched /\bgitleaks\b/
against raw `run:` text and fired on a shell COMMENT in
almamesh/deploy.yml. Comments are now stripped quote-aware, so a '#'
inside a string cannot hide a real command either — a false negative is
the worse failure. Live count corrected 30 -> 29.

NO SILENT SKIPS. consumer-drift.sh could inspect zero repositories and
print "0 hand-rolled control(s) across 0 repo(s)" with exit 0 — a clean
bill of health from a run that never looked. It now exits 2 on a total
miss and warns on a partial one. consumer-drift.yml did the same at the
workflow level, exiting 0 with a ::notice:: when the token was absent,
so the daily run reported SUCCESS while inspecting nothing; a scheduled
run now fails, a pull_request run warns into the step summary.

The existing case asserting "an unreachable repository is skipped, not
failed" ENCODED that defect for the zero-repo case, so it is inverted
and labelled. Its real intent — one unreachable repo must not fail the
build — is preserved as a separate case.

ALLOWLIST. Four reasons asserted something untrue. almamesh's gate was
said to have "never been split": test.yml has five jobs and `backend` is
an exact python-gate.yml fit. edgeproc-core was said to have "no
converged example yet" three times: examples/shared-libs-python IS its
example set (same GitHub repo id, renamed), now git mv'd to
examples/edgeproc-core.

Also pins Ruby (.ruby-version 3.4.10 + ruby/setup-ruby): 1,300+ lines of
gate logic ran on whatever the runner shipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
security-audit.yml gates both of its jobs on caller inputs, and BOTH
default to false. A consumer that named the workflow and passed neither
flag got two skipped jobs and a SUCCESS conclusion from a security audit
that inspected nothing — a skip indistinguishable from a pass, in the
control most likely to be adopted by copy-paste. All six shipped
examples happen to set at least one flag, so this was latent rather than
live, but the brick handed every future adopter a loaded gun.

The defaults stay false on purpose: each consumer has exactly one
relevant ecosystem, and defaulting either to true would fail the other
kind of repo on a missing lockfile. So the fix is to refuse the no-op
rather than guess — a new unconditional `configured` job fails when both
toggles are off.

validate_no_vacuous_success generalises the property: a reusable
workflow whose EVERY job is gated on `inputs.*` can report success
having done nothing, so at least one job must be unconditional. Gating
on `github.event.*` is deliberately not flagged — cloudflare-pages-
deploy.yml's fork-PR guard skips a job precisely because it must not
run, which is the control working, not a vacuous pass. The distinction
is caller-configuration versus trust boundary, and both polarities plus
both exclusions are fixtured.

Proven red by deleting the `configured` job from a copy: guard exits 1.
Proven green on the real file: exits 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The P0 fix is only worth as much as the guard that keeps it. Two
properties are now machine-checked across .github/ and examples/:

  1. A key-scrub step must carry `if: always()`. Without it the scrub
     inherits the job's success condition, so it does not run on the
     failure that matters.
  2. A single `run:` block must not both WRITE a key file and SCRUB it.
     Any non-zero command between the two skips the scrub. Property 1
     cannot catch this — the step may legitimately have no `if:` at all
     — which is exactly why the original defect survived review.

Proven red against the real thing rather than a synthetic mutation:
restoring examples/aml-filter/deploy.yml from origin/main and running
the guard over it prints

  a single run: block both writes and scrubs a key file

and exits 1. The fixed file on this branch exits 0. Four fixture cases
pin both properties in both polarities, including the "split into steps
but no if: always()" middle case, which is the shape someone would
plausibly write while fixing this by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
README asserted things that stopped being true. It said every
first-party ref pins ci-v2.0.3 (all 40 now pin ci-v3.0.0), named
edgeproc-core by its pre-rename name, and described adoption without
saying how thin it actually is. CHANGELOG's top section was still
`Unreleased (after ci-v2.0.3)` and still called cutting ci-v3.0.0 a
pending owner action — it was cut on 2026-07-30 and is GitHub's Latest.

CHANGELOG now carries a real `## ci-v3.0.0` section with its commit SHA,
led by the fact consumers actually need: v3.0.0 added the
`workflow_run.event == 'push'` fork-PR guard that v2.0.3 lacked, and
moved ts-publish provenance false -> true. A new Unreleased section
covers this branch.

README gains a **Which brick do I want?** table — one row per workflow
and composite, with a plain sentence for when to reach for it and an
honest "Adopted by" column. Most rows are blank, which is the point: a
reader can see at a glance that they would be the first adopter, rather
than discovering it after wiring one up. Adoption is now stated exactly
— four repos, six call-sites, all on the publish path; almamesh,
aml-filter and edge-reco have none — alongside the 29 hand-rolled
controls the daily sweep still counts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI runs `shellcheck -x` with no severity floor, so an info-level finding
fails it. Two dead variables (KEY_WRITE/KEY_SCRUB — leftovers from
before the patterns moved inside the Ruby) and seven SC2016 hits on
fixture strings would have gone red there while passing a casual local
run. The SC2016 suppressions sit ABOVE the function, which is where
shellcheck honours them; placed inside the body they only cover the next
command, which is a quiet way to think a directive applied when it did
not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ruby-version-file: .ruby-version` is not an input ruby/setup-ruby
v1.321.0 declares. CI printed

  ##[warning]Unexpected input(s) 'ruby-version-file', valid inputs are
  ['ruby-version', 'rubygems', 'bundler', ...]

and ignored it — then loaded 3.4.10 anyway, because setup-ruby reads
.ruby-version by default when no version is given. So the pin worked
while the line expressing it did nothing, which is the same
looks-like-a-control-but-is-not shape this branch spent its time
removing. The default path is the real mechanism; say so in a comment
instead of miming it with a dead input.

Verified on the first run of this branch: `ruby 3.4.10 ... +PRISM
[x86_64-linux]` in both jobs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hseshadr
hseshadr merged commit a634c9c into main Jul 31, 2026
4 checks passed
@hseshadr
hseshadr deleted the fix/examples-are-not-verified branch July 31, 2026 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant