2 Commits
Author SHA1 Message Date
lyrathorpe d5f4329f46 Merge pull request 'ci: build the image once instead of twice' (#5) from ci/one-build-not-two into main
Reviewed-on: #5
2026-08-24 17:32:01 +01:00
Emma Thorpe ef59e52bca ci: build the image once instead of twice
Build and publish container / build (pull_request) Successful in 4m4s
Runs take fifteen to eighteen minutes, and the log shows why: the image is
built twice, in full.

The test stage is built by the runner's docker daemon. The runtime stage was
then built by docker/build-push-action, which runs under a buildx builder that
setup-buildx-action creates in its own container with its own cache. The two
share nothing, so the second build spent seventy-six seconds booting buildkit
and then installed ffmpeg and the package all over again -- around a hundred
and ten seconds for the apk and another hundred for pip, neither of which
produced anything the first build had not already made. The comment above the
test step claimed those layers were shared, which is what made this look
reasonable.

buildx earns that overhead when producing several architectures. This produces
linux/amd64 only, by an explicit decision recorded in the workflow, so it earns
nothing here. Use plain docker build against the same daemon that ran the
tests, and push with docker push. The runtime stage is a strict prefix of the
test stage, so every layer is a cache hit: measured at 1.3 seconds locally.

Identical to the change made in music-curator, whose workflow this one was
copied from.
2026-08-24 17:29:54 +01:00
+30 -19
View File
@@ -45,7 +45,8 @@ jobs:
# The suite runs inside the image, against the ffmpeg that ships, rather
# than against whatever the runner happens to provide. A failing test
# fails the build. Layers are shared with the push build below.
# fails the build. The runtime stage below is built from the same daemon
# afterwards, so its layers are already in cache.
- name: Run the test suite inside the image
run: docker build --target test -t music-mirror:test .
@@ -124,9 +125,6 @@ jobs:
echo "release=${release}" >> "$GITHUB_OUTPUT"
echo "Computed bump=${bump}, release=${release}, base=${base}"
- name: Set up Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Log in to the Gitea container registry
if: github.event_name != 'pull_request'
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
@@ -135,21 +133,34 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.PACKAGES_TOKEN }}
- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
context: .
# Without this the last stage in the Dockerfile -- the test stage --
# would be what gets published.
target: runtime
# The NAS is the only host this runs on. Building arm64 as well would
# mean emulating it under QEMU for no consumer.
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.version.outputs.tags }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
# Plain `docker build` rather than buildx. buildx boots its own buildkit
# in a container with a cache of its own, so it shared nothing with the
# test build above and rebuilt the image from the base up -- installing
# ffmpeg and the package a second time, for nothing. It earns that cost
# when building for several platforms; this only ever targets the amd64
# NAS, so it does not.
#
# `--target runtime` is a strict prefix of the test stage, so every layer
# is already in the daemon's cache and this resolves in seconds.
- name: Build the runtime image
run: |
set -euo pipefail
tags=()
while IFS= read -r tag; do
[ -n "$tag" ] && tags+=(-t "$tag")
done <<< "${{ steps.version.outputs.tags }}"
docker build --target runtime \
--label "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \
--label "org.opencontainers.image.revision=${GITHUB_SHA}" \
"${tags[@]}" .
- name: Push
if: github.event_name != 'pull_request'
run: |
set -euo pipefail
while IFS= read -r tag; do
[ -n "$tag" ] && docker push "$tag"
done <<< "${{ steps.version.outputs.tags }}"
# Record the release: write the computed version into pyproject.toml, then
# commit and tag it, so the packaging metadata always matches the release