-
Notifications
You must be signed in to change notification settings - Fork 0
429 lines (392 loc) · 15.7 KB
/
release.yml
File metadata and controls
429 lines (392 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
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