Skip to content

claude: narrow the image-publish trigger to real Dockerfile inputs (#777)#790

Open
TomHennen wants to merge 1 commit into
mainfrom
fix/777-narrow-publish-trigger
Open

claude: narrow the image-publish trigger to real Dockerfile inputs (#777)#790
TomHennen wants to merge 1 commit into
mainfrom
fix/777-narrow-publish-trigger

Conversation

@TomHennen

@TomHennen TomHennen commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Fixes #777.

The bug

publish tool images triggered on all of tools/**, but most files there feed no image. A dev script landing (open_catalog_bump_pr.sh, bump_version_refs.sh, …) rebuilt all 5 images; the builds are not bit-reproducible, so unchanged content still minted new digests → catalog stale → bump PR → pin convergence → merge commit. It fired four needless times cutting v0.4.0.

Derivation (from the Dockerfiles, not intuition)

Build context is the repo root. Context reads per image, after this PR:

image reads from the context
osv tools/go.mod, tools/go.sum, tools/osv/adapter.sh, tools/osv/render_md.sh, lib/sanitize.sh, lib/sarif_adapter_exit.sh
wrangle-lint tools/go.mod, tools/go.sum, tools/wrangle-lint/ (whole dir), lib/sarif_adapter_exit.sh
attest-toolbox tools/go.mod, tools/go.sum, tools/wrangle-attest/ (whole dir)
zizmor tools/zizmor/requirements.txt, tools/zizmor/adapter.sh, lib/sarif_adapter_exit.sh
syft tools/syft/install.sh, tools/syft/adapter.sh, lib/download_verify.sh

Plus each image's own Dockerfile (which carries the base-image digest pins).

The three Go images previously did COPY tools/ ./tools/ — the whole directory, dev scripts and all — which is why the trigger had to be tools/**. They now copy only go.mod/go.sum plus the first-party package dir they compile (whole dir, so a future //go:embed target is covered). External tools (osv-scanner, ampel, bnd, cosign) build from the module graph alone — verified by a real build of all three images. A cross-package import added later is now a hard build failure, not a silently stale image.

The trigger

paths:
  - tools/*/Dockerfile
  - tools/*/adapter.sh
  - tools/*/install.sh
  - tools/*/render_md.sh
  - tools/*/requirements.txt
  - tools/go.mod
  - tools/go.sum
  - tools/wrangle-attest/**
  - tools/wrangle-lint/**
  - tools/*.go             # any Go source a first-party binary could compile in
  - "tools/**/*.go"
  - lib/**
  - "!tools/catalog.json"  # kept: a catalog-only push is a digest bump, not a source change

The enforcement (the part that matters)

test/check_publish_trigger.py reads every job that calls build_and_publish_container.yml, parses each image's Dockerfile, expands every context COPY/ADD source to files, and fails if any is not matched by on.push.paths — GitHub's glob semantics (* stops at /, last match wins, ! negates). A Dockerfile cannot grow an input without the trigger growing with it. What it cannot model, it refuses (exit 2): a bind-mounted build context, ONBUILD, wildcard sources, a dockerfile: that isn't the matrix path.

The release gate had to move too. check_catalog_provenance_freshness.sh diffed a coarse tools+lib set to decide an image is stale. Narrowing only the trigger would have made that worse: a dev-script push would no longer rebuild, yet would still red the gate — with nothing able to clear it. Its PROVENANCE_DIFF_PATHS is now the same set, and --check-gate holds the two in agreement in both directions (gate ⊇ Dockerfile inputs, so it cannot call a moved image fresh; gate ⊆ trigger, so it cannot red the release with a staleness no push can rebuild away). The two globs are different dialects — git's :(glob) ** matches zero directories, GitHub's does not — and the checker models each separately.

test/test_publish_trigger_coverage.bats (12 tests) pins all of it, including every red path: each was verified to actually fail by injecting the corresponding disagreement.

Adversarial review

An independent reviewer with one brief — find a change to a real build input this trigger would MISS — found no live under-trigger, but four ways the enforcement failed open: a bind mount masked by a stage mount on the same line, a second publishing job, the git-vs-GitHub ** dialect gap, and ONBUILD. All four are fixed, each with a test (see the comment below). Those fixes postdate the review and rest on my own tests.

Candor

  • This PR self-triggers. It edits three Dockerfiles (real image inputs), so merging fires one rebuild + catalog-bump cycle. That one is correct, and the last of its kind for a change like this.
  • Known gap, pre-existing: image content also depends on build_and_publish_container.yml (buildx flags, labels). Neither the old nor the new trigger watches it, and the coverage test cannot derive it from a Dockerfile. Say the word and I'll add it to paths: — the cost is a rebuild cycle on any edit to that workflow.
  • tools/osv-scanner.toml is not an image input (no Dockerfile copies it; it is read from the scanned source tree at run time).
  • build_shell.yml's tool-image cache key still uses the coarse hashFiles('tools/**', 'lib/**', ...). Left alone deliberately: a coarse cache key only costs a cache miss, never staleness.
  • Verified: all three narrowed images build; the 4 test/image/ contract suites pass (14/14); the osv image was run against a fixture and writes output.sarif under the adapter contract.

./test.sh quick: 1438 ok, 0 not ok (11 skips, all pre-existing integration-tool skips). All 12 new tests run no-skip in the CI test job.

🤖 Generated with Claude Code

@TomHennen
TomHennen temporarily deployed to integration-test July 13, 2026 00:06 — with GitHub Actions Inactive
The publish trigger matched all of tools/, so a dev script or an
action-pattern action.yml — in no image's build context — rebuilt all five
images, minted new digests (the builds are not bit-reproducible), and forced
a catalog bump + pin convergence. It also red check_catalog_provenance_freshness,
a release gate, which diffed the same coarse tools/+lib/ set.

Narrow both to the paths the Dockerfiles actually read, and make the go build
stages COPY only what they compile so that set is derivable rather than
asserted — a cross-package import added later is then a hard build failure,
not a silently stale image.

test/check_publish_trigger.py derives each image's inputs from the publish
matrix's Dockerfiles and fails if the trigger or the gate's diff-set misses
one. The two must also agree with each other, so no source change can red the
release gate without a rebuild that clears it.

Fixes #777

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@TomHennen

Copy link
Copy Markdown
Owner Author

claude: Adversarial review round — an independent reviewer (fresh context) was given one brief: find a change to a tool image's real build inputs that this trigger would MISS.

It found no live under-trigger (every file the 5 Dockerfiles read is matched), but four ways the enforcement failed open. All four are now fixed, each with a test that fails without the fix:

  1. Bind mount masked by a stage mount (high) — the guard was per-line, so RUN --mount=type=bind,from=build,... --mount=type=bind,source=lib,... suppressed it and the context read went unseen. Now parsed per-mount; any bind mount without from= fails the check.
  2. A second publishing job bypassed the check — only jobs.publish's matrix was read. Now every job that calls build_and_publish_container.yml is checked, and a workflow where none does fails closed.
  3. git :(glob) ** vs GitHub ** — git's matches zero directories, GitHub's does not, so a tools/foo.go at the root of tools/ would sit in the gate's diff-set but outside the trigger — a red release gate no push could clear. The checker now models each glob dialect separately, and tools/*.go was added to the trigger.
  4. ONBUILD COPY was invisible to the parser — now fails closed.

Suite: 1438 ok, 0 not ok (11 pre-existing integration skips). The trigger↔Dockerfile↔release-gate agreement tests are up to 12.

@TomHennen
TomHennen force-pushed the fix/777-narrow-publish-trigger branch from ac58931 to c33b536 Compare July 13, 2026 00:20
@TomHennen
TomHennen marked this pull request as ready for review July 13, 2026 00:20
@TomHennen
TomHennen temporarily deployed to integration-test July 13, 2026 00:20 — with GitHub Actions Inactive
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.

claude: the image-publish trigger is too broad — dev scripts under tools/ force a needless rebuild + catalog cycle

1 participant