Skip to content

fix(doctor,sweep): waive system-requirement errors under --skip-safet… #39

fix(doctor,sweep): waive system-requirement errors under --skip-safet…

fix(doctor,sweep): waive system-requirement errors under --skip-safet… #39

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'
# mlugg/setup-zig@v2 still declares `using: node20` in its
# action.yml (verified upstream as of 2026-05-02). GitHub's official
# opt-in for forcing Node 24 on legacy actions is this env var per
# https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/.
# Drop this once setup-zig ships a node24 action.yml.
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
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'
permissions:
# `id-token: write` is required for keyless cosign signing via
# the GitHub Actions OIDC token. The token is short-lived
# (~5 min), scoped to this workflow + this run, and exchanged
# at Sigstore Fulcio for a one-time signing certificate. No
# long-lived keys are stored anywhere in the repo or in CI.
# See https://docs.sigstore.dev/cosign/keyless.
id-token: write
contents: read
# GitHub-hosted Ubuntu — universal build host. zig + cargo-zigbuild
# cross-compile every target from the same x86_64 Linux box; no
# macOS SDK / osxcross / musl-tools needed. mlugg/setup-zig
# materialises zig itself in-job. Free for public repos, runner
# is fresh each run (no leftover footprint to fight).
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
- target: x86_64-apple-darwin
- target: x86_64-unknown-linux-musl
- target: aarch64-unknown-linux-musl # AWS Graviton, Hetzner CAX, Pi 4/5
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@v6
- 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@v2
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"
# Keyless cosign signing via Sigstore + GitHub Actions OIDC.
# The signing identity recorded in the Rekor transparency log is:
# https://github.com/<repo>/.github/workflows/release.yml@refs/tags/<tag>
# which install.sh's cosign verify-blob path matches against.
- uses: sigstore/cosign-installer@v3
with:
# Pinned to a long-stable cosign release. Newer minor versions
# have been signature-format-compatible since 2.x; bump
# deliberately when audit log shape changes are needed.
cosign-release: 'v2.4.1'
- name: Sign archive (cosign keyless)
env:
ARCHIVE: ${{ steps.package.outputs.archive }}
shell: bash
run: |
set -euo pipefail
cd dist
# `--yes` skips the "I'm about to log to a public transparency
# ledger, ok?" prompt — required for non-interactive CI.
cosign sign-blob --yes \
--output-signature "${ARCHIVE}.sig" \
--output-certificate "${ARCHIVE}.pem" \
"$ARCHIVE"
ls -lh
- uses: actions/upload-artifact@v7
with:
name: mxnode-${{ matrix.target }}
path: dist/*
retention-days: 14
if-no-files-found: error
build-min:
# Stage E (per docs/superpowers/specs/2026-05-05-binary-size-rollout.md):
# parallel build with nightly Rust + `-Zbuild-std=std,panic_abort` +
# `-Cpanic=immediate-abort` (gated by `-Zunstable-options`). Produces
# an opt-in `-min.tar.gz` artefact ~10-18% smaller than the stable
# build for operators on bandwidth-constrained hosts. Uses the same
# cargo-zigbuild + cosign + signing flow as the stable build job —
# only the toolchain channel and the archive name differ.
#
# Workflow input handling follows the same pattern as `build`: the
# `tag` input is read into the `INPUT_TAG` env-var via the env: map
# and never interpolated directly into a `run:` line. See:
# https://github.blog/security/vulnerability-research/how-to-catch-github-actions-workflow-injections-before-attackers-do/
name: build-min ${{ matrix.target }}
if: github.repository_owner == 'XOXNO'
# Don't block the release on nightly churn. Stage E is a bonus
# variant — the canonical artefact remains the stable one.
continue-on-error: true
permissions:
id-token: write
contents: read
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
- target: x86_64-apple-darwin
- target: x86_64-unknown-linux-musl
- target: aarch64-unknown-linux-musl
steps:
- name: Resolve release tag (build-min job)
id: tag
env:
INPUT_TAG: ${{ github.event.inputs.tag }}
REF_NAME: ${{ github.ref_name }}
shell: bash
run: |
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
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@v6
- uses: dtolnay/rust-toolchain@nightly
with:
components: rust-src
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
# Distinct cache key from the stable build — nightly's
# rebuilt-std artefacts must not commingle with stable's
# prebuilt std.
key: ${{ matrix.target }}-min
- name: Install zig (universal cross-toolchain)
uses: mlugg/setup-zig@v2
with:
version: 0.13.0
- name: Install cargo-zigbuild
# Same `--locked` reasoning as the stable job. Installed once
# per runner; nightly cargo invokes the same binary.
run: cargo install --locked cargo-zigbuild
- name: Build (cargo +nightly zigbuild + build-std)
env:
TARGET: ${{ matrix.target }}
# `-Cpanic=immediate-abort` requires -Zunstable-options (it's
# gated even though the underlying mechanism is stable in shape).
# See: https://github.com/rust-lang/rust/issues/32837
run: |
RUSTFLAGS="-C strip=symbols -Cpanic=immediate-abort -Zunstable-options" \
cargo +nightly zigbuild --release --target "$TARGET" --bin mxnode \
-Z build-std=std,panic_abort \
-Z unstable-options
- 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
BIN="target/${TARGET}/release/mxnode"
DIST=dist
ARCHIVE="mxnode-${RELEASE_TAG}-${TARGET}-min.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"
(cd "$DIST" && shasum -a 256 "$ARCHIVE" > "$ARCHIVE.sha256" 2>/dev/null \
|| sha256sum "$ARCHIVE" > "$ARCHIVE.sha256")
ls -lh "$DIST"
echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT"
- uses: sigstore/cosign-installer@v3
with:
cosign-release: 'v2.4.1'
- name: Sign archive (cosign keyless)
env:
ARCHIVE: ${{ steps.package.outputs.archive }}
shell: bash
run: |
set -euo pipefail
cd dist
cosign sign-blob --yes \
--output-signature "${ARCHIVE}.sig" \
--output-certificate "${ARCHIVE}.pem" \
"$ARCHIVE"
ls -lh
- uses: actions/upload-artifact@v7
with:
name: mxnode-${{ matrix.target }}-min
path: dist/*
retention-days: 14
if-no-files-found: error
release:
name: publish release
# Wait for both jobs but only require `build` to succeed — `build-min`
# is opt-in / experimental and `continue-on-error: true` already lets
# nightly churn fail without blocking. Use `if: always()` + an explicit
# success check on `build` so a failed `build-min` doesn't gate
# release publication.
needs: [build, build-min]
if: always() && github.repository_owner == 'XOXNO' && needs.build.result == 'success'
# Same runner pool as the build jobs.
runs-on: ubuntu-latest
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@v6
- name: Download every per-target artifact
uses: actions/download-artifact@v8
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 + one .sig + one .pem.
find artifacts -type f \
\( -name '*.tar.gz' -o -name '*.sha256' \
-o -name '*.sig' -o -name '*.pem' \) \
-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@v3
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/*.tar.gz.sig
dist/*.tar.gz.pem
dist/SHA256SUMS