Check that examples still converge to the repos they serve - #9
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
cibuilt 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 byactionlintandzizmoronly. 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:
P0 — the seed that outlived its shred
examples/aml-filter/deploy.ymldecoded an Ed25519 production signing seed to/tmpandshreded it on the last line of the samerun:block — after averifystep 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:
if: always()/tmp/signing.key$RUNNER_TEMP— outside the checkout, never a deploy sourceumask 077shred -ushred -f -n 3 -z --removeOn whether the seed should be on disk at all:
@amlfilter/publishertakes--key <path>and exposes no stdin or env interface (verified —git grepoverfrontend/packages/amlfilter-publisherfinds 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.ymlalready 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 inci-v2.0.3.The new guard
tests/example-fidelity.sh+tests/lib/example-references.rbresolve every path, package script, node script, poe task, brick ref, and brick input name an example uses, against the consumer's committed default branch.UNVERIFIABLEis never counted as a pass and never as a failure.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:
edge-reco/ci.ymlx2,deploy.ymlfrontend/.node-version(repo has.nvmrc)aml-filter/ci.ymlfrontend/public/models,fetch-weights.mjsaml-filter/deploy.ymlx3build:bundle/sign:bundle/verify:bundledo not existIt also caught its own false alarm during development:
-Fis pnpm's short--filter, sopnpm -C frontend -F frontend run build:pagesresolves by package name, not by the-Cdirectory. Reading it as a directory flagged a script that exists. Fixed, and pinned in both directions.Everything else
bc68fde(v2.0.3) ->2a575cd(v3.0.0), version comments included. No# ci-vX.Y.Zcomment now disagrees with the SHA it annotates. v2.0.3'scloudflare-pages-deploy.ymllacks theevent == '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.detect_secret_scanmatched rawrun: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..ruby-version3.4.10 +ruby/setup-ruby) — 1,335 lines of gate logic ran on whatever the runner shipped.Silent-skip audit (repo-wide)
consumer-drift.sh0 across 0 repos, exit 0consumer-drift.yml::notice::+exit 0; daily run reported SUCCESS having looked at nothingsecurity-audit.ymlfalse-> two skipped jobs, green check, nothing auditedconfiguredjob refuses the no-op; generalised asvalidate_no_vacuous_successclassify-workflow.rb,scan-*.rbrescue StandardError; nextsilently skips an unparseable filevalidate_yamlfails first on any malformed file in scope, so it is covered by a different checkapply-security-headers.shexit 0when an app-owned_headersexists::notice::, and behaviourally tested*-publish.ymlrun-gatetrue; a caller can disable the gateThe 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
test.ymlhas five jobs;backendis an exactpython-gate.ymlfit (uv sync --locked --extra dev->uv run poe gate)examples/shared-libs-python/is its example set — same GitHub repo id1124367465, renamed.git mv'd toexamples/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:
setup-python-uvts-publish.ymlsetup-pnpm,setup-playwright,pages-deploy-dist,restore-model-cacheComposites 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-publisherbecause Trusted Publishing matchesjob_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 thegitleaksanddependency-auditcomposites 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.0tag 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:pages-deploy-dist:commit-hash/commit-dirtyinputs. edge-reco's CF-API identity check cannot pass without--commit-hash, so converging today deletes it.cloudflare-pages-deploy.yml:post-deploy-run(env-routed viaTRUSTED_COMMAND, exactly like the existingpre-build-run—run: ${{ inputs.… }}would fail this repo's ownscan-run-interpolation.rb), plus aconcurrency-groupoverride (aml-filter deliberately sharesdeploy-aml-filter-combetween two workflows).gitleaksanddependency-audit.github.event.workflow_run.head_sha || github.sha, not|| github.ref— onworkflow_dispatchgithub.refisrefs/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_ENVvalues and build-tree files a downstream job cannot see, so verification must stay in-job — workflowoutputs:are a complement, not a substitute.Known follow-up (not in scope)
1,335 lines of gate logic are Ruby, with no
Gemfileand no dependency manifest. This PR pins the interpreter (.ruby-version3.4.10); the language question is tracked separately and deliberately untouched here.Evidence
MISSINGon unmodifiedexamples/, pasted above172 resolved, 0 MISSING, 0 UNVERIFIABLE across 7 consumer repo(s)example-fidelity-cases.sh: 20 cases, every property in both polarities, incl. floors and the 3-status contractlint-examples.shred run above — actionlint + zizmor clean, fidelity redconfiguredfrom a copy -> guard exits 1; real file -> exits 0consumer-drift-cases.shred first (expected [], got [dependency-audit DRIFT, secret-scan DRIFT]), then green29 hand-rolled control(s) across 7 repo(s); 29 allowlisted; 0 new2a575cd… # ci-v3.0.0; grep for a mismatch returns nothingsecurity-policy.shOK,lineage-guard-cases.shOK,consumer-drift-cases.shOK,example-fidelity-cases.shOK,shellcheckclean, actionlint + zizmor cleanUnverified, stated plainly
wrangler pages deployor a registry upload. The three remote-mutating bricks are asserted structurally (validate_publish_provenance,validate_trusted_command_contracts,validate_pages_headers— which does really executeapply-security-headers.sh), never behaviourally. Inherent to publishing, but a stated gap.edge-procadoption PR, which this PR unblocks.🤖 Generated with Claude Code
https://claude.ai/code/session_01CRTjUqZ8AcsC5srjBMMbc9