Skip to content

Commit 703ba26

Browse files
committed
ci(publish): scan every published platform before push (Trivy W3)
Build amd64 and arm64 separately with --load, run the Trivy gate on each, then push from the buildx cache so the published images are exactly the ones scanned. Closes the gap where only a locally-built amd64 was gated while a rebuilt multi-arch image (incl. an unscanned arm64) was pushed. arm64 stays best-effort (amd64-only bases fall back to an amd64-only, scanned publish).
1 parent ac27946 commit 703ba26

1 file changed

Lines changed: 37 additions & 22 deletions

File tree

.github/workflows/publish.yml

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,20 @@ jobs:
8585
- uses: docker/setup-qemu-action@v3
8686
- uses: docker/setup-buildx-action@v3
8787

88-
- name: Build image (amd64) for validation
89-
run: docker buildx build --load --platform linux/amd64 -t bcimg:local "$C/$V"
88+
# Build each target platform locally so Trivy can gate the EXACT images that get
89+
# published. buildx caches these builds, so the later `--push` reuses the same
90+
# layers — the pushed image is byte-for-byte what was scanned. arm64 is
91+
# best-effort: many legacy base images are amd64-only, so its build fails fast.
92+
- name: Build amd64 image
93+
run: docker buildx build --load --platform linux/amd64 -t bcimg:amd64 "$C/$V"
94+
95+
- name: Build arm64 image (best-effort)
96+
id: arm64
97+
continue-on-error: true
98+
run: docker buildx build --load --platform linux/arm64 -t bcimg:arm64 "$C/$V"
9099

91100
- name: Extract labels
92-
run: docker inspect --format '{{json .Config.Labels}}' bcimg:local > labels.json
101+
run: docker inspect --format '{{json .Config.Labels}}' bcimg:amd64 > labels.json
93102

94103
- name: Check labels and compute tag
95104
id: check
@@ -107,26 +116,25 @@ jobs:
107116
cmd="${cmd%$'\r'}" # tolerate CRLF line endings
108117
case "$cmd" in ''|\#*) continue ;; esac # skip blank and comment lines
109118
echo "::group::TEST $cmd"
110-
if docker run --rm bcimg:local $cmd; then echo "ok"; else echo "FAILED"; status=1; fi
119+
if docker run --rm bcimg:amd64 $cmd; then echo "ok"; else echo "FAILED"; status=1; fi
111120
echo "::endgroup::"
112121
done < "$TESTS"
113122
exit $status
114123
115-
# --- Security gate: scan the locally-built image BEFORE pushing. ---
116-
# A fixable HIGH/CRITICAL CVE fails the job, so nothing is pushed. Unfixable
117-
# OS-level CVEs are ignored. Maintainers can accept specific CVEs by adding a
118-
# `.trivyignore` (CVE IDs, one per line) in the container's version directory.
124+
# --- Security gate: scan every platform that WILL be published, before push. ---
125+
# A fixable HIGH/CRITICAL CVE fails the job so nothing is pushed. Unfixable OS
126+
# CVEs are ignored; a per-container `.trivyignore` (CVE IDs) can accept specific ones.
119127
- name: Prepare Trivy ignore file
120128
run: |
121129
if [ -f "$C/$V/.trivyignore" ]; then
122130
cp "$C/$V/.trivyignore" .trivyignore
123131
echo "Using per-container .trivyignore:"; cat .trivyignore
124132
fi
125133
126-
- name: Trivy security scan (gate on fixable HIGH/CRITICAL)
134+
- name: Trivy gate (amd64)
127135
uses: aquasecurity/trivy-action@0.35.0
128136
with:
129-
image-ref: bcimg:local
137+
image-ref: bcimg:amd64
130138
format: sarif
131139
output: trivy.sarif
132140
severity: HIGH,CRITICAL
@@ -141,30 +149,37 @@ jobs:
141149
sarif_file: trivy.sarif
142150
category: trivy-${{ env.C }}
143151

152+
- name: Trivy gate (arm64)
153+
if: ${{ steps.arm64.outcome == 'success' }}
154+
uses: aquasecurity/trivy-action@0.35.0
155+
with:
156+
image-ref: bcimg:arm64
157+
format: table
158+
severity: HIGH,CRITICAL
159+
ignore-unfixed: true
160+
exit-code: '1'
161+
144162
- name: Log in to DockerHub
145163
uses: docker/login-action@v3
146164
with:
147165
username: ${{ secrets.DOCKERHUB_USERNAME }}
148166
password: ${{ secrets.DOCKERHUB_TOKEN }}
149167

150-
- name: Build and push (multi-arch, amd64 fallback)
168+
- name: Push scanned image(s)
151169
env:
152170
TAG: ${{ steps.check.outputs.tag }} # validated [A-Za-z0-9._-]; used only as "$TAG"
171+
ARM64_OK: ${{ steps.arm64.outcome }}
153172
run: |
154173
IMG="biocontainers/${C}:${TAG}"
155-
# No --pull: reuse the exact amd64 base/layers that the Trivy gate scanned
156-
# above (bcimg:local was built from the same context/cache), so the amd64
157-
# image we publish is the one that passed the gate. The arm64 image is built
158-
# fresh and is not independently scanned; it uses the same Dockerfile/packages.
159-
# Many legacy base images are amd64-only, so the arm64 leg may fail fast at the
160-
# FROM pull — fall back to amd64-only in that case.
161-
if docker buildx build --platform linux/amd64,linux/arm64 --push -t "$IMG" "$C/$V"; then
162-
echo "Published multi-arch (amd64+arm64): $IMG"
163-
echo "- \`$IMG\` — **multi-arch** (amd64 + arm64)" >> "$GITHUB_STEP_SUMMARY"
174+
# --push reuses the buildx cache from the --load builds above, so the pushed
175+
# images are exactly the ones Trivy just gated.
176+
if [ "$ARM64_OK" = "success" ]; then
177+
docker buildx build --platform linux/amd64,linux/arm64 --push -t "$IMG" "$C/$V"
178+
echo "Published multi-arch (amd64+arm64, both scanned): $IMG"
179+
echo "- \`$IMG\` — **multi-arch** (amd64 + arm64, both scanned)" >> "$GITHUB_STEP_SUMMARY"
164180
else
165-
echo "arm64 build failed (base likely amd64-only); publishing amd64-only."
166181
docker buildx build --platform linux/amd64 --push -t "$IMG" "$C/$V"
167-
echo "Published amd64-only: $IMG"
182+
echo "Published amd64-only (scanned): $IMG"
168183
echo "- \`$IMG\` — **amd64-only** (base has no arm64)" >> "$GITHUB_STEP_SUMMARY"
169184
fi
170185

0 commit comments

Comments
 (0)