Skip to content

Commit c695c7c

Browse files
authored
Merge pull request #56 from lissy93/ref/faster-docker-build
ci: makes docker workflow into matrix, faster
2 parents fd08d59 + d4b85cd commit c695c7c

1 file changed

Lines changed: 179 additions & 87 deletions

File tree

.github/workflows/docker.yml

Lines changed: 179 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# Builds the multi-arch image and publishes to GHCR (+ DockerHub if configured).
2+
#
3+
# Triggered by git tag creation, which publishes to the same docker tag.
4+
# Each platform is built on its own runner in parallel and pushed by digest;
5+
# a final merge job stitches the digests into one manifest list per registry,
6+
# then attaches provenance + SBOM attestations. arm64 builds natively on an
7+
# arm runner; the 32-bit glibc variants (arm/v7, 386) are emulated via QEMU.
8+
19
name: 🐳 Docker
210

311
on:
@@ -13,172 +21,256 @@ on:
1321
tags:
1422
- '*'
1523

16-
permissions:
17-
contents: read
18-
packages: write
19-
id-token: write
20-
attestations: write
21-
2224
concurrency:
2325
group: docker-${{ github.workflow }}-${{ github.ref }}
2426
cancel-in-progress: true
2527

28+
permissions:
29+
contents: read
30+
2631
env:
2732
IMAGE_NAME: adguardian
2833
DOCKER_USER: lissy93
2934
GHCR_REGISTRY: ghcr.io
3035
DOCKERHUB_REGISTRY: docker.io
3136

3237
jobs:
33-
docker:
38+
# Resolve the tag + target registries once, shared by every build leg + merge
39+
prepare:
40+
name: 🧭 Prepare
3441
runs-on: ubuntu-latest
35-
42+
timeout-minutes: 5
43+
outputs:
44+
tag: ${{ steps.meta.outputs.tag }}
45+
ghcr_image: ${{ steps.meta.outputs.ghcr_image }}
46+
publish_images: ${{ steps.meta.outputs.publish_images }}
47+
dockerhub_enabled: ${{ steps.meta.outputs.dockerhub_enabled }}
3648
steps:
37-
- name: Checkout
38-
uses: actions/checkout@v6
39-
with:
40-
persist-credentials: false
41-
4249
- name: Compute image metadata
4350
id: meta
4451
shell: bash
4552
env:
53+
EVENT_NAME: ${{ github.event_name }}
4654
MANUAL_TAG: ${{ inputs.tag }}
4755
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
4856
run: |
49-
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
50-
if [[ -n "${MANUAL_TAG}" ]]; then
51-
TAG="${MANUAL_TAG}"
52-
else
53-
TAG="latest"
54-
fi
55-
elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
57+
set -euo pipefail
58+
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
59+
if [[ -n "$MANUAL_TAG" ]]; then TAG="$MANUAL_TAG"; else TAG="latest"; fi
60+
elif [[ "$GITHUB_REF" == refs/tags/* ]]; then
5661
TAG="${GITHUB_REF#refs/tags/}"
5762
else
5863
TAG="latest"
5964
fi
6065
61-
TAG="$(echo "${TAG}" | sed -E 's/[^A-Za-z0-9_.-]+/-/g; s/^[.-]+//' | cut -c1-128)"
62-
if [[ -z "${TAG}" ]]; then
63-
echo "Computed Docker tag is empty" >&2
66+
TAG="$(echo "$TAG" | sed -E 's/[^A-Za-z0-9_.-]+/-/g; s/^[.-]+//' | cut -c1-128)"
67+
if [[ -z "$TAG" ]]; then
68+
echo "::error::Computed Docker tag is empty"
6469
exit 1
6570
fi
6671
6772
GHCR_IMAGE="${GHCR_REGISTRY}/${DOCKER_USER}/${IMAGE_NAME}"
68-
PUBLISH_IMAGES="${GHCR_IMAGE}"
69-
if [[ -n "${DOCKERHUB_PASSWORD}" ]]; then
70-
DOCKERHUB_IMAGE="${DOCKERHUB_REGISTRY}/${DOCKER_USER}/${IMAGE_NAME}"
71-
PUBLISH_IMAGES="${PUBLISH_IMAGES},${DOCKERHUB_IMAGE}"
72-
echo "dockerhub_enabled=true" >> "$GITHUB_OUTPUT"
73-
else
74-
echo "dockerhub_enabled=false" >> "$GITHUB_OUTPUT"
73+
PUBLISH_IMAGES="$GHCR_IMAGE"
74+
DOCKERHUB_ENABLED="false"
75+
if [[ -n "$DOCKERHUB_PASSWORD" ]]; then
76+
PUBLISH_IMAGES="${PUBLISH_IMAGES},${DOCKERHUB_REGISTRY}/${DOCKER_USER}/${IMAGE_NAME}"
77+
DOCKERHUB_ENABLED="true"
7578
fi
7679
77-
echo "TAG=${TAG}" >> "$GITHUB_ENV"
78-
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
79-
echo "GHCR_IMAGE=${GHCR_IMAGE}" >> "$GITHUB_ENV"
80-
echo "ghcr_image=${GHCR_IMAGE}" >> "$GITHUB_OUTPUT"
81-
echo "PUBLISH_IMAGES=${PUBLISH_IMAGES}" >> "$GITHUB_ENV"
82-
echo "publish_images=${PUBLISH_IMAGES}" >> "$GITHUB_OUTPUT"
80+
{
81+
echo "tag=$TAG"
82+
echo "ghcr_image=$GHCR_IMAGE"
83+
echo "publish_images=$PUBLISH_IMAGES"
84+
echo "dockerhub_enabled=$DOCKERHUB_ENABLED"
85+
} >> "$GITHUB_OUTPUT"
8386
84-
- name: Set up QEMU
87+
# Build each platform on its own runner, in parallel, pushing by digest
88+
build:
89+
name: 🏗️ Build ${{ matrix.platform }}
90+
needs: prepare
91+
runs-on: ${{ matrix.runner }}
92+
timeout-minutes: 60
93+
permissions:
94+
contents: read
95+
packages: write
96+
strategy:
97+
fail-fast: false
98+
matrix:
99+
include:
100+
# 64-bit: ultra-light scratch/musl image (Dockerfile)
101+
- platform: linux/amd64
102+
runner: ubuntu-latest
103+
dockerfile: ./Dockerfile
104+
qemu: false
105+
- platform: linux/arm64
106+
runner: ubuntu-24.04-arm # native arm64 — no emulation
107+
dockerfile: ./Dockerfile
108+
qemu: false
109+
# 32-bit: glibc/Debian image (Dockerfile.full); emulated, but parallel
110+
- platform: linux/arm/v7
111+
runner: ubuntu-latest
112+
dockerfile: ./Dockerfile.full
113+
qemu: true
114+
- platform: linux/386
115+
runner: ubuntu-latest
116+
dockerfile: ./Dockerfile.full
117+
qemu: true
118+
steps:
119+
- name: 🛎️ Checkout
120+
uses: actions/checkout@v6
121+
with:
122+
persist-credentials: false
123+
124+
- name: 🏷️ Prepare platform name
125+
env:
126+
PLATFORM: ${{ matrix.platform }}
127+
run: echo "PLATFORM_PAIR=${PLATFORM//\//-}" >> "$GITHUB_ENV"
128+
129+
- name: 🧰 Set up QEMU
130+
if: matrix.qemu
85131
uses: docker/setup-qemu-action@v4
86132

87-
- name: Set up Docker Buildx
133+
- name: 🧱 Set up Docker Buildx
88134
uses: docker/setup-buildx-action@v4
89135

90-
- name: Login to GitHub Container Registry
136+
- name: 🔑 Login to GitHub Container Registry
91137
uses: docker/login-action@v4
92138
with:
93139
registry: ${{ env.GHCR_REGISTRY }}
94140
username: ${{ github.actor }}
95141
password: ${{ secrets.GITHUB_TOKEN }}
96142

97-
- name: Login to DockerHub
98-
if: steps.meta.outputs.dockerhub_enabled == 'true'
143+
- name: 🔑 Login to DockerHub
144+
if: needs.prepare.outputs.dockerhub_enabled == 'true'
99145
uses: docker/login-action@v4
100146
with:
101147
registry: ${{ env.DOCKERHUB_REGISTRY }}
102148
username: ${{ env.DOCKER_USER }}
103149
password: ${{ secrets.DOCKERHUB_PASSWORD }}
104150

105-
# Build the ultra-lightweight from scratch version for 64-bit targets
106-
- name: Build lightweight image (amd64, arm64)
107-
id: light
151+
- name: 🏗️ Build and push by digest
152+
id: build
108153
uses: docker/build-push-action@v7
109154
with:
110155
context: .
111-
file: ./Dockerfile
112-
platforms: linux/amd64,linux/arm64
156+
file: ${{ matrix.dockerfile }}
157+
platforms: ${{ matrix.platform }}
113158
provenance: false
114159
sbom: false
115-
cache-from: type=gha,scope=light
116-
cache-to: type=gha,mode=max,scope=light
117-
outputs: type=image,"name=${{ steps.meta.outputs.publish_images }}",push-by-digest=true,name-canonical=true,push=true
160+
cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }}
161+
cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }}
162+
outputs: type=image,"name=${{ needs.prepare.outputs.publish_images }}",push-by-digest=true,name-canonical=true,push=true
118163

119-
# Build the fat (Debian glibc) image for everything else
120-
- name: Build full image (armv7, 386)
121-
id: full
122-
uses: docker/build-push-action@v7
164+
- name: 📤 Export digest
165+
env:
166+
DIGEST: ${{ steps.build.outputs.digest }}
167+
run: |
168+
set -euo pipefail
169+
mkdir -p /tmp/digests
170+
touch "/tmp/digests/${DIGEST#sha256:}"
171+
172+
- name: ⬆️ Upload digest
173+
uses: actions/upload-artifact@v7
123174
with:
124-
context: .
125-
file: ./Dockerfile.full
126-
platforms: linux/arm/v7,linux/386
127-
provenance: false
128-
sbom: false
129-
cache-from: type=gha,scope=full
130-
cache-to: type=gha,mode=max,scope=full
131-
outputs: type=image,"name=${{ steps.meta.outputs.publish_images }}",push-by-digest=true,name-canonical=true,push=true
175+
name: digests-${{ env.PLATFORM_PAIR }}
176+
path: /tmp/digests/*
177+
if-no-files-found: error
178+
retention-days: 1
132179

133-
# Merge the variants into single manifest
134-
- name: Create combined manifest list
135-
id: manifest
180+
# Combine the per-platform digests into one multi-arch manifest per registry
181+
merge:
182+
name: 🧩 Merge manifest
183+
needs: [prepare, build]
184+
runs-on: ubuntu-latest
185+
timeout-minutes: 15
186+
permissions:
187+
contents: read
188+
packages: write
189+
id-token: write
190+
attestations: write
191+
steps:
192+
- name: 📥 Download digests
193+
uses: actions/download-artifact@v8
194+
with:
195+
path: /tmp/digests
196+
pattern: digests-*
197+
merge-multiple: true
198+
199+
- name: 🧱 Set up Docker Buildx
200+
uses: docker/setup-buildx-action@v4
201+
202+
- name: 🔑 Login to GitHub Container Registry
203+
uses: docker/login-action@v4
204+
with:
205+
registry: ${{ env.GHCR_REGISTRY }}
206+
username: ${{ github.actor }}
207+
password: ${{ secrets.GITHUB_TOKEN }}
208+
209+
- name: 🔑 Login to DockerHub
210+
if: needs.prepare.outputs.dockerhub_enabled == 'true'
211+
uses: docker/login-action@v4
212+
with:
213+
registry: ${{ env.DOCKERHUB_REGISTRY }}
214+
username: ${{ env.DOCKER_USER }}
215+
password: ${{ secrets.DOCKERHUB_PASSWORD }}
216+
217+
- name: 🧩 Create combined manifest list
136218
shell: bash
137219
env:
138-
LIGHT_DIGEST: ${{ steps.light.outputs.digest }}
139-
FULL_DIGEST: ${{ steps.full.outputs.digest }}
220+
TAG: ${{ needs.prepare.outputs.tag }}
221+
PUBLISH_IMAGES: ${{ needs.prepare.outputs.publish_images }}
140222
run: |
141-
IFS=',' read -ra IMAGES <<< "${PUBLISH_IMAGES}"
223+
set -euo pipefail
224+
cd /tmp/digests
225+
shopt -s nullglob
226+
DIGEST_FILES=(*)
227+
if [ ${#DIGEST_FILES[@]} -eq 0 ]; then
228+
echo "::error::No digests found to merge"
229+
exit 1
230+
fi
231+
IFS=',' read -ra IMAGES <<< "$PUBLISH_IMAGES"
142232
for IMG in "${IMAGES[@]}"; do
143-
docker buildx imagetools create -t "${IMG}:${TAG}" \
144-
"${IMG}@${LIGHT_DIGEST}" \
145-
"${IMG}@${FULL_DIGEST}"
233+
REFS=()
234+
for f in "${DIGEST_FILES[@]}"; do
235+
REFS+=("${IMG}@sha256:${f}")
236+
done
237+
docker buildx imagetools create -t "${IMG}:${TAG}" "${REFS[@]}"
146238
done
147-
GHCR_DIGEST=$(docker buildx imagetools inspect "${GHCR_IMAGE}:${TAG}" --format '{{.Manifest.Digest}}')
148-
echo "ghcr_digest=${GHCR_DIGEST}" >> "$GITHUB_OUTPUT"
149239
150-
- name: Inspect published manifests
240+
- name: 🔎 Inspect and capture GHCR digest
241+
id: digest
151242
shell: bash
243+
env:
244+
GHCR_IMAGE: ${{ needs.prepare.outputs.ghcr_image }}
245+
TAG: ${{ needs.prepare.outputs.tag }}
152246
run: |
153-
IFS=',' read -ra IMAGES <<< "${PUBLISH_IMAGES}"
154-
for IMG in "${IMAGES[@]}"; do
155-
echo "::group::${IMG}:${TAG}"
156-
docker buildx imagetools inspect "${IMG}:${TAG}"
157-
echo "::endgroup::"
158-
done
247+
set -euo pipefail
248+
GHCR_DIGEST=$(docker buildx imagetools inspect "${GHCR_IMAGE}:${TAG}" --format '{{.Manifest.Digest}}')
249+
echo "ghcr_digest=${GHCR_DIGEST}" >> "$GITHUB_OUTPUT"
250+
docker buildx imagetools inspect "${GHCR_IMAGE}:${TAG}"
159251
160-
- name: Attest build provenance
252+
- name: 🪪 Attest build provenance
161253
uses: actions/attest-build-provenance@v4
162254
continue-on-error: true
163255
with:
164-
subject-name: ${{ steps.meta.outputs.ghcr_image }}
165-
subject-digest: ${{ steps.manifest.outputs.ghcr_digest }}
256+
subject-name: ${{ needs.prepare.outputs.ghcr_image }}
257+
subject-digest: ${{ steps.digest.outputs.ghcr_digest }}
166258
push-to-registry: true
167259

168-
- name: Generate SBOM
260+
- name: 📋 Generate SBOM
169261
uses: anchore/sbom-action@v0.24.0
170262
continue-on-error: true
171263
with:
172-
image: ${{ steps.meta.outputs.ghcr_image }}@${{ steps.manifest.outputs.ghcr_digest }}
264+
image: ${{ needs.prepare.outputs.ghcr_image }}@${{ steps.digest.outputs.ghcr_digest }}
173265
format: spdx-json
174266
output-file: sbom.spdx.json
175267

176-
- name: Attest SBOM
268+
- name: 🪪 Attest SBOM
177269
uses: actions/attest@v4
178270
continue-on-error: true
179271
with:
180-
subject-name: ${{ steps.meta.outputs.ghcr_image }}
181-
subject-digest: ${{ steps.manifest.outputs.ghcr_digest }}
272+
subject-name: ${{ needs.prepare.outputs.ghcr_image }}
273+
subject-digest: ${{ steps.digest.outputs.ghcr_digest }}
182274
predicate-type: https://spdx.dev/Document
183275
predicate-path: sbom.spdx.json
184276
push-to-registry: true

0 commit comments

Comments
 (0)