Skip to content

Commit 2e721f5

Browse files
authored
Add native Docker image support for Apple Silicon Macs (#19)
1 parent b5da92e commit 2e721f5

2 files changed

Lines changed: 119 additions & 20 deletions

File tree

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

Lines changed: 113 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ jobs:
3636
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3737
run: echo "${GITHUB_TOKEN}" | docker login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin
3838

39+
- name: Set up QEMU
40+
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130
41+
with:
42+
platforms: arm64
43+
44+
- name: Set up Docker Buildx
45+
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
46+
3947
- name: Resolve published package source
4048
id: package
4149
env:
@@ -56,6 +64,7 @@ jobs:
5664
5765
version="$(jq -r '.version' <<<"${metadata}")"
5866
git_head="$(jq -r '.gitHead' <<<"${metadata}")"
67+
short_sha="${git_head:0:7}"
5968
tarball="$(jq -r '.dist.tarball' <<<"${metadata}")"
6069
latest_version="$(npm view @atomicmemory/core@latest version)"
6170
@@ -69,32 +78,64 @@ jobs:
6978
exit 1
7079
fi
7180
72-
manifest_digest() {
73-
docker manifest inspect "$1" --verbose 2>/dev/null | jq -r 'if type == "array" then .[0].Descriptor.digest else .Descriptor.digest // empty end'
81+
tag_exists() {
82+
docker manifest inspect "$1" >/dev/null 2>&1
83+
}
84+
85+
tag_has_platform() {
86+
local image_ref="$1"
87+
local os="$2"
88+
local arch="$3"
89+
docker manifest inspect "${image_ref}" --verbose 2>/dev/null | jq -e --arg os "${os}" --arg arch "${arch}" '
90+
if type == "array" then
91+
any(.[]; .Descriptor.platform.os == $os and .Descriptor.platform.architecture == $arch)
92+
else
93+
.Descriptor.platform.os == $os and .Descriptor.platform.architecture == $arch
94+
end
95+
' >/dev/null
96+
}
97+
98+
tag_has_required_platforms() {
99+
tag_has_platform "$1" linux amd64 && tag_has_platform "$1" linux arm64
74100
}
75101
76-
version_digest="$(manifest_digest "${IMAGE_NAME}:${version}" || true)"
77-
latest_digest="$(manifest_digest "${IMAGE_NAME}:latest" || true)"
78102
is_latest="$([[ "${version}" == "${latest_version}" ]] && echo true || echo false)"
103+
release_refs=(
104+
"${IMAGE_NAME}:${version}"
105+
"${IMAGE_NAME}:${short_sha}"
106+
"${IMAGE_NAME}:sha-${short_sha}"
107+
)
108+
missing_platform_refs=()
109+
110+
for image_ref in "${release_refs[@]}"; do
111+
if ! tag_exists "${image_ref}" || ! tag_has_required_platforms "${image_ref}"; then
112+
missing_platform_refs+=("${image_ref}")
113+
fi
114+
done
115+
116+
release_tags_have_required_platforms=false
117+
if [[ "${#missing_platform_refs[@]}" == "0" ]]; then
118+
release_tags_have_required_platforms=true
119+
fi
79120
80-
if [[ -n "${version_digest}" && "${is_latest}" == "true" && "${version_digest}" != "${latest_digest}" ]]; then
121+
if [[ "${release_tags_have_required_platforms}" == "true" && "${is_latest}" == "true" ]]; then
81122
{
82123
echo "should_publish=true"
83124
echo "retag_latest_only=true"
84125
echo "version=${version}"
85126
echo "git_head=${git_head}"
86-
echo "short_sha=${git_head:0:7}"
127+
echo "short_sha=${short_sha}"
87128
echo "tarball=${tarball}"
88129
echo "is_latest=true"
89130
} >>"${GITHUB_OUTPUT}"
90-
echo "${IMAGE_NAME}:${version} already exists; moving latest to the same digest."
131+
echo "Release tags already include linux/amd64 and linux/arm64; ensuring latest points to ${IMAGE_NAME}:${version}."
91132
exit 0
92133
fi
93134
94-
if [[ -n "${version_digest}" ]]; then
135+
if [[ "${release_tags_have_required_platforms}" == "true" ]]; then
95136
echo "should_publish=false" >>"${GITHUB_OUTPUT}"
96-
echo "skip_reason=${IMAGE_NAME}:${version} already exists and latest is current." >>"${GITHUB_OUTPUT}"
97-
echo "${IMAGE_NAME}:${version} already exists and latest is current; no Docker publish required."
137+
echo "skip_reason=Release tags already include linux/amd64 and linux/arm64." >>"${GITHUB_OUTPUT}"
138+
echo "Release tags already include linux/amd64 and linux/arm64; no Docker publish required."
98139
exit 0
99140
fi
100141
@@ -103,14 +144,16 @@ jobs:
103144
echo "retag_latest_only=false"
104145
echo "version=${version}"
105146
echo "git_head=${git_head}"
106-
echo "short_sha=${git_head:0:7}"
147+
echo "short_sha=${short_sha}"
107148
echo "tarball=${tarball}"
108149
echo "is_latest=${is_latest}"
109150
} >>"${GITHUB_OUTPUT}"
110151
111152
echo "Resolved @atomicmemory/core@${version}"
112153
echo "gitHead=${git_head}"
113154
echo "tarball=${tarball}"
155+
printf 'Rebuilding release tags without complete linux/amd64 and linux/arm64 coverage:\n'
156+
printf ' - %s\n' "${missing_platform_refs[@]}"
114157
115158
- name: Report skipped publish
116159
if: steps.package.outputs.should_publish != 'true'
@@ -140,6 +183,7 @@ jobs:
140183
set -euo pipefail
141184
142185
docker build \
186+
--platform linux/amd64 \
143187
--file release-source/packages/core/Dockerfile \
144188
--label "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}" \
145189
--label "org.opencontainers.image.revision=${{ steps.package.outputs.git_head }}" \
@@ -295,25 +339,78 @@ jobs:
295339
-d '{"user_id":"x"}')"
296340
test "${bad_status}" = "400"
297341
298-
- name: Push release tags
342+
- name: Build and push multi-platform release tags
299343
if: steps.package.outputs.should_publish == 'true' && steps.package.outputs.retag_latest_only != 'true'
300344
run: |
301345
set -euo pipefail
302346
303-
docker push "${IMAGE_NAME}:${{ steps.package.outputs.version }}"
304-
docker push "${IMAGE_NAME}:${{ steps.package.outputs.short_sha }}"
305-
docker push "${IMAGE_NAME}:sha-${{ steps.package.outputs.short_sha }}"
347+
tag_args=(
348+
--tag "${IMAGE_NAME}:${{ steps.package.outputs.version }}"
349+
--tag "${IMAGE_NAME}:${{ steps.package.outputs.short_sha }}"
350+
--tag "${IMAGE_NAME}:sha-${{ steps.package.outputs.short_sha }}"
351+
)
306352
307353
if [[ "${{ steps.package.outputs.is_latest }}" == "true" ]]; then
308-
docker push "${IMAGE_NAME}:latest"
354+
tag_args+=(--tag "${IMAGE_NAME}:latest")
309355
else
310356
echo "Not moving latest: ${{ steps.package.outputs.version }} is not npm latest."
311357
fi
312358
359+
docker buildx build \
360+
--platform linux/amd64,linux/arm64 \
361+
--file release-source/packages/core/Dockerfile \
362+
--label "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}" \
363+
--label "org.opencontainers.image.revision=${{ steps.package.outputs.git_head }}" \
364+
--label "org.opencontainers.image.version=${{ steps.package.outputs.version }}" \
365+
--label "org.opencontainers.image.title=@atomicmemory/core" \
366+
"${tag_args[@]}" \
367+
--push \
368+
release-source
369+
313370
- name: Move latest to existing version image
314371
if: steps.package.outputs.should_publish == 'true' && steps.package.outputs.retag_latest_only == 'true'
315372
run: |
316373
set -euo pipefail
317374
docker buildx imagetools create \
318375
--tag "${IMAGE_NAME}:latest" \
319376
"${IMAGE_NAME}:${{ steps.package.outputs.version }}"
377+
378+
- name: Verify release platforms
379+
if: steps.package.outputs.should_publish == 'true'
380+
run: |
381+
set -euo pipefail
382+
383+
assert_platform() {
384+
local image_ref="$1"
385+
local os="$2"
386+
local arch="$3"
387+
docker manifest inspect "${image_ref}" --verbose | jq -e --arg os "${os}" --arg arch "${arch}" '
388+
if type == "array" then
389+
any(.[]; .Descriptor.platform.os == $os and .Descriptor.platform.architecture == $arch)
390+
else
391+
.Descriptor.platform.os == $os and .Descriptor.platform.architecture == $arch
392+
end
393+
' >/dev/null
394+
}
395+
396+
verify_tag() {
397+
local image_ref="$1"
398+
for attempt in {1..12}; do
399+
if assert_platform "${image_ref}" linux amd64 && assert_platform "${image_ref}" linux arm64; then
400+
echo "${image_ref} includes linux/amd64 and linux/arm64."
401+
return 0
402+
fi
403+
sleep 5
404+
done
405+
406+
echo "::error::${image_ref} does not include both linux/amd64 and linux/arm64."
407+
exit 1
408+
}
409+
410+
verify_tag "${IMAGE_NAME}:${{ steps.package.outputs.version }}"
411+
verify_tag "${IMAGE_NAME}:${{ steps.package.outputs.short_sha }}"
412+
verify_tag "${IMAGE_NAME}:sha-${{ steps.package.outputs.short_sha }}"
413+
414+
if [[ "${{ steps.package.outputs.is_latest }}" == "true" ]]; then
415+
verify_tag "${IMAGE_NAME}:latest"
416+
fi

packages/core/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,16 @@ docker run --rm -it --pull always \
6363
```
6464

6565
The image is published as `ghcr.io/atomicstrata/atomicmemory-core` with
66-
`latest`, semver, and commit-SHA tags.
66+
`latest`, semver, and commit-SHA tags. Release images are published for
67+
`linux/amd64` and `linux/arm64`, so the same tag works on common Linux
68+
servers and Apple Silicon Macs.
6769

6870
The public monorepo's `Publish Core Docker Image` workflow runs after
6971
`@atomicmemory/core` is published to npm and verified by the ops publishing
7072
helper. It resolves the npm package version, skips if that version is already
71-
present in GHCR, checks out the package `gitHead`, builds
72-
`packages/core/Dockerfile`, smoke-tests the local image, and then pushes the
73-
matching GHCR tags.
73+
present in GHCR with both required platforms, checks out the package `gitHead`,
74+
builds `packages/core/Dockerfile`, smoke-tests the local `linux/amd64` image,
75+
and then pushes the matching multi-platform GHCR tags.
7476

7577
Local Docker defaults use `Authorization: Bearer local-dev-key`, OpenAI
7678
embeddings at 1536 dimensions, and `RAW_STORAGE_DEPLOYMENT_ENV=local`. The

0 commit comments

Comments
 (0)