-
Notifications
You must be signed in to change notification settings - Fork 265
225 lines (204 loc) · 9.45 KB
/
Copy pathpublish.yml
File metadata and controls
225 lines (204 loc) · 9.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# Build and publish containers merged to master, on GitHub-hosted runners.
# A merge normally touches one container (enforced at PR time), but a direct push
# may touch several, so `detect` produces a matrix and `publish` fans out over it.
name: publish
on:
push:
branches: [master]
workflow_dispatch:
inputs:
container:
description: "Container name (top-level directory, e.g. aragorn)"
required: true
version:
description: "Version directory (e.g. 1.2.41)"
required: true
permissions:
contents: read
concurrency:
group: publish-${{ github.sha }}-${{ github.event.inputs.container }}
jobs:
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.list.outputs.matrix }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine containers to publish
id: list
env:
IN_CONTAINER: ${{ github.event.inputs.container }}
IN_VERSION: ${{ github.event.inputs.version }}
EVENT_BEFORE: ${{ github.event.before }}
EVENT_SHA: ${{ github.sha }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ ! -f "$IN_CONTAINER/$IN_VERSION/Dockerfile" ]; then
echo "No Dockerfile at $IN_CONTAINER/$IN_VERSION/Dockerfile"; exit 1
fi
MATRIX=$(jq -nc --arg c "$IN_CONTAINER" --arg v "$IN_VERSION" \
'[{path:($c+"/"+$v), container:$c, version:$v}]')
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
echo "Manual publish: $IN_CONTAINER/$IN_VERSION"
else
# Use the push range so merge commits and multi-commit pushes are covered
# (git diff-tree on a merge commit alone yields no files).
if [ -z "$EVENT_BEFORE" ] || [ "$EVENT_BEFORE" = "0000000000000000000000000000000000000000" ]; then
git diff --name-only "${EVENT_SHA}^" "${EVENT_SHA}" > changed_files.txt 2>/dev/null || \
git show --pretty= --name-only "${EVENT_SHA}" > changed_files.txt
else
git diff --name-only "$EVENT_BEFORE" "$EVENT_SHA" > changed_files.txt
fi
echo "Changed files:"; cat changed_files.txt
python3 .github/scripts/validate.py list \
--changed-files changed_files.txt --out matrix.json
fi
publish:
needs: detect
if: ${{ needs.detect.outputs.matrix != '' && needs.detect.outputs.matrix != '[]' }}
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # upload the Trivy SARIF
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.detect.outputs.matrix) }}
env:
C: ${{ matrix.container }}
V: ${{ matrix.version }}
# Singularity is uploaded to the BioContainers S3 only when these are set as
# secrets; job-level so the step `if` guard can see the endpoint.
S3_ENDPOINT: ${{ secrets.BIOCONTAINERS_S3_ENDPOINT }}
S3_BUCKET: ${{ secrets.BIOCONTAINERS_S3_BUCKET }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
# Build each target platform locally so Trivy can gate the EXACT images that get
# published. buildx caches these builds, so the later `--push` reuses the same
# layers — the pushed image is byte-for-byte what was scanned. arm64 is
# best-effort: many legacy base images are amd64-only, so its build fails fast.
- name: Build amd64 image
run: docker buildx build --load --platform linux/amd64 -t bcimg:amd64 "$C/$V"
- name: Build arm64 image (best-effort)
id: arm64
continue-on-error: true
run: docker buildx build --load --platform linux/arm64 -t bcimg:arm64 "$C/$V"
- name: Extract labels
run: docker inspect --format '{{json .Config.Labels}}' bcimg:amd64 > labels.json
- name: Check labels and compute tag
id: check
run: |
python3 .github/scripts/validate.py check \
--container "$C" --version "$V" \
--labels labels.json --dockerfile "$C/$V/Dockerfile" --out report.json
- name: Run tests (test-cmds.txt)
run: |
TESTS="$C/$V/test-cmds.txt"
if [ ! -f "$TESTS" ]; then echo "No test-cmds.txt, skipping."; exit 0; fi
status=0
while IFS= read -r cmd || [ -n "$cmd" ]; do
cmd="${cmd%$'\r'}" # tolerate CRLF line endings
case "$cmd" in ''|\#*) continue ;; esac # skip blank and comment lines
echo "::group::TEST $cmd"
if docker run --rm bcimg:amd64 $cmd; then echo "ok"; else echo "FAILED"; status=1; fi
echo "::endgroup::"
done < "$TESTS"
exit $status
# --- Security gate: scan every platform that WILL be published, before push. ---
# A fixable HIGH/CRITICAL CVE fails the job so nothing is pushed. Unfixable OS
# CVEs are ignored; a per-container `.trivyignore` (CVE IDs) can accept specific ones.
- name: Prepare Trivy ignore file
run: |
if [ -f "$C/$V/.trivyignore" ]; then
cp "$C/$V/.trivyignore" .trivyignore
echo "Using per-container .trivyignore:"; cat .trivyignore
fi
- name: Trivy gate (amd64)
uses: aquasecurity/trivy-action@0.35.0
with:
image-ref: bcimg:amd64
format: sarif
output: trivy.sarif
severity: HIGH,CRITICAL
ignore-unfixed: true
exit-code: '1'
- name: Upload Trivy results to Security tab
if: always()
continue-on-error: true
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy.sarif
category: trivy-${{ env.C }}
- name: Trivy gate (arm64)
if: ${{ steps.arm64.outcome == 'success' }}
uses: aquasecurity/trivy-action@0.35.0
with:
image-ref: bcimg:arm64
format: table
severity: HIGH,CRITICAL
ignore-unfixed: true
exit-code: '1'
- name: Log in to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push scanned image(s)
env:
TAG: ${{ steps.check.outputs.tag }} # validated [A-Za-z0-9._-]; used only as "$TAG"
ARM64_OK: ${{ steps.arm64.outcome }}
run: |
IMG="biocontainers/${C}:${TAG}"
# --push reuses the buildx cache from the --load builds above, so the pushed
# images are exactly the ones Trivy just gated.
if [ "$ARM64_OK" = "success" ]; then
docker buildx build --platform linux/amd64,linux/arm64 --push -t "$IMG" "$C/$V"
echo "Published multi-arch (amd64+arm64, both scanned): $IMG"
echo "- \`$IMG\` — **multi-arch** (amd64 + arm64, both scanned)" >> "$GITHUB_STEP_SUMMARY"
else
docker buildx build --platform linux/amd64 --push -t "$IMG" "$C/$V"
echo "Published amd64-only (scanned): $IMG"
echo "- \`$IMG\` — **amd64-only** (base has no arm64)" >> "$GITHUB_STEP_SUMMARY"
fi
# --- Singularity: build a .sif and upload to the BioContainers S3, served at
# https://containers.biocontainers.pro/s3/SingImgsRepo/<tool>/<tag>/<tool>_<tag>.sif
# Runs only when the S3 secrets are configured (otherwise skipped, no failure). ---
# These run only when the S3 endpoint is configured, and are best-effort
# (continue-on-error): the DockerHub push above is the release of record, so a
# missing bucket/credential or S3 hiccup must never fail an already-published
# container. Failures surface as a warning annotation.
- name: Set up Apptainer (Singularity)
if: ${{ env.S3_ENDPOINT != '' }}
continue-on-error: true
uses: eWaterCycle/setup-apptainer@v2
- name: Convert to Singularity .sif
if: ${{ env.S3_ENDPOINT != '' }}
continue-on-error: true
env:
TAG: ${{ steps.check.outputs.tag }}
run: apptainer build "${C}_${TAG}.sif" "docker://biocontainers/${C}:${TAG}"
- name: Upload Singularity image to BioContainers S3
if: ${{ env.S3_ENDPOINT != '' }}
continue-on-error: true
env:
TAG: ${{ steps.check.outputs.tag }}
AWS_ACCESS_KEY_ID: ${{ secrets.BIOCONTAINERS_S3_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.BIOCONTAINERS_S3_SECRET_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.BIOCONTAINERS_S3_REGION }}
# The S3 store is Ceph/RGW. aws-cli v2.23+ adds default CRC32 checksums to
# multipart uploads, which this RGW rejects (InvalidRequest on
# CreateMultipartUpload). Revert to legacy behaviour: only checksum when the
# API strictly requires it. Without this, every .sif over ~8 MB fails to upload.
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
run: |
if [ ! -f "${C}_${TAG}.sif" ]; then echo "No .sif built, skipping upload."; exit 0; fi
KEY="SingImgsRepo/${C}/${TAG}/${C}_${TAG}.sif"
aws s3 cp "${C}_${TAG}.sif" "s3://${S3_BUCKET}/${KEY}" --endpoint-url "$S3_ENDPOINT"
URL="https://containers.biocontainers.pro/s3/${KEY}"
echo "Singularity image served at: ${URL}"
echo "- Singularity: [\`${KEY}\`](${URL})" >> "$GITHUB_STEP_SUMMARY"