Skip to content

Commit c6dcecd

Browse files
authored
Merge pull request #22 from seonghobae/feature/multi-arch-images
feat: publish amd64 and arm64 images
2 parents e85e4a5 + 5f20f03 commit c6dcecd

6 files changed

Lines changed: 308 additions & 4 deletions

File tree

‎.github/workflows/docker-publish.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ jobs:
4747
with:
4848
cosign-release: 'v2.2.4'
4949

50+
# Set up QEMU so Buildx can build arm64 images on the hosted runner
51+
# https://github.com/docker/setup-qemu-action
52+
- name: Set up QEMU
53+
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
54+
5055
# Set up BuildKit Docker container builder to be able to build
5156
# multi-platform images and export cache
5257
# https://github.com/docker/setup-buildx-action
@@ -78,6 +83,7 @@ jobs:
7883
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
7984
with:
8085
context: .
86+
platforms: linux/amd64,linux/arm64
8187
push: ${{ github.event_name != 'pull_request' }}
8288
tags: ${{ steps.meta.outputs.tags }}
8389
labels: ${{ steps.meta.outputs.labels }}

‎AGENTS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
- Docker regression checks: `python3 -m unittest discover -s tests -v`
1212
- Local image build probe:
1313
`docker build --progress=plain -t opencpu-psychometrics .`
14+
- Local multi-arch workflow probe:
15+
`docker buildx build --platform linux/amd64,linux/arm64 --progress=plain .`
1416

1517
## Code style
1618

‎ARCHITECTURE.md‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,30 @@
22

33
## Overview
44

5-
This repository ships a single Docker image for
5+
This repository ships a multi-architecture Docker image manifest for
66
`ghcr.io/seonghobae/opencpu-psychometrics`.
77
The image layers Ubuntu, OpenCPU, system libraries, Rust, and a
88
large R package set used for psychometrics workloads.
9-
GitHub Actions verifies the repository by building and publishing
10-
that image from `.github/workflows/docker-publish.yml`.
9+
GitHub Actions verifies the repository by building the image on pull
10+
requests and by building plus publishing it from
11+
`.github/workflows/docker-publish.yml` on pushes to `main`, the nightly
12+
schedule, and release tag paths.
1113

1214
## Build Pipeline
1315

1416
1. `.github/workflows/docker-publish.yml` runs
17+
`docker/setup-qemu-action`, `docker/setup-buildx-action`, and
1518
`docker/build-push-action` on pushes, pull requests, and the
1619
nightly schedule.
1720
2. `Dockerfile` installs Ubuntu packages, OpenCPU from the official
1821
PPA, the CRAN apt repository, and the required R ecosystem
1922
packages.
20-
3. The workflow signs published images and uploads an SBOM when the
23+
3. Pull requests build the `linux/amd64` and `linux/arm64` targets for
24+
verification without publishing them.
25+
4. Pushes, tags, and scheduled runs publish a manifest list for
26+
`linux/amd64` and `linux/arm64`, so the same image tag resolves to
27+
the native architecture on supported runners and hosts.
28+
5. The workflow signs published images and uploads an SBOM when the
2129
event is not a pull request.
2230

2331
## Stability Constraints
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Multi-Arch Images Design
2+
3+
## Context
4+
5+
- The repository currently publishes a single-platform image from
6+
`.github/workflows/docker-publish.yml`.
7+
- Buildx is already configured, but the workflow does not declare
8+
`platforms` and does not install QEMU.
9+
- Signing and SBOM steps already consume the digest from
10+
`docker/build-push-action`, which can remain the manifest-list digest
11+
in a multi-arch build.
12+
- The product goal is a GHCR image that is easy to consume from CI and
13+
deployments; adding `linux/amd64` and `linux/arm64` aligns with that
14+
goal without changing the runtime contract.
15+
16+
## Constraints
17+
18+
- Keep the Dockerfile behavior intact unless architecture-specific
19+
breakage is observed.
20+
- Preserve current tag, signing, and SBOM behavior.
21+
- Keep the workflow pinned to explicit action SHAs.
22+
- Add tests before workflow changes.
23+
- Update `ARCHITECTURE.md` because build behavior changes.
24+
25+
## Approaches
26+
27+
### Approach A: Single Buildx job with QEMU and multi-platform output
28+
29+
- Add `docker/setup-qemu-action`.
30+
- Set `platforms: linux/amd64,linux/arm64` in the existing
31+
`docker/build-push-action` step.
32+
- Keep one workflow job and reuse the manifest-list digest for signing
33+
and SBOM generation.
34+
35+
Trade-offs:
36+
37+
- Smallest diff and preserves the existing release flow.
38+
- Slowest step becomes slower, but caching and current downstream logic
39+
stay simple.
40+
41+
### Approach B: Matrix builds per architecture plus manifest assembly
42+
43+
- Build amd64 and arm64 separately.
44+
- Publish per-arch images, then assemble a manifest list.
45+
46+
Trade-offs:
47+
48+
- Better isolation and clearer per-arch failures.
49+
- Considerably more workflow complexity and more moving parts for
50+
signing/SBOM generation.
51+
52+
### Approach C: Keep PR builds single-arch and publish multi-arch only on main/schedule
53+
54+
- Use multi-arch on push/schedule and single-arch on pull requests.
55+
56+
Trade-offs:
57+
58+
- Reduces PR cost.
59+
- Leaves pull requests unable to validate the arm64 path, which weakens
60+
confidence in the main publish path.
61+
62+
## Recommendation
63+
64+
Choose Approach A.
65+
66+
It is the smallest safe change, matches the current workflow structure,
67+
and preserves current signing and SBOM logic. The repository already
68+
accepts long Docker builds, so the most important outcome is making both
69+
target architectures part of the same verified pipeline rather than
70+
adding a second publishing model.
71+
72+
## Design
73+
74+
- Add a workflow regression test in
75+
`tests/test_docker_publish_workflow.py` that asserts the Docker
76+
workflow sets up QEMU and declares both `linux/amd64` and
77+
`linux/arm64`.
78+
- Update the Docker workflow to install QEMU before Buildx and to set the
79+
multi-platform list on the existing build step.
80+
- Keep signing and SBOM logic on the existing digest output.
81+
- Update `ARCHITECTURE.md` to record that the publish pipeline now emits
82+
a multi-architecture manifest list.
83+
84+
## Error Handling
85+
86+
- If arm64 package availability breaks later, the workflow should fail in
87+
the existing build step rather than silently publishing amd64 only.
88+
- No fallback to single-arch publishing is added in this change; failure
89+
should remain explicit.
90+
91+
## Testing
92+
93+
- Add failing tests in `tests/test_docker_publish_workflow.py` for QEMU
94+
setup and the `platforms` declaration.
95+
- Run `python3 -m unittest discover -s tests -v`.
96+
- Run a YAML/workflow lint pass if available through the repo tooling.
97+
98+
## Decisions
99+
100+
- Safe default: validate both architectures in the same workflow rather
101+
than treating arm64 as optional.
102+
- Safe default: workflow-only change first; no Dockerfile branching.
103+
- Safe default: keep PR builds multi-arch so the arm64 path is exercised
104+
before merge.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Multi-Arch Images Implementation Plan
2+
3+
> **For Claude:** REQUIRED SUB-SKILL: Use
4+
> superpowers:executing-plans to implement this plan task-by-task.
5+
6+
**Goal:** Build and publish `opencpu-psychometrics` images for both
7+
`linux/amd64` and `linux/arm64` in the existing GitHub Actions
8+
workflow.
9+
10+
**Architecture:** Keep the single Docker workflow job and extend it to
11+
build a multi-platform manifest list with QEMU + Buildx. Add
12+
regression tests that assert the workflow is configured for both target
13+
architectures, then update build documentation to reflect the new
14+
publish behavior.
15+
16+
**Tech Stack:** GitHub Actions workflow YAML, Docker Buildx, QEMU,
17+
Python `unittest`
18+
19+
---
20+
21+
## Task 1: Add failing workflow tests
22+
23+
**Files:**
24+
25+
- Create: `tests/test_docker_publish_workflow.py`
26+
- Test: `tests/test_docker_publish_workflow.py`
27+
28+
### Step 1: Write the failing test
29+
30+
```python
31+
import pathlib
32+
import unittest
33+
34+
35+
WORKFLOW = (
36+
pathlib.Path(__file__).resolve().parents[1]
37+
/ ".github/workflows/docker-publish.yml"
38+
)
39+
40+
41+
class DockerWorkflowMultiArchTest(unittest.TestCase):
42+
def setUp(self) -> None:
43+
self.content = WORKFLOW.read_text(encoding="utf-8")
44+
45+
def test_workflow_sets_up_qemu_for_cross_arch_builds(self) -> None:
46+
self.assertIn("docker/setup-qemu-action", self.content)
47+
48+
def test_workflow_builds_amd64_and_arm64(self) -> None:
49+
self.assertIn("platforms: linux/amd64,linux/arm64", self.content)
50+
```
51+
52+
### Step 2: Run test to verify it fails
53+
54+
Run: `python3 -m unittest tests.test_docker_publish_workflow -v`
55+
Expected: FAIL because QEMU and the multi-arch `platforms` line do
56+
not exist yet.
57+
58+
### Step 3: Commit
59+
60+
Do not commit yet.
61+
62+
## Task 2: Update the workflow minimally
63+
64+
**Files:**
65+
66+
- Modify: `.github/workflows/docker-publish.yml`
67+
- Test: `tests/test_docker_publish_workflow.py`
68+
69+
### Step 1: Write minimal implementation
70+
71+
Add a pinned `docker/setup-qemu-action` step before Buildx and add:
72+
73+
```yaml
74+
platforms: linux/amd64,linux/arm64
75+
```
76+
77+
to the `docker/build-push-action` step.
78+
79+
### Step 2: Run targeted tests to verify they pass
80+
81+
Run: `python3 -m unittest tests.test_docker_publish_workflow -v`
82+
Expected: PASS
83+
84+
### Step 3: Run the full test suite
85+
86+
Run: `python3 -m unittest discover -s tests -v`
87+
Expected: PASS
88+
89+
## Task 3: Update architecture docs
90+
91+
**Files:**
92+
93+
- Modify: `ARCHITECTURE.md`
94+
- Modify: `AGENTS.md` (if verification guidance needs to mention
95+
workflow config tests)
96+
97+
### Step 1: Document the new behavior
98+
99+
Update `ARCHITECTURE.md` so the build pipeline section states that the
100+
workflow now emits a multi-architecture manifest list for amd64 and
101+
arm64.
102+
103+
### Step 2: Verify docs remain lint-clean
104+
105+
Run: `markdownlint-cli2 AGENTS.md ARCHITECTURE.md README.md`
106+
107+
Run: `markdownlint-cli2 docs/plans/2026-03-11-multi-arch-images-design.md docs/plans/2026-03-11-multi-arch-images.md`
108+
109+
Expected: PASS
110+
111+
## Task 4: Final verification and commit
112+
113+
**Files:**
114+
115+
- Modify: `.github/workflows/docker-publish.yml`
116+
- Modify: `AGENTS.md`
117+
- Create: `tests/test_docker_publish_workflow.py`
118+
- Modify: `ARCHITECTURE.md`
119+
- Create: `docs/plans/2026-03-11-multi-arch-images-design.md`
120+
- Create: `docs/plans/2026-03-11-multi-arch-images.md`
121+
122+
### Step 1: Run verification
123+
124+
Run:
125+
126+
```bash
127+
python3 -m unittest discover -s tests -v
128+
markdownlint-cli2 AGENTS.md ARCHITECTURE.md README.md
129+
markdownlint-cli2 docs/plans/2026-03-11-multi-arch-images-design.md docs/plans/2026-03-11-multi-arch-images.md
130+
```
131+
132+
Expected: PASS
133+
134+
### Step 2: Commit
135+
136+
```bash
137+
git add .github/workflows/docker-publish.yml \
138+
AGENTS.md \
139+
tests/test_docker_publish_workflow.py \
140+
ARCHITECTURE.md \
141+
docs/plans/2026-03-11-multi-arch-images-design.md \
142+
docs/plans/2026-03-11-multi-arch-images.md
143+
git commit -m "feat: publish multi-arch Docker images"
144+
```
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import pathlib
2+
import unittest
3+
4+
5+
WORKFLOW = (
6+
pathlib.Path(__file__).resolve().parents[1]
7+
/ ".github"
8+
/ "workflows"
9+
/ "docker-publish.yml"
10+
)
11+
12+
13+
class DockerPublishWorkflowTest(unittest.TestCase):
14+
def setUp(self) -> None:
15+
self.content = WORKFLOW.read_text(encoding="utf-8")
16+
self.lines = self.content.splitlines()
17+
18+
def test_workflow_configures_qemu_before_buildx_for_multi_arch_images(self) -> None:
19+
qemu_marker = "uses: docker/setup-qemu-action@"
20+
buildx_marker = "uses: docker/setup-buildx-action@"
21+
22+
qemu_index = next(
23+
index for index, line in enumerate(self.lines) if qemu_marker in line
24+
)
25+
buildx_index = next(
26+
index for index, line in enumerate(self.lines) if buildx_marker in line
27+
)
28+
29+
self.assertIn(qemu_marker, self.content)
30+
self.assertIn(buildx_marker, self.content)
31+
self.assertLess(qemu_index, buildx_index)
32+
33+
def test_workflow_builds_amd64_and_arm64_images(self) -> None:
34+
self.assertIn("platforms:", self.content)
35+
self.assertIn("linux/amd64", self.content)
36+
self.assertIn("linux/arm64", self.content)
37+
38+
39+
if __name__ == "__main__":
40+
unittest.main()

0 commit comments

Comments
 (0)