Skip to content

Commit 2ece696

Browse files
committed
Build images natively per architecture
Replace the QEMU-based multi-platform build with native amd64 and arm64 runner jobs, then publish the final image tags by assembling a manifest from the architecture-specific tags. Fixes nikolaik#258. Run the smoke test suite against each architecture-specific image before publishing the final manifest, instead of only testing the locally loaded amd64 image. The build-matrix helper now emits an architecture-expanded matrix for the workflow, and the new unit test covers that expansion. Fixes nikolaik#314.
1 parent 08d5175 commit 2ece696

3 files changed

Lines changed: 109 additions & 27 deletions

File tree

.github/workflows/build.yaml

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ jobs:
2222
runs-on: ubuntu-latest
2323
needs: [test]
2424
outputs:
25-
matrix: ${{ steps.set-matrix.outputs.matrix }}
25+
version_matrix: ${{ steps.set-matrix.outputs.matrix }}
26+
arch_matrix: ${{ steps.set-matrix.outputs.arch_matrix }}
2627
steps:
2728
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
2829
with:
@@ -37,23 +38,24 @@ jobs:
3738
uv run dpn $FORCE build-matrix --event ${{ github.event_name }}
3839
3940
40-
deploy:
41-
name: ${{ matrix.key }}
42-
runs-on: ubuntu-latest
43-
if: needs.generate-matrix.outputs.matrix != ''
41+
build-arch:
42+
name: ${{ matrix.key }} (${{ matrix.arch }})
43+
runs-on: ${{ matrix.runner }}
44+
if: needs.generate-matrix.outputs.arch_matrix != ''
4445
needs: [generate-matrix]
4546
strategy:
46-
matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}
47+
fail-fast: false
48+
matrix: ${{ fromJSON(needs.generate-matrix.outputs.arch_matrix) }}
4749
steps:
4850
# Setup
4951
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
5052
- uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7
5153
with:
5254
enable-cache: true
5355
- name: Generate Dockerfile from config
54-
run: uv run dpn dockerfile --context '${{ toJSON(matrix) }}'
55-
- name: Set up QEMU
56-
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
56+
run: |
57+
context="$(echo '${{ toJSON(matrix) }}' | jq -c '{key, python, python_canonical, python_image, nodejs, nodejs_canonical, distro, platforms, digest}')"
58+
uv run dpn dockerfile --context "${context}"
5759
- name: Set up Docker Buildx
5860
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
5961
- name: Login to Docker Hub
@@ -62,36 +64,51 @@ jobs:
6264
username: ${{ secrets.DOCKERHUB_USERNAME }}
6365
password: ${{ secrets.DOCKERHUB_TOKEN }}
6466

65-
# Build
67+
# Build and push
6668
- name: Build image
69+
id: build-and-push
6770
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
6871
with:
6972
context: .
7073
file: dockerfiles/${{ matrix.key }}.Dockerfile
71-
load: true
72-
tags: ${{ env.IMAGE_NAME }}:${{ matrix.key }}
74+
platforms: ${{ matrix.platform }}
75+
push: true
76+
tags: ${{ env.IMAGE_NAME }}:${{ matrix.key }}-${{ matrix.arch }}
7377

7478
# Test
7579
- name: Run smoke tests
7680
run: |
77-
docker run --rm ${{ env.IMAGE_NAME }}:${{ matrix.key }} sh -c "node --version && npm --version && yarn --version && python --version && pip --version && pipenv --version && poetry --version && uv --version"
81+
docker run --rm ${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} sh -c "node --version && npm --version && yarn --version && python --version && pip --version && pipenv --version && poetry --version && uv --version"
7882
79-
# Push image
80-
- name: Push image
81-
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
82-
id: build-and-push
83+
deploy:
84+
name: Publish ${{ matrix.key }}
85+
runs-on: ubuntu-latest
86+
if: needs.generate-matrix.outputs.version_matrix != ''
87+
needs: [generate-matrix, build-arch]
88+
strategy:
89+
fail-fast: false
90+
matrix: ${{ fromJSON(needs.generate-matrix.outputs.version_matrix) }}
91+
steps:
92+
- name: Set up Docker Buildx
93+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
94+
- name: Login to Docker Hub
95+
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
8396
with:
84-
context: .
85-
file: dockerfiles/${{ matrix.key }}.Dockerfile
86-
platforms: ${{ join(matrix.platforms) }}
87-
push: true
88-
tags: ${{ env.IMAGE_NAME }}:${{ matrix.key }}
97+
username: ${{ secrets.DOCKERHUB_USERNAME }}
98+
password: ${{ secrets.DOCKERHUB_TOKEN }}
99+
100+
- name: Publish multi-arch manifest
101+
run: |
102+
tags=("${IMAGE_NAME}:${{ matrix.key }}-amd64")
103+
if echo '${{ toJSON(matrix.platforms) }}' | jq -e '.[] == "linux/arm64"' > /dev/null; then
104+
tags+=("${IMAGE_NAME}:${{ matrix.key }}-arm64")
105+
fi
106+
docker buildx imagetools create --tag "${IMAGE_NAME}:${{ matrix.key }}" "${tags[@]}"
89107
90-
# Store build context
91108
- name: Add digest to build context
92109
run: |
93110
mkdir builds/
94-
digest="${{ steps.build-and-push.outputs.digest }}"
111+
digest="$(docker buildx imagetools inspect "${IMAGE_NAME}:${{ matrix.key }}" | awk '/^Digest:/ {print $2}')"
95112
echo '${{ toJSON(matrix) }}' | jq --arg digest "$digest" '. +={"digest": $digest}' >> "builds/${{ matrix.key }}.json"
96113
97114
- name: Upload build context

src/docker_python_nodejs/build_matrix.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,42 @@ def _github_action_set_output(key: str, value: str) -> None:
2525
sys.exit(1)
2626

2727
with Path(GITHUB_OUTPUT).open("a") as fp:
28-
fp.write(f"{key}={value}")
28+
fp.write(f"{key}={value}\n")
29+
30+
31+
def _build_matrix_json(new_or_updated: list[BuildVersion]) -> str:
32+
return json.dumps({"include": [dataclasses.asdict(ver) for ver in new_or_updated]}) if new_or_updated else ""
33+
34+
35+
def _build_arch_matrix_json(new_or_updated: list[BuildVersion]) -> str:
36+
if not new_or_updated:
37+
return ""
38+
39+
include: list[dict[str, object]] = []
40+
for version in new_or_updated:
41+
include.extend(
42+
(
43+
dataclasses.asdict(version)
44+
| {
45+
"platform": platform,
46+
"arch": platform.split("/")[1],
47+
"runner": "ubuntu-24.04-arm" if platform == "linux/arm64" else "ubuntu-latest",
48+
}
49+
)
50+
for platform in version.platforms
51+
)
52+
53+
return json.dumps({"include": include})
2954

3055

3156
def build_matrix(new_or_updated: list[BuildVersion], ci_event: str) -> None:
3257
if not new_or_updated and ci_event == CI_EVENT_SCHEDULED:
3358
logger.info("\n# Scheduled run with no new or updated versions. Doing nothing.")
3459
return
3560

36-
matrix = json.dumps({"include": [dataclasses.asdict(ver) for ver in new_or_updated]}) if new_or_updated else ""
37-
_github_action_set_output("MATRIX", matrix)
61+
matrix = _build_matrix_json(new_or_updated)
62+
arch_matrix = _build_arch_matrix_json(new_or_updated)
63+
_github_action_set_output("matrix", matrix)
64+
_github_action_set_output("arch_matrix", arch_matrix)
3865
logger.info("\n# New or updated versions:")
3966
logger.info("Nothing" if not new_or_updated else "\n".join(version.key for version in new_or_updated))

tests/test_all.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pytest
99
import responses
1010

11+
from docker_python_nodejs.build_matrix import _build_arch_matrix_json
1112
from docker_python_nodejs.dockerfiles import render_dockerfile_with_context
1213
from docker_python_nodejs.readme import update_dynamic_readme
1314
from docker_python_nodejs.settings import BASE_PATH, DOCKERFILES_PATH
@@ -292,6 +293,43 @@ def test_find_new_or_updated_with_digest() -> None:
292293
assert len(res) == 0
293294

294295

296+
def test_build_arch_matrix_json(build_version: BuildVersion) -> None:
297+
matrix = json.loads(_build_arch_matrix_json([build_version]))
298+
299+
assert matrix == {
300+
"include": [
301+
{
302+
"key": "python3.11-nodejs20",
303+
"python": "3.11",
304+
"python_canonical": "3.11.3",
305+
"python_image": "3.11.3-trixie",
306+
"nodejs": "20",
307+
"nodejs_canonical": "20.2.0",
308+
"distro": "trixie",
309+
"platforms": ["linux/amd64", "linux/arm64"],
310+
"digest": "",
311+
"platform": "linux/amd64",
312+
"arch": "amd64",
313+
"runner": "ubuntu-latest",
314+
},
315+
{
316+
"key": "python3.11-nodejs20",
317+
"python": "3.11",
318+
"python_canonical": "3.11.3",
319+
"python_image": "3.11.3-trixie",
320+
"nodejs": "20",
321+
"nodejs_canonical": "20.2.0",
322+
"distro": "trixie",
323+
"platforms": ["linux/amd64", "linux/arm64"],
324+
"digest": "",
325+
"platform": "linux/arm64",
326+
"arch": "arm64",
327+
"runner": "ubuntu-24.04-arm",
328+
},
329+
],
330+
}
331+
332+
295333
@responses.activate
296334
def test_latest_tag_key_matches_legacy_latest_sources() -> None:
297335
responses.add(

0 commit comments

Comments
 (0)