mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-14 17:40:37 +02:00
fix(release): run native builds and canary smoke after skipped channels (#16149)
## Thinking Path > - Paperclip lets people manage AI agents for work. > - Published packages need the native runner artifacts from their exact source. > - The release workflow selects one channel and deliberately skips the other channels. > - The native producer used an implicit success condition across those skipped dependencies. > - The merged-source release selected native artifacts but skipped their build and canary publication. > - This pull request makes the existing producer and canary smoke conditions explicit. > - Successful source selection, artifact verification and cancellation checks still apply. ## Linked Issues or Issue Description Refs #16028. **What happened?** [Release 38389963483](https://github.com/paperclipai/paperclip/actions/runs/38389963483) passed source selection, then skipped native production, canary publication and onboarding smoke. The exact source script requires native artifacts; the API does not expose the source job outputs. The registry canary still points to an older source. **Expected behavior** The selected release channel builds its required native artifacts, publishes its verified package, then runs the exact-version onboarding smoke. Skipped inactive channels must not suppress these jobs. **Steps to reproduce** Inspect the linked master-push run at `d67e3507b23041212fa26302ce6de078c5ebcc05`. Compare the successful source job and exact source script with the three skipped jobs. The two existing workflow contract cases also fail against the original conditions. **Paperclip version or commit** `d67e3507b23041212fa26302ce6de078c5ebcc05`. **Deployment mode** GitHub Actions release workflow. ## What Changed - Require `!cancelled()`, successful source selection and `required=true` for native production. - Require `!cancelled()` and successful publication for canary onboarding smoke. - Extend the existing workflow tests for selected channels, skipped ancestors, missing outputs, failed jobs and cancellation. ## Verification - The two affected contract cases fail before treatment and pass after treatment. - Focused source controls cover 28 producer and five smoke condition combinations. - Independent source review found no changes to source, trust, artifact, channel, permission or publication gates. - These tests check source predicates. Actual hosted routing and publication must still pass after merge. - Full existing release suite: 70/70 pass. Existing source-verification neighbors: 20/20 pass. - Workflow structure passes actionlint. Full shell lint reports five hints in unchanged commands. - Full hosted checks and fresh Greptile review are pending. ## Risks The producer serves all release channels. The explicit source-result and required-output checks retain each channel's admission gates. Cancellation still prevents new work. This does not change runtime code, packages, agent selection, or stored configurations. [GitHub documents the implicit status check](https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#status-check-functions) and [skipped dependency behavior](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-jobs). ## Model Used OpenAI Codex, based on GPT-6, with reasoning and code tools. This session does not expose a verifiable exact model ID or context window size. ## 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 Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
fd61dc1dd5
commit
60bc0d5fee
2 files changed
+41
-3
No files matched your search
@@ -377,7 +377,7 @@ jobs:
|
||||
|
||||
release_runner_binaries:
|
||||
needs: release_runner_source
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
if: ${{ !cancelled() && needs.release_runner_source.result == 'success' && needs.release_runner_source.outputs.required == 'true' }}
|
||||
uses: ./.github/workflows/release-verify.yml
|
||||
with:
|
||||
ref: ${{ needs.release_runner_source.outputs.sha }}
|
||||
@@ -525,7 +525,7 @@ jobs:
|
||||
# anyone mistakes an installable package for an onboardable one.
|
||||
smoke_canary_onboarding:
|
||||
needs: publish_canary
|
||||
if: needs.publish_canary.result == 'success'
|
||||
if: ${{ !cancelled() && needs.publish_canary.result == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
|
||||
@@ -7,6 +7,7 @@ import path from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
import { runInNewContext } from "node:vm";
|
||||
import { createServer } from "node:http";
|
||||
import { canaryStartup } from "./canary-startup-fixture.mjs";
|
||||
import { canRetryPublishedTarball404, TARBALL_METADATA_TIMEOUT_MS } from "../../tests/canary-onboarding/start-published-canary.mjs";
|
||||
@@ -307,8 +308,20 @@ test("published canaries are gated by the exact-version onboarding browser smoke
|
||||
);
|
||||
assert.match(
|
||||
releaseWorkflow,
|
||||
/smoke_canary_onboarding:\n\s+needs: publish_canary\n\s+if: needs\.publish_canary\.result == 'success'/,
|
||||
/smoke_canary_onboarding:\n\s+needs: publish_canary\n\s+if: \$\{\{ !cancelled\(\) && needs\.publish_canary\.result == 'success' \}\}/,
|
||||
);
|
||||
const smoke = releaseWorkflow.split(" smoke_canary_onboarding:\n")[1].split(/\n [a-z_]+:\n/)[0];
|
||||
const condition = smoke.match(/^ if: \$\{\{ (.+) \}\}$/m)?.[1];
|
||||
assert.match(condition, /^!cancelled\(\) && needs\.publish_canary\.result == 'success'$/);
|
||||
for (const [result, cancelled, expected] of [
|
||||
["success", false, true], ["failure", false, false],
|
||||
["cancelled", false, false], ["skipped", false, false], ["success", true, false],
|
||||
]) {
|
||||
const needs = { publish_canary: { result }, release_runner_source: { result: "success" },
|
||||
select_nightly: { result: "skipped" }, select_beta: { result: "skipped" }, preflight_stable: { result: "skipped" } };
|
||||
assert.equal(runInNewContext(condition, { cancelled: () => cancelled, needs }), expected,
|
||||
`Canary smoke: publisher=${result}, cancelled=${cancelled}`);
|
||||
}
|
||||
assert.match(
|
||||
releaseWorkflow,
|
||||
/PAPERCLIPAI_VERSION: \$\{\{ needs\.publish_canary\.outputs\.canary_version \}\}/,
|
||||
@@ -772,6 +785,31 @@ test("release daemon production uses exact-source native hosts and no publisher
|
||||
assert.ok(workflow.includes(`${job}:\n if: \${{ !inputs.runner_binaries_only }}`));
|
||||
}
|
||||
const release = readWorkflow("release.yml");
|
||||
const source = release.split(" release_runner_source:\n")[1].split(/\n [a-z_]+:\n/)[0];
|
||||
const binaries = release.split(" release_runner_binaries:\n")[1].split(/\n [a-z_]+:\n/)[0];
|
||||
assert.match(source, /needs: \[verify_canary, select_nightly, select_beta, preflight_stable\]/);
|
||||
assert.match(source, /!cancelled\(\)/);
|
||||
assert.match(binaries, /needs: release_runner_source/);
|
||||
assert.match(binaries, /ref: \$\{\{ needs\.release_runner_source\.outputs\.sha \}\}/);
|
||||
assert.match(binaries, /runner_binaries_only: true/);
|
||||
const condition = binaries.match(/^ if: (.+)$/m)?.[1].replace(/^\$\{\{\s*|\s*\}\}$/g, "");
|
||||
assert.match(condition, /^!cancelled\(\) && needs\.release_runner_source\.result == 'success' && needs\.release_runner_source\.outputs\.required == 'true'$/,
|
||||
"Native compilation must bypass deliberately skipped channel ancestors and require successful exact-source selection");
|
||||
const laneJobs = ["verify_canary", "select_nightly", "select_beta", "preflight_stable"];
|
||||
for (const activeLane of laneJobs) {
|
||||
for (const [result, required, cancelled, expected] of [
|
||||
["success", "true", false, true], ["failure", "true", false, false],
|
||||
["cancelled", "true", false, false], ["skipped", "true", false, false],
|
||||
["success", "false", false, false], ["success", undefined, false, false],
|
||||
["success", "true", true, false],
|
||||
]) {
|
||||
const needs = Object.fromEntries(laneJobs.map(job => [job, { result: job === activeLane ? "success" : "skipped" }]));
|
||||
needs.release_runner_source = { result, outputs: { required } };
|
||||
assert.equal(Object.values(needs).filter(job => job.result === "skipped").length, result === "skipped" ? 4 : 3);
|
||||
assert.equal(runInNewContext(condition, { cancelled: () => cancelled, needs }), expected,
|
||||
`${activeLane}: source=${result}, required=${required}, cancelled=${cancelled}`);
|
||||
}
|
||||
}
|
||||
for (const job of ["publish_canary", "publish_nightly", "publish_beta", "preview_stable", "publish_stable"]) {
|
||||
const block = release.split(` ${job}:\n`)[1].split(/\n [a-z_]+:\n/)[0];
|
||||
assert.match(block, /needs: .*release_runner_binaries/);
|
||||
|
||||
Reference in new issue
Block a user