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:
DottaandPaperclip authored and GitHub committed 2026-10-13 07:12:11 -05:00
1 parent fd61dc1dd5
commit 60bc0d5fee
2 files changed
+41 -3

No files matched your search

+2 -2
View File
@@ -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/);