From 60bc0d5fee9a6e48db08fc016521fee3d9f57d67 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:12:11 -0500 Subject: [PATCH] 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 --- .github/workflows/release.yml | 4 +- .../release-verify-workflow.test.mjs | 40 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 19d2853b4f..a02b40609f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: diff --git a/scripts/__tests__/release-verify-workflow.test.mjs b/scripts/__tests__/release-verify-workflow.test.mjs index c6db590248..5c17222263 100644 --- a/scripts/__tests__/release-verify-workflow.test.mjs +++ b/scripts/__tests__/release-verify-workflow.test.mjs @@ -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/);