Add Makefile and bin/ scripts for one-command multi-arch release

make release builds, smoke-tests and pushes amd64 (on fsn1app1 via
DOCKER_HOST) and arm64 (local), joins them into latest and dated
multi-arch manifests, and commits the day's logs. Logic lives in bin/;
the Makefile only wires dependencies so -j2 runs the arches in parallel.
This commit is contained in:
2026-11-05 04:41:47 -08:00
parent df31d11028
commit ce924fd762
10 changed files with 217 additions and 20 deletions
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Build the image for one arch on that arch's daemon, logging to logs/DATE/.
set -euo pipefail
cd "$(dirname "$0")/.."
. bin/lib.sh
arch="${1:?usage: bin/build <arch>}"
use_arch_host "$arch"
docker build --progress=plain -t "$(dev_tag "$arch")" . 2>&1 |
tee "$LOGDIR/build-$arch.log"
Executable
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Remove dev-tagged images from every arch's daemon.
set -euo pipefail
cd "$(dirname "$0")/.."
. bin/lib.sh
for arch in ${ARCHES:-amd64 arm64}; do
use_arch_host "$arch" || continue
docker rmi -f "$(dev_tag "$arch")" 2>/dev/null || true
done
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Commit logs/DATE/ and push to origin.
set -euo pipefail
cd "$(dirname "$0")/.."
. bin/lib.sh
git add "$LOGDIR"
if git diff --cached --quiet; then
echo "no new logs to commit"
exit 0
fi
git commit -m "Release $DATE: build and push logs"
git push origin HEAD
+32
View File
@@ -0,0 +1,32 @@
# Shared setup for bin/ scripts. Source after `set -euo pipefail`.
#
# Expects IMAGE and DATE in the environment (the Makefile exports them) and
# takes the target arch as $1 where relevant.
: "${IMAGE:=git.eeqj.de/sneak/baseimage}"
: "${DATE:=$(date +%Y-%m-%d)}"
LOGDIR="logs/$DATE"
mkdir -p "$LOGDIR"
# Point docker at the daemon for the given arch. DOCKER_HOST_<arch> empty or
# unset means the local daemon.
use_arch_host() {
local arch="$1" var host
var="DOCKER_HOST_${arch}"
host="${!var:-}"
if [ -n "$host" ]; then
export DOCKER_HOST="$host"
else
unset DOCKER_HOST
fi
local actual
actual="$(docker version --format '{{.Server.Arch}}')"
if [ "$actual" != "$arch" ]; then
echo "error: daemon for $arch (${host:-local}) reports arch $actual" >&2
return 1
fi
}
dev_tag() { echo "baseimage:dev-$1"; }
arch_tag() { echo "$IMAGE:$1"; }
Executable
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Join the per-arch tags into multi-arch manifests IMAGE:latest and
# IMAGE:DATE, and record what was published.
set -euo pipefail
cd "$(dirname "$0")/.."
. bin/lib.sh
unset DOCKER_HOST
refs=()
for arch in ${ARCHES:-amd64 arm64}; do
refs+=("$(arch_tag "$arch")")
done
docker buildx imagetools create \
-t "$IMAGE:latest" -t "$IMAGE:$DATE" "${refs[@]}"
docker buildx imagetools inspect "$IMAGE:$DATE" | tee "$LOGDIR/manifest.txt"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Print this machine's arch in docker naming (amd64 or arm64).
case "$(uname -m)" in
x86_64 | amd64) echo amd64 ;;
aarch64 | arm64) echo arm64 ;;
*) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;;
esac
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Tag the built image IMAGE:<arch> and push it from that arch's daemon.
set -euo pipefail
cd "$(dirname "$0")/.."
. bin/lib.sh
arch="${1:?usage: bin/push <arch>}"
use_arch_host "$arch"
docker tag "$(dev_tag "$arch")" "$(arch_tag "$arch")"
docker push "$(arch_tag "$arch")" 2>&1 | tee "$LOGDIR/push-$arch.log"
Executable
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Smoke-test a built image: toolchains resolve, yarn/pnpm work offline,
# nothing left in /tmp, and runsvinit stops a runit service cleanly on
# docker stop with exit code 0.
set -euo pipefail
cd "$(dirname "$0")/.."
. bin/lib.sh
arch="${1:?usage: bin/test <arch>}"
use_arch_host "$arch"
img="$(dev_tag "$arch")"
name="baseimage-test-$arch-$$"
echo "== $img: toolchains and cleanup"
docker run --rm "$img" bash -ec '
# cleanup checks first: node writes a compile cache into /tmp when run
test -z "$(ls -A /tmp)"
test ! -e /usr/share/doc/bash
test ! -e /tmp/build.sh
go version; node --version; python3 --version; hugo version | head -1
COREPACK_ENABLE_NETWORK=0 yarn --version
COREPACK_ENABLE_NETWORK=0 pnpm --version
'
echo "== $img: runit service lifecycle"
docker rm -f "$name" >/dev/null 2>&1 || true
docker run -d --name "$name" "$img" bash -ec '
mkdir -p /etc/sv/demo
printf "%s\n" "#!/bin/bash" "exec 2>&1" \
"trap \"echo demo: got TERM; sleep 1; echo demo: clean exit; exit 0\" TERM" \
"echo demo: started" "while true; do sleep 1; done" > /etc/sv/demo/run
chmod +x /etc/sv/demo/run
ln -s /etc/sv/demo /etc/service/demo
exec /usr/local/sbin/runsvinit
' >/dev/null
sleep 3
docker exec "$name" sv status demo
docker stop "$name" >/dev/null
code="$(docker inspect "$name" --format '{{.State.ExitCode}}')"
logs="$(docker logs "$name" 2>&1)"
docker rm "$name" >/dev/null
echo "$logs"
if [ "$code" != 0 ] || ! grep -q 'demo: clean exit' <<<"$logs"; then
echo "error: graceful stop failed (exit $code)" >&2
exit 1
fi
echo "== $img: ok"