Skip to content

release

release #6

Workflow file for this run

name: release
# Triggers:
# - push of a `v*` tag (`git tag v0.1.0 && git push --tags`) — the
# normal release path.
# - manual `workflow_dispatch` with a tag input — useful for
# re-running a build after fixing a release-time issue without
# bumping the version.
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
tag:
description: 'Version tag (e.g. v0.1.0). Must already exist as a git tag.'
required: true
env:
CARGO_TERM_COLOR: always
# Strip debuginfo aggressively — the release profile already does
# `strip = "symbols"` but cargo-zigbuild can leave more behind.
RUSTFLAGS: '-C strip=symbols'
permissions:
contents: read
jobs:
build:
name: build ${{ matrix.target }}
# Self-hosted runner gate: only run on the canonical repo so a
# malicious fork can't dispatch this workflow against the
# XOXNO Hetzner box and execute arbitrary code on it. Tag-push
# and manual-dispatch already require write access in the
# canonical repo; this is belt-and-braces.
if: github.repository_owner == 'XOXNO'
# Single runner for every target. The self-hosted XOXNO Hetzner
# box (Ubuntu x86_64) acts as the universal build host:
#
# - Linux x86_64 musl — native, via zig as a hermetic linker
# - macOS arm64 — cross-compile via zig (Mach-O 64)
# - macOS x86_64 — cross-compile via zig (Mach-O 64)
#
# `cargo-zigbuild` invokes a single `zig` binary as the C linker
# for every target, which means we don't need the macOS SDK,
# osxcross, Docker, musl-tools, or any per-target toolchain.
# zig itself ships every libc shim it needs.
runs-on: [self-hosted, Linux, X64]
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
- target: x86_64-apple-darwin
- target: x86_64-unknown-linux-musl
# aarch64 Linux: deliberately dropped from the matrix.
# Add it back if/when an operator deploys to AWS Graviton
# / ARM Hetzner / Pi — until then it'd be a maintenance
# burden with no consumer.
steps:
# Resolve the release tag once, into the per-job env. Every
# downstream shell step reads `$RELEASE_TAG` — never substitutes
# `${{ github.event.inputs.tag }}` directly into a shell line,
# which would let a maliciously-crafted dispatch input inject
# commands. See:
# https://github.blog/security/vulnerability-research/how-to-catch-github-actions-workflow-injections-before-attackers-do/
- name: Resolve release tag (build job)
id: tag
env:
INPUT_TAG: ${{ github.event.inputs.tag }}
REF_NAME: ${{ github.ref_name }}
shell: bash
run: |
# Prefer the workflow-dispatch input; fall back to the tag
# that was pushed.
TAG="${INPUT_TAG:-$REF_NAME}"
if [ -z "$TAG" ]; then
echo "no tag resolved — push a v* tag or pass `tag` input" >&2
exit 1
fi
# Allowlist the tag shape — must look like a semver tag.
# Anything else is rejected up front so we never expand a
# hostile string into an archive name or shell command.
case "$TAG" in
v[0-9]*) ;;
*)
echo "rejecting tag '$TAG' — must start with v + digit" >&2
exit 1
;;
esac
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "RELEASE_TAG=$TAG" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
# Targets are also declared in rust-toolchain.toml; this
# mirror exists so the `stable` toolchain dtolnay installs
# carries them too. The actual build runs under 1.94.1
# (per rust-toolchain.toml) which auto-installs targets
# the first time rustup materialises that channel.
targets: ${{ matrix.target }}
- name: Materialise pinned toolchain + targets
# Triggers rustup to install 1.94.1 (per rust-toolchain.toml)
# WITH the targets declared there. Without this, the first
# cargo invocation would silently install 1.94.1 minus the
# cross targets and the build would explode with
# "can't find crate for `core`".
run: rustup show active-toolchain || rustup show
- uses: Swatinem/rust-cache@v2
with:
# Per-target cache — Linux and macOS targets warm up
# different artifact sets.
key: ${{ matrix.target }}
- name: Install zig (universal cross-toolchain)
# Pinned to a long-published, fully-mirrored version. 0.16.0
# was unreachable on every mirror at v0.1.0 release time
# (404 / 503 across machengine, hryx, nekos, linus,
# liujiacai, ziglang.org). 0.13.0 has been mirror-stable
# since 2024-08 and is what cargo-zigbuild's CI also uses.
uses: mlugg/setup-zig@v1
with:
version: 0.13.0
- name: Install cargo-zigbuild
# `--locked` so we don't silently grab a fresh transitive
# dep mid-release. Cached by Swatinem/rust-cache above.
run: cargo install --locked cargo-zigbuild
- name: Build (cargo-zigbuild)
env:
TARGET: ${{ matrix.target }}
run: cargo zigbuild --release --target "$TARGET" --bin mxnode
- name: Sanity check (binary exists + is the right ELF/Mach-O)
env:
TARGET: ${{ matrix.target }}
shell: bash
run: |
BIN="target/${TARGET}/release/mxnode"
ls -lh "$BIN"
file "$BIN"
[ -x "$BIN" ]
- name: Package
id: package
env:
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
# $RELEASE_TAG and $TARGET are both controlled — the tag is
# allowlisted to ^v[0-9].* above, the target comes from the
# matrix (workflow author).
BIN="target/${TARGET}/release/mxnode"
DIST=dist
ARCHIVE="mxnode-${RELEASE_TAG}-${TARGET}.tar.gz"
mkdir -p "$DIST"
STAGE=$(mktemp -d)
cp "$BIN" "$STAGE/mxnode"
[ -f README.md ] && cp README.md "$STAGE/"
[ -f LICENSE ] && cp LICENSE "$STAGE/"
tar -czf "$DIST/$ARCHIVE" -C "$STAGE" .
rm -rf "$STAGE"
# Per-archive sha256, joined into SHA256SUMS in the publish job.
(cd "$DIST" && shasum -a 256 "$ARCHIVE" > "$ARCHIVE.sha256" 2>/dev/null \
|| sha256sum "$ARCHIVE" > "$ARCHIVE.sha256")
ls -lh "$DIST"
echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
with:
name: mxnode-${{ matrix.target }}
path: dist/*
retention-days: 14
if-no-files-found: error
release:
name: publish release
needs: build
# Stay on the same runner pool the build jobs ran on — single
# source of CI hardware, no GitHub-hosted minutes consumed.
if: github.repository_owner == 'XOXNO'
runs-on: [self-hosted, Linux, X64]
permissions:
# Required by softprops/action-gh-release to attach assets +
# write release notes.
contents: write
steps:
- name: Resolve release tag (publish job)
id: tag
env:
INPUT_TAG: ${{ github.event.inputs.tag }}
REF_NAME: ${{ github.ref_name }}
shell: bash
run: |
TAG="${INPUT_TAG:-$REF_NAME}"
case "$TAG" in
v[0-9]*) ;;
*)
echo "rejecting tag '$TAG' — must start with v + digit" >&2
exit 1
;;
esac
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
- name: Download every per-target artifact
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Stage release files
shell: bash
run: |
set -euo pipefail
mkdir -p dist
# Flatten — each `mxnode-<target>` artifact dir contains
# one .tar.gz + one .sha256 stub.
find artifacts -type f \( -name '*.tar.gz' -o -name '*.sha256' \) \
-exec cp -v {} dist/ \;
# Combine the per-archive .sha256 stubs into one
# SHA256SUMS file the install script can curl directly.
(cd dist && cat *.sha256 > SHA256SUMS && rm *.sha256)
ls -lh dist
- name: Create / update GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: ${{ steps.tag.outputs.tag }}
generate_release_notes: true
fail_on_unmatched_files: true
files: |
dist/*.tar.gz
dist/SHA256SUMS