Repository navigation
380 lines (360 loc) · 17.6 KB
/
Copy pathrelease.yml
File metadata and controls
380 lines (360 loc) · 17.6 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
name: Release
on:
push:
# Publishing releases is tag-gated (see the `release` job's `if:`).
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'
# ALSO build the full cross-target matrix on main once a night —
# artifacts only, no GitHub Release. This catches cross-compilation
# breakage (musl, aarch64, Windows OpenSSL, macOS) that the CI
# workflow's single linux-gnu build cannot, so a real tag never
# discovers a broken target for the first time at release time.
# It used to run on every push to main; with dozens of merges a day each
# push started 8 runner-heavy jobs (2 on macOS) and starved PR CI, while
# cancel-in-progress threw most of them away. Nightly keeps the guarantee
# (a broken target is found within a day, before any tag) at 1/N the cost;
# dispatch against main for an on-demand check before cutting a tag.
schedule:
- cron: "23 3 * * *"
# Manual runs take no inputs: the version and the publish decision are both
# derived from the ref you dispatch against (GITHUB_REF_NAME / `refs/tags/v*`).
# Dispatch against the TAG ref to rebuild and republish a release; dispatch
# against a branch for an artifacts-only cross-compile check.
workflow_dispatch:
permissions:
contents: write
# Cancel a superseded main-branch build when new commits land; tag runs
# use the immutable ref so they never cancel each other.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
# Opt every JS action into Node 24 so the runner skips the Node 20
# deprecation warning (removal date 2026-09-16). All the action
# versions pinned here already ship a Node 24 entrypoint.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
# Tier 3 with extra room: this matrix cross-compiles (zigbuild, musl,
# Windows MSVC + vendored OpenSSL) and its cache key includes the
# target, so a Cargo.lock change makes every leg fully cold. Over the
# last 27 runs the slowest leg is aarch64-pc-windows-msvc at 38.3 min
# (median 18.9); 75 is 1.96x that and still 4.8x below GitHub's 360-min
# default. A tag run cannot rely on `cancel-in-progress` to clear a
# hang -- the tag ref is immutable, so no later push supersedes it.
timeout-minutes: 75
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
# Same-arch musl: `musl-tools` is enough (musl-gcc wraps
# the host gcc with musl libc/headers — verified working
# in the previous run).
linker_pkgs: musl-tools
linker_env: |
CC_x86_64_unknown_linux_musl=musl-gcc
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=musl-gcc
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
# Build jemalloc with a 64 KiB page (LG_PAGE=16). jemalloc refuses
# to start when the runtime page size exceeds its compile-time page,
# so a default 4 KiB build SIGABRTs on 16 KiB-page aarch64 kernels
# (Asahi, many Ampere/Graviton configs, RPi 5 16K). 64 KiB is valid
# on 4/16/64 KiB kernels — the same choice rustc and TiKV ship.
jemalloc_lg_page: "16"
# `gcc-aarch64-linux-gnu` alone provides only the compiler;
# `libc6-dev-arm64-cross` ships the cross-libc headers
# (notably `bits/libc-header-start.h`) that `cc-rs` needs
# when it shells out to the cross-gcc to build C deps like
# `ring`. Without this, ring's build.rs aborts on `<stdint.h>`.
linker_pkgs: gcc-aarch64-linux-gnu libc6-dev-arm64-cross
linker_env: |
CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
# 64 KiB jemalloc page — see the gnu target above. Same abort hits
# the musl binary on 16 KiB-page aarch64 kernels.
jemalloc_lg_page: "16"
# musl.cc's prebuilt aarch64-linux-musl-cross toolchain is
# not reachable from GitHub-hosted runners (port 443
# connection drops). cargo-zigbuild ships zig's full musl
# cross-toolchain (libc + linker) and is fetched from
# crates.io / pypi, both of which are mirrored inside GHA.
use_zigbuild: true
# Both Apple targets cross-compile from macos-14 (Apple
# Silicon). Apple's clang+ld accept `-arch x86_64` and ship
# the matching SDK, so a single arm64 host can produce
# universal-quality binaries for both archs. Avoiding
# macos-13 also dodges the heavily-congested free-tier
# x86 macOS queue (often 30+ min wait).
- target: aarch64-apple-darwin
os: macos-14
- target: x86_64-apple-darwin
os: macos-14
- target: x86_64-pc-windows-msvc
os: windows-latest
windows: true
- target: aarch64-pc-windows-msvc
os: windows-latest
windows: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install linker packages (apt)
if: matrix.linker_pkgs && startsWith(matrix.os, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends ${{ matrix.linker_pkgs }}
- name: Setup Perl + NASM for vendored OpenSSL (Windows)
if: matrix.windows == true
shell: pwsh
run: |
# openssl-src's Configure needs a Perl that understands Windows
# paths — the Git-for-Windows perl that shadows it on PATH fails
# with exit code 2. The runner image ships Strawberry Perl.
"OPENSSL_SRC_PERL=C:\Strawberry\perl\bin\perl.exe" | Out-File -Append $env:GITHUB_ENV
# x64 OpenSSL asm needs nasm; ARM64 (VC-WIN64-ARM) builds no-asm.
choco install nasm -y --no-progress
"C:\Program Files\NASM" | Out-File -Append $env:GITHUB_PATH
- name: Install zigbuild (musl targets)
if: matrix.use_zigbuild
shell: bash
run: |
set -euo pipefail
# `ziglang` PyPI package ships the zig compiler (~50 MB) — no
# external mirror, and pip is always available on the runner.
python3 -m pip install --user ziglang
# Expose `python -m ziglang` as a `zig` wrapper on PATH.
mkdir -p "$RUNNER_TEMP/bin"
cat > "$RUNNER_TEMP/bin/zig" <<'SH'
#!/usr/bin/env bash
exec python3 -m ziglang "$@"
SH
chmod +x "$RUNNER_TEMP/bin/zig"
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
# cargo-zigbuild from crates.io. `--locked` keeps it
# reproducible across runs.
cargo install --locked cargo-zigbuild
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/cache
~/.cargo/registry/index
~/.cargo/git/db
engine/target
key: cargo-${{ matrix.target }}-${{ hashFiles('engine/Cargo.lock') }}
restore-keys: cargo-${{ matrix.target }}-
- name: Build xerj-server
shell: bash
working-directory: engine
run: |
# Export per-target linker env (cargo reads CARGO_TARGET_*_LINKER).
if [ -n '${{ matrix.linker_env }}' ]; then
while IFS= read -r line; do
[ -z "$line" ] && continue
export "$line"
done <<< '${{ matrix.linker_env }}'
fi
# jemalloc-sys reads this at build time and passes --with-lg-page.
# Set on 16 KiB-page-prone aarch64 targets so the binary starts on
# 4/16/64 KiB kernels instead of SIGABRTing before main (see #1663).
if [ -n '${{ matrix.jemalloc_lg_page }}' ]; then
export JEMALLOC_SYS_WITH_LG_PAGE='${{ matrix.jemalloc_lg_page }}'
echo "JEMALLOC_SYS_WITH_LG_PAGE=${JEMALLOC_SYS_WITH_LG_PAGE}"
fi
# Binary --version is env!("CARGO_PKG_VERSION"); without this it stays
# frozen at whatever engine/Cargo.toml last shipped (rc.18). Stamp it
# from the tag — but ONLY on a v-tag: this matrix also runs on main
# pushes / workflow_dispatch as a cross-compile canary, where
# GITHUB_REF_NAME is a branch name and `version = "main"` would make
# cargo reject the manifest and fail the build.
case "${GITHUB_REF_NAME}" in
v[0-9]*)
VER="${GITHUB_REF_NAME#v}"
# perl, not `sed 0,/re/`: the address-0 form is a GNU extension and
# the macOS legs run BSD sed, which errors on it and (being non-final
# in `sed && rm`) leaves the version unchanged. This rewrites only the
# first `version = "..."` line, the [workspace.package] version.
perl -0777 -i -pe 's/^version = "[^"]*"/version = "'"$VER"'"/m' Cargo.toml
echo "set workspace version to ${VER}"; grep -m1 '^version = ' Cargo.toml
cargo update --workspace --offline 2>/dev/null || cargo update --workspace
;;
*)
echo "ref ${GITHUB_REF_NAME} is not a version tag; leaving engine/Cargo.toml version as-is (canary build)"
;;
esac
if [ "${{ matrix.use_zigbuild }}" = "true" ]; then
cargo zigbuild --release --locked --target ${{ matrix.target }} -p xerj-server
else
cargo build --release --locked --target ${{ matrix.target }} -p xerj-server
fi
- name: Stage artifacts (Unix)
if: matrix.windows != true
shell: bash
working-directory: engine
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
STAGE="xerj-${VERSION}-${{ matrix.target }}"
mkdir -p "../dist/${STAGE}"
cp "target/${{ matrix.target }}/release/xerj" "../dist/${STAGE}/xerj"
[ -f ../README.md ] && cp ../README.md "../dist/${STAGE}/" || true
[ -f ../LICENSE ] && cp ../LICENSE "../dist/${STAGE}/" || true
tar -C ../dist -czf "../dist/${STAGE}.tar.gz" "${STAGE}"
cd ../dist
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${STAGE}.tar.gz" > "${STAGE}.tar.gz.sha256"
else
shasum -a 256 "${STAGE}.tar.gz" > "${STAGE}.tar.gz.sha256"
fi
# .deb for the linux-gnu targets (issue #810), packed from the SAME
# staged binary the tar.gz above just packed — same file, no rebuild,
# so the two assets cannot drift. musl targets stay tar.gz-only.
# These legs all run on ubuntu-latest, where dpkg-deb is preinstalled.
# The control-file version must be a real Debian version string: on a
# tag that is the tag; on a main-push canary the ref name ("main") is
# not, so the engine workspace version ships instead while the ASSET
# keeps the stage name (xerj-main-<target>) to match its tar.gz
# sibling — build-deb.sh refuses a non-numeric version, which is the
# guard that keeps that honest.
case "${{ matrix.target }}" in
x86_64-unknown-linux-gnu|aarch64-unknown-linux-gnu)
command -v dpkg-deb >/dev/null 2>&1 \
|| { echo "dpkg-deb not on PATH on ${RUNNER_OS} — cannot build the .deb asset" >&2; exit 1; }
case "${GITHUB_REF_NAME}" in
v[0-9]*) DEB_VERSION="${GITHUB_REF_NAME#v}" ;;
*) DEB_VERSION=$(sed -n 's/^version = "\(.*\)"$/\1/p' ../engine/Cargo.toml | head -1) ;;
esac
../scripts/build-deb.sh \
--stage-dir "../dist/${STAGE}" \
--target "${{ matrix.target }}" \
--version "${DEB_VERSION}" \
--out-dir ../dist
;;
esac
- name: Stage artifacts (Windows)
if: matrix.windows == true
shell: pwsh
working-directory: engine
run: |
$Version = "${env:GITHUB_REF_NAME}".TrimStart('v')
$Stage = "xerj-${Version}-${{ matrix.target }}"
New-Item -ItemType Directory -Force -Path "..\dist\$Stage" | Out-Null
Copy-Item "target\${{ matrix.target }}\release\xerj.exe" "..\dist\$Stage\xerj.exe"
if (Test-Path "..\README.md") { Copy-Item "..\README.md" "..\dist\$Stage\" }
if (Test-Path "..\LICENSE") { Copy-Item "..\LICENSE" "..\dist\$Stage\" }
Push-Location ..\dist
Compress-Archive -Path $Stage -DestinationPath "$Stage.zip" -Force
$Hash = Get-FileHash -Algorithm SHA256 "$Stage.zip"
"$($Hash.Hash.ToLower()) $Stage.zip" | Out-File "$Stage.zip.sha256" -Encoding ascii
Pop-Location
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: xerj-${{ matrix.target }}
path: |
dist/xerj-*.tar.gz
dist/xerj-*.zip
dist/xerj-*.deb
dist/xerj-*.sha256
if-no-files-found: error
retention-days: 7
release:
name: Publish GitHub Release
needs: build
runs-on: ubuntu-latest
timeout-minutes: 15
if: startsWith(github.ref, 'refs/tags/v')
steps:
# Must be the FIRST step of this job: actions/checkout wipes the
# workspace when it is not already a matching git repo
# (prepareExistingDirectory -> rmRF every entry), so running it after
# download-artifact would delete dist/ and ship a release with no
# assets. Full history so the generate-notes step below sees every
# v-tag (the repo clones at ~110 MB; seconds against this job's
# 15-min budget).
- name: Checkout (full history, for the release-notes tag list)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all build artifacts
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: List artifacts
run: ls -la dist/
# GitHub anchors auto-generated release notes ("Full Changelog") on the
# most recently *published* release. Since the daily pack-rust-vulns-*
# corpus-pack releases started, that anchor is a corpus pack, not the
# previous engine release: v1.0.0-rc.78 shipped comparing
# pack-rust-vulns-2026-09-29...v1.0.0-rc.78 and had to be hand-patched
# via REST. So the compare base is computed here and pinned explicitly,
# instead of trusting auto-detection at publish time.
- name: Generate release notes (compare base pinned to previous v-tag)
id: relnotes
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
CURRENT="${GITHUB_REF_NAME}"
# The derivation lives in .github/scripts/prev_tag.sh so the code
# CI runs and the code under test are the same file: the
# version-successor of this cut among ^vX.Y.Z tags (an rc suffix
# counts — an rc is a real xerj release). pack-rust-vulns-*, backup/*
# and rb* never match. versionsort.suffix=-rc keeps a final vX.Y.Z
# above its own -rc.N — plain --sort=-v:refname does NOT, which
# would have pinned the first v1.0.1 cut to v1.0.0-rc.79 instead of
# v1.0.0. The self-test locks that ordering in before the output is
# trusted (and fails on a runner git too old for versionsort.suffix).
bash .github/scripts/prev_tag.sh --self-test
PREV="$(bash .github/scripts/prev_tag.sh "${CURRENT}")"
if [ -n "${PREV}" ]; then
echo "release-notes compare base: ${PREV}...${CURRENT}"
# Renders the notes without creating anything; previous_tag_name
# pins the compare range. (The path is .../generate-notes — the
# older .../generate-release-notes spelling 404s.)
gh api --method POST \
"/repos/${GITHUB_REPOSITORY}/releases/generate-notes" \
-f tag_name="${CURRENT}" \
-f previous_tag_name="${PREV}" \
--jq '.body' > release-notes.md
echo "prev=${PREV}" >> "${GITHUB_OUTPUT}"
else
# First-ever v-tag: no predecessor to pin against; the publish
# step falls back to GitHub's auto-generated notes.
echo "no previous v-tag found - falling back to auto-generated notes"
echo "prev=" >> "${GITHUB_OUTPUT}"
fi
- name: Publish release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: xerj ${{ github.ref_name }}
draft: false
prerelease: false
# Notes were pre-generated above against the pinned compare base.
# When no previous v-tag exists (prev empty) both inputs invert to
# the pre-fix behaviour: auto-generated notes, no body file. Both
# halves are safe with this action — the @v2 dist ignores an empty
# body_path (`if (input_body_path)`) and compares
# generate_release_notes to the exact string "true".
body_path: ${{ steps.relnotes.outputs.prev && 'release-notes.md' || '' }}
generate_release_notes: ${{ steps.relnotes.outputs.prev && 'false' || 'true' }}
files: |
dist/*.tar.gz
dist/*.zip
dist/*.deb
dist/*.sha256