ref:96f6fe91a6f16cc9091c63ecf5dc8acc2e7907a4

ci: per-ISA parallel runner builds (multi-arch split) (#45)

The deferred half of fangorn/anvil-cli#43, now unblocked by the affinity-escape fix (fangorn/anvil#361) being **deployed to prod**. ## What Splits the single `build-runner` job into two per-ISA jobs that run **in parallel on native-architecture workers**: | Job | `runs_on` | Builds | |---|---|---| | `build-arm64` | `[linux, arm64]` | linux/arm64 + macos/arm64 | | `build-amd64` | `[linux, amd64]` | linux/amd64 + macos/amd64 | Each pays only cheap cross-**OS** codegen within its own ISA — never the slow cross-**architecture** codegen that made a cold build recompile `ring`'s C/asm for a foreign arch (a cold `macos arm64` cross-compile was 4m33s on the last release run). ## Fan-in A runner can't share a workspace across workers, so publishing goes through the release-asset API: - `build-arch.sh <arch>` builds its two targets and, on main, uploads them to a release **draft** (either job creates it; the duplicate-tag race is tolerated). - `publish-release.sh` (`depends_on` both) downloads all four, builds the combined `SHA256SUMS`, and **publishes**. A missing binary fails the download and rolls the draft back — an incomplete release never ships (the #40 property, preserved). - `ci/release.sh` now **excludes drafts** from the version computation, so the run's own draft doesn't bump the version out from under the parallel jobs. ## What this PR run validates Publishing is main-only, so **this PR run is safe** — it exercises the parallel per-ISA builds and the affinity routing (build-arm64 → arm worker, build-amd64 → amd worker, concurrently) **without cutting a release**. That's the risky part proven before any release logic runs. Merging activates the publish path on main; I'll watch that release cut. Supersedes `ci/build-runner.sh`.
SHA: 96f6fe91a6f16cc9091c63ecf5dc8acc2e7907a4
Author: Anvil <noreply@anvil.fangorn.io>
Date: 2026-07-23 05:11
Parents: 447d028
5 files changed +252 -218
Type
.anvil.yml +39 −17
@@ -43,17 +43,22 @@
- name: anvil-binary
path: target/release/anvil
- name: build-runner
timeout_seconds: 1800
# The cross-compilation toolchain is baked into a cached prepared image
# (fangorn/anvil#354), so it is installed once and reused across runs rather
# than re-downloaded and re-compiled every build (~1750s → seconds).
#
# Version pins live HERE, in the prepare commands, on purpose: the prepared
# image's cache key hashes these command strings. A bump must change a
# command to invalidate the image — bumping a value in ci/build-runner.sh
# would not. See ci/build-runner.sh for why each tool is pinned as it is.
prepare:
# Runner binaries are built per-ISA on native-architecture workers, in
# parallel (fangorn/anvil-cli#43): the arm64 worker builds linux/arm64 +
# macos/arm64, the amd64 worker builds linux/amd64 + macos/amd64. Each pays
# only cheap cross-*OS* codegen within its own ISA, never slow cross-*arch*
# codegen. Running two per-ISA jobs in one pipeline relies on the affinity
# escape fix (fangorn/anvil#361), now deployed.
#
# The cross toolchain is baked into a cached prepared image (fangorn/anvil#354).
# Version pins live in the prepare commands on purpose: the image's cache key
# hashes those command strings, so a bump must change a command to rebuild —
# bumping a value in the shell scripts would not. Both jobs use the identical
# prepare block (same image content, built once per worker).
- name: build-arm64
timeout_seconds: 3600
runs_on: [linux, arm64]
prepare: &runner_toolchain
- apt-get update && apt-get install -y --no-install-recommends bash curl jq xz-utils ca-certificates
# zig 0.15.2 — 0.14.x has a macho-linker regression that can't resolve
# -liconv/-lcharset for apple-darwin under rust >= 1.82.
@@ -62,18 +67,35 @@
- cargo install --locked cargo-zigbuild --version 0.23.0
- rustup target add aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu aarch64-apple-darwin x86_64-apple-darwin
# macOS SDK 12.3 — libSystem + frameworks zig does not bundle. Unpacks to
# /opt/MacOSX12.3.sdk; ci/build-runner.sh globs /opt/MacOSX*.sdk for it.
# /opt/MacOSX12.3.sdk; ci/build-arch.sh globs /opt/MacOSX*.sdk for it.
- curl -sSL "https://github.com/joseluisq/macosx-sdks/releases/download/12.3/MacOSX12.3.sdk.tar.xz" -o /tmp/macos-sdk.tar.xz && mkdir -p /opt && tar -xJf /tmp/macos-sdk.tar.xz -C /opt && rm /tmp/macos-sdk.tar.xz
# Delegated to bash: the runner invokes /bin/sh (dash), and the script needs
# bash features.
run: exec bash ci/build-arch.sh arm64
# Delegated to a bash script because the runner invokes /bin/sh (dash)
# and we need bash features (pipefail, trap ERR) for publish rollback.
run: exec bash ci/build-runner.sh
depends_on: [test, clippy, fmt]
artifacts:
- name: anvil_linux_arm64
path: runner-dist/anvil_linux_arm64
- name: anvil_linux_amd64
path: runner-dist/anvil_linux_amd64
- name: anvil_macos_arm64
path: runner-dist/anvil_macos_arm64
- name: build-amd64
timeout_seconds: 3600
runs_on: [linux, amd64]
prepare: *runner_toolchain
run: exec bash ci/build-arch.sh amd64
depends_on: [test, clippy, fmt]
artifacts:
- name: anvil_linux_amd64
path: runner-dist/anvil_linux_amd64
- name: anvil_macos_amd64
path: runner-dist/anvil_macos_amd64
# Fan-in: both arch jobs uploaded their binaries to the release draft; this
# job pulls all four back (release-asset API — the runner can't share a
# workspace across workers), builds the combined SHA256SUMS, and publishes.
# On a PR it is a no-op, so PR runs still exercise the parallel builds without
# cutting a release.
- name: publish-release
run: exec bash ci/publish-release.sh
depends_on: [build-arm64, build-amd64]
ci/build-arch.sh +134 −0
@@ -1,0 +1,134 @@
#!/usr/bin/env bash
set -euo pipefail
# Build the two same-ISA runner binaries for one architecture (native Linux +
# same-ISA cross-OS macOS), and on main upload them to the release draft.
#
# Split per-ISA (fangorn/anvil-cli#43) so each build runs natively on a matching
# runner and never pays slow cross-*architecture* codegen — only cheap
# cross-*OS* codegen within one ISA. The two arch jobs run in parallel on
# different workers, which the affinity-escape fix (fangorn/anvil#361) makes
# possible.
#
# Usage: ci/build-arch.sh <arm64|amd64>
#
# The toolchain (zig, cargo-zigbuild, macOS SDK, rustup targets) is baked into a
# cached prepared image by the step's prepare: block — not installed here.
ARCH="${1:?usage: build-arch.sh <arm64|amd64>}"
case "$ARCH" in
arm64)
LINUX_TARGET=aarch64-unknown-linux-gnu
MAC_TARGET=aarch64-apple-darwin
LINUX_NAME=anvil_linux_arm64
MAC_NAME=anvil_macos_arm64
;;
amd64)
LINUX_TARGET=x86_64-unknown-linux-gnu
MAC_TARGET=x86_64-apple-darwin
LINUX_NAME=anvil_linux_amd64
MAC_NAME=anvil_macos_amd64
;;
*)
echo "ERROR: unknown arch '$ARCH' (want arm64 or amd64)" >&2
exit 1
;;
esac
git config --global --add safe.directory /workspace
# Publish only on main, and only if the release token is present.
PUBLISH=0
if [ "${ANVIL_BRANCH:-}" = "main" ] || [ "${ANVIL_BRANCH:-}" = "refs/heads/main" ]; then
if [ -z "${ANVIL_TOKEN:-}" ]; then
echo "ERROR: ANVIL_TOKEN secret not set. Run:" >&2
echo " anvil ci set-secret --name ANVIL_TOKEN --value <pat> --repo fangorn/anvil-cli" >&2
exit 1
fi
PUBLISH=1
fi
# The version must match across both arch jobs. ci/release.sh derives it from
# the latest *published* release only (drafts excluded), so both jobs — and the
# publish job — compute the same tag even while this run's draft exists.
if [ "$PUBLISH" = "1" ]; then
VERSION=$(bash ci/release.sh)
if [ -z "$VERSION" ]; then
echo "ERROR: ci/release.sh returned empty version" >&2
exit 1
fi
# Local-only tag so build.rs stamps the version via `git describe`.
git tag -f "$VERSION"
fi
# ── Toolchain env (binaries come from the prepared image) ────────────────
export PATH="/opt/zig:$PATH"
SDKROOT=$(echo /opt/MacOSX*.sdk)
if [ ! -d "$SDKROOT" ]; then
echo "ERROR: macOS SDK not found under /opt (expected from the prepared image)" >&2
exit 1
fi
export SDKROOT
export MACOSX_DEPLOYMENT_TARGET=11.0
# glibc floor: keep the Linux binary loadable on older runtimes (anvil-cli#20).
GLIBC_FLOOR=2.31
echo "==> Building linux $ARCH (glibc $GLIBC_FLOOR floor)..."
cargo zigbuild --release --target "${LINUX_TARGET}.${GLIBC_FLOOR}" 2>&1
echo "==> Building macos $ARCH..."
cargo zigbuild --release --target "$MAC_TARGET" 2>&1
LINUX_BIN="target/${LINUX_TARGET}/release/anvil"
MAC_BIN="target/${MAC_TARGET}/release/anvil"
# Hard-gate the glibc floor on the Linux binary.
assert_glibc_floor() {
local bin="$1" max
max=$(grep -aoE 'GLIBC_[0-9]+\.[0-9]+' "$bin" | sort -V | tail -1)
if [ -z "$max" ]; then
echo "ERROR: no GLIBC version symbols found in $bin (unexpected)" >&2
exit 1
fi
if [ "$(printf '%s\nGLIBC_%s\n' "$max" "$GLIBC_FLOOR" | sort -V | tail -1)" != "GLIBC_${GLIBC_FLOOR}" ]; then
echo "ERROR: $bin requires $max, above the GLIBC_${GLIBC_FLOOR} floor" >&2
exit 1
fi
echo " OK: $bin max $max (<= GLIBC_${GLIBC_FLOOR})"
}
echo "==> Verifying glibc floor ($GLIBC_FLOOR)..."
assert_glibc_floor "$LINUX_BIN"
# Stage unversioned names for the CI artifact upload.
mkdir -p runner-dist
cp "$LINUX_BIN" "runner-dist/$LINUX_NAME"
cp "$MAC_BIN" "runner-dist/$MAC_NAME"
if [ "$PUBLISH" != "1" ]; then
exit 0
fi
# This runner is native $ARCH Linux, so the Linux binary it just built drives
# the release commands.
ANVIL="$PWD/$LINUX_BIN"
# Ensure the draft release exists. Either arch job may get here first; the
# loser's create hits a duplicate-tag 422, which we tolerate — the draft it
# needs already exists.
if ! "$ANVIL" release view "$VERSION" --repo fangorn/anvil-cli >/dev/null 2>&1; then
"$ANVIL" release create \
--tag "$VERSION" \
--title "anvil-cli $VERSION" \
--body "Runner binaries for linux/{amd64,arm64} and macos/{amd64,arm64}." \
--draft \
--repo fangorn/anvil-cli || true
fi
# Upload this ISA's two binaries to the draft.
cp "$LINUX_BIN" "runner-dist/${LINUX_NAME}_${VERSION}"
cp "$MAC_BIN" "runner-dist/${MAC_NAME}_${VERSION}"
"$ANVIL" release upload "$VERSION" "runner-dist/${LINUX_NAME}_${VERSION}" --repo fangorn/anvil-cli
"$ANVIL" release upload "$VERSION" "runner-dist/${MAC_NAME}_${VERSION}" --repo fangorn/anvil-cli
echo "==> Uploaded $ARCH binaries to draft release $VERSION"
ci/build-runner.sh +0 −200
@@ -1,200 +1,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Build and (on main) publish the runner binaries for all four targets.
#
# The cross-compilation toolchain — zig, cargo-zigbuild, the macOS SDK, and the
# rustup targets — is NOT installed here. It is baked into a cached prepared
# image by the `prepare:` block on the build-runner step in .anvil.yml, so it is
# not reinstalled on every run (fangorn/anvil-cli#43, fangorn/anvil#354). This
# script assumes that image: zig on PATH under /opt/zig, cargo-zigbuild in
# CARGO_HOME, the targets added, and the SDK unpacked under /opt.
#
# IMPORTANT: the toolchain *version pins* live in .anvil.yml's prepare commands,
# because the prepared-image cache key hashes those command strings, not this
# script. Bumping a version here would not rebuild the image. Keep pins there.
# The CI runner bind-mounts the workspace owned by a non-root host user
# into a root-run container. Git's safe-directory check rejects that,
# exiting 128 when we later try to read log history for the changelog.
git config --global --add safe.directory /workspace
# Fail fast if we're about to publish a release but the secret is missing —
# don't waste minutes on a build that will error at the end.
PUBLISH=0
if [ "${ANVIL_BRANCH:-}" = "main" ] || [ "${ANVIL_BRANCH:-}" = "refs/heads/main" ]; then
if [ -z "${ANVIL_TOKEN:-}" ]; then
echo "ERROR: ANVIL_TOKEN secret not set. Run:" >&2
echo " anvil ci set-secret --name ANVIL_TOKEN --value <pat> --repo fangorn/anvil-cli" >&2
exit 1
fi
PUBLISH=1
fi
# Compute the CalVer version and tag HEAD locally so that build.rs can
# read it via 'git describe --tags'. The tag is local-only — it never
# gets pushed. The server-side tag is created by 'anvil release create'.
if [ "$PUBLISH" = "1" ]; then
VERSION=$(bash ci/release.sh)
if [ -z "$VERSION" ]; then
echo "ERROR: ci/release.sh returned empty version" >&2
exit 1
fi
echo "==> Tagging HEAD as $VERSION (local only)"
git tag -f "$VERSION"
fi
# ── Toolchain env (binaries come from the prepared image) ────────────────
#
# The prepare block installs the toolchain but a `docker commit` snapshots the
# filesystem, not exported env vars — so set PATH/SDKROOT here at build time.
export PATH="/opt/zig:$PATH"
# macOS targets: no glibc floor (Apple libSystem). zig bundles glibc/musl but
# NOT Apple's non-redistributable libSystem + frameworks, so cross-linking
# against Darwin needs a macOS SDK, found via SDKROOT. Glob the SDK dir so this
# script carries no SDK version (the version pin lives in .anvil.yml).
SDKROOT=$(echo /opt/MacOSX*.sdk)
if [ ! -d "$SDKROOT" ]; then
echo "ERROR: macOS SDK not found under /opt (expected the prepared image to unpack it)" >&2
exit 1
fi
export SDKROOT
# Link against an older Darwin so the binaries run on macOS 11+ (Big Sur).
export MACOSX_DEPLOYMENT_TARGET=11.0
# ── Pinned glibc floor (anvil-cli#20) ───────────────────────────────────
#
# Building natively on the trixie image bakes a GLIBC_2.39 requirement into the
# binary, which then fails to load on older runtimes. cargo-zigbuild links
# against an older glibc supplied by zig, so the binaries run on
# glibc >= GLIBC_FLOOR. This floor is a build/verify constant (not an install
# version), so it stays here.
GLIBC_FLOOR=2.31
echo "==> Building linux arm64 (glibc $GLIBC_FLOOR floor)..."
cargo zigbuild --release --target "aarch64-unknown-linux-gnu.${GLIBC_FLOOR}" 2>&1
echo "==> Building linux amd64 (glibc $GLIBC_FLOOR floor)..."
cargo zigbuild --release --target "x86_64-unknown-linux-gnu.${GLIBC_FLOOR}" 2>&1
echo "==> Building macos arm64..."
cargo zigbuild --release --target aarch64-apple-darwin 2>&1
echo "==> Building macos amd64..."
cargo zigbuild --release --target x86_64-apple-darwin 2>&1
ARM64_BIN="target/aarch64-unknown-linux-gnu/release/anvil"
AMD64_BIN="target/x86_64-unknown-linux-gnu/release/anvil"
MACOS_ARM64_BIN="target/aarch64-apple-darwin/release/anvil"
MACOS_AMD64_BIN="target/x86_64-apple-darwin/release/anvil"
# Hard-gate the glibc floor: fail the build if either linux binary references a
# GLIBC symbol newer than the floor. Without the gate a future dep or toolchain
# bump silently raises it again. The version tags live as ASCII in the dynamic
# string table, so grep finds them without binutils.
assert_glibc_floor() {
local bin="$1" max
max=$(grep -aoE 'GLIBC_[0-9]+\.[0-9]+' "$bin" | sort -V | tail -1)
if [ -z "$max" ]; then
echo "ERROR: no GLIBC version symbols found in $bin (unexpected)" >&2
exit 1
fi
if [ "$(printf '%s\nGLIBC_%s\n' "$max" "$GLIBC_FLOOR" | sort -V | tail -1)" != "GLIBC_${GLIBC_FLOOR}" ]; then
echo "ERROR: $bin requires $max, above the GLIBC_${GLIBC_FLOOR} floor" >&2
exit 1
fi
echo " OK: $bin max $max (<= GLIBC_${GLIBC_FLOOR})"
}
echo "==> Verifying glibc floor ($GLIBC_FLOOR)..."
assert_glibc_floor "$ARM64_BIN"
assert_glibc_floor "$AMD64_BIN"
# Stage for CI artifact upload (unversioned names)
mkdir -p runner-dist
cp "$ARM64_BIN" runner-dist/anvil_linux_arm64
cp "$AMD64_BIN" runner-dist/anvil_linux_amd64
cp "$MACOS_ARM64_BIN" runner-dist/anvil_macos_arm64
cp "$MACOS_AMD64_BIN" runner-dist/anvil_macos_amd64
if [ "$PUBLISH" != "1" ]; then
exit 0
fi
# Use the binary matching THIS runner's architecture to drive `release
# list`/`release create` — the publish job can land on either an arm64 (carl)
# or amd64 (xps) runner. Hardcoding one made publishing fail with "Exec format
# error" whenever the job ran on the other runner.
case "$(uname -m)" in
aarch64 | arm64) ANVIL_CLI="$PWD/$ARM64_BIN" ;;
x86_64 | amd64) ANVIL_CLI="$PWD/$AMD64_BIN" ;;
*)
echo "ERROR: unsupported runner architecture: $(uname -m)" >&2
exit 1
;;
esac
echo "==> Publishing release $VERSION"
cp "$ARM64_BIN" "runner-dist/anvil_linux_arm64_${VERSION}"
cp "$AMD64_BIN" "runner-dist/anvil_linux_amd64_${VERSION}"
cp "$MACOS_ARM64_BIN" "runner-dist/anvil_macos_arm64_${VERSION}"
cp "$MACOS_AMD64_BIN" "runner-dist/anvil_macos_amd64_${VERSION}"
# Compute SHA256 checksums for every versioned asset — published as
# SHA256SUMS_${VERSION} alongside the binaries so install scripts can verify
# what they downloaded. Format matches `sha256sum` output: `<hash> <filename>`.
echo "==> Computing SHA256 checksums..."
(
cd runner-dist
ASSETS="anvil_linux_arm64_${VERSION} anvil_linux_amd64_${VERSION} anvil_macos_arm64_${VERSION} anvil_macos_amd64_${VERSION}"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum $ASSETS > "SHA256SUMS_${VERSION}"
else
shasum -a 256 $ASSETS > "SHA256SUMS_${VERSION}"
fi
)
cat "runner-dist/SHA256SUMS_${VERSION}"
# Generate changelog body from commits since the previous CalVer tag.
PREV_TAG=$("$ANVIL_CLI" release list --format json fangorn/anvil-cli \
| jq -r '[.[] | select(.tag_name | test("^[0-9]{4}\\.[0-9]{2}\\.[0-9]+$"))]
| sort_by(.tag_name | split(".") | map(tonumber))
| .[-1].tag_name // empty')
if [ -n "$PREV_TAG" ] && git rev-parse --verify "$PREV_TAG" >/dev/null 2>&1; then
CHANGELOG=$(git log --oneline "${PREV_TAG}..HEAD" || echo "(no commits since $PREV_TAG)")
else
CHANGELOG=$(git log --oneline -n 20)
fi
BODY=$(printf 'Runner binaries for linux/{amd64,arm64} and macos/{amd64,arm64}.\n\n## Changes\n\n%s\n' "$CHANGELOG")
# Publish atomically: create the release as a DRAFT, upload every asset, and
# only publish once they are all present. A failure mid-upload leaves a draft
# (invisible to `anvil update` and to the version bump), never a half-populated
# public release — the failure mode that shipped 2026.07.6 broken (#40).
"$ANVIL_CLI" release create \
--tag "$VERSION" \
--title "anvil-cli $VERSION" \
--body "$BODY" \
--draft \
--repo fangorn/anvil-cli
# Roll back the draft if any subsequent step fails — don't leave orphaned
# drafts lying around. A SIGKILL (e.g. step timeout) can't run this trap, but
# a killed run leaves only an unpublished draft, which is harmless.
cleanup_release() {
echo "==> Publish failed — rolling back release $VERSION" >&2
"$ANVIL_CLI" release delete "$VERSION" --repo fangorn/anvil-cli >&2 || true
}
trap cleanup_release ERR
for asset in \
"anvil_linux_arm64_${VERSION}" \
"anvil_linux_amd64_${VERSION}" \
"anvil_macos_arm64_${VERSION}" \
"anvil_macos_amd64_${VERSION}" \
"SHA256SUMS_${VERSION}"; do
"$ANVIL_CLI" release upload "$VERSION" "runner-dist/${asset}" --repo fangorn/anvil-cli
done
# Everything is present — flip the draft to published in one step.
"$ANVIL_CLI" release publish "$VERSION" --repo fangorn/anvil-cli
trap - ERR
echo "==> Published release $VERSION"
ci/publish-release.sh +75 −0
@@ -1,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
# Finalize the release: fan in the four binaries the per-ISA build jobs uploaded
# to the draft, compute the combined SHA256SUMS, and publish.
#
# Runs after build-arm64 and build-amd64 (depends_on), so all four assets are
# present on the draft. Because the runner can't pull another job's *workspace*,
# fan-in goes through the release-asset API: `release download` retrieves each
# binary from the draft. Publishing last keeps the release invisible to
# `anvil update` (which filters drafts) until it is complete — the #40 property.
git config --global --add safe.directory /workspace
# Only main publishes. On a PR this job is a no-op, so the pipeline still
# exercises the parallel per-ISA builds without cutting a release.
if [ "${ANVIL_BRANCH:-}" != "main" ] && [ "${ANVIL_BRANCH:-}" != "refs/heads/main" ]; then
echo "Not on main — nothing to publish."
exit 0
fi
if [ -z "${ANVIL_TOKEN:-}" ]; then
echo "ERROR: ANVIL_TOKEN secret not set." >&2
exit 1
fi
# Same version the build jobs used: latest published release + 1 (drafts
# excluded), so this resolves to the tag they created the draft under.
VERSION=$(bash ci/release.sh)
if [ -z "$VERSION" ]; then
echo "ERROR: ci/release.sh returned empty version" >&2
exit 1
fi
# A native CLI binary to drive the release commands. No cross toolchain needed
# here — this job runs on the plain base image.
echo "==> Building native anvil CLI..."
cargo build --release 2>&1
ANVIL="$PWD/target/release/anvil"
ASSETS="anvil_linux_arm64 anvil_linux_amd64 anvil_macos_arm64 anvil_macos_amd64"
# Roll the draft back if anything below fails, so a broken run leaves nothing —
# not even a draft — behind.
cleanup_release() {
echo "==> Publish failed — rolling back release $VERSION" >&2
"$ANVIL" release delete "$VERSION" --repo fangorn/anvil-cli >&2 || true
}
trap cleanup_release ERR
# Pull the four binaries the build jobs uploaded. A missing one fails the
# download and trips the rollback — so an incomplete draft never gets published.
echo "==> Downloading built binaries from draft $VERSION..."
mkdir -p dist
FILES=""
for name in $ASSETS; do
asset="${name}_${VERSION}"
"$ANVIL" release download "$VERSION" "$asset" --output "dist/${asset}" --repo fangorn/anvil-cli
FILES="$FILES ${asset}"
done
# Combined SHA256SUMS across all four (format matches `sha256sum`).
echo "==> Computing SHA256 checksums..."
(
cd dist
sha256sum $FILES > "SHA256SUMS_${VERSION}"
)
cat "dist/SHA256SUMS_${VERSION}"
"$ANVIL" release upload "$VERSION" "dist/SHA256SUMS_${VERSION}" --repo fangorn/anvil-cli
# Everything is present — flip the draft to published.
"$ANVIL" release publish "$VERSION" --repo fangorn/anvil-cli
trap - ERR
echo "==> Published release $VERSION"
ci/release.sh +4 −1
@@ -26,9 +26,12 @@
exit 1
fi
# Drafts are excluded (`.draft != true`): a multi-arch release run creates its
# draft up front, and every job — both arch builds and the publish job — must
# still compute the same next version, not bump off the in-flight draft.
LATEST=$(jq -r '
.releases
| [.[] | select(.tag_name | test("^[0-9]{4}\\.[0-9]{2}\\.[0-9]+$"))]
| [.[] | select((.tag_name | test("^[0-9]{4}\\.[0-9]{2}\\.[0-9]+$")) and (.draft != true))]
| sort_by(.tag_name | split(".") | map(tonumber))
| .[-1].tag_name // empty
' < "$TMP")