Files
chains/.hanzo/workflows/release.yml
zeekayandHanzo Dev 2a95c62551 Workflows ask for the runner label that exists
One label per runner, spelled <os>-<arch>. The aliases these jobs named
were removed, and a job naming a label no runner carries waits instead of
failing.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 01:54:37 -07:00

211 lines
7.8 KiB
YAML

name: Release
# Builds one ghcr.io/luxfi/chains/<vm> plugin image per entry in
# .github/images.json, on the native runners (git.hanzo.ai → git-runner).
#
# This replaces .github/workflows/release.yml, which delegated the build to
# `hanzoai/.github/.github/workflows/docker-build.yml@main`. A cross-org
# workflow_call from luxfi/* into hanzoai/.github fails before any job starts —
# luxfi/operator recorded the same thing against the same reusable
# ("startup_failure on EVERY run since the migration to that reusable ...
# every attempt failed at 0-2s with zero jobs dispatched") and inlined its build
# for exactly this reason. So these thirteen plugin images have not been built
# by this workflow at all.
#
# The build is inlined here instead. It is short: compile the VM to build/<vm>
# on the runner, then Dockerfile.plugin copies that one binary into a scratch
# image. Two runner labels were wrong on the way in — the `plan` job named
# `lux-build-amd64` and the reusable was handed `["self-hosted","linux","amd64"]`,
# neither of which any runner on this forge carries, and a job naming a label
# nothing carries does not fail: it waits.
#
# ghcr auth is GHCR_USER / GHCR_TOKEN, org Actions secrets sourced from KMS. The
# instance GITHUB_TOKEN is a Hanzo Git token and has no rights at ghcr.io.
#
# Both architectures, each compiled on its own native pool. Dockerfile.plugin
# compiles nothing — it copies build/<vm>, the binary the step above it produced
# with `go build` on the runner — so the architecture of the image is the
# architecture of the machine that ran that go build. Asking one amd64 runner
# for two platforms would therefore stamp an arm64 manifest onto an amd64 ELF:
# an image that pulls happily on an arm64 host and dies with `exec format
# error`, which is worse than the amd64-only manifest it replaced, because the
# manifest promises it will run. One job per arch compiles natively — evm's CGO
# included, with no cross-toolchain and no emulation — pushes
# <image>:sha-<sha7>-<arch>, and the merge job joins the pair under the release
# names.
#
# Semver-only: a v* tag produces the version tags, and every run produces an
# immutable sha tag. No :latest, no floating tags.
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
permissions:
contents: read
# A release is not made irrelevant by a newer one — let it finish.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
plan:
runs-on: [linux-amd64]
timeout-minutes: 10
outputs:
images: ${{ steps.load.outputs.images }}
steps:
- uses: actions/checkout@v4
- id: load
run: echo "images=$(jq -c . .github/images.json)" >> "$GITHUB_OUTPUT"
build:
needs: plan
strategy:
fail-fast: false
matrix:
image: ${{ fromJson(needs.plan.outputs.images) }}
arch: [amd64, arm64]
runs-on: ['linux-${{ matrix.arch }}']
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- name: Build ${{ matrix.image.vm }}
env:
CGO_ENABLED: ${{ matrix.image.cgo }}
# blst needs the portable build when CGO is on.
CGO_CFLAGS: ${{ matrix.image.cgo == '1' && '-O2 -D__BLST_PORTABLE__' || '' }}
run: |
set -euo pipefail
mkdir -p build
vm='${{ matrix.image.vm }}'
# Some VMs are their own main package, others keep it under cmd/plugin.
if [ -f "${vm}/main.go" ]; then
go build -trimpath -ldflags="-s -w" -o "build/${vm}" "./${vm}"
else
go build -trimpath -ldflags="-s -w" -o "build/${vm}" "./${vm}/cmd/plugin"
fi
test -s "build/${vm}"
- uses: docker/setup-buildx-action@v3
with:
driver: docker-container
driver-opts: network=host
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USER }}
password: ${{ secrets.GHCR_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ matrix.image.image }}
flavor: latest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern=v{{version}}
type=sha,prefix=sha-
- name: Name this arch's image
id: arch
# A per-arch name, never a release name: only the merge job writes those.
# A release tag that resolves to one architecture is worse than one that
# does not exist yet, because it pulls on that architecture and fails on
# the other.
run: echo "image=${{ matrix.image.image }}:sha-${GITHUB_SHA:0:7}-${{ matrix.arch }}" >> "$GITHUB_OUTPUT"
- uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.plugin
platforms: linux/${{ matrix.arch }}
build-args: |
VM=${{ matrix.image.vm }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.arch.outputs.image }}
provenance: false
push: true
merge:
needs: [plan, build]
strategy:
fail-fast: false
matrix:
image: ${{ fromJson(needs.plan.outputs.images) }}
# Registry-side only: imagetools reads the two per-arch manifests and writes
# a list that points at both. No builder and no platform of its own, so which
# pool runs this does not matter.
runs-on: [linux-amd64]
timeout-minutes: 15
steps:
- name: Compute the release names
id: meta
# The same set docker/metadata-action produces in the build job: the
# semver pair on a v* tag, and the immutable sha tag on every run.
run: |
set -euo pipefail
SHA_TAG="${{ matrix.image.image }}:sha-${GITHUB_SHA:0:7}"
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VERSION="${GITHUB_REF#refs/tags/v}"
{
echo "tags<<EOF"
echo "${{ matrix.image.image }}:v${VERSION}"
echo "${{ matrix.image.image }}:${VERSION}"
echo "$SHA_TAG"
echo "EOF"
} >> "$GITHUB_OUTPUT"
else
{
echo "tags<<EOF"
echo "$SHA_TAG"
echo "EOF"
} >> "$GITHUB_OUTPUT"
fi
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USER }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Join both architectures under every release name
run: |
set -euo pipefail
base="${{ matrix.image.image }}:sha-${GITHUB_SHA:0:7}"
args=''
while IFS= read -r t; do
[ -n "$t" ] && args="$args -t $t"
done <<< "${{ steps.meta.outputs.tags }}"
# shellcheck disable=SC2086
docker buildx imagetools create $args "${base}-amd64" "${base}-arm64"
- name: Prove every release name carries BOTH architectures
# A manifest list with one entry pulls fine on the arch that built it and
# fails everywhere else, which is exactly the state this workflow is
# replacing. Counting the platforms is what makes "multi-arch" a fact
# rather than an intention.
run: |
set -euo pipefail
while IFS= read -r t; do
[ -z "$t" ] && continue
got=$(docker buildx imagetools inspect "$t" --format '{{range .Manifest.Manifests}}{{.Platform.OS}}/{{.Platform.Architecture}} {{end}}')
echo "$t -> ${got}"
for want in linux/amd64 linux/arm64; do
case " $got " in
*" $want "*) ;;
*) echo "::error::$t does not carry $want"; exit 1 ;;
esac
done
done <<< "${{ steps.meta.outputs.tags }}"