mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-07 16:11:46 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The runner package now has protocol, transport, provider, catalog, and authorization foundations. > - Its first upstream package boundary should expose only the implemented runtime and test-helper surfaces. > - Rust correctness belongs in the repository existing build verification, without introducing a parallel release process. > - Direct package creation must build the files declared by the package manifest. > - This pull request defines the minimal package API and verifies the optimized runner binaries in the existing PR and release Build jobs. > - The benefit is a production-ready runner package boundary with minimal build-process change. ## Linked Issues or Issue Description Refs #11962 This pull request replaces one bounded part of the archived large runner change. It follows the package-local authorization change in #12126. ## What Changed - Export only `@paperclipai/paperclip-runner` and `@paperclipai/paperclip-runner/testing`. - Keep Node-only fixture loading and semantic conformance helpers out of the runtime root. - Add a provider-neutral semantic conformance kit with stable JSON comparison and fail-closed input checks. - Keep deferred SDK, eval, browser, React, lab, and command surfaces private. - Pin the runner Rust toolchain to 1.97.1 with the minimal profile and `rustfmt`. - Run the Rust workspace tests in release mode. - Launch the optimized `paperclip-runnerd` and fake-harness binaries in process-level integration coverage. - Add one `pnpm --filter @paperclipai/paperclip-runner check:all` step to each existing PR and release Build job. - Make the existing server `prepack` lifecycle run its existing build after it prepares UI assets. - Document that no production adapter starts runnerd yet. This revision adds no standalone GitHub Actions job. It adds no server runner dependency or runner vendoring. It adds no Docker bootstrap or clean-consumer harness. It does not change `pnpm-lock.yaml`. ## Verification - `pnpm --filter @paperclipai/paperclip-runner check:all` - 66 TypeScript tests - 8 protocol contract tests - 56 Rust unit and integration tests - Release-mode integration coverage launches the optimized runnerd and fake-harness binaries. - `pnpm --filter @paperclipai/server exec vitest run src/__tests__/server-package-build-script.test.ts` (2 tests) - Clean `pnpm pack` from `server/` rebuilt the server and produced both `package/dist/index.js` and `package/dist/index.d.ts`. - `node --test scripts/__tests__/release-verify-workflow.test.mjs` (8 tests) - `pnpm -r typecheck` - `pnpm build` - `pnpm check:token-gates` - `git diff --check` - No `pnpm-lock.yaml` diff. - The diff changes 12 files. ## Risks The runner adds Rust work to the existing Build jobs. These jobs can take longer on a cold cache. The pinned toolchain makes contributor and CI behavior reproducible. Cargo tests use `--release` to verify optimized executables. The server prepack lifecycle now performs the build that its published entry points require. This can make direct server packing slower. This pull request does not wire runnerd into the server. It does not select runnerd for any adapter. Existing application execution and finalization paths remain unchanged. ## Model Used OpenAI Codex with GPT-5. Agentic coding mode used repository tools, code execution, and automated tests. ## 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 - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
512 lines
17 KiB
YAML
512 lines
17 KiB
YAML
name: PR
|
|
|
|
on:
|
|
pull_request:
|
|
branches:
|
|
- master
|
|
|
|
concurrency:
|
|
group: pr-${{ github.event.pull_request.number }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
policy:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
outputs:
|
|
lockfile_regenerated: ${{ steps.regen_lockfile.outputs.regenerated }}
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Block manual lockfile edits
|
|
if: >-
|
|
github.head_ref != 'chore/refresh-lockfile' &&
|
|
github.event.pull_request.user.login != 'dependabot[bot]'
|
|
run: |
|
|
# Diff the PR branch against its merge base so recent base-branch commits
|
|
# do not masquerade as changes made by the PR itself.
|
|
changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}")"
|
|
if printf '%s\n' "$changed" | grep -qx 'pnpm-lock.yaml'; then
|
|
echo "Do not commit pnpm-lock.yaml in pull requests. CI owns lockfile updates."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9.15.4
|
|
run_install: false
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version: 24
|
|
|
|
- name: Validate Dockerfile deps stage
|
|
run: node ./scripts/check-docker-deps-stage.mjs
|
|
|
|
- name: Validate Node version policy
|
|
run: pnpm check:node-version
|
|
|
|
- name: Reject git push in adapter/runtime code
|
|
run: node ./scripts/check-no-git-push.mjs
|
|
|
|
- name: Test no-git-push check
|
|
run: node --test ./scripts/check-no-git-push.test.mjs
|
|
- name: Test PR quality-gate scripts
|
|
run: node --test '.github/scripts/tests/*.test.mjs'
|
|
|
|
- name: Test general-server shard partition
|
|
run: node --test ./scripts/__tests__/run-vitest-stable-shard.test.mjs
|
|
|
|
- name: Test e2e shard partition
|
|
run: node --test ./scripts/__tests__/e2e-shard.test.mjs
|
|
|
|
- name: Test release verify workflow wiring
|
|
run: node --test ./scripts/__tests__/release-verify-workflow.test.mjs
|
|
|
|
- name: Test standalone package build concurrency
|
|
run: node --test ./scripts/__tests__/build-standalone-concurrency.test.mjs
|
|
|
|
- name: Validate release package manifest
|
|
run: node ./scripts/release-package-map.mjs check
|
|
|
|
- name: Verify release package bootstrap for changed manifests
|
|
run: |
|
|
mapfile -t changed_paths < <(git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}")
|
|
PAPERCLIP_RELEASE_BOOTSTRAP_BASE_SHA="${{ github.event.pull_request.base.sha }}" \
|
|
node ./scripts/check-release-package-bootstrap.mjs "${changed_paths[@]}"
|
|
|
|
- name: Validate dependency resolution when manifests change
|
|
id: regen_lockfile
|
|
run: |
|
|
changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}")"
|
|
manifest_pattern='(^|/)package\.json$|^pnpm-workspace\.yaml$|^\.npmrc$|^pnpmfile\.(cjs|js|mjs)$|^patches/'
|
|
if printf '%s\n' "$changed" | grep -Eq "$manifest_pattern"; then
|
|
pnpm install --lockfile-only --ignore-scripts --no-frozen-lockfile
|
|
echo "regenerated=1" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "regenerated=0" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# Manifest-only PRs (where pnpm-lock.yaml stays at base because the policy
|
|
# job above blocks committing it) need the regenerated lockfile for the
|
|
# downstream `pnpm install --frozen-lockfile` steps. Upload it here so
|
|
# every job consumes the same hash without recomputing.
|
|
- name: Upload regenerated lockfile for downstream jobs
|
|
if: steps.regen_lockfile.outputs.regenerated == '1'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: pr-lockfile
|
|
path: pnpm-lock.yaml
|
|
retention-days: 1
|
|
if-no-files-found: error
|
|
|
|
typecheck_release_registry:
|
|
name: Typecheck + Release Registry
|
|
needs: [policy]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Restore regenerated PR lockfile (if policy uploaded one)
|
|
uses: actions/download-artifact@v8
|
|
continue-on-error: true
|
|
with:
|
|
name: pr-lockfile
|
|
path: .
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Typecheck workspaces whose build scripts skip TypeScript
|
|
run: pnpm run typecheck:build-gaps
|
|
|
|
- name: Verify release registry test coverage
|
|
run: pnpm run test:release-registry
|
|
|
|
general_tests:
|
|
name: General tests (${{ matrix.group_label }})
|
|
needs: [policy]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
# The server suite is pinned to maxWorkers=1 (server/vitest.config.ts),
|
|
# so it can only be parallelized across runners. Shard it to keep this
|
|
# lane off the PR critical path. Five shards because the suite has
|
|
# grown to ~946s of serial vitest wall time (run 30930345729,
|
|
# 2026-08-04): at four shards the worst shard ran 311s and was the
|
|
# slowest check in the whole PR run; five brings each shard to ~196s
|
|
# of suite time (~240s job), level with the other ~250-300s lanes.
|
|
- group: general-server
|
|
group_label: server (1/5)
|
|
shard_index: 0
|
|
shard_count: 5
|
|
- group: general-server
|
|
group_label: server (2/5)
|
|
shard_index: 1
|
|
shard_count: 5
|
|
- group: general-server
|
|
group_label: server (3/5)
|
|
shard_index: 2
|
|
shard_count: 5
|
|
- group: general-server
|
|
group_label: server (4/5)
|
|
shard_index: 3
|
|
shard_count: 5
|
|
- group: general-server
|
|
group_label: server (5/5)
|
|
shard_index: 4
|
|
shard_count: 5
|
|
# workspaces-a was the slowest check in the fully-green PR run
|
|
# 31371439296 (2026-08-10) at 319s, with the ui project's single
|
|
# vitest invocation accounting for ~224s and the paperclipai CLI
|
|
# ~37s. Two shards use Vitest's native --shard on each project's
|
|
# file list (ui: 439 files, cli: 54), bringing each job to roughly
|
|
# half the suite time (~130s + setup) without a duration manifest.
|
|
- 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
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Restore regenerated PR lockfile (if policy uploaded one)
|
|
uses: actions/download-artifact@v8
|
|
continue-on-error: true
|
|
with:
|
|
name: pr-lockfile
|
|
path: .
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --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
|
|
|
|
verify:
|
|
# Preserve the legacy required-check name while the underlying work runs in parallel.
|
|
name: verify
|
|
if: ${{ always() }}
|
|
needs: [typecheck_release_registry, general_tests, build]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
|
|
steps:
|
|
- name: Fail if any split verify lane failed
|
|
env:
|
|
TYPECHECK_RELEASE_REGISTRY_RESULT: ${{ needs.typecheck_release_registry.result }}
|
|
GENERAL_TESTS_RESULT: ${{ needs.general_tests.result }}
|
|
BUILD_RESULT: ${{ needs.build.result }}
|
|
run: |
|
|
test "$TYPECHECK_RELEASE_REGISTRY_RESULT" = "success"
|
|
test "$GENERAL_TESTS_RESULT" = "success"
|
|
test "$BUILD_RESULT" = "success"
|
|
|
|
build:
|
|
name: Build
|
|
needs: [policy]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Restore regenerated PR lockfile (if policy uploaded one)
|
|
uses: actions/download-artifact@v8
|
|
continue-on-error: true
|
|
with:
|
|
name: pr-lockfile
|
|
path: .
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Verify Paperclip Runner
|
|
run: pnpm --filter @paperclipai/paperclip-runner check:all
|
|
|
|
- name: Build
|
|
run: pnpm build
|
|
|
|
verify_serialized_server:
|
|
name: Verify serialized server suites (${{ matrix.shard_label }})
|
|
needs: [policy]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
# A successful PR run on 2026-08-17 (32012408876) spent 291s in
|
|
# serialized shard 1/5 while its siblings ran 170-201s: round-robin
|
|
# clustered the heavy suites on one runner. Shards are now balanced
|
|
# by recorded duration (scripts/serialized-shard-durations.json),
|
|
# which levels the measured 968s suite total to about 194s per
|
|
# runner before setup overhead.
|
|
- 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@v7
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Restore regenerated PR lockfile (if policy uploaded one)
|
|
uses: actions/download-artifact@v8
|
|
continue-on-error: true
|
|
with:
|
|
name: pr-lockfile
|
|
path: .
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Run serialized server test shard
|
|
run: pnpm test:run:serialized -- --shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }}
|
|
|
|
canary_dry_run:
|
|
name: Canary Dry Run
|
|
needs: [policy]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Restore regenerated PR lockfile (if policy uploaded one)
|
|
uses: actions/download-artifact@v8
|
|
continue-on-error: true
|
|
with:
|
|
name: pr-lockfile
|
|
path: .
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
# `release.sh` always executes its Step 2/7 workspace build, even when
|
|
# `--skip-verify` bypasses the initial verification gate. release.sh
|
|
# also requires a clean working tree, so any in-place lockfile churn
|
|
# from `pnpm install --frozen-lockfile` must be reverted first — unless
|
|
# the policy job uploaded a regenerated lockfile (manifest-changing
|
|
# PRs), in which case we stage the artifact-restored copy into an
|
|
# ephemeral local commit so release.sh sees a clean tree and its
|
|
# workspace build sees a lockfile that matches the manifest.
|
|
- name: Release canary dry run via release.sh internal build
|
|
env:
|
|
USED_ARTIFACT_LOCKFILE: ${{ needs.policy.outputs.lockfile_regenerated || '0' }}
|
|
run: |
|
|
git checkout -B master HEAD
|
|
if [ "$USED_ARTIFACT_LOCKFILE" = "1" ]; then
|
|
git add pnpm-lock.yaml
|
|
if ! git diff --cached --quiet; then
|
|
git -c user.email=ci@paperclip.local -c user.name=CI \
|
|
commit --no-verify -m "ci(canary): stage regenerated lockfile"
|
|
fi
|
|
else
|
|
git checkout -- pnpm-lock.yaml
|
|
fi
|
|
./scripts/release.sh canary --skip-verify --dry-run
|
|
|
|
e2e_shards:
|
|
name: e2e shard (${{ matrix.shard_label }})
|
|
needs: [policy]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
# The Playwright lane is pinned to workers=1 (tests/e2e/playwright.config.ts)
|
|
# because every spec shares one throwaway server and some toggle
|
|
# instance-level flags, so it can only be parallelized across runners.
|
|
# Each shard boots its own server, which keeps that isolation intact.
|
|
# Three shards let the ~3min smoke-lab spec ride alone while the rest
|
|
# of the catalog splits evenly, pulling this lane off the PR critical
|
|
# path (it was the slowest check at ~8min20s with two shards).
|
|
- shard_index: 0
|
|
shard_count: 3
|
|
shard_label: 1/3
|
|
- shard_index: 1
|
|
shard_count: 3
|
|
shard_label: 2/3
|
|
- shard_index: 2
|
|
shard_count: 3
|
|
shard_label: 3/3
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Restore regenerated PR lockfile (if policy uploaded one)
|
|
uses: actions/download-artifact@v8
|
|
continue-on-error: true
|
|
with:
|
|
name: pr-lockfile
|
|
path: .
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Verify runner Chrome
|
|
# GitHub's Ubuntu runner image already ships Google Chrome, so use that
|
|
# directly for the headless e2e lane instead of downloading Playwright
|
|
# browser bundles inside the 30 minute job budget.
|
|
run: google-chrome --version
|
|
|
|
- name: Generate Paperclip config
|
|
run: |
|
|
mkdir -p ~/.paperclip/instances/default
|
|
cat > ~/.paperclip/instances/default/config.json << 'CONF'
|
|
{
|
|
"$meta": { "version": 1, "updatedAt": "2026-01-01T00:00:00.000Z", "source": "onboard" },
|
|
"database": { "mode": "embedded-postgres" },
|
|
"logging": { "mode": "file" },
|
|
"server": { "deploymentMode": "local_trusted", "host": "127.0.0.1", "port": 3100 },
|
|
"auth": { "baseUrlMode": "auto" },
|
|
"storage": { "provider": "local_disk" },
|
|
"secrets": { "provider": "local_encrypted", "strictMode": false }
|
|
}
|
|
CONF
|
|
|
|
- name: Run e2e tests
|
|
env:
|
|
PAPERCLIP_E2E_SKIP_LLM: "true"
|
|
PAPERCLIP_PLAYWRIGHT_CHANNEL: "chrome"
|
|
run: |
|
|
# Playwright's own --shard balances by test count, and one spec
|
|
# (smoke-lab) is ~40% of the lane's wall clock. Partition by recorded
|
|
# spec duration instead so both runners finish together.
|
|
specs="$(node ./scripts/e2e-shard.mjs \
|
|
--shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }})"
|
|
echo "shard ${{ matrix.shard_label }} specs: $specs"
|
|
pnpm run test:e2e $specs
|
|
|
|
- name: Upload Playwright report
|
|
uses: actions/upload-artifact@v7
|
|
if: always()
|
|
with:
|
|
name: playwright-report-${{ matrix.shard_index }}
|
|
path: |
|
|
tests/e2e/playwright-report/
|
|
tests/e2e/test-results/
|
|
retention-days: 14
|
|
|
|
e2e:
|
|
# Preserve the legacy required-check name while the specs run sharded
|
|
# across the matrix above (same pattern as the `verify` aggregate).
|
|
name: e2e
|
|
if: ${{ always() }}
|
|
needs: [e2e_shards]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
|
|
steps:
|
|
- name: Fail if any e2e shard failed
|
|
env:
|
|
E2E_SHARDS_RESULT: ${{ needs.e2e_shards.result }}
|
|
run: test "$E2E_SHARDS_RESULT" = "success"
|