-
Notifications
You must be signed in to change notification settings - Fork 1
463 lines (456 loc) · 21.8 KB
/
Copy pathpublish.yml
File metadata and controls
463 lines (456 loc) · 21.8 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
# Model: AgentWorkforce/relayfile/.github/workflows/publish.yml.
# Configure each npm trusted publisher for AgentWorkforce/flows, publish.yml.
name: Publish Package
on:
workflow_dispatch:
inputs:
package:
description: Package to publish (single-package selections are dry-run only)
required: true
type: choice
options: [all, surface, sdk, runtime-linux-x64, runtime-darwin-arm64, relayflows]
default: all
version:
description: Version bump type
required: true
type: choice
options: [patch, minor, major, prepatch, preminor, premajor, prerelease]
default: patch
custom_version:
description: Custom version (overrides bump type)
required: false
type: string
preid:
description: Prerelease identifier
type: choice
options: [beta, alpha, rc]
default: beta
dry_run:
description: Dry run (build, pack and verify without publishing)
type: boolean
default: true
tag:
description: NPM dist-tag
type: choice
options: [latest, next, beta, alpha]
default: latest
concurrency:
group: publish-package
cancel-in-progress: false
permissions:
contents: write
id-token: write
env:
NPM_CONFIG_FUND: 'false'
jobs:
build:
name: Build & Version
runs-on: ubuntu-24.04
timeout-minutes: 30
outputs:
new_version: ${{ steps.bump.outputs.new_version }}
is_prerelease: ${{ steps.bump.outputs.is_prerelease }}
steps:
- name: Validate release mode
env:
PACKAGE: ${{ inputs.package }}
DRY_RUN: ${{ inputs.dry_run }}
REF_TYPE: ${{ github.ref_type }}
run: |
if [[ "$DRY_RUN" != true && ( "$PACKAGE" != all || "$REF_TYPE" != branch ) ]]; then
echo 'Real releases require package=all and a branch: all versions and internal dependencies advance together.' >&2
exit 1
fi
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: packages/sdk/package-lock.json
registry-url: https://registry.npmjs.org
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.4.0'
- uses: dtolnay/rust-toolchain@stable
- name: Install build dependencies
run: |
npm install --prefix packages/surface --ignore-scripts
npm ci --prefix packages/sdk --ignore-scripts
- name: Test release tooling
run: node --test scripts/publish.test.mjs
- name: Version all packages
id: bump
env:
CUSTOM_VERSION: ${{ inputs.custom_version }}
VERSION_TYPE: ${{ inputs.version }}
PREID: ${{ inputs.preid }}
run: node scripts/version-packages.mjs
- name: Build surface
working-directory: packages/surface
run: ./node_modules/.bin/tsc
- name: Pack and assert surface
id: surface
run: node scripts/pack-release.mjs surface
# Install the actual packed surface, without saving a file: dependency.
# npm ci's development link must not be the SDK's build-time dependency.
- name: Build SDK against packed surface
env:
SURFACE_TARBALL: ${{ steps.surface.outputs.tarball }}
working-directory: packages/sdk
run: |
npm install --no-save --package-lock=false --ignore-scripts "$SURFACE_TARBALL"
test ! -L node_modules/@relayflows/surface
./node_modules/.bin/tsc
node scripts/make-cli-executable.mjs
- name: Pack and assert SDK
id: sdk
run: node scripts/pack-release.mjs sdk
# relayflows only re-exposes the SDK's CLI under the unscoped name, so it
# builds against the packed SDK the same way the SDK builds against the
# packed surface — and it's cheap enough to smoke-test for real here
# rather than only asserting the tarball's shape in pack-release.mjs.
#
# Both tarballs, not just the SDK's: relayflows doesn't depend on
# @relayflows/surface directly, but the SDK tarball does (transitively),
# at this same freshly-bumped, never-published version. Installing only
# the SDK tarball leaves npm to resolve that transitive dependency from
# the real registry — which 404s on every version bump, since nothing
# is published yet at build time. Feeding the surface tarball too
# satisfies it locally, the same reason the SDK step above installs the
# surface tarball rather than letting its own dependency resolve remotely.
- name: Build relayflows CLI wrapper against packed SDK
env:
SDK_TARBALL: ${{ steps.sdk.outputs.tarball }}
SURFACE_TARBALL: ${{ steps.surface.outputs.tarball }}
working-directory: packages/relayflows
run: |
npm install --no-save --package-lock=false --ignore-scripts "$SDK_TARBALL" "$SURFACE_TARBALL"
test ! -L node_modules/@relayflows/sdk
report=$(node bin/flows.js check --json ../../testdata/hello-deterministic.flow.yaml)
echo "$report" | node -e '
const report = JSON.parse(require("fs").readFileSync(0, "utf8"));
if (report.ok !== true) { console.error(report); process.exit(1); }
'
- name: Pack and assert relayflows CLI wrapper
run: node scripts/pack-release.mjs relayflows
- name: Build relayflowd
working-directory: kernel
run: cargo build --locked --release -p relayflowd
- name: Build and execute runtime binaries
run: |
mkdir -p packages/runtime-linux-x64/bin
cp kernel/target/release/relayflowd packages/runtime-linux-x64/bin/relayflowd
node scripts/build-standalone-cli.mjs bun-linux-x64 packages/runtime-linux-x64/bin/flows
chmod +x packages/runtime-linux-x64/bin/relayflowd packages/runtime-linux-x64/bin/flows
packages/runtime-linux-x64/bin/relayflowd --help
packages/runtime-linux-x64/bin/flows check --json testdata/hello-deterministic.flow.yaml
- name: Pack and assert runtime (executes both unpacked binaries)
run: node scripts/pack-release.mjs runtime-linux-x64
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/publish/*.tgz
if-no-files-found: error
retention-days: 7
# A native macOS job, not a cross-compile from the linux `build` job above.
# relayflowd's release build has no cross-compile setup (no osxcross, no
# macOS SDK on the linux runner), and macos-14 runners are Apple Silicon, so
# `cargo build --release` already targets aarch64-apple-darwin natively —
# the same reason no `--target` flag is needed below.
#
# This job independently re-derives `new_version` by re-running
# version-packages.mjs with the same workflow inputs against the same
# commit as the `build` job. `npm version <bump>` is a pure function of the
# committed version and the bump type, so both jobs compute the identical
# version without either depending on the other's output — the alternative
# (pass new_version as a job output) would force this job to wait on `build`
# for no reason; they can run in parallel instead.
build-darwin-arm64:
name: Build & pack darwin-arm64 runtime
runs-on: macos-14
timeout-minutes: 30
steps:
- name: Validate release mode
env:
PACKAGE: ${{ inputs.package }}
DRY_RUN: ${{ inputs.dry_run }}
REF_TYPE: ${{ github.ref_type }}
run: |
if [[ "$DRY_RUN" != true && ( "$PACKAGE" != all || "$REF_TYPE" != branch ) ]]; then
echo 'Real releases require package=all and a branch: all versions and internal dependencies advance together.' >&2
exit 1
fi
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.4.0'
- uses: dtolnay/rust-toolchain@stable
- name: Install build dependencies
run: |
npm install --prefix packages/surface --ignore-scripts
npm ci --prefix packages/sdk --ignore-scripts
- name: Version all packages
env:
CUSTOM_VERSION: ${{ inputs.custom_version }}
VERSION_TYPE: ${{ inputs.version }}
PREID: ${{ inputs.preid }}
run: node scripts/version-packages.mjs
- name: Build surface
working-directory: packages/surface
run: ./node_modules/.bin/tsc
- name: Pack and assert surface
id: surface
run: node scripts/pack-release.mjs surface
# Only the SDK's module graph is needed here (bun bundles cli-executable.ts
# straight from source) — not a tsc build, which is the SDK's own
# publishable dist and unrelated to the standalone bun binary below.
- name: Install SDK against packed surface
env:
SURFACE_TARBALL: ${{ steps.surface.outputs.tarball }}
working-directory: packages/sdk
run: |
npm install --no-save --package-lock=false --ignore-scripts "$SURFACE_TARBALL"
test ! -L node_modules/@relayflows/surface
- name: Build relayflowd
working-directory: kernel
run: cargo build --locked --release -p relayflowd
- name: Build and execute runtime binaries
run: |
mkdir -p packages/runtime-darwin-arm64/bin
cp kernel/target/release/relayflowd packages/runtime-darwin-arm64/bin/relayflowd
node scripts/build-standalone-cli.mjs bun-darwin-arm64 packages/runtime-darwin-arm64/bin/flows
chmod +x packages/runtime-darwin-arm64/bin/relayflowd packages/runtime-darwin-arm64/bin/flows
packages/runtime-darwin-arm64/bin/relayflowd --help
packages/runtime-darwin-arm64/bin/flows check --json testdata/hello-deterministic.flow.yaml
- name: Pack and assert runtime (executes both unpacked binaries)
run: node scripts/pack-release.mjs runtime-darwin-arm64
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-darwin-arm64
path: dist/publish/*.tgz
if-no-files-found: error
retention-days: 7
publish-packages:
name: Publish packages in dependency order
needs: [build, build-darwin-arm64]
runs-on: ubuntu-24.04
timeout-minutes: 30 # publish + up to 10 minutes of registry propagation wait
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.sha }}
- uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: https://registry.npmjs.org
# OIDC requires npm >=11.5.1. Keep Node 22 compatible with npm's major.
- name: Update npm for OIDC support
run: npm install -g npm@11
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/build-output
- uses: actions/download-artifact@v4
with:
name: build-output-darwin-arm64
path: dist/build-output
# relayflows is the one unscoped package, so `npm pack` names its tarball
# `relayflows-<version>.tgz`, not `relayflows-relayflows-<version>.tgz`.
- name: Restore built packages
env:
NEW_VERSION: ${{ needs.build.outputs.new_version }}
run: |
for package in surface sdk runtime-linux-x64 runtime-darwin-arm64 relayflows; do
if [[ "$package" == relayflows ]]; then
tarball="relayflows-${NEW_VERSION}.tgz"
else
tarball="relayflows-${package}-${NEW_VERSION}.tgz"
fi
tar -xzf "dist/build-output/$tarball" --strip-components=1 -C "packages/$package"
done
# Repack and check EVERYTHING before the first publish. Tar archives
# preserve executable bits across Actions artifact upload/download.
# runtime-darwin-arm64's own execution smoke does not re-run here — this
# runner is linux, so pack-release.mjs asserts its shape only, per the
# foreign-host skip described where that check lives. It already ran for
# real on the macos-14 runner that built it.
- name: Pack and assert all release tarballs
run: |
node scripts/pack-release.mjs surface
node scripts/pack-release.mjs sdk
node scripts/pack-release.mjs runtime-linux-x64
node scripts/pack-release.mjs runtime-darwin-arm64
node scripts/pack-release.mjs relayflows
- name: Publish to NPM (surface before SDK, relayflows last)
env:
PACKAGE: ${{ inputs.package }}
NEW_VERSION: ${{ needs.build.outputs.new_version }}
DRY_RUN: ${{ inputs.dry_run }}
NPM_TAG: ${{ inputs.tag }}
run: |
for package in surface sdk runtime-linux-x64 runtime-darwin-arm64 relayflows; do
if [[ "$PACKAGE" != all && "$PACKAGE" != "$package" ]]; then continue; fi
if [[ "$package" == relayflows ]]; then
tarball="relayflows-${NEW_VERSION}.tgz"
else
tarball="relayflows-${package}-${NEW_VERSION}.tgz"
fi
args=()
if [[ "$DRY_RUN" == true ]]; then args+=(--dry-run); fi
npm publish "dist/publish/$tarball" \
--access public --provenance --ignore-scripts --tag "$NPM_TAG" "${args[@]}"
done
- name: Wait for the registry to serve every published version
if: ${{ !inputs.dry_run }}
env:
PACKAGE: ${{ inputs.package }}
NEW_VERSION: ${{ needs.build.outputs.new_version }}
# `npm publish` above already fetched and cached each packument from
# before the new version existed, and npm honors the registry's
# 5-minute max-age on that cache. Bypass it or this loop can spin on
# a stale copy after the registry has caught up.
NPM_CONFIG_PREFER_ONLINE: 'true'
run: |
# `npm publish` returns before the registry serves the new version.
# The lockfile regeneration below resolves the just-published
# packages from the registry, and on the v2.0.10 release it hit
# ETARGET for @relayflows/surface@2.0.10 seconds after that publish
# succeeded, which skipped the release commit and tag. Poll the
# packument for each version and probe its tarball (the CDN edge can
# still 404 after the packument lists it) before touching lockfiles.
for package in surface sdk runtime-linux-x64 runtime-darwin-arm64 relayflows; do
if [[ "$PACKAGE" != all && "$PACKAGE" != "$package" ]]; then continue; fi
if [[ "$package" == relayflows ]]; then name=relayflows; else name="@relayflows/$package"; fi
spec="$name@$NEW_VERSION"
ready=false
for attempt in $(seq 1 20); do
tarball=$(npm view "$spec" dist.tarball 2>/dev/null || true)
if [[ -n "$tarball" ]]; then
status=$(curl -sS -o /dev/null -w '%{http_code}' -I \
--connect-timeout 5 --max-time 15 "$tarball" || echo 000)
if [[ "$status" == 200 ]]; then
echo "$spec is served (attempt $attempt)"
ready=true
break
fi
echo "$spec listed but tarball returned HTTP $status (attempt $attempt/20)"
else
echo "$spec not yet in the registry (attempt $attempt/20)"
fi
sleep 30
done
if [[ "$ready" != true ]]; then
echo "::error::$spec was published but is still not served after 10 minutes; finish the release by rerunning this job once it propagates."
exit 1
fi
done
- name: Regenerate release lockfiles
if: ${{ !inputs.dry_run }}
env:
NEW_VERSION: ${{ needs.build.outputs.new_version }}
NPM_CONFIG_PREFER_ONLINE: 'true' # same stale-packument cache as the wait step above
run: |
npm install --prefix packages/surface --package-lock-only --ignore-scripts
npm install --prefix packages/sdk --package-lock-only --ignore-scripts --save-exact "@relayflows/surface@$NEW_VERSION"
# No explicit --save-exact target here, unlike the SDK line above.
# package.json (restored from the just-published tarball) already
# carries the exact new-version pin for every dependency, including
# both runtime-* optionalDependencies — so a bare install is enough
# to record them, and it has to be bare: a targeted `install pkg@v`
# only reconciles that one named package and, found the hard way
# against this exact command on a real release run, silently drops
# OTHER already-resolved entries rather than refreshing them —
# emptying node_modules/@relayflows/runtime-linux-x64 out of the
# lockfile entirely and leaving `npm ci` downstream refusing with
# "Missing ... from lock file" for both runtime packages at once.
npm install --prefix packages/relayflows --package-lock-only --ignore-scripts
node --input-type=module - <<'NODE'
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const version = process.env.NEW_VERSION;
const check = (lockPath, depNodeModulesPath) => {
const lock = JSON.parse(readFileSync(lockPath, 'utf8'));
const dep = lock.packages[depNodeModulesPath];
assert.equal(dep.version, version);
assert.match(dep.resolved, /^https:\/\/registry\.npmjs\.org\//);
assert(!dep.link, `release lockfile must resolve the published dependency (${lockPath})`);
};
check('packages/sdk/package-lock.json', 'node_modules/@relayflows/surface');
check('packages/relayflows/package-lock.json', 'node_modules/@relayflows/sdk');
// Deliberately not asserting the two runtime-* optionalDependencies
// resolve here. Empirically (found the hard way: a real release run
// against a linux runner), npm's package-lock v3 writer only fully
// resolves the optional-dependency variant matching the CURRENT
// machine's os/cpu — it fetches the others' packuments but omits
// their node_modules/ entries entirely, and can even drop an
// existing entry for the *matching* platform when the install is a
// targeted/incremental one rather than a from-scratch resolve. This
// isn't a correctness problem: `npm install -g relayflows` (the
// thing that actually matters) never consults this lockfile — a
// global install always resolves fresh from package.json against
// the installing machine's own os/cpu. This lockfile is monorepo
// tooling hygiene (`npm ci` here), not an end-user install path.
NODE
npm ci --prefix packages/surface --dry-run --ignore-scripts
npm ci --prefix packages/sdk --dry-run --ignore-scripts
# `npm ci` (unlike plain `npm install`) refuses outright — before
# even looking at --omit — if the lockfile is missing an entry for
# ANY package.json dependency, optional included. Confirmed on two
# separate real release runs that which of the two runtime-*
# optionalDependencies ends up resolved by the installs above is
# not reliably both from a single linux runner (one run dropped
# both, the next dropped only the one matching this runner's own
# platform) — an npm quirk with platform-gated optional deps in
# --package-lock-only mode, not something worth chasing further.
# `npm install --dry-run` verifies the same "is this installable"
# question without that all-or-nothing gate.
npm install --prefix packages/relayflows --dry-run --package-lock-only --ignore-scripts
- name: Commit version bump and create tag
if: ${{ !inputs.dry_run }}
env:
NEW_VERSION: ${{ needs.build.outputs.new_version }}
RELEASE_BRANCH: ${{ github.ref_name }}
run: |
git config user.name 'GitHub Actions'
git config user.email 'actions@github.com'
git add packages/surface/package.json packages/surface/package-lock.json \
packages/sdk/package.json packages/sdk/package-lock.json \
packages/runtime-linux-x64/package.json \
packages/runtime-darwin-arm64/package.json \
packages/relayflows/package.json packages/relayflows/package-lock.json
if ! git diff --staged --quiet; then
git commit -m "chore(release): v${NEW_VERSION}"
fi
git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION}"
git push --atomic origin "HEAD:refs/heads/${RELEASE_BRANCH}" "refs/tags/v${NEW_VERSION}"
- name: Create GitHub Release
if: ${{ !inputs.dry_run }}
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.build.outputs.new_version }}
name: v${{ needs.build.outputs.new_version }}
prerelease: ${{ needs.build.outputs.is_prerelease == 'true' }}
generate_release_notes: true
files: dist/publish/*.tgz
- name: Summary
if: always()
env:
NEW_VERSION: ${{ needs.build.outputs.new_version }}
PACKAGE: ${{ inputs.package }}
DRY_RUN: ${{ inputs.dry_run }}
NPM_TAG: ${{ inputs.tag }}
RESULT: ${{ job.status }}
run: |
{
echo "Package: $PACKAGE"
echo "Version: $NEW_VERSION"
echo "NPM tag: $NPM_TAG"
echo "Dry run: $DRY_RUN"
echo "Publish job: $RESULT"
} >> "$GITHUB_STEP_SUMMARY"