Skip to content

fix(cli): support --version and -V (#579) #1101

fix(cli): support --version and -V (#579)

fix(cli): support --version and -V (#579) #1101

name: Relayflow v2 Cloud runtime artifact
on:
workflow_dispatch:
# Also on push to main, because a PR is checked at its own head and never as
# merged. Every PR squash-merges onto a main that has moved since its CI ran,
# so the composed result was going unverified: on 2026-09-05, twelve merges
# landed in one evening across the exactly-once claim path, resume adoption
# and the authored-flow executor -- each green on its own branch, none of them
# ever run together until this was dispatched by hand.
#
# A broken compose would otherwise surface as an unrelated PR going red, which
# is the most expensive way to find it: the author debugs their own change
# first. Paths are deliberately NOT filtered here -- on main the question is
# whether the tree is good, not whether this commit touched the kernel.
push:
branches:
- main
pull_request:
paths:
- ".github/workflows/cloud-runtime-artifact.yml"
- "kernel/**"
- "packages/sdk/**"
- "scripts/cloud-artifact.mjs"
- "scripts/build-standalone-cli.mjs"
- "scripts/cloud-artifact.test.mjs"
- "testdata/**"
permissions:
contents: read
jobs:
linux-x64-artifact:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
# PR workflows otherwise build GitHub's ephemeral merge ref and stamp
# a provenance SHA that can disappear after the PR closes.
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- 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: Test artifact contract
run: node --test scripts/cloud-artifact.test.mjs
- name: Build relayflowd
working-directory: kernel
run: cargo build --locked --release -p relayflowd
# The kernel suite had never run in CI. The only cargo invocation was the
# release build above, so every kernel-side defect was invisible here by
# construction -- including, on 2026-09-03, an exactly-once double-fire
# where one effect fired twice, a `$ref` cycle that aborted the daemon and
# re-ran the effect on every resume, and two tests in the tree encoding
# opposite contracts that both passed because neither was ever executed.
#
# The toolchain and the build are already paid for above; this adds the
# test run and nothing else.
#
# Plain `cargo`, NOT ops/cargo.sh. That wrapper unconditionally redirects
# RUSTUP_HOME to $HOME/.relayflows-toolchain/rustup, which on a runner is
# empty, so the rustup shim has nothing to choose:
# error: rustup could not choose a version of cargo to run, because one
# wasn't specified explicitly, and no default is configured
# The wrapper exists for a cloud sandbox, where the propagated tree drops
# files over a size cap; a GitHub runner has neither that constraint nor
# that tree, and dtolnay/rust-toolchain has already installed a default
# toolchain into the standard home. Matching the build step above is both
# simpler and the configuration that is known to work here.
- name: Test kernel
working-directory: kernel
run: cargo test --workspace
- name: Build authoring surface
working-directory: packages/surface
run: |
bun install --frozen-lockfile --ignore-scripts
bun run build
# --ignore-scripts because the surface is already built above; without it
# npm runs the file: dependency's prepare before its own devDependencies
# exist. The SDK's own build is the next step, so nothing is skipped.
- name: Install SDK dependencies
run: npm ci --prefix packages/sdk --ignore-scripts
# `npm ci` above installs `@relayflows/surface@2.0.8` from the REGISTRY,
# not the local one built two steps up. Any SDK change that imports a
# symbol that landed in the local surface but is not yet in the published
# 2.0.8 (e.g. LlmOptions on flows#296) then fails typecheck. Override the
# registry copy with the local directory install — --no-save keeps
# package.json / package-lock.json unchanged so this is a CI-only local
# override.
#
# The `./` prefix is load-bearing: `npm install packages/surface`
# without it makes npm treat the string as a GitHub org/repo shorthand
# (ssh://git@github.com/packages/surface.git) which then fails
# authentication.
- name: Override registry surface with local build
run: npm install ./packages/surface --prefix packages/sdk --no-save --ignore-scripts
# `workflows/*.flow.ts` import `@relayflows/surface` from the repo root,
# where Node's resolution finds no node_modules at all in CI: everything
# above installs under packages/sdk. A workflow-driving test (e.g.
# tests/stuck-run-triage.test.ts) therefore cannot load its subject.
# Link the same local surface at the root, which is also what a checkout
# needs for `flows check` to run against a workflow file.
- name: Link the local surface at the repo root
run: |
mkdir -p node_modules/@relayflows
ln -sfn ../../packages/sdk/node_modules/@relayflows/surface \
node_modules/@relayflows/surface
node -e "console.log(require.resolve('@relayflows/surface'))"
- name: Provision hosted extension sandbox
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends bubblewrap
# Ubuntu 24.04 restricts unprivileged user namespaces through
# AppArmor by default. This runner is an ephemeral VM dedicated to
# the job; enable the kernel facility so the exact production
# user/PID/network/mount namespace command can be exercised.
if sysctl kernel.apparmor_restrict_unprivileged_userns >/dev/null 2>&1; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
/usr/bin/bwrap --version
/usr/bin/bwrap --unshare-all --die-with-parent --new-session \
--ro-bind / / /bin/true
- name: Test SDK and type-level authoring contracts
working-directory: packages/sdk
env:
# `live-kernel.test.ts` runs one case against the REAL Claude analyzer
# and fails by default when it cannot, deliberately: an unavailable
# analyzer is diagnostics, never acceptance, and a reader must not get
# a green that proves nothing about gate 2.
#
# A GitHub runner is exactly the case the flag was written for -- it
# has no `claude` binary and no model access:
# LIVE_ANALYZER_UNAVAILABLE: ... cannot run "claude": spawnSync claude ENOENT
# so without this the full SDK suite can never pass here, which is a
# defect in #153: it enabled the suite without giving CI a way to run
# it.
#
# Setting it is an explicit statement, not a convenience: **this
# workflow is not gate-2 acceptance evidence.** Gate-2 evidence has to
# come from a machine that can actually reach a model, and the skipped
# case says so in its own output. Everything else in the suite still
# runs and still gates.
RELAYFLOWS_ALLOW_ANALYZER_SKIP: '1'
run: |
# The whole suite, not four named files. Naming files means a test
# added to any other file never runs, which is how ~22 of ~26 SDK
# test files were uncovered.
#
# This is `npm test` expanded, minus test:prep. `npm test` is
# test:prep && typecheck && build && typecheck:tests && vitest run,
# and test:prep shells out to ops/cargo.sh -- which fails on a runner
# for the reason given on the kernel step above. Its only purpose is
# to produce the relayflowd binary that tests/live-kernel.test.ts
# execs, and this job has already built one: the release binary from
# the build step. Point RELAYFLOWD_BIN at it rather than compiling a
# second, debug copy through a wrapper that cannot run here.
#
# The build is still required: several test files fail at collection
# without packages/sdk/dist, which is why a bare `vitest run` is not enough.
# test:prep's other half, kept: it re-asserts the executable bit on the
# preflight CLI fixtures. They are committed 100755 so actions/checkout
# already restores them, but the guard is one line and the failure it
# prevents is an opaque EACCES deep inside a preflight test.
( [ ! -d ../testdata/preflight ] || \
find ../testdata/preflight -name '*-cli' -type f -exec chmod +x {} + )
npm run typecheck
npm run build
npm run typecheck:tests
RELAYFLOWD_BIN="$GITHUB_WORKSPACE/kernel/target/release/relayflowd" \
./node_modules/.bin/vitest run
- name: Build standalone flows CLI
run: |
mkdir -p dist/cloud-artifact-input
node scripts/build-standalone-cli.mjs bun-linux-x64 dist/cloud-artifact-input/flows
- name: Assemble artifact and smoke verifier path
env:
SOURCE_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
node scripts/cloud-artifact.mjs build \
--relayflowd kernel/target/release/relayflowd \
--flows-executable dist/cloud-artifact-input/flows \
--output-dir dist/cloud-artifact \
--source-commit "$SOURCE_COMMIT"
archive="$(find dist/cloud-artifact -name '*.tar.gz' -type f -print -quit)"
checksum="$(awk '{print $1}' "$archive.sha256")"
node scripts/cloud-artifact.mjs verify \
--archive "$archive" \
--sha256 "$checksum"
- name: Smoke exact Linux artifact
run: |
archive="$(find dist/cloud-artifact -name '*.tar.gz' -type f -print -quit)"
mkdir -p dist/cloud-artifact-smoke
tar -xzf "$archive" -C dist/cloud-artifact-smoke
dist/cloud-artifact-smoke/bin/relayflowd --help
flows_output="$(dist/cloud-artifact-smoke/bin/flows check --json testdata/hello-deterministic.flow.yaml)"
printf '%s\n' "$flows_output"
node -e '
const report = JSON.parse(process.argv[1]);
if (report.ok !== true) throw new Error("flows smoke report was not ok");
if (report.path !== "testdata/hello-deterministic.flow.yaml") {
throw new Error(`flows smoke reported unexpected path: ${report.path}`);
}
' "$flows_output"
- uses: actions/upload-artifact@v4
with:
name: relayflow-v2-linux-x64-${{ github.event.pull_request.head.sha || github.sha }}
path: |
dist/cloud-artifact/*.tar.gz
dist/cloud-artifact/*.sha256
if-no-files-found: error
retention-days: 14