-
Notifications
You must be signed in to change notification settings - Fork 0
496 lines (445 loc) · 19.5 KB
/
Copy pathrelease.yml
File metadata and controls
496 lines (445 loc) · 19.5 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
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
name: Release
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
dry_run:
description: 'Build and verify without publishing'
type: boolean
default: true
env:
CARGO_TERM_COLOR: always
NODE_DIR: src/bindings/nodejs
jobs:
# Both of these are cheap, and both catch a mistake that cannot be undone
# afterwards: npm refuses to republish a version, and a tag that has already
# been consumed by a release is awkward to retract.
guard:
name: Release preconditions
runs-on: ubuntu-latest
permissions:
contents: read
checks: read
steps:
- uses: actions/checkout@v7
# The tag is the only input this workflow takes, but `npm publish` ships
# whatever version package.json declares and never looks at it. When the
# two disagree the tag names one release and the registry gets another.
# Both published packages are checked, because one tag releases both and
# @retrigger/daemon peer-depends on the exact @retrigger/core line this
# workflow is about to publish.
- name: Tag must match the versions that will be published
shell: bash
run: |
if [ "$GITHUB_REF_TYPE" != tag ]; then
echo "Not a tag build, so there is no tag to disagree with."
exit 0
fi
tagged="${GITHUB_REF_NAME#v}"
status=0
for manifest in "$NODE_DIR/package.json" src/daemon/package.json; do
declared=$(node -p "require('./$manifest').version")
name=$(node -p "require('./$manifest').name")
echo "tag: $tagged / $name: $declared"
if [ "$tagged" != "$declared" ]; then
echo "::error::tag $GITHUB_REF_NAME would publish $name@$declared"
status=1
fi
done
exit $status
# @retrigger/daemon declares the core line it works with. A release that
# moved core's major without moving that range would publish a pair that
# npm refuses to install together -- which is exactly what shipping core
# 2.0.0 against the 1.0.4 daemon would have done.
- name: The daemon must accept the core version being published
shell: bash
run: |
node -e '
const core = require(`./${process.env.NODE_DIR}/package.json`).version;
const range = require("./src/daemon/package.json").peerDependencies["@retrigger/core"];
const wanted = core.split(".")[0];
// Deliberately permissive about range syntax and strict about the
// only thing that has ever been wrong here: the major.
const named = [...range.matchAll(/(\d+)\.\d+\.\d+/g)].map((m) => m[1]);
if (!named.includes(wanted)) {
console.error(`::error::daemon peer range "${range}" does not admit core ${core}`);
process.exit(1);
}
console.log(`daemon accepts core ${core} via "${range}"`);
'
# Releases are cut from tags, and CI runs on branches and pull requests --
# so nothing in this repository previously established that the commit
# being published had ever passed its own test suite. Rather than re-run
# the matrix here, require the result that already exists for this exact
# commit.
- name: The commit being released must have passed CI
if: github.event_name == 'push'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
conclusion=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/check-runs" \
--jq '.check_runs[] | select(.name == "CI passed") | .conclusion' | head -n1)
if [ "$conclusion" = success ]; then
echo "CI passed on $GITHUB_SHA."
exit 0
fi
echo "::error::no successful 'CI passed' check on $GITHUB_SHA (found: ${conclusion:-none}). Push the commit to a branch, let CI finish, then tag it."
exit 1
# Every target is built on a runner of that same OS and architecture rather
# than cross-compiled. That costs a few more runners and buys two things:
# the C hash engine needs no cross toolchain, and — more importantly — each
# artifact can be executed on its real platform before it is published.
# Publishing a binary nobody ever ran is how "works on my machine" ships.
#
# This matrix plus `build-musl` must cover exactly package.json#napi.targets.
# `napi artifacts` in CLI v3 fails on a configured target with no binary
# rather than skipping it the way v2 did, so a target added in one place and
# not the other breaks the release instead of silently shipping nothing.
# api-contract.test.mjs holds napi.targets and optionalDependencies together
# from the other side, so the three lists cannot drift apart unnoticed.
build:
name: ${{ matrix.target }}
runs-on: ${{ matrix.host }}
needs: guard
strategy:
fail-fast: false
matrix:
include:
- { host: ubuntu-latest, target: x86_64-unknown-linux-gnu, native: true }
- { host: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu, native: true }
# macos-15-intel is the last x86_64 macOS image Actions offers; the
# macos-13 labels were retired on 2025-12-04 and a job requesting one
# queues forever rather than failing, which would hang the release
# with nothing anywhere saying why.
- { host: macos-15-intel, target: x86_64-apple-darwin, native: true }
- { host: macos-latest, target: aarch64-apple-darwin, native: true }
- { host: windows-latest, target: x86_64-pc-windows-msvc, native: true }
# No aarch64 Windows runner exists yet, so this one is cross-built
# and therefore cannot be smoke-tested here. It is flagged as such.
- { host: windows-latest, target: aarch64-pc-windows-msvc, native: false }
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
cache-dependency-path: src/bindings/nodejs/package-lock.json
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Install clang (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y clang
- name: Install dependencies
run: npm ci --no-audit --no-fund
working-directory: ${{ env.NODE_DIR }}
# Note the `--`: without it npm swallows the flag and silently builds for
# the host. That bug shipped host binaries under foreign platform names.
- name: Build native addon
run: npm run build -- --target ${{ matrix.target }}
working-directory: ${{ env.NODE_DIR }}
- name: Verify the artifact loads and computes
if: matrix.native
shell: bash
run: node scripts/verify-artifact.js
working-directory: ${{ env.NODE_DIR }}
- name: Note unverified cross build
if: '!matrix.native'
run: echo "::warning::${{ matrix.target }} was cross-built and could not be executed on this runner."
- uses: actions/upload-artifact@v7
with:
name: bindings-${{ matrix.target }}
path: ${{ env.NODE_DIR }}/*.node
if-no-files-found: error
# musl gets its own job because it is built *inside* Alpine rather than
# cross-compiled: the addon is then linked against the same libc it will be
# loaded with, and — the reason this is worth a separate job at all — it can
# be executed on the spot, so a musl binary is proven before it is published
# exactly like every other native target.
#
# Two details are load-bearing and were each found the hard way:
#
# * `-C target-feature=-crt-static`. Rust's musl targets default to static
# linking, which is right for an executable and fatal for a `cdylib`: the
# addon has to resolve symbols from the Node process that dlopens it.
# Without this the build succeeds and produces a `.node` that cannot load.
# * No `--target` flag. The container is already musl, so this is a native
# build and `napi build --platform` names the artifact `*-musl.node` on
# its own. Passing the triple instead asks Alpine's system rustc for a
# std it does not ship.
build-musl:
name: ${{ matrix.target }}
runs-on: ${{ matrix.host }}
container: node:22-alpine
needs: guard
strategy:
fail-fast: false
matrix:
include:
- { host: ubuntu-latest, target: x86_64-unknown-linux-musl }
- { host: ubuntu-24.04-arm, target: aarch64-unknown-linux-musl }
env:
RUSTFLAGS: '-C target-feature=-crt-static'
steps:
- uses: actions/checkout@v7
- name: Install toolchain
run: apk add --no-cache build-base clang clang-dev llvm-dev rust cargo make bash python3 git
- name: Install dependencies
run: npm ci --no-audit --no-fund
working-directory: ${{ env.NODE_DIR }}
- name: Build the C hash engine
run: make build-core BUILD_TYPE=release
- name: Build native addon
run: npm run build
working-directory: ${{ env.NODE_DIR }}
# The whole point of building in Alpine instead of cross-compiling.
- name: Verify the artifact loads and computes
run: node scripts/verify-artifact.js
working-directory: ${{ env.NODE_DIR }}
# A native build is named by the host, so this is also the check that the
# runner really was musl and not a glibc image that quietly stood in.
- name: Confirm the artifact is the musl one
shell: sh
run: |
cd "$NODE_DIR"
ls *.node
test -f retrigger-nodejs-bindings.$(node -p "process.arch === 'x64' ? 'linux-x64-musl' : 'linux-arm64-musl'").node
- uses: actions/upload-artifact@v7
with:
name: bindings-${{ matrix.target }}
path: ${{ env.NODE_DIR }}/*.node
if-no-files-found: error
publish:
name: Publish
runs-on: ubuntu-latest
needs: [build, build-musl]
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
cache: npm
cache-dependency-path: src/bindings/nodejs/package-lock.json
- name: Install dependencies
run: npm ci --no-audit --no-fund
working-directory: ${{ env.NODE_DIR }}
- uses: actions/download-artifact@v8
with:
path: ${{ env.NODE_DIR }}/artifacts
# `napi artifacts` copies each .node into its per-platform npm/<platform>/
# package directory, and it fails on a binary with no matching package
# rather than dropping it. That makes `create-npm-dirs` a prerequisite,
# not a convenience: npm/ is generated from napi.targets and is not
# committed. An earlier workflow copied the binaries to the package root
# and skipped both steps, so the platform packages it published were empty.
- name: Create the platform package directories
run: npx napi create-npm-dirs
working-directory: ${{ env.NODE_DIR }}
- name: Distribute artifacts into platform packages
run: npx napi artifacts --output-dir artifacts
working-directory: ${{ env.NODE_DIR }}
# `prepublish` does two separable things: it rewrites each platform
# package's version, and — unless `--skip-optional-publish` — it npm
# publishes them. Both halves matter here.
#
# The publishing half needs a registry token of its own. Without one the
# eight platform packages never reach npm, and the root package below
# still ships optionalDependencies naming them: every install then
# resolves no native package and degrades to the JavaScript engine while
# looking like a clean install. That is exactly how 1.0.4 shipped.
#
# The publishing half must also not run on a dry run. Publishing is the
# one step in this workflow that cannot be undone — npm refuses to reuse
# a version — so a dry run that published the platform packages would
# burn the version it was meant to rehearse.
- name: Prepare and publish the platform packages
shell: bash
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
PUBLISHING: ${{ github.event_name == 'push' || inputs.dry_run == false }}
run: |
if [ "$PUBLISHING" = true ]; then
npx napi prepublish -t npm --no-gh-release
else
npx napi prepublish -t npm --no-gh-release --skip-optional-publish
fi
working-directory: ${{ env.NODE_DIR }}
- name: Show what would be published
run: |
echo "--- root package ---"
npm pack --dry-run
echo "--- platform packages ---"
ls -R npm 2>/dev/null || echo "(no npm/ directory)"
working-directory: ${{ env.NODE_DIR }}
# Last, and only after the platform packages are on the registry: the
# root package is the one that points at them, so publishing it first
# would leave a window where installs resolve nothing native.
- name: Publish
if: github.event_name == 'push' || inputs.dry_run == false
run: npm publish --access public --provenance
working-directory: ${{ env.NODE_DIR }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# @retrigger/daemon is a pure-JavaScript shim -- it has no artifact of its own
# to build here, because the per-platform Rust binaries it looks for are not
# published yet and it is written to degrade politely when they are absent.
# It still has to be released in step with core: it peer-depends on the core
# line, so leaving it a major version behind makes `npm install` of the two
# together fail outright with ERESOLVE.
#
# It goes after core rather than beside it, so the peer it names already
# exists on the registry by the time anyone can install it.
publish-daemon:
name: Publish the daemon package
runs-on: ubuntu-latest
needs: publish
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
# The shim, the shipped config, and the documented no-binary degradation.
# Cheap, and it is the whole package.
- name: Smoke test the shim
run: node scripts/test-daemon.js
working-directory: src/daemon
- name: Show what would be published
run: npm pack --dry-run
working-directory: src/daemon
- name: Publish
if: github.event_name == 'push' || inputs.dry_run == false
run: npm publish --access public --provenance
working-directory: src/daemon
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# Installs the just-published package from the real registry on every OS and
# confirms it works there. Until this passes, a release is not proven; it is
# only uploaded.
verify-published:
name: Verify install / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
needs: publish
if: github.event_name == 'push'
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, macos-15-intel, windows-latest]
steps:
- uses: actions/setup-node@v7
with: { node-version: 22 }
- name: Wait for registry propagation
shell: bash
run: sleep 45
- name: Install from the registry into a clean project
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/verify" && cd "$RUNNER_TEMP/verify"
npm init -y >/dev/null
npm install "@retrigger/core@${GITHUB_REF_NAME#v}"
- name: Require it and exercise both engines
shell: bash
run: |
cd "$RUNNER_TEMP/verify"
node -e "
const r = require('@retrigger/core');
const info = r.getEngineInfo();
console.log('engine:', JSON.stringify(info));
if (!r.hashBytesSync(Buffer.from('retrigger'))) throw new Error('hash failed');
if (info.engine !== 'native') {
throw new Error('expected the native engine on this platform, got ' + info.engine);
}
console.log('ok');
"
# The same proof as above on musl, which is the platform this release exists
# to fix: before the musl packages were built, an Alpine install resolved to
# no native package and degraded to the JavaScript engine without complaining.
# Asserting `engine === 'native'` here is what makes that regression loud.
verify-published-musl:
name: Verify install / alpine-${{ matrix.arch }}
runs-on: ${{ matrix.host }}
container: node:22-alpine
needs: publish
if: github.event_name == 'push'
strategy:
fail-fast: false
matrix:
include:
- { host: ubuntu-latest, arch: x64 }
- { host: ubuntu-24.04-arm, arch: arm64 }
steps:
- name: Wait for registry propagation
run: sleep 45
- name: Install from the registry and require it
shell: sh
run: |
mkdir -p /tmp/verify && cd /tmp/verify
npm init -y >/dev/null
npm install "@retrigger/core@${GITHUB_REF_NAME#v}"
node -e "
const r = require('@retrigger/core');
const info = r.getEngineInfo();
console.log('engine:', JSON.stringify(info));
if (!r.hashBytesSync(Buffer.from('retrigger'))) throw new Error('hash failed');
if (info.engine !== 'native') {
throw new Error('expected the native engine on musl, got ' + info.engine);
}
console.log('ok');
"
# The two packages are released together because npm resolves them together.
# A default `npm install` of both is the exact command that fails with
# ERESOLVE when the daemon's peer range trails core's major, so it is the
# command that proves the pair -- no --force, no --legacy-peer-deps.
verify-published-pair:
name: Verify install / core + daemon
runs-on: ubuntu-latest
needs: publish-daemon
if: github.event_name == 'push'
steps:
- uses: actions/setup-node@v7
with: { node-version: 22 }
- name: Wait for registry propagation
run: sleep 45
- name: Install both from the registry
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/pair" && cd "$RUNNER_TEMP/pair"
npm init -y >/dev/null
version="${GITHUB_REF_NAME#v}"
npm install "@retrigger/core@$version" "@retrigger/daemon@$version"
node -e "
const core = require('@retrigger/core');
require('@retrigger/daemon');
if (core.getEngineInfo().engine !== 'native') {
throw new Error('expected the native engine alongside the daemon');
}
console.log('ok');
"
github-release:
name: GitHub release
runs-on: ubuntu-latest
needs: [build, build-musl, publish, publish-daemon]
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with: { path: artifacts }
- uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
generate_release_notes: true
files: artifacts/**/*.node