Skip to content

Commit fe7d164

Browse files
authored
Build an image for every Alpine-supported architecture (#212)
1 parent 2e70ace commit fe7d164

1 file changed

Lines changed: 92 additions & 28 deletions

File tree

.github/workflows/build-test-push.yml

Lines changed: 92 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
setup:
5454
runs-on: ubuntu-latest
5555
outputs:
56-
# build job's matrix: cross product of selected directories and platforms
56+
# build job's matrix: flat include list, one entry per (directory, arch)
5757
build_matrix: ${{ steps.select.outputs.build_matrix }}
5858
# merge job's matrix: selected directories only (one manifest list per directory)
5959
merge_matrix: ${{ steps.select.outputs.merge_matrix }}
@@ -62,6 +62,11 @@ jobs:
6262
steps:
6363
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
6464

65+
# Needed for `docker buildx imagetools inspect` to read the Alpine base and
66+
# published-tag manifests when detecting architectures.
67+
- name: Set up Docker Buildx
68+
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
69+
6570
- name: Detect changed directories
6671
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4
6772
id: filter
@@ -93,35 +98,94 @@ jobs:
9398
{"directory": "2.1", "version": "2.1.0", "tags": "2.1 2.1-alpine 2 2-alpine latest"}
9499
]
95100
run: |
96-
SELECTED='[]'
101+
# Each platform builds on the closest runner architecture to minimize QEMU
102+
# emulation: amd64 (and 386, which x86-64 runs in hardware) on amd64, and the
103+
# arm variants on ARM hosts (arm64v8 natively; armv7/armv6 via aarch32 emulation).
104+
# The remaining arches (ppc64le, s390x, riscv64) have no native runner and are
105+
# fully emulated.
106+
runner_for() {
107+
case "$1" in
108+
linux/arm*) echo "ubuntu-24.04-arm" ;;
109+
*) echo "ubuntu-latest" ;;
110+
esac
111+
}
112+
113+
# Slug: strip leading "linux/", drop remaining slashes (linux/arm/v7 -> armv7).
114+
slug_for() {
115+
local p="${1#linux/}"
116+
echo "${p//\//}"
117+
}
118+
119+
# Print one "os/arch[/variant]" line per real platform in an image's manifest,
120+
# dropping attestation entries (platform.os == "unknown"). Non-zero if the
121+
# image ref cannot be inspected (e.g. the tag does not exist).
122+
platforms_of() {
123+
docker buildx imagetools inspect --raw "$1" 2>/dev/null \
124+
| jq -r '.manifests[]?
125+
| select(.platform.os != null and .platform.os != "unknown")
126+
| .platform
127+
| .os + "/" + .architecture
128+
+ (if (.variant // "") == "" then "" else "/" + .variant end)'
129+
}
130+
131+
# Apply containerd's default-variant rule so a base image's "arm64/v8" compares
132+
# equal to a buildx-pushed image's normalized "arm64". Without this, arm64 would
133+
# always look missing and every directory would rebuild forever.
134+
canonical() {
135+
sed -E 's#^linux/arm64/v8$#linux/arm64#'
136+
}
137+
138+
BUILD='[]'
139+
MERGE='[]'
97140
while read -r ENTRY; do
98141
DIRECTORY=$(jq -r '.directory' <<< "${ENTRY}")
99142
VERSION=$(jq -r '.version' <<< "${ENTRY}")
143+
144+
# Expected arches come from the directory's pinned Alpine base image.
145+
BASE_REF=$(grep -iE '^FROM alpine:' "${DIRECTORY}/Dockerfile" | tail -1 | awk '{print $2}')
146+
EXPECTED=$(platforms_of "${BASE_REF}" | sort -u)
147+
if [[ -z "${EXPECTED}" ]]; then
148+
echo "::error::Could not determine architectures for ${DIRECTORY} from ${BASE_REF}"
149+
exit 1
150+
fi
151+
100152
if jq -e --arg d "${DIRECTORY}" 'index($d) != null' <<< "${CHANGED}" > /dev/null; then
101153
echo "${DIRECTORY}: files changed, building"
102-
elif ! curl -fsS "https://hub.docker.com/v2/repositories/${REPO_NAME}/tags/${VERSION}" &> /dev/null; then
103-
echo "${DIRECTORY}: ${REPO_NAME}:${VERSION} not yet published, building"
104154
else
105-
echo "${DIRECTORY}: unchanged and already published, skipping"
106-
continue
155+
PUBLISHED=$(platforms_of "${REPO_NAME}:${VERSION}" | canonical | sort -u || true)
156+
if [[ -z "${PUBLISHED}" ]]; then
157+
echo "${DIRECTORY}: ${REPO_NAME}:${VERSION} not yet published, building"
158+
else
159+
MISSING=$(comm -23 <(printf '%s\n' "${EXPECTED}" | canonical) <(printf '%s\n' "${PUBLISHED}"))
160+
if [[ -n "${MISSING}" ]]; then
161+
echo "${DIRECTORY}: ${REPO_NAME}:${VERSION} building; architectures missing:"
162+
printf '%s\n' "${MISSING}" | sed 's/^/ /'
163+
else
164+
echo "${DIRECTORY}: unchanged and all arches published, skipping"
165+
continue
166+
fi
167+
fi
107168
fi
108-
SELECTED=$(jq -c ". + [${ENTRY}]" <<< "${SELECTED}")
169+
170+
# Selected: record the directory for merge, and one build entry per arch.
171+
MERGE=$(jq -c ". + [${ENTRY}]" <<< "${MERGE}")
172+
while read -r PLATFORM; do
173+
[[ -z "${PLATFORM}" ]] && continue
174+
BUILD=$(jq -c \
175+
--arg directory "${DIRECTORY}" \
176+
--arg version "${VERSION}" \
177+
--arg platform "${PLATFORM}" \
178+
--arg runner "$(runner_for "${PLATFORM}")" \
179+
--arg slug "$(slug_for "${PLATFORM}")" \
180+
'. + [{directory: $directory, version: $version, platform: $platform, runner: $runner, slug: $slug}]' \
181+
<<< "${BUILD}")
182+
done <<< "${EXPECTED}"
109183
done < <(jq -c '.[]' <<< "${CANDIDATES}")
110184
111-
# Each platform builds on the closest runner architecture to minimize QEMU
112-
# emulation: amd64 natively on amd64, and the arm variants on ARM hosts
113-
# (arm64v8 natively; armv7/armv6 via aarch32 emulation).
114-
PLATFORMS='[
115-
{"name": "linux/amd64", "slug": "amd64", "runner": "ubuntu-latest"},
116-
{"name": "linux/arm64/v8", "slug": "arm64v8", "runner": "ubuntu-24.04-arm"},
117-
{"name": "linux/arm/v7", "slug": "armv7", "runner": "ubuntu-24.04-arm"},
118-
{"name": "linux/arm/v6", "slug": "armv6", "runner": "ubuntu-24.04-arm"}
119-
]'
120-
121-
echo "Selected directories: ${SELECTED}"
122-
echo "merge_matrix={\"include\":${SELECTED}}" >> "${GITHUB_OUTPUT}"
123-
echo "build_matrix={\"entry\":${SELECTED},\"platform\":$(jq -c . <<< "${PLATFORMS}")}" >> "${GITHUB_OUTPUT}"
124-
if [[ "${SELECTED}" == "[]" ]]; then
185+
echo "Selected build matrix: ${BUILD}"
186+
echo "build_matrix={\"include\":${BUILD}}" >> "${GITHUB_OUTPUT}"
187+
echo "merge_matrix={\"include\":${MERGE}}" >> "${GITHUB_OUTPUT}"
188+
if [[ "${BUILD}" == "[]" ]]; then
125189
echo "has_builds=false" >> "${GITHUB_OUTPUT}"
126190
else
127191
echo "has_builds=true" >> "${GITHUB_OUTPUT}"
@@ -135,16 +199,16 @@ jobs:
135199
- lint
136200
- setup
137201
if: needs.setup.outputs.has_builds == 'true'
138-
runs-on: ${{ matrix.platform.runner }}
202+
runs-on: ${{ matrix.runner }}
139203
strategy:
140204
fail-fast: false
141205
matrix: ${{ fromJSON(needs.setup.outputs.build_matrix) }}
142206
env:
143-
DIRECTORY: ${{ matrix.entry.directory }}
144-
VERSION: ${{ matrix.entry.version }}
145-
PLATFORM: ${{ matrix.platform.name }}
207+
DIRECTORY: ${{ matrix.directory }}
208+
VERSION: ${{ matrix.version }}
209+
PLATFORM: ${{ matrix.platform }}
146210
# Cache scope keeps each directory+platform's layers separate
147-
CACHE_SCOPE: ${{ matrix.entry.directory }}-${{ matrix.platform.slug }}
211+
CACHE_SCOPE: ${{ matrix.directory }}-${{ matrix.slug }}
148212
steps:
149213
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
150214

@@ -203,13 +267,13 @@ jobs:
203267
if: github.ref == 'refs/heads/master'
204268
run: |
205269
mkdir -p digests
206-
echo "${{ steps.push.outputs.digest }}" > "digests/${{ matrix.platform.slug }}"
270+
echo "${{ steps.push.outputs.digest }}" > "digests/${{ matrix.slug }}"
207271
208272
- name: Upload digest
209273
if: github.ref == 'refs/heads/master'
210274
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
211275
with:
212-
name: digests-${{ matrix.entry.directory }}-${{ matrix.platform.slug }}
276+
name: digests-${{ matrix.directory }}-${{ matrix.slug }}
213277
path: digests/*
214278
if-no-files-found: error
215279

0 commit comments

Comments
 (0)