fix(doctor,sweep): waive system-requirement errors under --skip-safet… #39
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |