Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 116 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 '
Expand Down Expand Up @@ -150,9 +160,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 <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
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:
Expand All @@ -171,13 +265,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-<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 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
Expand All @@ -187,11 +285,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:
Expand All @@ -200,7 +303,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"
Expand Down Expand Up @@ -233,6 +336,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
Expand All @@ -248,6 +358,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}"
Expand Down
11 changes: 11 additions & 0 deletions packages/relayflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
24 changes: 24 additions & 0 deletions packages/relayflows/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/relayflows/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
20 changes: 20 additions & 0 deletions packages/runtime-darwin-arm64/README.md
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 27 additions & 0 deletions packages/runtime-darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
33 changes: 22 additions & 11 deletions scripts/pack-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>) except the CLI alias,
// which is published unscoped so `npm install -g relayflows` names it directly.
const expectedName = name === 'relayflows' ? 'relayflows' : `@relayflows/${name}`;
Expand All @@ -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']
Expand All @@ -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) {
Expand Down
Loading
Loading