-
Notifications
You must be signed in to change notification settings - Fork 1
333 lines (308 loc) · 15.4 KB
/
Copy pathrelease.yml
File metadata and controls
333 lines (308 loc) · 15.4 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
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
create-release:
name: Create Release
runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- name: Extract changelog content
id: extract_changelog
run: |
# Extract the latest version's changelog content
VERSION="${{ github.ref_name }}"
# Remove 'v' prefix if present
VERSION_NUM="${VERSION#v}"
# Find the section for this version and extract content until next version or end
awk "/^## \[$VERSION_NUM\]/{flag=1; next} /^## \[/{if(flag) exit} flag" CHANGELOG.md > release_notes.md
# If no specific version found, use a default message
if [ ! -s release_notes.md ]; then
echo "Release $VERSION" > release_notes.md
fi
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: Release ${{ github.ref_name }}
draft: false
# Tag names containing `-` are treated as pre-releases (e.g.
# `vX.Y.Z-rc.1`, `-beta.N`, `-alpha.N`) so `install.sh`'s
# `releases/latest` lookup never serves an RC build as the default
# download. Stable semver tags (`vX.Y.Z`) remain non-prerelease.
prerelease: ${{ contains(github.ref_name, '-') }}
body_path: release_notes.md
build:
name: Build and Upload
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# Native x86_64 Linux gnu — built directly with cargo on the runner.
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
binary_name: subx-cli
asset_name: subx-linux-x86_64
# ARM64 Linux gnu — cross-compiled directly on the runner using
# Ubuntu's `crossbuild-essential-arm64` toolchain. We do NOT use
# `cross` here: its pinned v0.2.5 image is Ubuntu 20.04 based
# (GCC 9 / glibc 2.31), which is too old to link the ONNX Runtime
# prebuilt static archives shipped by `ort-sys` 2.0.0-rc.10. Those
# archives reference `std::__throw_bad_array_new_length` (libstdc++
# from GCC 11+) and `__libc_single_threaded` (glibc 2.32+), so the
# build host must provide a sufficiently new aarch64 toolchain and
# sysroot. ubuntu-latest (24.04) ships GCC 13 + glibc 2.39 via
# `crossbuild-essential-arm64`, which satisfies both.
#
# musl targets are intentionally NOT released: upstream `ort`
# (Pyke) does not publish ONNX Runtime prebuilts for
# *-unknown-linux-musl, and building ONNX Runtime from source for
# musl is out of scope for this release pipeline.
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
binary_name: subx-cli
asset_name: subx-linux-aarch64
- os: windows-latest
target: x86_64-pc-windows-msvc
binary_name: subx-cli.exe
asset_name: subx-windows-x86_64.exe
- os: macos-latest
target: x86_64-apple-darwin
binary_name: subx-cli
asset_name: subx-macos-x86_64
- os: macos-latest
target: aarch64-apple-darwin
binary_name: subx-cli
asset_name: subx-macos-aarch64
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
# Cross-compile setup for ARM64 Linux gnu on the x86_64 runner. Installs
# the full aarch64 cross-toolchain (gcc + g++ + libc/libstdc++ sysroot)
# and points the relevant `cc` / `cargo` env vars at it. Both compilers
# are required: `unrar` (enabled by `--features archive-rar`) compiles
# C++ via `cc-rs`, while most other `*-sys` build scripts only need C.
- name: Install aarch64 gnu cross toolchain
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
crossbuild-essential-arm64 pkg-config libssl-dev curl ca-certificates
{
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc"
echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc"
echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++"
echo "AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar"
} >> "$GITHUB_ENV"
# Pre-fetch the official Pyke ONNX Runtime tarball for aarch64 and
# point `ORT_LIB_LOCATION` at its `lib/` directory. This bypasses
# `ort-sys`'s built-in CDN download path, which is unreliable in CI
# (see Cross.toml history for the original cross-container variant of
# this workaround). The hash matches the entry in `ort-sys`'s
# `dist.txt` for `none aarch64-unknown-linux-gnu` at v2.0.0-rc.10.
- name: Pre-fetch ONNX Runtime (aarch64)
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo mkdir -p /opt/onnxruntime
curl --fail --silent --show-error --location --retry 5 --retry-delay 2 \
https://cdn.pyke.io/0/pyke:ort-rs/ms@1.22.0/aarch64-unknown-linux-gnu.tgz -o /tmp/ort.tgz
echo '24E4760207136FC50B854BB5012AB81DE6189039CF6D4FD3F5B8D3DB7E929F1E /tmp/ort.tgz' \
| tr 'A-F' 'a-f' | sha256sum -c -
sudo tar -xzf /tmp/ort.tgz -C /opt/onnxruntime --strip-components=1
rm /tmp/ort.tgz
echo "ORT_LIB_LOCATION=/opt/onnxruntime/lib" >> "$GITHUB_ENV"
# `--features archive-rar` is required on every Linux artifact (see
# spec: archive-rar feature parity). A failure here is a release
# blocker — do not silently drop the feature. Non-Linux targets accept
# the flag too — `unrar` is feature-gated but the crate compiles on
# all platforms.
- name: Build
run: cargo build --release --features archive-rar --target ${{ matrix.target }}
- name: Package
shell: bash
run: |
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
cp target/${{ matrix.target }}/release/${{ matrix.binary_name }} ${{ matrix.asset_name }}
else
cp target/${{ matrix.target }}/release/${{ matrix.binary_name }} ${{ matrix.asset_name }}
chmod +x ${{ matrix.asset_name }}
fi
# Smoke-test every Linux artifact before upload. x86_64 runs natively
# on the runner; aarch64 runs under qemu-user-static. The aarch64
# binary is dynamically linked against glibc/libstdc++/libgcc_s, so
# qemu needs an aarch64 sysroot to resolve `ld-linux-aarch64.so.1`
# and friends — `gcc-aarch64-linux-gnu` provides exactly that under
# `/usr/aarch64-linux-gnu`, and `QEMU_LD_PREFIX` points qemu at it.
# A non-zero exit, segfault, or 30s timeout fails the workflow before
# Upload Release Asset runs (see spec: Smoke test of Linux artifacts).
- name: Smoke test
if: matrix.os == 'ubuntu-latest'
shell: bash
run: |
set -euo pipefail
if [[ "${{ matrix.target }}" == aarch64-* ]]; then
sudo apt-get update
sudo apt-get install -y qemu-user-static binfmt-support gcc-aarch64-linux-gnu
QEMU_LD_PREFIX=/usr/aarch64-linux-gnu \
timeout 30 qemu-aarch64-static ./${{ matrix.asset_name }} --version
else
timeout 30 ./${{ matrix.asset_name }} --version
fi
- name: Upload Release Asset
uses: softprops/action-gh-release@v3
with:
files: ${{ matrix.asset_name }}
publish-crates:
name: Publish subx-cli to crates.io
runs-on: ubuntu-latest
# Skip pre-release tags (e.g., vX.Y.Z-rc.1, -beta.N, -alpha.N) so RC
# validation never publishes a pre-release version to crates.io.
# See openspec/changes/add-arm-linux-release-targets/design.md
# Decision 5 / Migration Plan step 1.
if: ${{ !contains(github.ref_name, '-') }}
# Publishes subx-cli ONLY. subx-core publishes from its own repository
# (jim60105/subx-core/.github/workflows/release.yml, fired by that
# repository's own `v*` tag): subx-cli is not an umbrella project — the
# crate has a second consumer (the Tauri GUI at jim60105/subx) that
# depends on the core repository directly, so the library owns its own
# release. See openspec/changes/publish-subx-core-from-own-ci.
#
# RECOVERY — read before re-running anything. A crates.io upload is
# irreversible: an accepted version can never be re-uploaded, only
# yanked. This job performs exactly ONE upload, with every probe below
# run before it, so the half-published state the retired `--workspace`
# publish could create cannot occur here. If the Publish step fails,
# first check the sparse index: if `subx-cli@<version>` is listed, the
# crate IS published and a re-run only produces a rejected duplicate.
# Fix the cause and ship a new version; a defective published artifact
# is yanked and superseded — never re-published.
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
# No Cargo version floor: the former >= 1.90 assertion existed solely
# because `cargo publish --workspace` requires it. This job publishes
# one named package with `cargo publish -p`, which long predates that
# floor; asserting a floor the chosen command does not need is theatre
# (openspec/changes/publish-subx-core-from-own-ci, design.md
# Decision 4).
# The two conditions Cargo's recursive dirty check would otherwise report
# as a generic "working directory" error, named here as a submodule
# problem: '-' not initialised, '+' pointer moved but not committed,
# 'U' merge conflict; and a dirty submodule worktree.
- name: Assert the submodule is committed and clean
run: |
git submodule status --recursive | grep -E '^[-+U]' && {
echo "::error::subx-core submodule pointer is not committed/init clean (see status prefix above)"
exit 1
}
test -z "$(git -C subx-core status --porcelain)" || {
echo "::error::subx-core submodule worktree is dirty"
exit 1
}
# Mechanical guard, not a convention: the publish flag "--allow-<d>irty"
# (written split so this guard does not match itself) records
# "dirty": true in the published archive's .cargo_vcs_info.json
# permanently, asserting the crate corresponds to no commit. There is no
# correct use of it here, so a workflow that ever contains it fails the
# release (Decision 2). The [-] class is load-bearing: a plain pattern
# would match this step's own grep line and fail every release.
- name: Assert allow-dirty is absent
run: |
grep -rn -- '--allow[-]dirty' .github/workflows/ && exit 1
echo "no allow-dirty publish flag in any workflow"
# Hard ancestry assertion (unlike build-test-audit-coverage.yml's
# submodule-pointer job, which is advisory on pull requests, this is
# blocking with no exceptions — tag pushes never run that workflow, so
# this job is the only place a release-time pointer is checked. A tag
# must never publish a subx-cli whose gitlink names a core commit that
# exists only on an unmerged branch. Stays even though this job no
# longer uploads subx-core: the published CLI is still built against
# the pointer.)
- name: Assert the submodule pointer is on subx-core master
run: |
git -C subx-core fetch origin master
git -C subx-core merge-base --is-ancestor HEAD origin/master || {
echo "::error::the subx-core gitlink is not reachable from subx-core's master — publish a core release first and bump the pointer"
exit 1
}
# The ordering contract, enforced mechanically: subx-core publishes
# from its OWN repository's tag, and this job refuses to publish a CLI
# whose library dependency is not on the index yet. Read the caret
# requirement from cargo metadata (Cargo's normalised form, never
# manifest text) and ask the sparse index — no auth needed — whether
# any published, non-pre-release subx-core version satisfies it (caret
# semantics for release versions: same major, not below the
# requirement). A miss is the tag-order mistake: `cargo publish` below
# would fail late, resolving the stripped dependency manifest against
# the registry; this probe makes the same failure arrive at second zero
# with the remedy named.
- name: Assert a published subx-core satisfies the caret requirement
run: |
REQ=$(cargo metadata --no-deps --format-version 1 | jq -er \
'.packages[] | select(.name=="subx-cli") | .dependencies[]
| select(.name=="subx-core" and .kind == null) | .req')
# Caret lower bound = the requirement with its operator stripped.
MIN=$(echo "$REQ" | sed -E 's/^[\^~>=<]*//')
MAJ=${MIN%%.*}
curl -sf "https://index.crates.io/su/bx/subx-core" | jq -sr '.[].vers' \
| grep -v -- '-' \
| awk -v maj="$MAJ" -v min="$MIN" '
{ split($0, a, ".")
if (a[1] == maj && ($1 "." $2 "." $3) ~ /^[0-9.]+$/) {
cmd = "printf \"%s\\n%s\\n\" \"" min "\" \"" $1"."$2"."$3 "\" | sort -V -C"
if (system(cmd) == 0) { found = 1 }
} }
END { exit found ? 0 : 1 }' \
|| {
echo "::error::no published subx-core version satisfies the requirement \"$REQ\" on the crates.io index — publish subx-core first by tagging v<X> in jim60105/subx-core (its release workflow uploads the crate when that tag's job goes green)"
exit 1
}
# The tag must name the manifest version, and the registry must not
# already hold it: a tag naming an index-present version is a mistake,
# not a re-run — fail loudly instead of uploading a guaranteed
# rejection.
- name: Assert the tag matches the manifest and is not already published
run: |
CLI_VER=$(cargo metadata --no-deps --format-version 1 | jq -er \
'.packages[] | select(.name=="subx-cli") | .version')
if [ "v${CLI_VER}" != "${GITHUB_REF_NAME}" ]; then
echo "::error::tag ${GITHUB_REF_NAME} does not match subx-cli's manifest version v${CLI_VER}"
exit 1
fi
if curl -sf "https://index.crates.io/su/bx/subx-cli" \
| jq -sr '.[].vers' | grep -qx "$CLI_VER"; then
echo "::error::subx-cli@${CLI_VER} is already published; this tag is a mistake"
exit 1
fi
# Full package-and-verify cycle, no upload: catches a path-only
# dependency, a missing version key, an exclude list that drops a needed
# file, or a manifest that does not survive normalisation — before the
# first irreversible step. The verification build resolves subx-core
# against the index (cargo package strips the path), so it re-checks the
# probe above against the registry for real.
- name: Dry run
run: cargo publish -p subx-cli --dry-run
# One upload, guarded by every probe above. No retry: an accepted
# version is irreversible; see the recovery comment on the job.
- name: Publish to crates.io
run: cargo publish -p subx-cli --token ${{ secrets.CARGO_REGISTRY_TOKEN }}