fix(cli): support --version and -V (#579) #1101
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |