Refactor: Converge the image discovery copies - #92
Merged
ModeSevenIndustrialSolutions merged 2 commits intoSep 18, 2026
Conversation
Image discovery exists three times, and the copies had drifted apart: build-test and merge were byte-identical at 138 lines, while build-test-release carried its own 128-line variant. Every new repository layout, naming rule or ordering fix therefore lands in three places, and lfreleng-actions#34 records that divergence between copies has already shipped bugs. The copies now share one byte-identical body, differing only in the values their env blocks supply. That is the shape an extracted action would take, so lfreleng-actions#29 becomes a lift-and-shift with no behavioural decisions left in it, and the divergence stops accumulating meanwhile. Only two things actually differed. build-test-release errors on an empty discovery result unconditionally, where the other two tolerate it when a build_command will produce the images; and the build id hashed a different field set. The first difference was already no difference: the shared form errors identically once BUILD_COMMAND is empty, so the release lane's variant was the shared one with unreachable branches removed. It now passes an empty value explicitly. The build id hashes the same seven fields everywhere, each lane supplying empty values for inputs it does not offer. It names artifacts within a run rather than keying a cache across runs, so its values may change; what matters is that parallel legs stay distinct, and adding a field cannot reduce that. The eight legs of the build-test matrix remain distinct, checked before and after. Equivalence is measured, not argued. Fifteen layouts covering discovery, ordering, explicit image lists, malformed input, name collisions and the excluded directories were run against each lane's old and new bodies, comparing exit status, images_json, image_count and emitted annotations: 45 comparisons, no differences. The build_command hatch was exercised set as well as empty, since it is the behaviour the release lane does not share. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Matthew Watkins <mwatkins@linuxfoundation.org>
ModeSevenIndustrialSolutions
requested review from
a team
and
a balanced review from Copilot
September 17, 2026 22:28
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
September 17, 2026 22:29
View session
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
September 17, 2026 22:40
View session
ModeSevenIndustrialSolutions
force-pushed
the
refactor/converge-image-discovery
branch
from
September 17, 2026 22:50
5b0ebcf to
4f5c642
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
September 17, 2026 22:50
View session
ModeSevenIndustrialSolutions
force-pushed
the
refactor/converge-image-discovery
branch
from
September 17, 2026 23:01
4f5c642 to
d01a643
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
September 17, 2026 23:02
View session
The build id names the per-run artifacts the build job uploads and the test, SBOM and scan jobs download. It hashed the repository and the image inputs, which left two ways for one name to serve two invocations. The first collides today. The self-test calls build-test against test-docker-monorepo at v0.0.1 and the merge lane against the same repository at v0.1.0, both with default image inputs, and both upload docker-archives-<id> into the artifact namespace a run shares. The last CI run carried two artifacts named docker-archives-d58b78462613 with different sizes, one per lane. Nothing failed, because the duplicate name is accepted; the hazard is quieter than that. download-artifact resolves a name to the newest match, so a job can load another lane's images and test the wrong commit while reporting success. The hash now covers the checkout identity, and each lane binds it to the ref its own checkout step resolves, because that is what decides the tree discovery walked. The three bindings differ: build-test takes inputs.ref; merge prefers the immutable Gerrit patchset revision, matching its checkout; and the release lane takes the validated tag, since its own ref input is limited to the metadata and housekeeping jobs. Hashing inputs.ref there would have left two releases of different tags sharing an id. The second way needs no unusual inputs: a hash of inputs cannot separate two invocations whose inputs match, and nothing stops a caller making that call twice. The id gains a 64-bit nonce, drawn once in the metadata job and reaching the other jobs through its outputs, so it is constant within an invocation and distinct between them. A caller-supplied key would work too, but it fails silently when someone forgets to pass one. The hash stays as a prefix, so identical configurations keep a recognisable stem. Checked: nine distinct ids across every leg the self-test launches, where one pair previously matched, and five repeats of one configuration yield five ids sharing one stem. Consumers see artifact names change, which is safe: the id is scoped to a run and never keys a cache across runs. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Matthew Watkins <mwatkins@linuxfoundation.org>
ModeSevenIndustrialSolutions
force-pushed
the
refactor/converge-image-discovery
branch
from
September 17, 2026 23:12
d01a643 to
d5a1171
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
September 17, 2026 23:12
View session
zxiiro
approved these changes
Sep 18, 2026
zxiiro
left a comment
There was a problem hiding this comment.
🤖 Auto-approved by agent: reviewed workflow/code change for security and CI/CD impact, found low risk. Converges the three image-discovery step copies (build-test / merge / build-test-release): same field set including REF/GERRIT_REFSPEC/PLATFORMS, config hash plus 64-bit urandom nonce for build_id uniqueness, release lane still errors on empty discovery without build_command. No new/widened permissions or secrets; self-test matrix green.
ModeSevenIndustrialSolutions
merged commit Sep 18, 2026
f514775
into
lfreleng-actions:main
83 checks passed
ModeSevenIndustrialSolutions
deleted the
refactor/converge-image-discovery
branch
September 18, 2026 13:42
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.
First step of #29, a prerequisite for #34 — and it turned up a live artifact collision, fixed in the second commit.
Two commits, deliberately separate: the convergence is a proven no-op, the
build_idchange is behavioural and carries its own evidence.1.
Refactor: Converge the image discovery copiesImage discovery exists three times. The copies had drifted:
build-test.yamlandmerge.yamlwere byte-identical at 138 lines, whilebuild-test-release.yamlcarried its own 128-line variant. Every new repository layout, naming rule or ordering fix has to land in three places, and #34 records that divergence between copies has already shipped bugs.The three now share one byte-identical body, differing only in the values their
env:blocks supply — the shape an extracted action takes. #29 becomes a lift-and-shift with no behavioural decisions left in it, and the divergence stops accumulating meanwhile.This is deliberately not the extraction. #39's own caution warns a thin action repository can cost more than the duplication it replaces, so proving the three can share one implementation in place comes first.
What actually differed
Only two things, across ~100 otherwise-identical lines:
Empty-discovery handling — the release lane errors unconditionally; the others tolerate an empty result when a
build_commandwill produce the images. This turned out to be no difference: the shared form errors identically onceBUILD_COMMANDis empty, so the release variant was the shared one with unreachable branches removed. It now passes an empty value explicitly, and the comment is reworded to stay true in a lane without the hatch.build_idcomposition — see below; this is where the bug was.Equivalence is measured
Fifteen layouts — root
Dockerfile,docker/,src/main/docker/, multi-image monorepo, root+subdirs, the excludeddocker/andsrc/directories, no Dockerfiles at all, valid and malformed explicitimagesinput, empty[], post-normalisation name collisions, uppercase directory names, declared ordering,build_args/targetpassthrough — run against each lane's old and new body, comparing exit status,images_json,image_countand emitted annotations.build_commandemptybuild_commandset45 comparisons, zero differences, re-run after every subsequent change. Bodies confirmed byte-identical by raw SHA-256.
2.
Fix: Make the build id unique per invocationbuild_idnames the per-run artifacts the build job uploads and the test, SBOM and scan jobs download. It hashed the repository and image inputs, which left two ways for one name to serve two invocations.(a) Different commits — colliding today
Artifacts are scoped to a run, and
testing.yamlcalls more than one lane per run, so the namespace is shared across lanes. The hash omitted the checkout identity:build_idv0.0.1d58b78462613v0.1.0d58b78462613The artifact listing from an earlier run on this branch:
Two artifacts, one name, different sizes. Nothing failed — the duplicate is accepted, which is worse than a hard error:
download-artifactresolves a name to the newest match, so a job can load the other lane's images and test the wrong commit while reporting success. Latent since the merge dry run was added.The hash now covers the checkout identity, and each lane binds it to the ref its own checkout step resolves, since that is what decides the tree discovery walked:
ref:REFbindingbuild-test.yaml${{ inputs.ref }}merge.yaml${{ inputs.gerrit_revision || inputs.ref }}build-test-release.yaml${{ needs.tag-validate.outputs.tag }}The release lane matters here:
inputs.refis documented as applying only to its metadata jobs, so hashing it would have left two releases of different tags sharing an id.(b) Identical invocations — a hash cannot help
A hash of inputs cannot separate two invocations whose inputs match, and nothing stops a caller making that call twice.
merge-dry-runtargetstest-docker-monorepo@v0.1.0with default inputs, so adding abuild-testleg at that tag would silently recreate the collision.The id gains a 64-bit nonce, drawn once in the metadata job and reaching the other jobs through its outputs — constant within an invocation, distinct between them, nothing asked of the caller. The configuration hash stays as a prefix, so identical configurations keep a recognisable, traceable stem.
I declined the caller-supplied key that review suggested: it fails silently when omitted, and puts a correctness obligation on the public interface of three reusable workflows to solve a problem the workflow can solve itself.
Width is not arbitrary — repeat probability across n invocations is about n²/2 over the space:
Verified in CI
Artifact names change for consumers, which is safe: the id is scoped to a run and never keys a cache across runs. A partial re-run is also safe — re-running only failed jobs does not re-run the metadata job, so the preserved output keeps downloads pointing at the artifacts already uploaded.
Note on the dead branch
The release lane now carries two lines of
build_commandhandling for an input it deliberately does not expose. That is the cost of a single shared body, and it is what the extracted action will contain regardless. Both theenv:block and the inline comment say so.Relationship to #89
No conflict — #89's hunks skip the
docker-metadatajob entirely. These can merge in either order.Validation
actionlint,zizmor --persona auditor(no findings),prek run --all-files,aislop ci --staged— all clean.