mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-11 14:10:50 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Release verification must run the same server integration coverage as PR verification. > - Three server suites use real Rust Runner binaries. > - PR checks already run them with the shared Rust dependency cache, but release checks cold-build them inside ordinary server test shards. > - A cold Dot Runner build can consume its entire setup deadline before any test runs. > - This pull request gives release verification a required cached native integration lane and preserves every test. ## Linked Issues or Issue Description **What happened?** On master commit `1881894973a2b25838d8abed9bd8aeebc3af4441`, [Cloud readiness run 37837810707](https://github.com/paperclipai/paperclip/actions/runs/37837810707) failed in server shard 7. Dot Runner's `cargo build --release` exceeded its 300-second child-process deadline. The other 1,640 tests in that shard passed. The failed setup then tried to remove an undefined temporary path and emitted a second error. Cargo output was captured as an opaque buffer, which obscured build progress. **What did you expect to happen?** Build fixture binaries before tests in a lane with the existing Rust dependency cache. Run all native integration tests and make their result required for release readiness. A setup failure should keep its original diagnostic. **Steps to reproduce** Run release verification on a clean runner. The existing `general-server-without-chat` group retains the three Cargo-backed suites outside the PR workflow. The Dot suite can time out during a cold release build. Run the new workflow and partition tests against the previous source to reproduce five routing and coverage failures deterministically. **Version / commit** Observed at `1881894973a2b25838d8abed9bd8aeebc3af4441`; this change is based on `ed6abbf158b`. **Deployment mode** GitHub Actions Release and Cloud readiness verification. No application runtime or deployment action changes. Related work: #15581 also edits Dot integration tests for onboarding behavior. It does not repair release test routing. Searches found no open PR for this failure. ## What Changed - Add an explicit server test group that excludes the dedicated chat and native suites. Preserve the existing PR and local test groups. - Run all three native server suites in one required matrix lane with the existing trusted Rust dependency cache. - Build debug and release fixture binaries in a visible step with a 10-minute limit before tests start. Keep Cargo freshness checks and the existing test deadlines. - Stream Cargo diagnostics and clean up safely when Dot setup stops before creating its temporary directory. - Test complete, non-overlapping partitions for Release, Cloud readiness, other callers, and existing PR/local groups. Check cache restrictions, build order, and the source verification dependency. ## Verification - Passed 44 workflow and partition tests: `node --test scripts/__tests__/run-vitest-stable-shard.test.mjs scripts/__tests__/release-verify-workflow.test.mjs`. - The same tests fail in five relevant cases against the previous workflow and selector. They pass after this correction. - An independent review repeated all 44 tests successfully. - Passed `actionlint .github/workflows/release-verify.yml`, `git diff --check`, and a secret scan. - Passed all 381 workflow policy tests: `node --test '.github/scripts/tests/*.test.mjs'`. - Passed full `pnpm build` in a clean worktree. - Built debug and release fixture binaries, then passed all 32 tests in the three real native integration suites: `pnpm test:run:general -- --group general-server-native-runner` (49.5 seconds, no skips). - Passed full `pnpm -r typecheck`. - Injected a synthetic Cargo setup failure. The suite reports that failure without the secondary undefined-path cleanup error. - Exact-head CI completed: 53 successful checks, including all server shards, both native Runner lanes, build, typecheck, browser tests, and the canary dry run. Two optional Storybook checks were skipped. - Started the duplicate local `pnpm test:run` aggregate and stopped it after exact-head CI passed. No completed local aggregate result is claimed. All test processes owned by that run exited. - Greptile scored the final head `ffe5ab5a28af2dfea9db1c1952c652f070bf52f8` at 5/5 with no review threads. - `Superagent Supply Chain Scan` is neutral, not passed: it only supports exact dependency pin replacements and cannot verify structural edits to `.github/workflows/release-verify.yml`. It reported no annotations. Independent source review, Greptile, actionlint, and the workflow policy tests cover this structural change. - GitHub still requires a code-owner review for the workflow files. There are no merge conflicts. ## Risks - Adds one CI matrix job, which increases concurrent runner demand. The existing cache writer and trust restrictions stay in place. - Missing dependencies still compile from the lockfile. A build that exceeds its step limit fails visibly; failed tests still block source verification. - Workflow execution uses the new lane after merge. PR tests validate the workflow contract and run the existing native test lane. - The supply chain scanner cannot analyze this workflow structure. Its neutral result is an explicit coverage limit; it is not counted as a successful scan. - No runtime, schema, deployment, publication, credential, or test-timeout changes. ## Model Used OpenAI Codex, based on GPT-6, with repository inspection, code execution, and independent agent review. The exact deployed model ID and context-window size are not exposed in this session. ## 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>
81 lines
5.2 KiB
JavaScript
81 lines
5.2 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { runInNewContext } from "node:vm";
|
|
|
|
const workflow = readFileSync(new URL("../../workflows/release-verify.yml", import.meta.url), "utf8");
|
|
const runner = workflow.split(" verify_paperclip_runner:")[1].split(" build:")[0];
|
|
|
|
test("Runner dependency caching selects the package's pinned compiler before computing its key", () => {
|
|
const select = runner.indexOf(" - name: Select the pinned Runner Rust toolchain");
|
|
const cache = runner.indexOf(" - name: Cache Runner Rust dependencies");
|
|
assert.ok(select >= 0 && cache > select);
|
|
const setup = runner.slice(select, cache);
|
|
assert.match(setup, /working-directory: packages\/paperclip-runner/);
|
|
assert.match(setup, /rustup show active-toolchain/);
|
|
assert.match(setup, /echo "RUSTUP_TOOLCHAIN=\$toolchain" >> "\$GITHUB_ENV"/);
|
|
assert.match(runner, /uses: Swatinem\/rust-cache@[0-9a-f]{40} # v[0-9.]+/);
|
|
// The target path feeds the entry's version hash, so it must not resolve
|
|
// under the checkout, whose root differs between the fleet and GitHub-hosted
|
|
// runners. The pin step publishes a $HOME-anchored path to the same directory.
|
|
const pin = runner.indexOf(" - name: Pin the Runner Rust workspace path");
|
|
assert.ok(pin >= 0 && cache > pin, "the workspace path must be pinned before the cache step");
|
|
assert.match(runner, /workspaces: \$\{\{ steps\.runner_rust_workspace\.outputs\.path \}\} -> target/);
|
|
assert.match(runner, /shared-key: release-runner-v2/);
|
|
});
|
|
|
|
test("the shared cache excludes workspace artifacts and only restores or saves the exact master-push source", () => {
|
|
assert.match(runner, /cache-workspace-crates: false/);
|
|
assert.match(runner, /cache-bin: false/);
|
|
const saveIf = runner.match(/^\s*save-if: (.+)$/m)?.[1];
|
|
assert.equal(saveIf, "${{ matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}");
|
|
const cacheStep = runner.split(" - name: Cache Runner Rust dependencies")[1].split(" - name: Install dependencies")[0];
|
|
assert.equal(cacheStep.match(/^\s*if: (.+)$/m)?.[1], saveIf.replace("matrix.lane == 'rust' && ", ""));
|
|
assert.doesNotMatch(runner, /cache-on-failure: true|cache-all-crates: true/);
|
|
});
|
|
|
|
test("parallel lanes cover check:all exactly once and never bypass verification", () => {
|
|
const scripts = JSON.parse(readFileSync(new URL("../../../packages/paperclip-runner/package.json", import.meta.url))).scripts;
|
|
const checks = [...runner.matchAll(/^ checks: (.+)$/gm)].flatMap(([, value]) => value.split(" "));
|
|
assert.deepEqual(checks, scripts["check:all"].split(" && ").map((command) => command.replace(/^pnpm run /, "")));
|
|
assert.deepEqual([...runner.matchAll(/^ - lane: (.+)$/gm)].map(([, value]) => value), ["protocol", "rust", "server-integration"]);
|
|
assert.match(runner, /fail-fast: false/);
|
|
assert.doesNotMatch(runner, /max-parallel: 1|^ needs:|continue-on-error:/m);
|
|
const verify = runner.split(" - name: Verify Paperclip Runner\n")[1].split(" - name: Warm debug")[0];
|
|
assert.match(verify, /RUNNER_CHECKS: \$\{\{ matrix.checks \}\}/);
|
|
assert.match(verify, /set -euo pipefail/);
|
|
assert.match(verify, /for check in \$RUNNER_CHECKS; do\s+pnpm --filter @paperclipai\/paperclip-runner "\$check"\s+done/);
|
|
const verifyIf = verify.match(/^\s*if: \$\{\{ (.+) \}\}$/m)?.[1];
|
|
assert.equal(verifyIf, "matrix.lane != 'server-integration'");
|
|
for (const lane of ["protocol", "rust", "server-integration"]) {
|
|
assert.equal(runInNewContext(verifyIf, { matrix: { lane } }), lane !== "server-integration");
|
|
}
|
|
assert.doesNotMatch(verify, /cache-hit/);
|
|
assert.doesNotMatch(runner, /id-token: write|packages: write|secrets: inherit/);
|
|
});
|
|
|
|
test("only the trusted Rust lane writes, and warms both build profiles before saving", () => {
|
|
const cache = runner.split(" - name: Cache Runner Rust dependencies")[1].split(" - name: Install dependencies")[0];
|
|
const warm = runner.split(" - name: Warm debug dependencies for the shared Runner cache")[1];
|
|
const expr = (body, field) => body.match(new RegExp(`^ +${field}: \\$\\{\\{ (.+) \\}\\}$`, "m"))[1];
|
|
assert.equal(expr(cache, "save-if"), expr(warm, "if"));
|
|
assert.match(warm, /run: pnpm --filter @paperclipai\/paperclip-runner build:rust/);
|
|
const sha = "a".repeat(40);
|
|
const base = { repository: "paperclipai/paperclip", event_name: "push", ref: "refs/heads/master", sha };
|
|
for (const lane of ["protocol", "rust", "server-integration"]) {
|
|
for (const [overrides, ref, trusted] of [
|
|
[{}, sha, true],
|
|
[{ event_name: "pull_request", ref: "refs/pull/1/merge" }, sha, false],
|
|
[{ event_name: "pull_request_target" }, sha, false],
|
|
[{ event_name: "workflow_dispatch" }, sha, false],
|
|
[{ repository: "someone/paperclip" }, sha, false],
|
|
[{ ref: "refs/heads/feature" }, sha, false],
|
|
[{}, "b".repeat(40), false],
|
|
]) {
|
|
const context = { matrix: { lane }, github: { ...base, ...overrides }, inputs: { ref } };
|
|
assert.equal(runInNewContext(expr(cache, "if"), context), trusted);
|
|
assert.equal(runInNewContext(expr(cache, "save-if"), context), trusted && lane === "rust");
|
|
}
|
|
}
|
|
});
|