mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:48:12 +02:00
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - Cloud deployments consume a verified image and exact-source
migrator.
> - An image alone is not deployable until source checks and artifact
checks pass.
> - GitHub-hosted queues delayed those checks and the final readiness
signal.
> - This PR gives trusted master work a separate concurrency allowance
on existing AWS runners.
> - Community PRs and arbitrary source inputs keep the GitHub-hosted
fallback.
## Linked Issues or Issue Description
Refs #13243.
**What existing behavior does this improve?**
Time from a master merge to the Cloud deployable v1 signal.
**Current behavior**
For merge d0b7ba4, the image was available after 7m 42s, but readiness
took 16m 05s. Typecheck queued for 6m 40s and the final readiness job
queued for 1m 46s.
**Proposed behavior**
Allow up to 36 concurrent post-merge verification and migrator jobs on
the existing four-vCPU, 16-GiB AWS runners. Workers launch on demand and
terminate after their job; no always-on worker pool or AWS Reserved
Instance purchase is introduced. Keep the combined runner ceiling
unchanged. A separate operator switch enables this route only after the
restricted runner group and Fleet exist.
**Reason and benefit**
Remove GitHub-hosted queue delays from the cloud deployment path. The
gain depends on queue pressure and which remaining job finishes last;
the observed queues are not additive savings.
**Breaking changes**
None to source verification or readiness contracts. Paid routing is
limited to canonical master push/manual events, with exact source checks
on reusable and migrator jobs.
## What Changed
- Route source verification, artifact waiting, dispatch, and readiness
jobs to the separate post-merge Fleet when enabled.
- Require source inputs to match the event's master SHA. Preview inputs
and raced older migrator dispatches stay GitHub-hosted.
- Keep npm publication on GitHub-hosted runners for trusted publishing.
- Bound AWS job timeouts below the 45-minute instance lifetime.
- Document activation, capacity reservation, and rollback.
- Exercise each actual runner selector against allowed and rejected
event/source combinations.
## Verification
- 268 routing and timeout cases pass, including unapproved PR, fork,
branch/tag, arbitrary ref, and disabled-switch cases.
- All 461 focused workflow, preview, and readiness tests pass. The 284
routing/preview cases also pass after the review fixes.
- actionlint passes for changed workflows with the existing
SC2012/SC2016/SC2129 warnings excluded.
- Full local typecheck and build pass (167s and 206s). `pnpm test:run`
completed: 10,600 passed, 65 skipped, and 13 failed in the unchanged
company-skills-service/runtime-skill-cache suites with local filesystem
permission errors. Linux CI is the required test gate; this is not a
claim of a fully passing local suite. Current-head Linux CI is green,
Greptile is 5/5, and all findings are resolved. The final Build retry
passed on a verified 60 GiB AWS runner after correcting the earlier
disk-capacity failure.
- After activation, verify a master run selects the separate group and
all readiness prerequisites pass.
## Risks
- A missing or incorrectly restricted runner group can leave eligible
jobs queued. Enable the switch only after Fleet and group verification.
- PR bursts have 64 slots after reserving 36 for post-merge work. The
image Fleet retains eight, for the same 108-runner total.
- A migrator dispatch racing a newer merge uses GitHub-hosted runners.
This preserves source trust but can retain some queue delay.
- Roll back placement by disabling AWS_POST_MERGE_CI_ENABLED and
rerunning the whole workflow.
## Model Used
OpenAI GPT-6 through Codex, with reasoning, repository tools, and code
execution. The exact serving model ID and context window are not exposed
by this environment.
## 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 — focused change tests
pass; full-suite local permission failures are disclosed above
- [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>
84 lines
4.8 KiB
JavaScript
84 lines
4.8 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { runInNewContext } from "node:vm";
|
|
|
|
const fleet = "runs-on/fleet=paperclip-post-merge-x64/env=public-ci";
|
|
const sha = "a".repeat(40);
|
|
const base = {
|
|
repository: "paperclipai/paperclip", repository_id: "1170821064",
|
|
ref: "refs/heads/master", event_name: "push", sha,
|
|
};
|
|
const expectedJobs = {
|
|
"cloud-readiness.yml": ["artifacts", "source_verified", "ready"],
|
|
"cloud-artifacts.yml": ["dispatch_migrator"],
|
|
"release-verify.yml": ["typecheck", "general_tests", "serialized_tests", "runner_workflow_evals", "verify_paperclip_runner", "build"],
|
|
"runner-chaos-evals.yml": ["chaos_and_recovery"],
|
|
"release.yml": ["plan_preview", "package_preview"],
|
|
};
|
|
for (const [file, expectedNames] of Object.entries(expectedJobs)) {
|
|
const workflow = readFileSync(new URL(`../../workflows/${file}`, import.meta.url), "utf8");
|
|
const jobs = [...workflow.matchAll(/^ ([a-z_]+):\n([\s\S]*?)(?=^ [a-z_]+:\n|(?![\s\S]))/gm)];
|
|
const routed = jobs.filter(([, , body]) => body.includes(fleet));
|
|
test(`${file}: all intended jobs carry the post-merge guard`, () => {
|
|
assert.deepEqual(routed.map(([, name]) => name).sort(), [...expectedNames].sort());
|
|
});
|
|
for (const [, job, body] of routed) {
|
|
const expression = body.match(/^ runs-on: \$\{\{ (.+) \}\}$/m)?.[1];
|
|
assert.ok(expression, `${file}/${job} must use an explicit runner expression`);
|
|
const release = file === "release.yml";
|
|
const checkRef = release || file === "release-verify.yml" || file === "runner-chaos-evals.yml";
|
|
const inputs = { ref: sha, source_ref: sha, channel: "cloud-migrator" };
|
|
const defaultContext = { ...base, event_name: release ? "workflow_dispatch" : "push" };
|
|
const cases = [
|
|
{ name: "exact master source", expected: fleet },
|
|
{ name: "manual exact master source", github: { event_name: "workflow_dispatch" }, expected: fleet },
|
|
{ name: "switch disabled", enabled: "false" },
|
|
{ name: "switch absent", enabled: "" },
|
|
{ name: "malformed switch", enabled: "yes" },
|
|
{ name: "fork", github: { repository: "someone/paperclip", repository_id: "123" } },
|
|
{ name: "repository renamed or transferred", github: { repository_id: "123" } },
|
|
{ name: "unapproved PR", github: { event_name: "pull_request", ref: "refs/pull/1/merge" } },
|
|
{ name: "PR event even with master ref", github: { event_name: "pull_request" } },
|
|
{ name: "privileged PR event", github: { event_name: "pull_request_target" } },
|
|
{ name: "workflow completion event", github: { event_name: "workflow_run" } },
|
|
{ name: "repository dispatch", github: { event_name: "repository_dispatch" } },
|
|
{ name: "scheduled caller", github: { event_name: "schedule" } },
|
|
{ name: "branch workflow", github: { ref: "refs/heads/feature" } },
|
|
{ name: "release tag", github: { ref: "refs/tags/v2026.911.0" } },
|
|
];
|
|
if (checkRef) {
|
|
const key = release ? "source_ref" : "ref";
|
|
for (const value of ["b".repeat(40), "refs/pull/1/head", "master", "feature", "v1.0.0", ""]) {
|
|
cases.push({ name: `unverified source ${value || "(empty)"}`, inputs: { [key]: value } });
|
|
}
|
|
cases.push({ name: "missing source identity", github: { sha: "" }, inputs: { [key]: "" } });
|
|
}
|
|
if (release) {
|
|
cases.push({ name: "preview of master", inputs: { channel: "preview" } });
|
|
cases.push({ name: "stable release", inputs: { channel: "stable" } });
|
|
}
|
|
for (const { name, github = {}, inputs: overrides = {}, enabled = "true", expected = "ubuntu-latest" } of cases) {
|
|
test(`${file}/${job}: ${name}`, () => {
|
|
const context = { github: { ...defaultContext, ...github }, inputs: { ...inputs, ...overrides }, vars: { AWS_POST_MERGE_CI_ENABLED: enabled } };
|
|
// These canonical contexts use boolean operators and string comparisons
|
|
// whose results match GitHub's expression evaluation.
|
|
assert.equal(runInNewContext(expression, context), expected);
|
|
const timeout = body.match(/^ timeout-minutes: (.+)$/m)?.[1];
|
|
assert.ok(timeout, "AWS jobs need a timeout below the 45-minute instance lifetime");
|
|
const minutes = timeout.startsWith("${{") ? runInNewContext(timeout.slice(3, -2), context) : Number(timeout);
|
|
if (expected === fleet) assert.ok(minutes > 0 && minutes < 45);
|
|
if (release && job === "plan_preview") assert.equal(minutes, expected === fleet ? 10 : 360);
|
|
});
|
|
}
|
|
}
|
|
if (file === "release.yml") {
|
|
test("npm publisher always uses a GitHub-hosted runner", () => {
|
|
const publisher = jobs.find(([ , job]) => job === "publish_preview")?.[2];
|
|
assert.match(publisher, /^ runs-on: ubuntu-latest$/m);
|
|
assert.match(publisher, /^ environment: npm-canary$/m);
|
|
assert.match(publisher, /^ id-token: write$/m);
|
|
});
|
|
}
|
|
}
|