Repository navigation
312 lines (290 loc) 路 13.2 KB
/
Copy pathcontainer.yml
File metadata and controls
312 lines (290 loc) 路 13.2 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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
name: Container
# Builds both container images (the default, with the Azure CLI, for people signing in
# as themselves; and "slim", the tool alone), then smoke tests and scans them on every
# change. It publishes them to GitHub Container
# Registry in two cases:
#
# - a release: release.yml calls this workflow with the new tag;
# - the weekly patch run: it rebuilds the latest release's source on the same pinned
# base, taking the newest Debian security updates, and republishes only when a
# package actually changed.
#
# Floating tags (0.2, latest, slim) then move to the patched build, and each build also
# keeps an immutable stamped tag. Newer Python packages, a newer Azure CLI and new base
# image digests come through Dependabot and reach the images with the next release.
on:
pull_request:
branches: [main]
push:
branches: [main]
schedule:
- cron: "17 5 * * 1" # Mondays 05:17 UTC
workflow_dispatch:
inputs:
publish:
description: Rebuild and republish the latest release now, whether or not anything changed
type: boolean
default: false
workflow_call:
inputs:
ref:
description: The release tag to build and publish
type: string
required: true
permissions:
contents: read
concurrency:
group: container-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
TRIVY_VERSION: 0.74.0
# From the release's checksums file. Bump both together.
TRIVY_SHA256: 2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
# The public mirror first; ghcr.io rate limits anonymous database downloads.
TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db,ghcr.io/aquasecurity/trivy-db
PLATFORMS: linux/amd64,linux/arm64
jobs:
image:
name: Image (${{ matrix.variant }})
runs-on: ubuntu-24.04
timeout-minutes: 60
permissions:
contents: read
# Publishing, and the build provenance and SBOM attestations that go with it.
packages: write
id-token: write
attestations: write
# The vulnerability scan's results, for the Security tab.
security-events: write
strategy:
fail-fast: false
matrix:
include:
# The default image. The Azure CLI pins its own dependencies, so their high
# findings are reported (see the Security tab) and a fixed critical one fails.
# See container/azure-cli/pyproject.toml for forcing a fix.
- variant: az
gate: CRITICAL
# The tool alone is ours to keep clean: any fixed high or critical finding fails.
- variant: tool
gate: HIGH,CRITICAL
steps:
- name: Decide what to build
id: plan
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.ref }}
FORCE: ${{ inputs.publish }}
EVENT: ${{ github.event_name }}
run: |
set -euo pipefail
publish=false; latest=false; force=false; ref="${GITHUB_SHA}"
if [ -n "${RELEASE_TAG}" ]; then
# Called by the release workflow.
ref="${RELEASE_TAG}"; publish=true; latest=true; force=true
elif [ "${EVENT}" = schedule ] || [ "${FORCE}" = true ]; then
ref="$(gh release view --repo "${GITHUB_REPOSITORY}" --json tagName --jq .tagName 2>/dev/null || true)"
if [ -z "${ref}" ]; then
echo "::notice::There is no release yet, so there is nothing to rebuild."
echo "skip=true" >> "${GITHUB_OUTPUT}"
exit 0
fi
publish=true; latest=true; force="${FORCE:-false}"
fi
{
echo "skip=false"
echo "ref=${ref}"
echo "publish=${publish}"
echo "latest=${latest}"
echo "force=${force}"
echo "image=ghcr.io/${GITHUB_REPOSITORY,,}"
echo "stamp=$(date -u +%Y%m%d).${GITHUB_RUN_NUMBER}"
echo "created=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} >> "${GITHUB_OUTPUT}"
- name: Checkout
if: steps.plan.outputs.skip != 'true'
uses: actions/checkout@v7
with:
ref: ${{ steps.plan.outputs.ref }}
- name: Read the version
if: steps.plan.outputs.skip != 'true'
id: version
run: |
version="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
echo "version=${version}" >> "${GITHUB_OUTPUT}"
echo "revision=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"
- name: Build
if: steps.plan.outputs.skip != 'true'
env:
VARIANT: ${{ matrix.variant }}
VERSION: ${{ steps.version.outputs.version }}
REVISION: ${{ steps.version.outputs.revision }}
CREATED: ${{ steps.plan.outputs.created }}
run: |
podman build --target "${VARIANT}" --tag "localhost/image:${VARIANT}" \
--label "org.opencontainers.image.version=${VERSION}" \
--label "org.opencontainers.image.revision=${REVISION}" \
--label "org.opencontainers.image.created=${CREATED}" \
.
- name: Smoke test
if: steps.plan.outputs.skip != 'true'
env:
VARIANT: ${{ matrix.variant }}
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
image="localhost/image:${VARIANT}"
podman run --rm "${image}" --version | grep -F " ${VERSION}"
podman run --rm "${image}" devices check --help > /dev/null
test "$(podman run --rm --entrypoint id "${image}" -u)" != 0
if [ "${VARIANT}" = az ]; then
podman run --rm --entrypoint az "${image}" version --output none
# Signed out, the tool must reach az and report it, not crash.
if output="$(podman run --rm "${image}" az whoami 2>&1)"; then
echo "::error::az whoami succeeded without a sign-in"; exit 1
fi
grep -F "not signed in" <<< "${output}"
fi
- name: Install Trivy
if: steps.plan.outputs.skip != 'true'
run: |
set -euo pipefail
archive="trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz"
curl -fsSL -o "${RUNNER_TEMP}/${archive}" \
"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/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/${archive}"
echo "${TRIVY_SHA256} ${RUNNER_TEMP}/${archive}" | sha256sum -c -
tar -xzf "${RUNNER_TEMP}/${archive}" -C "${RUNNER_TEMP}" trivy
- name: Scan
if: steps.plan.outputs.skip != 'true'
env:
VARIANT: ${{ matrix.variant }}
run: |
set -euo pipefail
podman save --quiet --output "${RUNNER_TEMP}/image.tar" "localhost/image:${VARIANT}"
scan=("${RUNNER_TEMP}/trivy" image --input "${RUNNER_TEMP}/image.tar"
--cache-dir "${RUNNER_TEMP}/trivy-cache" --scanners vuln --quiet)
# The Security tab gets the findings that have a fix: the ones there is something
# to do about. Debian's unfixed ones (most of a slim base image's count) would sit
# there for months; the weekly rebuild takes each fix when Debian ships it. The
# full list, unfixed included, is kept with the run.
"${scan[@]}" --format sarif --ignore-unfixed --output "${RUNNER_TEMP}/trivy.sarif"
"${scan[@]}" --format table --output "${RUNNER_TEMP}/trivy-all.txt"
"${scan[@]}" --format cyclonedx --output "${RUNNER_TEMP}/sbom.cdx.json"
"${scan[@]}" --format table --severity HIGH,CRITICAL --ignore-unfixed
- name: Report the scan
if: steps.plan.outputs.skip != 'true' && !cancelled() && github.event_name != 'pull_request'
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: ${{ runner.temp }}/trivy.sarif
category: container-${{ matrix.variant }}
- name: Keep the full scan
if: steps.plan.outputs.skip != 'true' && !cancelled()
uses: actions/upload-artifact@v7
with:
name: trivy-${{ matrix.variant }}
path: ${{ runner.temp }}/trivy-all.txt
if-no-files-found: ignore
retention-days: 30
- name: Gate on fixed vulnerabilities
if: steps.plan.outputs.skip != 'true'
env:
GATE: ${{ matrix.gate }}
run: |
"${RUNNER_TEMP}/trivy" image --input "${RUNNER_TEMP}/image.tar" \
--cache-dir "${RUNNER_TEMP}/trivy-cache" --scanners vuln --quiet \
--severity "${GATE}" --ignore-unfixed --exit-code 1 --format table
# A weekly rebuild is only worth publishing when a package moved. The Python
# environments are locked, so the Debian packages are what can change.
- name: Compare with the published image
if: steps.plan.outputs.publish == 'true'
id: changed
env:
VARIANT: ${{ matrix.variant }}
IMAGE: ${{ steps.plan.outputs.image }}
VERSION: ${{ steps.version.outputs.version }}
FORCE: ${{ steps.plan.outputs.force }}
run: |
set -euo pipefail
if [ "${FORCE}" = true ]; then echo "changed=true" >> "${GITHUB_OUTPUT}"; exit 0; fi
tag="$(python3 scripts/image_tags.py --version "${VERSION}" --variant "${VARIANT}" | head -n 1)"
list() { podman run --rm --entrypoint dpkg-query "$1" --show --showformat='${Package}=${Version}\n' | sort; }
if ! podman pull --quiet "${IMAGE}:${tag}" > /dev/null 2>&1; then
echo "changed=true" >> "${GITHUB_OUTPUT}"; exit 0
fi
if diff <(list "${IMAGE}:${tag}") <(list "localhost/image:${VARIANT}"); then
echo "::notice::${IMAGE}:${tag} is already up to date; nothing to publish."
echo "changed=false" >> "${GITHUB_OUTPUT}"
else
echo "changed=true" >> "${GITHUB_OUTPUT}"
fi
# Every platform is built on every change, not only for a release, so a build that
# only breaks under emulation is found before a release depends on it.
- name: Set up emulation for other architectures
if: >-
steps.plan.outputs.skip != 'true' &&
(steps.plan.outputs.publish != 'true' || steps.changed.outputs.changed == 'true')
run: |
sudo apt-get update --quiet
sudo apt-get install --yes --quiet qemu-user-static
- name: Build for every platform
if: >-
steps.plan.outputs.skip != 'true' &&
(steps.plan.outputs.publish != 'true' || steps.changed.outputs.changed == 'true')
env:
VARIANT: ${{ matrix.variant }}
VERSION: ${{ steps.version.outputs.version }}
REVISION: ${{ steps.version.outputs.revision }}
CREATED: ${{ steps.plan.outputs.created }}
run: |
# The native platform comes from the layer cache: the image tested and scanned above.
podman build --target "${VARIANT}" --platform "${PLATFORMS}" \
--manifest "localhost/release:${VARIANT}" \
--label "org.opencontainers.image.version=${VERSION}" \
--label "org.opencontainers.image.revision=${REVISION}" \
--label "org.opencontainers.image.created=${CREATED}" \
.
- name: Publish
if: steps.changed.outputs.changed == 'true'
id: publish
env:
VARIANT: ${{ matrix.variant }}
IMAGE: ${{ steps.plan.outputs.image }}
VERSION: ${{ steps.version.outputs.version }}
STAMP: ${{ steps.plan.outputs.stamp }}
LATEST: ${{ steps.plan.outputs.latest }}
REGISTRY_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
podman login ghcr.io --username "${GITHUB_ACTOR}" --password-stdin <<< "${REGISTRY_TOKEN}"
# The attestation actions push with Docker's credentials, not podman's.
docker login ghcr.io --username "${GITHUB_ACTOR}" --password-stdin <<< "${REGISTRY_TOKEN}"
flags=(--version "${VERSION}" --variant "${VARIANT}" --stamp "${STAMP}")
if [ "${LATEST}" = true ]; then flags+=(--latest); fi
mapfile -t tags < <(python3 scripts/image_tags.py "${flags[@]}")
for tag in "${tags[@]}"; do
podman manifest push --all --digestfile "${RUNNER_TEMP}/digest" \
"localhost/release:${VARIANT}" "docker://${IMAGE}:${tag}"
echo "Pushed ${IMAGE}:${tag}"
done
echo "digest=$(cat "${RUNNER_TEMP}/digest")" >> "${GITHUB_OUTPUT}"
{
echo "## ${IMAGE} (${VARIANT})"
echo
echo "Digest \`$(cat "${RUNNER_TEMP}/digest")\`, tags:"
printf -- "- \`%s\`\n" "${tags[@]}"
} >> "${GITHUB_STEP_SUMMARY}"
- name: Attest build provenance
if: steps.changed.outputs.changed == 'true'
uses: actions/attest-build-provenance@v4
with:
subject-name: ${{ steps.plan.outputs.image }}
subject-digest: ${{ steps.publish.outputs.digest }}
push-to-registry: true
- name: Attest the SBOM
if: steps.changed.outputs.changed == 'true'
uses: actions/attest-sbom@v4
with:
subject-name: ${{ steps.plan.outputs.image }}
subject-digest: ${{ steps.publish.outputs.digest }}
sbom-path: ${{ runner.temp }}/sbom.cdx.json
push-to-registry: true