Skip to content

fix: scan the release image before its version tag is bound [#5] - #20

Merged
manjudr merged 2 commits into
developmentfrom
fix/5-release-image-scan
Sep 7, 2026
Merged

fix: scan the release image before its version tag is bound [#5]#20
manjudr merged 2 commits into
developmentfrom
fix/5-release-image-scan

Conversation

@manjudr

@manjudr manjudr commented Sep 7, 2026

Copy link
Copy Markdown
Member

Follow-up to #14. Found while reviewing discovery-service#15, which does this correctly and this repo did not.

The gap

ci-release.yml publishes an image that nothing has ever scanned.

image-build pushes by digest, image-publish binds the version tag, and there was no scan between them. The PR-time Security Scan does not cover it: it grades an image built from the PR's tree on the day the PR ran. A tag cut weeks later is a different build — freshly pulled cgr.dev/chainguard/wolfi-base (a rolling tag) and a freshly resolved module graph. The artifact that ships was not the artifact anything looked at.

How it got here, since it's worth recording: trivy-release-gate was removed early in #14 as dead code, which was correct at the time — its only caller was the deleted build-and-push.yml. ci-release.yml was added later in the same branch and put a release path back without putting the gate back with it.

The fix

make trivy-release-gate ARCH=<arch>, between image-build and upload-artifact:

  • Scans the pushed digest, not a local tag. push-by-digest leaves no local reference, so digest-<arch>.txt is the only handle on the layers just built.
  • Nothing tag-visible is at risk. That digest is unreachable by name until image-publish binds a tag, and the gate runs before it — a finding means the version tag is never created. publish-artifact needs both legs, so either arch failing is enough.
  • Per arch on its own native runner, so the arm64 artifact is scanned as arm64 rather than assumed to match amd64.
  • --exit-code 1 and a table, not SARIF at --exit-code 0. There is no PR to comment on, so the findings belong in the log the red check points at, and the scan is the gate rather than a report something else grades.

Same SEVERITY (CRITICAL,HIGH,MEDIUM,LOW) and the same trivy-cache composite action as the PR scan, so the release band can't drift from the PR band.

Also: timeout-minutes on all four jobs

Job Typical Limit
End to End verification ~1 min 15
Security Scan ~3 min 30
Build Artifact (<arch>) ~2 min 45
Publish Artifact ~20 s 10

The default is 360 minutes. This matters more now that a release leg pulls from a registry: a stalled pull would hold a runner for six hours, and because publish-artifact sits behind needs, the tag would simply never appear with no failure to point at. Generous multiples of the real durations — this is a hang detector, not a performance gate that goes red when the suite legitimately grows.

Checks

No change to the check set. Still CI / End to End verification + CI / Security Scan on a PR, CI / Build Artifact (amd64), CI / Build Artifact (arm64) and CI / Publish Artifact on a tag. Branch protection needs no further changes.

Verification

actionlint clean. trivy-release-gate exercised across all five paths with a stubbed trivy:

Case Result
digest-<arch>.txt missing exit 2, ::error::, trivy never invoked
digest-<arch>.txt empty exit 2, same guard
valid digest scans ghcr.io/openagrinet/network-adapter@sha256:… — correct ref, lowercased
trivy exits 1 (findings) exit 2, gate fails
GITHUB_REPOSITORY unset exit 2 from require-image-repo

End-to-end tag run linked in a comment below.

Also in this PR: coverage reported "Passed" when it measured nothing

Raised on review of this PR's own coverage comment: "what is the percentage? how do we say as passed?" — a fair question, because there is no percentage and there never was.

cover-diff has two paths that compute nothing:

  • no changed non-test .go files — no denominator
  • changed files carry no coverable statements — that's 0/0

Both printed 📊 **Test Coverage: ✅ Passed** — not applicable. From the PR that reads as a green pass, so a CI-only PR was indistinguishable from a well-tested one, and there was no number to reconcile the claim against. Now:

📊 Test Coverage: ➖ Not applicable — no non-test Go files changed vs origin/development, so there are no lines to measure and no percentage to report.

Three verdicts instead of two: ✅ Passed and ❌ Failed both now mean a percentage was computed and compared against MIN_COVERAGE. Exit status is unchanged (still 0) — a PR that changes no Go code must not be blocked on Go coverage — so the check verdict is the same; only the claim the comment makes about it changed.

Also corrects "no changed Go files" to "no non-test Go files changed": the changed-file set already filters _test.go, so a test-only PR took this path while the message said no Go files had changed at all.

Tag verification

Tag v0.0.0-citest3 cut on the head commit, run 34154000953 — all three jobs green. The gate is doing real work on both arches, not passing vacuously:

Build Artifact (amd64): bin/trivy image ghcr.io/openagrinet/network-adapter@sha256:92cb9554…
  INFO  Detected OS  family="wolfi" version="20230201"
  INFO  [wolfi] Detecting vulnerabilities...  pkg_num=15
  INFO  [gobinary] Detecting vulnerabilities...
Build Artifact (arm64): same, on ubuntu-24.04-arm

0 findings at CRITICAL,HIGH,MEDIUM,LOW, so both legs passed and Publish Artifact bound the tag to a proper multi-arch index:

MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:081a5d62f0fd4a9d9fed5e0b3b490b1fa0b76e65d5c9d6a96b769fdfdd2d5b23
  linux/amd64, linux/arm64
v0.0.0-citest3 is not a plain release — not moving :latest

The trivy cache missed on this run, as expected — the Makefile edit changed hashFiles('Makefile'), which is what that key is for. The log confirms it re-fetched from the pinned .../trivy/v0.74.0/contrib/install.sh.

Git tag deleted. The GHCR package version v0.0.0-citest3 needs manual deletion — my token lacks the scope.

image-build pushes by digest and image-publish binds the tag; nothing in
between looked at what was being published. The PR-time Security Scan does
not cover it — it grades an image built from the PR's tree on the day the PR
ran, while a tag cut weeks later is a fresh build off a rolling wolfi-base
and a freshly resolved module graph. trivy-release-gate was removed earlier
in this branch as dead code, correctly at the time: its only caller was
build-and-push.yml. Adding ci-release.yml put a release path back without
putting the gate back with it.

Reinstated against the pushed digest rather than a local tag, since
push-by-digest leaves no local reference. The digest is unreachable by name
until image-publish binds a tag, and the gate runs before that, so a finding
means the version tag is never created. --exit-code 1 with a table: there is
no PR to comment on, so the findings belong in the log the red check points
at. Scanned per arch on its own native runner.

Also adds timeout-minutes to all four jobs. The default is 360, and the new
gate pulls from a registry — a stalled pull would otherwise hold a runner for
six hours, and on a release leg it blocks Publish Artifact behind `needs`, so
the tag simply never appears with no failure to point at.
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

📊 Test Coverage: ➖ Not applicable — no non-test Go files changed vs origin/development, so there are no lines to measure and no percentage to report.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

🛡️ Trivy security scan (CRITICAL,HIGH,MEDIUM,LOW)

View full run

Go dependencies

No findings at CRITICAL,HIGH,MEDIUM,LOW.

Container image

No findings at CRITICAL,HIGH,MEDIUM,LOW.

]

cover-diff has two paths that compute no percentage — no changed non-test
.go files, so there is no denominator, and changed files carrying no
coverable statements, which would be 0/0 — and both reported
"Test Coverage: ✅ Passed — not applicable". Read from the PR that is a green
pass, so a CI-only or docs-only PR looked indistinguishable from a
well-tested one, and there was no number to reconcile the claim against.

Both now report "➖ Not applicable" and say which case applies and why there
is no percentage. Still exit 0 — a PR that changes no Go code must not be
blocked on Go coverage — so the check verdict is unchanged; only the claim
the comment makes about it is.

Also corrects "no changed Go files" to "no non-test Go files changed": the
changed-file set already filters _test.go, so a test-only PR took this path
while the message said no Go files had changed at all.
@manjudr
manjudr merged commit 5ab0242 into development Sep 7, 2026
2 checks passed
@manjudr
manjudr deleted the fix/5-release-image-scan branch September 7, 2026 19:13
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