mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 06:16:06 -10:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The local runner (`paperclip-runnerd`) supervises each provider process, such as the ACPX sidecar for Claude, in its own process group > - Run teardown stops that process group, and nothing else > - Claude Code starts each Bash tool command in a new session (`setsid`). A command such as `nohup ./bench.sh & echo $!` leaves a job in that session, and the wrapper exits at once. The job is then a child of init and outside the owned group > - In a restart chaos storm, a run failed after a restart resume. Its tool job kept running beside the task's next run, in the same workspace, for more than four minutes > - This pull request makes the supervisor record every process that its provider starts, including processes that leave the group, and stop them all before teardown reports success > - The benefit is that a run's tools cannot outlive the run and cause duplicate side effects in the next run ## Linked Issues or Issue Description **What happened?** A restart chaos storm ran a local ACPX Claude task. After a restart resume, the agent ran `nohup ./bench.sh <nonce> > log 2>&1 & echo "started pid $!"` and then ended its turn without a result. The run failed with `native_finalization_missing`. The runner tore down the sidecar group, and the guardian killed the provider group. `bench.sh` was in the session that Claude Code made for the tool (pgid and sid = the exited wrapper, ppid = 1). It was never signalled. It kept forking `sleep 1` while the task's next run started and finished in the same workspace. **Steps to reproduce** 1. Run a local ACPX Claude task whose agent runs a Bash tool command such as `nohup ./bench.sh x > log 2>&1 & echo $!`. 2. End the run (any outcome, including failure after a restart resume). 3. Run `ps -axo pid,ppid,pgid,command | grep bench.sh`. The job is still running with ppid 1. **Agent adapter(s) involved** Claude Code (local ACPX). The gap is in the shared supervisor, so any provider whose tools leave the group is affected. **Root cause** `SupervisedProcess` (`packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs`) spawns with `process_group(0)` and stops only that group in `terminate_group`, `terminate_group_immediately`, `wait` and `Drop`. Its own comment said that a caller that must exclude escaped or re-parented descendants needs a separate lifetime fence. No caller had one. The ACPX sidecar transport (`acpx_sidecar_transport.rs`, `shutdown` and `poison`) and the ACPX provider guardian (`installation-integrity.ts`, which kills only its own group) therefore left every `setsid` tool job behind. The failure path after the resume was not special: the same gap applies to every teardown. **Expected behavior** When a run ends for any reason, every process that its provider or tools started stops, and teardown proves that they are gone. Identity comes from PID, group, session and start time, never from a command name. ## What Changed - Add the `paperclip-process-table` crate. It reads PID, parent, process group and start time for every process (libproc on macOS, `/proc` on Linux). `runner-core` forbids unsafe code, so the two checked libproc calls live in this small crate. - Add `process_lineage.rs` to `runner-core`. A sampler thread (every 50 ms) records each descendant of the supervised process by PID and start time. It also records each group and session that a descendant leads, with the leader's start time. A process belongs to the lineage when its parent belongs, or when it is in a recorded group or session and started no earlier than its leader. Groups and sessions that the supervised process only inherited (runnerd's session) are never recorded. A recorded ID whose PID now names a different process is dropped. - `SupervisedProcess` samples the lineage just before it signals, and after the leader is reaped it stops every live member: TERM, then KILL after the shutdown grace. It fails with an error until it proves that no member is alive (grace plus 5 s). This applies to `terminate_group`, `terminate_group_immediately`, `wait` and `Drop`. - If the lineage sampler cannot start, the supervisor stops the process it just started and reports the failure, so a provider never runs without tool cleanup. - Table reads and state updates happen under one lock, so an older read cannot erase members that a newer read recorded. - The process table reports processes that exist but cannot be read. A recorded member that cannot be read stays unresolved, and teardown fails until its exit is proven. - The Runner workspace now has two members. `scripts/runner-release-crate-cache.mjs` and `scripts/runner-debug-crate-cache.mjs` cache and mark current the process-table lib unit with the Runner crate (otherwise Cargo rebuilds the Runner crate after every restore). The debug contract accepts only this library-only member and still refuses any other workspace change. The workflow cache path lists and the debug clean step are updated to match. - `terminate_group_immediately` now accepts `EPERM` from the group signal when the leader has already exited. macOS returns `EPERM` for a group whose only member is the unreaped leader. The lineage pass then proves that no other member remains. ## Verification - `cargo test -p paperclip-runner-core --lib -- process_lineage tool_job immediate_group_fence` (macOS, local). - New real-process tests in `process_supervisor.rs`: a supervised shell starts a wrapper with `setsid` (Perl `POSIX::setsid`), the wrapper runs `nohup <loop> &`, writes the job PID and exits. The test asserts that the job is outside the owned group and re-parented, then calls `terminate_group`, `terminate_group_immediately` or drops the process, and asserts that the job is gone when the call returns. - Red: with the tracker disabled, all three fail with `tool job <pid> outlived its provider`. - Green: all three pass. - Unit tests in `process_lineage.rs` cover a re-parented job, a job that left its group but stayed in the recorded session, a session made after the first sighting, PID reuse, and no claim through the supervisor's inherited session. - `node --test scripts/__tests__/runner-release-crate-cache.test.mjs scripts/__tests__/runner-debug-crate-cache.test.mjs` (new case: the process-table library is cached with the crate, and any other member still needs review). - `cargo test -p paperclip-process-table` (includes an exited process reported as gone), and `cargo test -p paperclip-runner-core --lib` (570 passed). `acpx_provider_backend::tests::unconfirmed_suspension_state_becomes_recoverable_only_after_cleanup_and_suspend` fails sometimes under full-suite load. It fails at the same rate with the tracker disabled, so it is a flake that already existed. - Sampler cost: one table read takes about 0.4 ms for 1,200 processes on macOS (about 0.8% of one core for each supervised process). - The restart-chaos replay (`--replay w-20261107T023921-s7106/t67`) did not run. The local session was not permitted to load provider credentials. It is not proven against a live Claude run. ## Risks - Teardown now stops all the processes that a provider started, also intentional background jobs. This is the intended contract: nothing outlives the run. - A wrapper that lives less than one sample (50 ms) and orphans a job before the sampler sees it is not recorded. Claude Code's wrapper sources a shell snapshot and lives for seconds. On Linux, a child-subreaper would close this gap. That is not in this change. - When `runnerd` itself is killed with SIGKILL, this code does not run. The provider guardian still kills its own group, but escaped tool sessions are not stopped in that case. - Teardown can return a new error when it cannot prove the lineage stopped. Callers already handle `terminate_group` errors as provider shutdown failures. - The server does not yet gate the next run on the tool lineage of the prior run directly. It relies on the runner finishing its teardown before the runner exits. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code execution in Claude Code. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
767 lines
40 KiB
YAML
767 lines
40 KiB
YAML
name: Release Verify
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
ref:
|
|
description: Commit SHA, branch, or tag to verify
|
|
required: true
|
|
type: string
|
|
runner_binaries_only:
|
|
description: Build exact-source release daemons without publishing or provider calls
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
outputs:
|
|
runner_artifact_prefix:
|
|
value: ${{ jobs.runner_binaries.outputs.artifact_prefix }}
|
|
provider_pack_artifact_prefix:
|
|
value: ${{ jobs.runner_binaries.outputs.provider_pack_artifact_prefix }}
|
|
native_consumer_matrix:
|
|
value: ${{ jobs.runner_binary_targets.outputs.native_consumers }}
|
|
|
|
# Caller-provided refs may name unmerged PR code. AWS is eligible only when
|
|
# the caller runs on canonical master and verifies that event's exact SHA.
|
|
# The organization group also restricts these workflow files to master.
|
|
jobs:
|
|
runner_chaos_evals:
|
|
if: ${{ !inputs.runner_binaries_only }}
|
|
name: Pre-release Runner chaos evals
|
|
uses: ./.github/workflows/runner-chaos-evals.yml
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
|
|
typecheck:
|
|
if: ${{ !inputs.runner_binaries_only }}
|
|
name: Typecheck
|
|
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
|
|
timeout-minutes: 20
|
|
permissions:
|
|
contents: read
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
with:
|
|
node-version: 24
|
|
package-manager-cache: false
|
|
|
|
# rust-cache hashes its absolute cache paths into the entry's version,
|
|
# and GitHub only serves an entry whose key and version both match. The
|
|
# target directory sits under the checkout, and the checkout root
|
|
# differs by runner: /home/runner/_work on the RunsOn fleets that write
|
|
# this cache against /home/runner/work on GitHub-hosted runners. Every
|
|
# key input agreed, yet all 25 completed pull requests on 2026-09-28
|
|
# logged "No cache found" (run 36424309181) and cold-compiled every
|
|
# crate for ~4 minutes in four lanes. Point rust-cache at the same
|
|
# directory through a checkout-independent path so both layouts hash
|
|
# the same version. Keep this block identical in both workflows: the
|
|
# reader and the writer must agree or the version matches nothing.
|
|
- name: Pin the Runner Rust workspace path
|
|
id: runner_rust_workspace
|
|
run: |
|
|
set -euo pipefail
|
|
pinned="$HOME/paperclip-runner-rust"
|
|
# A symlink here is removed as a link, never followed into the checkout.
|
|
rm -rf "$pinned"
|
|
ln -s "$GITHUB_WORKSPACE/packages/paperclip-runner/runner" "$pinned"
|
|
echo "path=$pinned" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Select the pinned Runner Rust toolchain
|
|
working-directory: packages/paperclip-runner
|
|
run: |
|
|
set -euo pipefail
|
|
rustup show
|
|
toolchain="$(rustup show active-toolchain | awk '{print $1}')"
|
|
echo "RUSTUP_TOOLCHAIN=$toolchain" >> "$GITHUB_ENV"
|
|
|
|
- name: Cache typecheck Rust dependencies
|
|
# Restore and save only within trusted master-push verification. GitHub
|
|
# isolates branch/PR caches from master; other callers compile afresh.
|
|
if: ${{ github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
|
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
|
with:
|
|
workspaces: ${{ steps.runner_rust_workspace.outputs.path }} -> target
|
|
shared-key: release-typecheck-v2-${{ hashFiles('packages/paperclip-runner/runner/Cargo.toml', '.cargo/config', '.cargo/config.toml', 'packages/.cargo/config', 'packages/.cargo/config.toml', 'packages/paperclip-runner/.cargo/config', 'packages/paperclip-runner/.cargo/config.toml', 'packages/paperclip-runner/runner/.cargo/config', 'packages/paperclip-runner/runner/.cargo/config.toml') }}
|
|
# Rebuild workspace code and rerun every check. Cache only compiled
|
|
# dependencies; never restore installed executables from cargo/bin.
|
|
cache-workspace-crates: false
|
|
cache-bin: false
|
|
# The step guard also restricts restores. Save only after a successful
|
|
# master-push verification of that push's exact commit.
|
|
save-if: ${{ github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
|
|
|
- name: Validate release package manifest
|
|
run: node ./scripts/release-package-map.mjs check
|
|
|
|
- name: Restore materialized dependencies and shared builds
|
|
if: hashFiles('.github/actions/setup-ci-dependencies/action.yml') != ''
|
|
uses: ./.github/actions/setup-ci-dependencies
|
|
with:
|
|
save-cache: ${{ github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
|
|
|
- name: Install dependencies
|
|
if: hashFiles('.github/actions/setup-ci-dependencies/action.yml') == ''
|
|
run: pnpm install --no-frozen-lockfile
|
|
|
|
- name: Typecheck
|
|
run: pnpm -r typecheck
|
|
|
|
general_tests:
|
|
if: ${{ !inputs.runner_binaries_only }}
|
|
name: General tests (${{ matrix.group_label }})
|
|
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
|
|
timeout-minutes: 20
|
|
permissions:
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
# Split the long chat file by collected test locations, and balance
|
|
# the remaining server files across ten runners. Rust-backed server
|
|
# suites run in the cached Runner lane below. Local invocations retain
|
|
# their complete general-server group.
|
|
- group: general-server-without-chat-or-native-runner
|
|
group_label: server (1/10)
|
|
shard_index: 0
|
|
shard_count: 10
|
|
- group: general-server-without-chat-or-native-runner
|
|
group_label: server (2/10)
|
|
shard_index: 1
|
|
shard_count: 10
|
|
- group: general-server-without-chat-or-native-runner
|
|
group_label: server (3/10)
|
|
shard_index: 2
|
|
shard_count: 10
|
|
- group: general-server-without-chat-or-native-runner
|
|
group_label: server (4/10)
|
|
shard_index: 3
|
|
shard_count: 10
|
|
- group: general-server-without-chat-or-native-runner
|
|
group_label: server (5/10)
|
|
shard_index: 4
|
|
shard_count: 10
|
|
- group: general-server-without-chat-or-native-runner
|
|
group_label: server (6/10)
|
|
shard_index: 5
|
|
shard_count: 10
|
|
- group: general-server-without-chat-or-native-runner
|
|
group_label: server (7/10)
|
|
shard_index: 6
|
|
shard_count: 10
|
|
- group: general-server-without-chat-or-native-runner
|
|
group_label: server (8/10)
|
|
shard_index: 7
|
|
shard_count: 10
|
|
- group: general-server-without-chat-or-native-runner
|
|
group_label: server (9/10)
|
|
shard_index: 8
|
|
shard_count: 10
|
|
- group: general-server-without-chat-or-native-runner
|
|
group_label: server (10/10)
|
|
shard_index: 9
|
|
shard_count: 10
|
|
- group: general-chat
|
|
group_label: chat (1/3)
|
|
shard_index: 0
|
|
shard_count: 3
|
|
- group: general-chat
|
|
group_label: chat (2/3)
|
|
shard_index: 1
|
|
shard_count: 3
|
|
- group: general-chat
|
|
group_label: chat (3/3)
|
|
shard_index: 2
|
|
shard_count: 3
|
|
# Keep parity with pr.yml: workspaces-a is split with Vitest's
|
|
# native --shard because the ui project dominates the lane.
|
|
- group: general-workspaces-a
|
|
group_label: workspaces-a (1/2)
|
|
shard_index: 0
|
|
shard_count: 2
|
|
- group: general-workspaces-a
|
|
group_label: workspaces-a (2/2)
|
|
shard_index: 1
|
|
shard_count: 2
|
|
- group: general-workspaces-b
|
|
group_label: workspaces-b
|
|
# Keep parity with pr.yml: the db project runs as its own lane.
|
|
- group: general-workspaces-c
|
|
group_label: workspaces-c
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --no-frozen-lockfile
|
|
|
|
- name: Run grouped general test suites
|
|
run: |
|
|
if [ -n "${{ matrix.shard_count }}" ]; then
|
|
pnpm test:run:general -- --group '${{ matrix.group }}' \
|
|
--shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }}
|
|
else
|
|
pnpm test:run:general -- --group '${{ matrix.group }}'
|
|
fi
|
|
|
|
serialized_tests:
|
|
if: ${{ !inputs.runner_binaries_only }}
|
|
name: Serialized tests (${{ matrix.shard_label }})
|
|
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
|
|
timeout-minutes: 20
|
|
permissions:
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- shard_index: 0
|
|
shard_count: 5
|
|
shard_label: 1/5
|
|
- shard_index: 1
|
|
shard_count: 5
|
|
shard_label: 2/5
|
|
- shard_index: 2
|
|
shard_count: 5
|
|
shard_label: 3/5
|
|
- shard_index: 3
|
|
shard_count: 5
|
|
shard_label: 4/5
|
|
- shard_index: 4
|
|
shard_count: 5
|
|
shard_label: 5/5
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --no-frozen-lockfile
|
|
|
|
- name: Run serialized server test shard
|
|
run: pnpm test:run:serialized -- --shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }}
|
|
|
|
runner_workflow_evals:
|
|
if: ${{ !inputs.runner_binaries_only }}
|
|
name: Runner workflow eval scorer contract
|
|
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: read
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --no-frozen-lockfile
|
|
|
|
- name: Run deterministic Runner workflow scorer tests
|
|
run: pnpm test:runner-workflow-evals
|
|
|
|
verify_paperclip_runner:
|
|
if: ${{ !inputs.runner_binaries_only }}
|
|
name: Verify Paperclip Runner (${{ matrix.lane }})
|
|
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
|
|
timeout-minutes: 20
|
|
permissions:
|
|
contents: read
|
|
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- lane: protocol
|
|
checks: check:eval-kernel check:protocol
|
|
- lane: rust
|
|
checks: check:runner check:api-authority
|
|
- lane: server-integration
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
|
|
# rust-cache hashes its absolute cache paths into the entry's version,
|
|
# and GitHub only serves an entry whose key and version both match. The
|
|
# target directory sits under the checkout, and the checkout root
|
|
# differs by runner: /home/runner/_work on the RunsOn fleets that write
|
|
# this cache against /home/runner/work on GitHub-hosted runners. Every
|
|
# key input agreed, yet all 25 completed pull requests on 2026-09-28
|
|
# logged "No cache found" (run 36424309181) and cold-compiled every
|
|
# crate for ~4 minutes in four lanes. Point rust-cache at the same
|
|
# directory through a checkout-independent path so both layouts hash
|
|
# the same version. Keep this block identical in both workflows: the
|
|
# reader and the writer must agree or the version matches nothing.
|
|
- name: Pin the Runner Rust workspace path
|
|
id: runner_rust_workspace
|
|
run: |
|
|
set -euo pipefail
|
|
pinned="$HOME/paperclip-runner-rust"
|
|
# A symlink here is removed as a link, never followed into the checkout.
|
|
rm -rf "$pinned"
|
|
ln -s "$GITHUB_WORKSPACE/packages/paperclip-runner/runner" "$pinned"
|
|
echo "path=$pinned" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Select the pinned Runner Rust toolchain
|
|
working-directory: packages/paperclip-runner
|
|
run: |
|
|
set -euo pipefail
|
|
rustup show
|
|
toolchain="$(rustup show active-toolchain | awk '{print $1}')"
|
|
echo "RUSTUP_TOOLCHAIN=$toolchain" >> "$GITHUB_ENV"
|
|
|
|
# rust-cache hashes every installed toolchain into the cache key, not
|
|
# only the active one. Each runner image also ships its own stable
|
|
# Rust, and those disagree across images: 1.98.0 on the RunsOn fleets
|
|
# against 1.98.1 on ubuntu-latest. Two runners that agreed on the pin
|
|
# therefore still computed different keys, and every GitHub-hosted
|
|
# pull request missed this cache while the fleet hit it. Remove every
|
|
# toolchain except the pin, so the key depends on the pinned compiler
|
|
# and the lockfile alone rather than on what the image happens to
|
|
# carry. Keep this block identical in both workflows: the reader and
|
|
# the writer must agree or the key matches nothing.
|
|
extra_toolchains="$(rustup toolchain list | awk '{print $1}' | grep -vx "$toolchain" || true)"
|
|
if [ -n "$extra_toolchains" ]; then
|
|
echo "$extra_toolchains" | xargs -n1 rustup toolchain uninstall \
|
|
|| echo '::notice title=Runner Rust cache::could not remove an extra toolchain; the cache key may not match'
|
|
fi
|
|
rustup toolchain list
|
|
|
|
- name: Cache Runner Rust dependencies
|
|
# Restore and save only within trusted master-push verification. GitHub
|
|
# isolates branch/PR caches from master; other callers compile afresh.
|
|
if: ${{ github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
|
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
|
with:
|
|
workspaces: ${{ steps.runner_rust_workspace.outputs.path }} -> target
|
|
shared-key: release-runner-v2-${{ hashFiles('packages/paperclip-runner/runner/Cargo.toml', '.cargo/config', '.cargo/config.toml', 'packages/.cargo/config', 'packages/.cargo/config.toml', 'packages/paperclip-runner/.cargo/config', 'packages/paperclip-runner/.cargo/config.toml', 'packages/paperclip-runner/runner/.cargo/config', 'packages/paperclip-runner/runner/.cargo/config.toml') }}
|
|
# Rebuild workspace code and rerun every check. Cache only compiled
|
|
# dependencies; never restore installed executables from cargo/bin.
|
|
cache-workspace-crates: false
|
|
cache-bin: false
|
|
# All lanes restore the existing dependency cache. Only the Rust
|
|
# lane saves it, after warming both release and debug dependencies.
|
|
save-if: ${{ matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --no-frozen-lockfile
|
|
|
|
- name: Build native server test binaries
|
|
if: ${{ matrix.lane == 'server-integration' }}
|
|
timeout-minutes: 10
|
|
working-directory: packages/paperclip-runner
|
|
run: |
|
|
set -euo pipefail
|
|
pnpm build:rust
|
|
cargo build --release --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --bin paperclip-runnerd --bin fake-codex-app-server
|
|
|
|
- name: Run native server integration suites
|
|
if: ${{ matrix.lane == 'server-integration' }}
|
|
run: pnpm test:run:general -- --group general-server-native-runner
|
|
|
|
- name: Verify Paperclip Runner
|
|
if: ${{ matrix.lane != 'server-integration' }}
|
|
env:
|
|
RUNNER_CHECKS: ${{ matrix.checks }}
|
|
run: |
|
|
set -euo pipefail
|
|
for check in $RUNNER_CHECKS; do
|
|
pnpm --filter @paperclipai/paperclip-runner "$check"
|
|
done
|
|
|
|
- name: Compute the Runner debug crate cache key
|
|
id: runner_debug_crate_key
|
|
if: ${{ matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
|
run: node scripts/runner-debug-crate-cache.mjs plan
|
|
|
|
- name: Check for this push's Runner debug crate
|
|
id: runner_debug_crate
|
|
if: ${{ matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
|
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
lookup-only: true
|
|
path: ${{ steps.runner_debug_crate_key.outputs.paths }}
|
|
key: ${{ steps.runner_debug_crate_key.outputs.key }}
|
|
|
|
- name: Clean debug test variants before publishing workspace binaries
|
|
if: ${{ matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha && steps.runner_debug_crate.outputs.cache-hit != 'true' }}
|
|
working-directory: packages/paperclip-runner
|
|
run: cargo clean --profile dev --manifest-path runner/Cargo.toml -p paperclip-runner-core -p paperclip-process-table
|
|
|
|
- name: Warm debug dependencies for the shared Runner cache
|
|
# Protocol tests need debug binaries. Populate their dependencies in
|
|
# the sole cache writer, so a cold save also serves the protocol lane.
|
|
if: ${{ matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
|
run: pnpm --filter @paperclipai/paperclip-runner build:rust
|
|
|
|
- name: Save the Runner debug crate
|
|
if: ${{ matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha && steps.runner_debug_crate.outputs.cache-hit != 'true' }}
|
|
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: ${{ steps.runner_debug_crate_key.outputs.paths }}
|
|
key: ${{ steps.runner_debug_crate_key.outputs.key }}
|
|
|
|
# The dependency cache above deliberately leaves the Runner's own crate
|
|
# out, so every pull request build compiled paperclip-runner-core in
|
|
# release mode (28-49s) although most pushes leave its inputs alone.
|
|
# This master push saves that crate's release artifacts for the readers
|
|
# in pr-trusted.yml ("Restore the Runner release crate") under a key
|
|
# made from the compiler, the checkout root and a hash of every input
|
|
# the build reads, so a reader only ever restores output of inputs
|
|
# byte-identical to its own. Same writer gate as the dependency cache:
|
|
# the Rust lane of an own-repository master push. The entry is looked
|
|
# up first; a push that left the inputs alone already has it. On a
|
|
# miss the crate is cleaned and built with the exact command the
|
|
# readers run, so the artifacts carry no test-profile variants. The
|
|
# path list and the key live in scripts/runner-release-crate-cache.mjs
|
|
# and must match the readers'; a guard test holds them together.
|
|
- name: Compute the Runner release crate cache key
|
|
id: runner_release_crate_key
|
|
if: ${{ matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
|
run: echo "scope=$(node scripts/runner-release-crate-cache.mjs scope)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Check for this push's Runner release crate
|
|
id: runner_release_crate
|
|
if: ${{ matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
|
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
lookup-only: true
|
|
path: |
|
|
packages/paperclip-runner/runner/target/release/.fingerprint/paperclip-runner-core-*
|
|
packages/paperclip-runner/runner/target/release/deps/libpaperclip_runner_core-*
|
|
packages/paperclip-runner/runner/target/release/deps/paperclip_runner_core-*.d
|
|
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
|
|
packages/paperclip-runner/runner/target/release/paperclip-runnerd
|
|
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
|
|
packages/paperclip-runner/runner/target/release/.fingerprint/paperclip-process-table-*
|
|
packages/paperclip-runner/runner/target/release/deps/libpaperclip_process_table-*
|
|
packages/paperclip-runner/runner/target/release/deps/paperclip_process_table-*.d
|
|
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/antigravity-distributions.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
|
|
|
|
- name: Build the Runner release crate for its cache
|
|
if: ${{ matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha && steps.runner_release_crate.outputs.cache-hit != 'true' }}
|
|
working-directory: packages/paperclip-runner
|
|
run: |
|
|
set -euo pipefail
|
|
cargo clean --release --manifest-path runner/Cargo.toml -p paperclip-runner-core
|
|
pnpm build:binary
|
|
|
|
- name: Save the Runner release crate
|
|
if: ${{ matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha && steps.runner_release_crate.outputs.cache-hit != 'true' }}
|
|
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
packages/paperclip-runner/runner/target/release/.fingerprint/paperclip-runner-core-*
|
|
packages/paperclip-runner/runner/target/release/deps/libpaperclip_runner_core-*
|
|
packages/paperclip-runner/runner/target/release/deps/paperclip_runner_core-*.d
|
|
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
|
|
packages/paperclip-runner/runner/target/release/paperclip-runnerd
|
|
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
|
|
packages/paperclip-runner/runner/target/release/.fingerprint/paperclip-process-table-*
|
|
packages/paperclip-runner/runner/target/release/deps/libpaperclip_process_table-*
|
|
packages/paperclip-runner/runner/target/release/deps/paperclip_process_table-*.d
|
|
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/antigravity-distributions.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
|
|
|
|
build:
|
|
if: ${{ !inputs.runner_binaries_only }}
|
|
name: Build
|
|
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
|
|
timeout-minutes: 20
|
|
permissions:
|
|
contents: read
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
persist-credentials: false
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
with:
|
|
node-version: 24
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --no-frozen-lockfile
|
|
|
|
- name: Build
|
|
run: pnpm build
|
|
|
|
runner_binary_targets:
|
|
if: inputs.runner_binaries_only
|
|
name: Read exact-source daemon targets
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
matrix: ${{ steps.targets.outputs.matrix }}
|
|
native_consumers: ${{ steps.targets.outputs.native_consumers }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
persist-credentials: false
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
|
with:
|
|
node-version: 24
|
|
- name: Preserve the immutable source's platform contract
|
|
id: targets
|
|
env:
|
|
SOURCE_SHA: ${{ inputs.ref }}
|
|
run: |
|
|
set -euo pipefail
|
|
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1
|
|
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
|
|
node --input-type=module <<'NODE'
|
|
import assert from 'node:assert/strict';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { appendFileSync } from 'node:fs';
|
|
const source = process.env.SOURCE_SHA;
|
|
const readBlob = file => {
|
|
const entries = execFileSync('git', ['ls-tree', '-z', source, '--', file], {
|
|
encoding: 'utf8', maxBuffer: 4096,
|
|
}).split('\0').filter(Boolean);
|
|
if (!entries.length) return undefined;
|
|
assert.equal(entries.length, 1, 'Ambiguous exact-source target contract');
|
|
assert.ok(entries[0].startsWith('100644 blob ') && entries[0].endsWith('\t' + file), 'Target contract must be a regular source file');
|
|
return execFileSync('git', ['show', source + ':' + file], { encoding: 'utf8', maxBuffer: 64 * 1024 });
|
|
};
|
|
const data = readBlob('packages/paperclip-runner/release-runner-targets.json');
|
|
let declared;
|
|
if (data !== undefined) {
|
|
const contract = JSON.parse(data);
|
|
assert.ok(contract && contract.schema === 'paperclip.runner.release-targets.v1'
|
|
&& Object.keys(contract).sort().join() === 'schema,targets', 'Unknown exact-source daemon platform contract');
|
|
declared = contract.targets;
|
|
} else {
|
|
const legacy = readBlob('packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs');
|
|
const declarations = [...(legacy ?? '').matchAll(/^[ \t]*export const RELEASE_RUNNER_TARGETS = (?:Object\.freeze\()?(\[[^\r\n]*\])\)?;[ \t]*$/gm)];
|
|
assert.equal(declarations.length, 1, 'Unknown exact-source daemon platform contract');
|
|
declared = JSON.parse(declarations[0][1]);
|
|
}
|
|
const historical = ['darwin-arm64', 'darwin-x64', 'linux-x64'];
|
|
const current = [...historical, 'linux-arm64'];
|
|
assert.ok(Array.isArray(declared), 'Unknown exact-source daemon platform contract');
|
|
const matches = expected => JSON.stringify([...declared].sort()) === JSON.stringify([...expected].sort());
|
|
assert.ok(matches(historical) || matches(current), 'Unknown exact-source daemon platform contract');
|
|
const targets = matches(current) ? current : historical;
|
|
const hosts = { 'linux-x64': targets.length === current.length ? 'ubuntu-24.04' : 'ubuntu-latest', 'linux-arm64': 'ubuntu-24.04-arm', 'darwin-arm64': 'macos-15', 'darwin-x64': 'macos-15-intel' };
|
|
const include = targets.map(target => ({ target, host: hosts[target] }));
|
|
appendFileSync(process.env.GITHUB_OUTPUT, `matrix=${JSON.stringify({ include })}\nnative_consumers=${JSON.stringify({ include: include.filter(row => row.target !== 'linux-x64') })}\n`);
|
|
NODE
|
|
|
|
runner_binaries:
|
|
if: inputs.runner_binaries_only
|
|
needs: runner_binary_targets
|
|
name: Release daemon (${{ matrix.target }})
|
|
runs-on: ${{ matrix.host }}
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJSON(needs.runner_binary_targets.outputs.matrix) }}
|
|
outputs:
|
|
artifact_prefix: ${{ steps.identity.outputs.artifact_prefix }}
|
|
provider_pack_artifact_prefix: ${{ steps.identity.outputs.provider_pack_artifact_prefix }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
persist-credentials: false
|
|
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86
|
|
with:
|
|
version: 9.15.4
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
|
with:
|
|
node-version: 24.21.0
|
|
- name: Bind immutable producer source and target
|
|
id: identity
|
|
env:
|
|
SOURCE_SHA: ${{ inputs.ref }}
|
|
TARGET: ${{ matrix.target }}
|
|
run: |
|
|
set -euo pipefail
|
|
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1
|
|
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
|
|
[[ "$(node -p 'process.platform + "-" + process.arch')" == "$TARGET" ]] || exit 1
|
|
echo "artifact_prefix=runner-release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
|
|
if node --input-type=module -e 'const m = await import("./packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs"); process.exit(m.RELEASE_PROVIDER_PACK_TARGETS ? 0 : 1)'; then
|
|
echo "provider_pack_artifact_prefix=runner-provider-packs-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
- name: Pin native Cargo workspace and toolchain
|
|
id: native_rust_workspace
|
|
working-directory: packages/paperclip-runner
|
|
run: |
|
|
set -euo pipefail
|
|
pinned="$HOME/paperclip-runner-rust"
|
|
if [[ -e "$pinned" && ! -L "$pinned" ]]; then
|
|
echo '::error::Native Cargo cache path must not replace a real directory'
|
|
exit 1
|
|
fi
|
|
rm -f "$pinned"
|
|
ln -s "$GITHUB_WORKSPACE/packages/paperclip-runner/runner" "$pinned"
|
|
echo "path=$pinned" >> "$GITHUB_OUTPUT"
|
|
rustup show
|
|
toolchain="$(rustup show active-toolchain | awk '{print $1}')"
|
|
echo "RUSTUP_TOOLCHAIN=$toolchain" >> "$GITHUB_ENV"
|
|
extra_toolchains="$(rustup toolchain list | awk '{print $1}' | grep -vx "$toolchain" || true)"
|
|
if [[ -n "$extra_toolchains" ]]; then
|
|
echo "$extra_toolchains" | xargs -n1 rustup toolchain uninstall \
|
|
|| echo '::notice title=Native Rust cache::could not remove an extra toolchain; the cache key may not match'
|
|
fi
|
|
rustup toolchain list
|
|
- name: Cache native Rust dependencies
|
|
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
|
with:
|
|
workspaces: ${{ steps.native_rust_workspace.outputs.path }} -> target
|
|
shared-key: release-native-v2-${{ hashFiles('packages/paperclip-runner/runner/Cargo.toml', '.cargo/config', '.cargo/config.toml', 'packages/.cargo/config', 'packages/.cargo/config.toml', 'packages/paperclip-runner/.cargo/config', 'packages/paperclip-runner/.cargo/config.toml', 'packages/paperclip-runner/runner/.cargo/config', 'packages/paperclip-runner/runner/.cargo/config.toml') }}-${{ matrix.host }}-${{ matrix.target }}-${{ hashFiles('packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/src/**', 'packages/paperclip-runner/*.json') }}
|
|
cache-workspace-crates: false
|
|
cache-bin: false
|
|
cache-on-failure: false
|
|
save-if: ${{ github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
|
- name: Install dependencies
|
|
run: pnpm install --no-frozen-lockfile
|
|
- name: Build TypeScript contract and host daemon concurrently
|
|
run: |
|
|
node --input-type=module <<'NODE'
|
|
import { mkdirSync, readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { startPrefetch, stopPrefetches } from './scripts/prefetch-child.mjs';
|
|
const root = process.cwd(), logs = join(process.env.RUNNER_TEMP, 'native-build-preparation');
|
|
mkdirSync(logs, { recursive: true });
|
|
const builds = [];
|
|
let failure;
|
|
try {
|
|
for (const [name, args, cwd] of [
|
|
['typescript', ['--filter', '@paperclipai/paperclip-runner', 'build:typescript'], root],
|
|
['native', ['build:binary'], join(root, 'packages/paperclip-runner')],
|
|
]) {
|
|
builds.push(startPrefetch({ name, cmd: 'pnpm', args, cwd, env: process.env, logPath: join(logs, name + '.log') }));
|
|
}
|
|
await Promise.all(builds.map(build => build.done));
|
|
} catch (error) { failure = error; }
|
|
finally {
|
|
await stopPrefetches(builds);
|
|
const results = await Promise.allSettled(builds.map(build => build.done));
|
|
builds.forEach((build, index) => {
|
|
console.log('::group::' + build.name + ' build');
|
|
process.stdout.write(readFileSync(join(logs, build.name + '.log'), 'utf8'));
|
|
console.log('::endgroup::');
|
|
console.log(JSON.stringify({ build: build.name, status: results[index].status,
|
|
exitCode: build.child.exitCode, signal: build.child.signalCode, durationMs: build.durationMs }));
|
|
});
|
|
}
|
|
if (failure) throw failure;
|
|
NODE
|
|
- name: Record actual daemon metadata and immutable bytes
|
|
env:
|
|
SOURCE_SHA: ${{ inputs.ref }}
|
|
TARGET: ${{ matrix.target }}
|
|
run: |
|
|
node packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs record "$SOURCE_SHA" "$TARGET" \
|
|
packages/paperclip-runner/dist/bin/paperclip-runnerd "$RUNNER_TEMP/runner-release/$TARGET"
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
|
with:
|
|
name: ${{ steps.identity.outputs.artifact_prefix }}-${{ matrix.target }}
|
|
path: ${{ runner.temp }}/runner-release/
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
|
|
- name: Build target-owned Linux provider pack
|
|
id: provider_pack
|
|
if: startsWith(matrix.target, 'linux-') && steps.identity.outputs.provider_pack_artifact_prefix != ''
|
|
env:
|
|
SOURCE_SHA: ${{ inputs.ref }}
|
|
TARGET: ${{ matrix.target }}
|
|
run: |
|
|
set -euo pipefail
|
|
pack="$RUNNER_TEMP/provider-pack"
|
|
PAPERCLIP_RUNNER_SOURCE_REVISION="$SOURCE_SHA" node packages/paperclip-runner/scripts/build-provider-pack.mjs "$pack"
|
|
node --input-type=module - "$pack/provider-pack.json" <<'NODE'
|
|
import { readFileSync } from 'node:fs';
|
|
import { verifyReleaseProviderPack } from './packages/paperclip-runner/scripts/release-provider-pack.mjs';
|
|
verifyReleaseProviderPack(JSON.parse(readFileSync(process.argv[2], 'utf8')), process.env.SOURCE_SHA, process.env.TARGET);
|
|
NODE
|
|
mkdir -p "$RUNNER_TEMP/release-provider-packs/$TARGET"
|
|
cp "$pack/provider-pack.json" "$RUNNER_TEMP/release-provider-packs/$TARGET/provider-pack.json"
|
|
tar -C "$pack" -czf "$RUNNER_TEMP/release-provider-packs/$TARGET/provider-pack.tar.gz" .
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
if: startsWith(matrix.target, 'linux-') && steps.identity.outputs.provider_pack_artifact_prefix != ''
|
|
with:
|
|
name: ${{ steps.identity.outputs.provider_pack_artifact_prefix }}-${{ matrix.target }}
|
|
path: ${{ runner.temp }}/release-provider-packs/
|
|
if-no-files-found: error
|
|
retention-days: 14
|