mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 15:35:12 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Release workflows publish npm packages and multi-platform images. > - A master push can finish its image before the npm canary dispatch starts. > - The dispatch currently builds that same source again on two runners. > - This PR verifies and reuses the signed master image by digest. > - Missing images retain the native build path. Channel promotion still follows npm. ## Linked Issues or Issue Description **What existing behavior does this improve?** The Docker workflow dispatched after npm canary publication. **Current behavior** Every canary dispatch builds both architectures, even when master has already published the same source. This consumes runner capacity and delays the image channel update. **Proposed behavior** Resolve the full-SHA master image. Verify its platform index, digest and signed source identity. Retag that immutable digest. Build normally when the image does not exist. **Reason and benefit** Remove duplicate image builds while keeping missing-image recovery and the current npm channel check. A recent duplicate used about 25 runner-minutes. The saving depends on whether the master image exists. ## What Changed - Add a canary-only reuse job before the native matrix. - Require signed provenance from the canonical Docker workflow on master at the exact source and signer commit. Read the proof from GHCR. - Reuse the validated digest or build both platforms when the full-SHA image is absent. Stop on registry and verification errors. - Keep the serialized npm dist-tag promotion after either path. Keep other build lanes independent. - Add failure tests, register them in CI and update release documentation. ## Verification - Passed 603 tests: `node --test '.github/scripts/tests/*.test.mjs' scripts/standard-image-contract.test.mjs scripts/reuse-canary-image.test.mjs scripts/__tests__/release-verify-workflow.test.mjs scripts/cloud-source-verification.test.mjs`. - Passed `actionlint -shellcheck='' .github/workflows/docker.yml .github/workflows/pr-trusted.yml` and `git diff --check`. - Full actionlint reports the same six existing shellcheck findings as the unchanged master workflow. No new findings. - A published master image passed the exact provenance command. This was a read-only check. No images were retagged. - Gitleaks and manual diff inspection found no secrets or private references. - Attempted `pnpm -r typecheck`, `pnpm test:run` and `pnpm build`. They cannot start because this isolated checkout has no installed dependencies (`tsc` and `tsx` are absent). Repository CI passed on `3c46f0152d4db94a7f393bc79f466aa6c2c745e2` after the documented browser-test rerun. All checks are complete. The original failure and prior baseline occurrence remain recorded in the PR comments; its root cause remains unknown. ## Risks - The image can appear just before its attestation. Verification then fails closed. Rerun after the master producer completes. - Reuse preserves the first build's resolved dependencies and tool versions. It intentionally does not rebuild the same source to refresh floating inputs. - A skipped or unfinished master image still needs a full build. No speedup is claimed for that path. - End-to-end reuse and runner savings require observation after merge. No live publication was performed. ## Model Used OpenAI `gpt-6.1-sol`, through the Codex harness in T3 Code, with xhigh reasoning effort and shell/code tools. The context window size is not exposed by this runtime. ## 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>
76 lines
3.3 KiB
JavaScript
76 lines
3.3 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import { reuseCanaryImage } from "./reuse-canary-image.mjs";
|
|
|
|
const sha = "a".repeat(40);
|
|
const digest = `sha256:${"b".repeat(64)}`;
|
|
|
|
test("reuses only an exact-source, master-signed multi-platform image by digest", async () => {
|
|
const calls = [];
|
|
const reused = await reuseCanaryImage(sha, {
|
|
resolve: async source => { assert.equal(source, sha); return digest; },
|
|
run: async (...args) => calls.push(args),
|
|
});
|
|
assert.equal(reused, true);
|
|
assert.deepEqual(calls, [
|
|
["gh", ["attestation", "verify", `oci://ghcr.io/paperclipai/paperclip@${digest}`,
|
|
"--repo", "paperclipai/paperclip", "--bundle-from-oci",
|
|
"--signer-workflow", "paperclipai/paperclip/.github/workflows/docker.yml",
|
|
"--source-ref", "refs/heads/master", "--source-digest", sha,
|
|
"--signer-digest", sha, "--deny-self-hosted-runners"]],
|
|
["docker", ["buildx", "imagetools", "create", "-t",
|
|
"ghcr.io/paperclipai/paperclip:sha-aaaaaaa", `ghcr.io/paperclipai/paperclip@${digest}`]],
|
|
]);
|
|
});
|
|
|
|
test("a missing master image enables the fallback without retagging", async () => {
|
|
assert.equal(await reuseCanaryImage(sha, {
|
|
resolve: async () => null,
|
|
run: () => assert.fail("A missing image must not be promoted"),
|
|
}), false);
|
|
});
|
|
|
|
test("registry and provenance failures stop promotion rather than bypass verification", async () => {
|
|
await assert.rejects(reuseCanaryImage(sha, {
|
|
resolve: async () => { throw new Error("HTTP 403"); },
|
|
run: () => assert.fail("A registry failure must not be promoted"),
|
|
}), /HTTP 403/);
|
|
let commands = 0;
|
|
await assert.rejects(reuseCanaryImage(sha, {
|
|
resolve: async () => digest,
|
|
run: command => {
|
|
assert.equal(command, "gh");
|
|
commands++;
|
|
throw new Error("Untrusted source or signature");
|
|
},
|
|
}), /Untrusted source/);
|
|
assert.equal(commands, 1);
|
|
});
|
|
|
|
test("invalid source refs and digests cannot reach external commands", async () => {
|
|
for (const invalid of ["master", "a".repeat(7), "a".repeat(40) + "\n"]) {
|
|
await assert.rejects(reuseCanaryImage(invalid, {
|
|
resolve: () => assert.fail("Invalid source accepted"),
|
|
}));
|
|
}
|
|
await assert.rejects(reuseCanaryImage(sha, {
|
|
resolve: async () => "mutable:tag",
|
|
run: () => assert.fail("Invalid digest accepted"),
|
|
}));
|
|
});
|
|
|
|
test("canary reuse gates both native builds and still runs convergent promotion", () => {
|
|
const workflow = readFileSync(new URL("../.github/workflows/docker.yml", import.meta.url), "utf8");
|
|
const reuse = workflow.split(" reuse_canary_image:\n")[1].split(" build-and-push:\n")[0];
|
|
assert.match(reuse, /github.repository_id == '1170821064'.*startsWith\(github.ref, 'refs\/tags\/canary\/v'\)/);
|
|
assert.match(reuse, /node scripts\/reuse-canary-image.mjs/);
|
|
const build = workflow.split(" build-and-push:\n")[1].split(" merge-and-push:\n")[0];
|
|
assert.match(build, /needs: \[reuse_canary_image\]/);
|
|
assert.match(build, /!cancelled\(\).*result == 'skipped'.*result == 'success'.*outputs.reused == 'false'/);
|
|
assert.match(build, /platform: linux\/amd64/);
|
|
assert.match(build, /platform: linux\/arm64/);
|
|
const promotion = workflow.split(" promote_canary_channel:\n")[1];
|
|
assert.match(promotion, /!cancelled\(\).*needs.merge-and-push.result == 'success'.*outputs.reused == 'true'/);
|
|
});
|