From 8836459b2ddc7a819824cac17aa37eeb90c405f4 Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Tue, 8 Sep 2026 10:05:39 +0200 Subject: [PATCH 1/2] feat(publish): add the darwin-arm64 runtime package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of "package flows so it's frictionless": relayflowd-path.ts's attach-or-spawn resolution has always been able to find a @relayflows/runtime-- optional dependency (kernel/DAEMON-LIFECYCLE.md §3.1 step 3) but nothing published one for macOS, and — a separate, pre-existing gap found while wiring this up — nothing declared any runtime package as an actual dependency of the installed `relayflows` CLI at all, on any platform. `npm install -g relayflows` never fetched a relayflowd binary regardless of OS; it only ever worked from inside a source checkout with the kernel already built. - packages/runtime-darwin-arm64: mirrors runtime-linux-x64 (os/cpu-gated package.json, relayflowd + bun-compiled flows in bin/). - packages/relayflows/package.json now declares both runtime packages as optionalDependencies, pinned in lockstep with @relayflows/sdk. npm installs only the one matching os/cpu and silently skips the rest. - scripts/pack-release.mjs generalized from a runtime-linux-x64 special case to any runtime-- package; its execution smoke test only runs when the host actually matches — packing/asserting a foreign-platform tarball (e.g. re-verifying darwin-arm64's tarball from the linux publish job) checks shape only, since a foreign binary cannot be executed there. - .github/workflows/publish.yml: new build-darwin-arm64 job on a macos-14 runner (native aarch64-apple-darwin, no cross-compile), independently re-deriving the same new_version via version-packages.mjs against the same commit and inputs as the linux build job, so neither job depends on the other's output. publish-packages downloads both build artifacts and publishes all five packages. Verified locally end to end on this machine (darwin-arm64): built the real relayflowd release binary and bun-compiled flows executable, ran pack-release.mjs against them for real (pack, unpack, execute `relayflowd --help` and `flows check` against testdata/hello-deterministic.flow.yaml), and confirmed relayflowd-path.ts's existing resolution tests are unaffected. scripts/publish.test.mjs covers the generalized pack-release.mjs behavior including the foreign-host skip path. Not yet covered: darwin-x64 (Intel) and Windows still fall through to relayflowd-path.ts's source-checkout/PATH resolution steps. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013Y8uLRUXqKSZsqeeUMNaS2 --- .github/workflows/publish.yml | 109 ++++++++++++++++++++- packages/relayflows/README.md | 11 +++ packages/relayflows/package-lock.json | 24 +++++ packages/relayflows/package.json | 4 + packages/runtime-darwin-arm64/README.md | 20 ++++ packages/runtime-darwin-arm64/package.json | 27 +++++ scripts/pack-release.mjs | 33 ++++--- scripts/publish.test.mjs | 42 +++++++- scripts/version-packages.mjs | 2 +- 9 files changed, 251 insertions(+), 21 deletions(-) create mode 100644 packages/runtime-darwin-arm64/README.md create mode 100644 packages/runtime-darwin-arm64/package.json diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6b1461011..8a6ad1823 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,7 @@ on: description: Package to publish (single-package selections are dry-run only) required: true type: choice - options: [all, surface, sdk, runtime-linux-x64, relayflows] + options: [all, surface, sdk, runtime-linux-x64, runtime-darwin-arm64, relayflows] default: all version: description: Version bump type @@ -150,9 +150,93 @@ jobs: 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 ` 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 + bun build packages/sdk/src/cli-executable.ts --compile --target=bun-darwin-arm64 \ + --outfile=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 + needs: [build, build-darwin-arm64] runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -171,13 +255,17 @@ jobs: 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-.tgz`, not `relayflows-relayflows-.tgz`. - name: Restore built packages env: NEW_VERSION: ${{ needs.build.outputs.new_version }} run: | - for package in surface sdk runtime-linux-x64 relayflows; do + for package in surface sdk runtime-linux-x64 runtime-darwin-arm64 relayflows; do if [[ "$package" == relayflows ]]; then tarball="relayflows-${NEW_VERSION}.tgz" else @@ -187,11 +275,16 @@ jobs: 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: @@ -200,7 +293,7 @@ jobs: DRY_RUN: ${{ inputs.dry_run }} NPM_TAG: ${{ inputs.tag }} run: | - for package in surface sdk runtime-linux-x64 relayflows; do + 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" @@ -233,6 +326,13 @@ jobs: }; check('packages/sdk/package-lock.json', 'node_modules/@relayflows/surface'); check('packages/relayflows/package-lock.json', 'node_modules/@relayflows/sdk'); + // Optional dependencies still resolve to real registry entries in + // the lockfile regardless of this runner's own os/cpu — npm records + // every platform variant so `npm ci` elsewhere can pick the right + // one. Only the resolution differs from a required dependency; the + // same freshness guarantee must hold. + check('packages/relayflows/package-lock.json', 'node_modules/@relayflows/runtime-linux-x64'); + check('packages/relayflows/package-lock.json', 'node_modules/@relayflows/runtime-darwin-arm64'); NODE npm ci --prefix packages/surface --dry-run --ignore-scripts npm ci --prefix packages/sdk --dry-run --ignore-scripts @@ -248,6 +348,7 @@ jobs: 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}" diff --git a/packages/relayflows/README.md b/packages/relayflows/README.md index 54888a9d3..234724207 100644 --- a/packages/relayflows/README.md +++ b/packages/relayflows/README.md @@ -17,4 +17,15 @@ carry the same version number, and this package pins its dependency to that exact version rather than a range, so `npm install -g relayflows` always resolves the SDK build it shipped with. +It also declares the per-platform runtime packages +(`@relayflows/runtime-linux-x64`, `@relayflows/runtime-darwin-arm64`) as +`optionalDependencies`, pinned the same way. Each declares `os`/`cpu`, so npm +installs only the one matching the current machine and silently skips the +rest — this is what lets `flows run` spawn `relayflowd` with no manual build +step (`kernel/DAEMON-LIFECYCLE.md` §3.1's `relayflowd-path.ts` resolution +finds it as an optional dependency of this package). A platform with no +runtime package yet (Intel Mac, Windows) installs `relayflows` fine; `flows` +then falls through to a source checkout or `PATH`, and refuses with +`relayflowd_not_found` if neither has a binary. + See `@relayflows/sdk` and `docs/SURFACE.md` for what the CLI actually does. diff --git a/packages/relayflows/package-lock.json b/packages/relayflows/package-lock.json index 1e787f6d8..53e4ef4b1 100644 --- a/packages/relayflows/package-lock.json +++ b/packages/relayflows/package-lock.json @@ -14,6 +14,30 @@ "bin": { "flows": "bin/flows.js" }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@relayflows/runtime-darwin-arm64": "2.0.1", + "@relayflows/runtime-linux-x64": "2.0.1" + } + }, + "node_modules/@relayflows/runtime-linux-x64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@relayflows/runtime-linux-x64/-/runtime-linux-x64-2.0.1.tgz", + "integrity": "sha512-NNTv2qhqGp7x8KnkKOLwtmDagO1bpniNhDiOlRs5uGyZbpmAEq1AdOfGhLdO7NEirk2e8e/YrsiZPBpLgMbeQg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "flows": "bin/flows", + "relayflowd": "bin/relayflowd" + }, "engines": { "node": ">=20" } diff --git a/packages/relayflows/package.json b/packages/relayflows/package.json index dd7aa0c5b..af03a9589 100644 --- a/packages/relayflows/package.json +++ b/packages/relayflows/package.json @@ -18,6 +18,10 @@ "dependencies": { "@relayflows/sdk": "2.0.1" }, + "optionalDependencies": { + "@relayflows/runtime-linux-x64": "2.0.1", + "@relayflows/runtime-darwin-arm64": "2.0.1" + }, "engines": { "node": ">=20" } diff --git a/packages/runtime-darwin-arm64/README.md b/packages/runtime-darwin-arm64/README.md new file mode 100644 index 000000000..60dafcd95 --- /dev/null +++ b/packages/runtime-darwin-arm64/README.md @@ -0,0 +1,20 @@ +# @relayflows/runtime-darwin-arm64 + +Prebuilt Relayflow v2 runtime for `darwin-arm64` (Apple Silicon): + +- `bin/relayflowd` — the kernel daemon (Rust, `cargo build --release -p relayflowd`, target `aarch64-apple-darwin`) +- `bin/flows` — the standalone CLI (`bun build --target=bun-darwin-arm64`) + +Built natively on a `macos-14` GitHub Actions runner from the same commit and +tag as every other release package, and published with npm provenance. + +This package is platform-specific by design. It declares `os`/`cpu`, so npm +refuses to install it anywhere else rather than yielding a binary that cannot +run. `@relayflows/sdk`'s `relayflowd-path.ts` resolves it as an optional +dependency of the `relayflows` CLI package — installing `relayflows` on an +Apple Silicon Mac pulls this in automatically; every other platform's npm +skips it. + +Intel Macs (`darwin-x64`) are not covered by this package and fall through to +`relayflowd-path.ts`'s later resolution steps (a source checkout or `PATH`) +until a `@relayflows/runtime-darwin-x64` package exists. diff --git a/packages/runtime-darwin-arm64/package.json b/packages/runtime-darwin-arm64/package.json new file mode 100644 index 000000000..b4f07422d --- /dev/null +++ b/packages/runtime-darwin-arm64/package.json @@ -0,0 +1,27 @@ +{ + "name": "@relayflows/runtime-darwin-arm64", + "version": "2.0.1", + "description": "Relayflow v2 runtime for darwin-arm64: the relayflowd kernel and the flows CLI, as prebuilt binaries", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/AgentWorkforce/flows.git", + "directory": "packages/runtime-darwin-arm64" + }, + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "files": [ + "bin/" + ], + "bin": { + "relayflowd": "./bin/relayflowd", + "flows": "./bin/flows" + }, + "engines": { + "node": ">=20" + } +} diff --git a/scripts/pack-release.mjs b/scripts/pack-release.mjs index 09a583c56..fbe78108c 100644 --- a/scripts/pack-release.mjs +++ b/scripts/pack-release.mjs @@ -5,7 +5,11 @@ import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; const [name, output = 'dist/publish'] = process.argv.slice(2); -assert(['surface', 'sdk', 'runtime-linux-x64', 'relayflows'].includes(name), 'unknown release package'); +const runtimeMatch = /^runtime-([a-z0-9]+)-([a-z0-9]+)$/.exec(name ?? ''); +assert( + ['surface', 'sdk', 'relayflows'].includes(name) || runtimeMatch !== null, + 'unknown release package', +); // Every release package is scoped (@relayflows/) except the CLI alias, // which is published unscoped so `npm install -g relayflows` names it directly. const expectedName = name === 'relayflows' ? 'relayflows' : `@relayflows/${name}`; @@ -30,7 +34,7 @@ try { if (dependency.startsWith('@relayflows/')) assert.equal(version, expected); } } - const required = name === 'runtime-linux-x64' + const required = runtimeMatch !== null ? ['bin/relayflowd', 'bin/flows'] : name === 'relayflows' ? ['bin/flows.js'] @@ -45,15 +49,22 @@ try { for (const file of Object.values(pkg.bin || {})) { assert(statSync(join(root, file)).mode & 0o111, `non-executable ${file}`); } - if (name === 'runtime-linux-x64') { - assert.equal(process.platform, 'linux', 'runtime smoke requires Linux'); - assert.equal(process.arch, 'x64', 'runtime smoke requires x64'); - execFileSync(join(root, 'bin/relayflowd'), ['--help'], { stdio: 'inherit' }); - const report = JSON.parse(execFileSync(join(root, 'bin/flows'), [ - 'check', '--json', 'testdata/hello-deterministic.flow.yaml', - ], { encoding: 'utf8' })); - assert.equal(report.ok, true); - assert.equal(report.path, 'testdata/hello-deterministic.flow.yaml'); + if (runtimeMatch !== null) { + const [, platform, arch] = runtimeMatch; + // A cross-arch repack (e.g. re-verifying runtime-darwin-arm64's tarball + // from the linux publish job) can only assert shape, above — a foreign + // binary cannot be executed here. Only the matching host actually runs + // it, which is also where CI originally built and smoke-tested it. + if (process.platform === platform && process.arch === arch) { + execFileSync(join(root, 'bin/relayflowd'), ['--help'], { stdio: 'inherit' }); + const report = JSON.parse(execFileSync(join(root, 'bin/flows'), [ + 'check', '--json', 'testdata/hello-deterministic.flow.yaml', + ], { encoding: 'utf8' })); + assert.equal(report.ok, true); + assert.equal(report.path, 'testdata/hello-deterministic.flow.yaml'); + } else { + console.log(`Skipping ${name} execution smoke: built for ${platform}-${arch}, running on ${process.platform}-${process.arch}`); + } } console.log(`PACK_OK ${pkg.name}@${pkg.version}: ${required.map((file) => `package/${file}`).join(', ')}`); if (process.env.GITHUB_OUTPUT) { diff --git a/scripts/publish.test.mjs b/scripts/publish.test.mjs index a303bf6fa..85e1b8d95 100644 --- a/scripts/publish.test.mjs +++ b/scripts/publish.test.mjs @@ -11,7 +11,7 @@ const read = (path) => JSON.parse(readFileSync(path, 'utf8')); function fixture(t) { const root = mkdtempSync(join(tmpdir(), 'flows-publish-test-')); t.after(() => rmSync(root, { recursive: true, force: true })); - for (const name of ['surface', 'sdk', 'runtime-linux-x64', 'relayflows']) { + for (const name of ['surface', 'sdk', 'runtime-linux-x64', 'runtime-darwin-arm64', 'relayflows']) { mkdirSync(join(root, 'packages', name), { recursive: true }); const path = join(root, 'packages', name, 'package.json'); cpSync(`packages/${name}/package.json`, path); @@ -47,7 +47,7 @@ test('one SDK anchor rewrites all internal dependency types and preserves extern writeFileSync(path, JSON.stringify(pkg)); const result = version(root, { CUSTOM_VERSION: '3.0.0-rc.2' }); assert.equal(result.status, 0, result.stderr); - for (const name of ['sdk', 'surface', 'runtime-linux-x64', 'relayflows']) { + for (const name of ['sdk', 'surface', 'runtime-linux-x64', 'runtime-darwin-arm64', 'relayflows']) { assert.equal(read(join(root, 'packages', name, 'package.json')).version, '3.0.0-rc.2'); } const updated = read(path); @@ -58,6 +58,10 @@ test('one SDK anchor rewrites all internal dependency types and preserves extern assert.equal(updated.dependencies.yaml, pkg.dependencies.yaml); const relayflows = read(join(root, 'packages/relayflows/package.json')); assert.equal(relayflows.dependencies['@relayflows/sdk'], '3.0.0-rc.2'); + // relayflows' real optionalDependencies (not the synthetic sdk one set up + // above) — both per-platform runtime packages must move together with it. + assert.equal(relayflows.optionalDependencies['@relayflows/runtime-linux-x64'], '3.0.0-rc.2'); + assert.equal(relayflows.optionalDependencies['@relayflows/runtime-darwin-arm64'], '3.0.0-rc.2'); }); test('prerelease bumps use the SDK anchor and output the resolved version', (t) => { @@ -112,6 +116,9 @@ test('relayflows tarball publishes unscoped and rejects a missing bin', (t) => { const path = join(root, 'packages/relayflows/package.json'); const pkg = read(path); pkg.dependencies['@relayflows/sdk'] = '2.0.0'; + for (const name of Object.keys(pkg.optionalDependencies || {})) { + pkg.optionalDependencies[name] = '2.0.0'; + } writeFileSync(path, JSON.stringify(pkg)); const run = () => spawnSync(process.execPath, [packScript, 'relayflows'], { cwd: root, encoding: 'utf8' }); const missing = run(); @@ -133,9 +140,34 @@ test('relayflows tarball publishes unscoped and rejects a missing bin', (t) => { test('runtime tarball refuses an unstaged binary package', (t) => { const root = fixture(t); - const result = spawnSync(process.execPath, [packScript, 'runtime-linux-x64'], { + for (const name of ['runtime-linux-x64', 'runtime-darwin-arm64']) { + const result = spawnSync(process.execPath, [packScript, name], { + cwd: root, encoding: 'utf8', + }); + assert.notEqual(result.status, 0, name); + assert.match(result.stderr, /missing package\/bin\/relayflowd/, name); + } +}); + +test('a runtime tarball packs and asserts shape, running the real smoke only on a matching host', (t) => { + const root = fixture(t); + const bin = join(root, 'packages/runtime-darwin-arm64/bin'); + mkdirSync(bin); + // Not a real Mach-O binary, but scriptable enough to behave correctly IF + // this test happens to run on an actual darwin-arm64 host (pack-release.mjs + // only skips execution on a *foreign* host — same-host still runs it for + // real, which this repo's own dev machines can be). + const stub = ['#!/bin/sh', 'if [ "$1" = "--help" ]; then exit 0; fi', + 'if [ "$1" = "check" ]; then echo \'{"ok":true,"path":"testdata/hello-deterministic.flow.yaml"}\'; exit 0; fi', + 'exit 1', ''].join('\n'); + writeFileSync(join(bin, 'relayflowd'), stub); + writeFileSync(join(bin, 'flows'), stub); + execFileSync('chmod', ['+x', join(bin, 'relayflowd'), join(bin, 'flows')]); + const result = spawnSync(process.execPath, [packScript, 'runtime-darwin-arm64'], { cwd: root, encoding: 'utf8', }); - assert.notEqual(result.status, 0); - assert.match(result.stderr, /missing package\/bin\/relayflowd/); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /PACK_OK @relayflows\/runtime-darwin-arm64@2\.0\.0/); + const isMatchingHost = process.platform === 'darwin' && process.arch === 'arm64'; + assert.equal(result.stdout.includes('Skipping runtime-darwin-arm64 execution smoke'), !isMatchingHost); }); diff --git a/scripts/version-packages.mjs b/scripts/version-packages.mjs index 99d696d5e..79a2352a9 100644 --- a/scripts/version-packages.mjs +++ b/scripts/version-packages.mjs @@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; // The SDK is the version anchor; no package independently computes a bump. -const paths = ['surface', 'sdk', 'runtime-linux-x64', 'relayflows'].map((name) => `packages/${name}/package.json`); +const paths = ['surface', 'sdk', 'runtime-linux-x64', 'runtime-darwin-arm64', 'relayflows'].map((name) => `packages/${name}/package.json`); if (process.env.CUSTOM_VERSION && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(process.env.CUSTOM_VERSION)) { throw new Error('custom_version must be a semantic version'); } From ade2a6d102ed5340a9aca2f86af03c0f398e9a38 Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Tue, 8 Sep 2026 10:18:28 +0200 Subject: [PATCH 2/2] fix(publish): feed the surface tarball to the relayflows build step too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by actually running a full dry-run of publish.yml against this branch (triggered to validate the new build-darwin-arm64 job) — the existing "Build relayflows CLI wrapper against packed SDK" step only installed the SDK tarball. @relayflows/sdk depends on @relayflows/surface transitively, at the same freshly-bumped, never-published version, so npm fell through to resolving it from the real registry and got ETARGET: no version bump can ever be live there yet at build time. This step apparently has never been exercised end to end since it was added in #237 — no PR check runs publish.yml (workflow_dispatch only), so nothing caught it until this run. Same fix already used one step earlier for the SDK-against-surface case: install both tarballs together so the transitive dependency resolves locally instead of hitting the registry. Confirmed locally end to end (bumped versions, packed both tarballs, ran the fixed install, ran the wrapper's own check smoke test — all green) before pushing. The new build-darwin-arm64 job doesn't touch packages/relayflows, so it isn't affected — it already succeeded for real on a macos-14 runner in the dry-run that surfaced this. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013Y8uLRUXqKSZsqeeUMNaS2 --- .github/workflows/publish.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8a6ad1823..e230f27a2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -114,12 +114,22 @@ jobs: # 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" + 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 '