Files
zeekayandHanzo Dev 6ab4e2e48b ci: a runner label names the platform, not the org
hanzo-build-linux-amd64, lux-build-linux-amd64, zoo-build-linux-amd64 and
every other org-prefixed spelling all meant the same machine. Which org a
job belongs to does not change which host can build it, so the org bought
one pool name per org per arch and each could go stale on its own -- the
evo host alone answered to fourteen names for one label.

One vocabulary now: linux-amd64, linux-arm64, macos-arm64, win-amd64. The
generic labels every plain workflow uses (ubuntu-latest, self-hosted) are
untouched, so naive CI keeps working.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 12:08:05 -07:00

205 lines
7.8 KiB
YAML

name: Release
# Builds ghcr.io/luxfi/mpc, cuts the GitHub release, and tells luxfi/universe
# the image exists — on the native runners (git.hanzo.ai → git-runner).
#
# This replaces .github/workflows/docker.yml and .github/workflows/release.yml,
# both of which fired on the same `v*` tag and both of whose jobs named
# `runs-on: lux-build-amd64`. No runner on this forge carries that label — the
# pools are `lux-build-linux-<arch>` — and a job naming a label nothing carries
# does not fail, it waits. Two workflows racing on one tag with no shared
# concurrency group is also how a release ends up half-published; they are one
# workflow here, under one group that does not cancel.
#
# ghcr auth is GHCR_USER / GHCR_TOKEN, org Actions secrets sourced from KMS. The
# old workflows logged in as `github.actor` with the instance GITHUB_TOKEN,
# which on this forge is a Hanzo Git token with no rights at ghcr.io.
#
# arm64 is built under QEMU on the amd64 pool. That is acceptable for a docker
# build and it is what the old workflow did; the Go binaries below cross-compile
# natively.
on:
push:
tags: ['v*']
workflow_dispatch:
permissions:
contents: write
packages: write
# A release is not made irrelevant by a newer one — let it finish.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
image:
strategy:
fail-fast: false
matrix:
arch: [amd64, arm64]
runs-on: [linux-amd64]
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
if: matrix.arch == 'arm64'
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- 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 }}
- name: Write the private-module token for BuildKit
# Stripped of trailing whitespace on purpose. Dockerfile:50 interpolates
# this into a URL:
# git config url."https://x-access-token:$(cat /run/secrets/gh_token)@github.com/"
# so a single trailing newline in the stored secret truncates the URL at
# it and git resolves the credential prefix as the HOST:
# Could not resolve host: x-access-token
# luxfi/node hit exactly this. The secret's bytes are not ours to
# control, so the trim happens here.
run: |
set -euo pipefail
umask 077
printf '%s' '${{ secrets.UNIVERSE_PAT }}' | tr -d '\r\n' > "${RUNNER_TEMP}/gh_token"
if [ ! -s "${RUNNER_TEMP}/gh_token" ]; then
echo "::error::UNIVERSE_PAT is empty — the luxfi/hsm module fetch will fail"
exit 1
fi
- name: Build and push (${{ matrix.arch }})
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
platforms: linux/${{ matrix.arch }}
secrets: |
gh_token=${{ runner.temp }}/gh_token
provenance: false
push: true
tags: ghcr.io/luxfi/mpc:${{ github.ref_name }}-${{ matrix.arch }}
manifest:
needs: image
runs-on: [linux-amd64]
timeout-minutes: 20
steps:
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USER }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Compose the multi-arch tag
# imagetools, not `docker manifest create`: `docker buildx build --push`
# emits a per-arch OCI index, and `docker manifest create` rejects those
# ("X is a manifest list"). imagetools handles them.
run: |
set -euo pipefail
IMG=ghcr.io/luxfi/mpc
TAG='${{ github.ref_name }}'
docker buildx imagetools create -t "$IMG:$TAG" \
"$IMG:${TAG}-amd64" \
"$IMG:${TAG}-arm64"
- name: Verify the pushed manifest resolves
# A tag that names nothing pullable is how a deployment ends up pinned
# to a phantom: the pin applies, the pod never starts, and the tag looks
# like a release. Prove it before the job is green.
run: |
set -euo pipefail
img="ghcr.io/luxfi/mpc:${{ github.ref_name }}"
for i in 1 2 3 4 5 6; do
if docker manifest inspect "$img" >/dev/null 2>&1; then
echo "$img is resolvable"; exit 0
fi
echo "manifest not visible yet (attempt ${i}/6) — retrying"; sleep 5
done
echo "::error::$img not pullable after push"; exit 1
binaries:
strategy:
fail-fast: false
matrix:
arch: [amd64, arm64]
# arm64 cross-compiles via GOARCH; CGO_ENABLED=0 keeps it pure Go, so one
# amd64 runner covers both and no cross-compiler is needed.
runs-on: [linux-amd64]
timeout-minutes: 30
# luxfi/* are PUBLIC modules, fetched and checksum-verified through the
# default immutable proxy.golang.org + sum.golang.org. No GOPRIVATE /
# GONOSUMDB / GOPROXY=direct bypass — those route around the checksum db.
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build (cross-compile via GOARCH)
run: |
GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=${{ matrix.arch }} \
go build -trimpath -ldflags="-s -w" -o mpcd ./cmd/mpcd/
# v3, not v4+: this runs on git.hanzo.ai, which presents as GHES, and the
# v4 action refuses outright — "@actions/artifact v2.0.0+ ... are not
# currently supported on GHES". Measured over 30 days on this forge, v4 is
# 0 successes in 72 attempts and v3 is 132 in 133. Both matrix legs built
# mpcd and then threw it away at the last step, so two good builds read as
# two failures — 20 of them in the last week alone.
- uses: actions/upload-artifact@v3
with:
name: mpcd-linux-${{ matrix.arch }}
path: mpcd
release:
needs: binaries
runs-on: [linux-amd64]
timeout-minutes: 20
steps:
# v3 for the same reason the upload above is v3, and it has to be the SAME
# version: v4 refuses this forge outright, and a v4 download cannot read a
# v3 artifact even where it runs. Fixing one side and not the other left
# every release building both binaries, publishing both images, and then
# failing at the last step — which reads as a failed release for a tag
# whose image is already live.
- uses: actions/download-artifact@v3
with:
path: dist/
- uses: softprops/action-gh-release@v2
with:
files: dist/**/*
notify:
needs: [manifest, release]
runs-on: [linux-amd64]
timeout-minutes: 10
steps:
- name: Tell luxfi/universe the image is published
# Called against api.github.com explicitly. The old workflow used
# peter-evans/repository-dispatch, which resolves GITHUB_API_URL — here
# that is this forge's API, where a github.com PAT does not authenticate
# and luxfi/universe's dispatch endpoint does not live.
run: |
set -euo pipefail
curl -fsSL -X POST https://api.github.com/repos/luxfi/universe/dispatches \
-H 'Accept: application/vnd.github+json' \
-H 'Authorization: Bearer ${{ secrets.UNIVERSE_PAT }}' \
-d '{
"event_type": "image-published",
"client_payload": {
"service": "mpc",
"image": "ghcr.io/luxfi/mpc:${{ github.ref_name }}",
"tag": "${{ github.ref_name }}"
}
}'