fix: assemble verified host runner assets before release

Reuse release verification producers for the supported native hosts. Validate source-bound data before publication and probe the installed Codex command in the existing offline consumer.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-08 07:02:16 -05:00
1 parent 5fa4abaead
commit f8e2bee66e
10 files changed
+1061 -19

No files matched your search

+190
View File
@@ -7,18 +7,28 @@ on:
description: Commit SHA, branch, or tag to verify
required: true
type: string
runner_assets_only:
description: Build only the release daemon/provider-pack data after the caller's source gate
type: boolean
default: false
outputs:
runner_assets_artifact:
description: Run-bound verified release daemon/provider-pack data
value: ${{ jobs.runner_release_assemble.outputs.artifact_name }}
# Caller-provided refs may name unmerged PR code. AWS is eligible only when
# the caller runs on canonical master and verifies that event's exact SHA.
# The organization group also restricts these workflow files to master.
jobs:
runner_chaos_evals:
if: ${{ !inputs.runner_assets_only }}
name: Pre-release Runner chaos evals
uses: ./.github/workflows/runner-chaos-evals.yml
with:
ref: ${{ inputs.ref }}
typecheck:
if: ${{ !inputs.runner_assets_only }}
name: Typecheck
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
@@ -97,6 +107,7 @@ jobs:
run: pnpm -r typecheck
general_tests:
if: ${{ !inputs.runner_assets_only }}
name: General tests (${{ matrix.group_label }})
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
@@ -204,6 +215,7 @@ jobs:
fi
serialized_tests:
if: ${{ !inputs.runner_assets_only }}
name: Serialized tests (${{ matrix.shard_label }})
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
@@ -253,6 +265,7 @@ jobs:
run: pnpm test:run:serialized -- --shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }}
runner_workflow_evals:
if: ${{ !inputs.runner_assets_only }}
name: Runner workflow eval scorer contract
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 10
@@ -283,6 +296,7 @@ jobs:
run: pnpm test:runner-workflow-evals
verify_paperclip_runner:
if: ${{ !inputs.runner_assets_only }}
name: Verify Paperclip Runner (${{ matrix.lane }})
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
@@ -396,6 +410,7 @@ jobs:
run: pnpm --filter @paperclipai/paperclip-runner build:rust
build:
if: ${{ !inputs.runner_assets_only }}
name: Build
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
@@ -424,3 +439,178 @@ jobs:
- name: Build
run: pnpm build
# Only the missing release producers. Source verification stays with the
# caller; these unprivileged jobs neither publish nor receive provider secrets.
runner_release_binaries:
needs: runner_release_request
if: inputs.runner_assets_only
name: Release daemon (${{ matrix.target }})
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- target: darwin-arm64
runner: macos-15
- target: darwin-x64
runner: macos-15-intel
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
- name: Build the exact native release daemon
working-directory: packages/paperclip-runner
env:
SOURCE_SHA: ${{ inputs.ref }}
run: |
set -euo pipefail
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]]
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
rustup show
cargo build --release --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --bin paperclip-runnerd
- name: Record native architecture, checksum and executable metadata
env:
SOURCE_SHA: ${{ inputs.ref }}
TARGET: ${{ matrix.target }}
run: |
node scripts/release-runner-artifacts.mjs record-binary "$SOURCE_SHA" "$TARGET" \
"$GITHUB_WORKSPACE/packages/paperclip-runner/runner/target/release/paperclip-runnerd" \
"$RUNNER_TEMP/runner-release/$TARGET"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: runner-release-${{ inputs.ref }}-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.target }}
path: ${{ runner.temp }}/runner-release/
if-no-files-found: error
retention-days: 7
runner_release_pack:
needs: runner_release_request
if: inputs.runner_assets_only
name: Actual release image Linux daemon and provider-pack identity
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
with:
version: 9.15.4
run_install: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
- name: Extract the actual exact-source image provider-pack
env:
SOURCE_SHA: ${{ inputs.ref }}
run: |
set -euo pipefail
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]]
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
mkdir -p "$RUNNER_TEMP/runner-release-input"
owner="paperclip-release-pack-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
trap 'docker rm -f "$owner" >/dev/null 2>&1 || true' EXIT
# Canary images publish after npm. Build the existing image target
# without registry credentials and consume its actual source bytes.
pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile
image="$owner"
docker buildx build --platform linux/amd64 --target runner-provider-pack \
--build-arg "PAPERCLIP_BUILD_COMMIT=$SOURCE_SHA" \
--label "org.opencontainers.image.revision=$SOURCE_SHA" \
--cache-from type=registry,ref=ghcr.io/paperclipai/paperclip:buildcache-amd64 \
--tag "$image" --load .
pack_path=/provider-pack/provider-pack.json
docker image inspect "$image" > "$RUNNER_TEMP/runner-release-input/image.json"
daemon_path=/app/server/dist/vendor/paperclip-runner/bin/paperclip-runnerd
docker create --name "$owner" --network none --user 1000:1000 --read-only \
--cap-drop ALL --security-opt no-new-privileges \
--entrypoint "$daemon_path" "$image" --build-metadata >/dev/null
docker cp "$owner:$pack_path" "$RUNNER_TEMP/runner-release-input/provider-pack.json"
docker cp "$owner:$daemon_path" "$RUNNER_TEMP/runner-release-input/paperclip-runnerd"
# Execute only inside the source image. Its libc may be newer than
# ubuntu-latest; host execution would not prove this image artifact.
timeout 30s docker start --attach "$owner" > "$RUNNER_TEMP/runner-release-input/daemon-metadata.json"
test "$(docker inspect --format '{{.State.ExitCode}}' "$owner")" = 0
node scripts/release-runner-artifacts.mjs record-image-binary "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-input/paperclip-runnerd" "$RUNNER_TEMP/runner-release-input/daemon-metadata.json" \
"$RUNNER_TEMP/runner-release-input/image.json" "$RUNNER_TEMP/runner-release/linux-x64"
node scripts/release-runner-artifacts.mjs record-pack "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-input/provider-pack.json" "$RUNNER_TEMP/runner-release-input/image.json" \
"$RUNNER_TEMP/runner-release/provider-pack"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: runner-release-${{ inputs.ref }}-${{ github.run_id }}-${{ github.run_attempt }}-provider-pack
path: ${{ runner.temp }}/runner-release/
if-no-files-found: error
retention-days: 7
runner_release_assemble:
if: inputs.runner_assets_only
name: Assemble verified release runner data
needs: [runner_release_binaries, runner_release_pack]
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
artifact_name: ${{ steps.identity.outputs.artifact_name }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: runner-release-${{ inputs.ref }}-${{ github.run_id }}-${{ github.run_attempt }}-*
path: ${{ runner.temp }}/runner-release-inputs
merge-multiple: true
- name: Run the sole source-aware assembler and bound its data envelope
env:
SOURCE_SHA: ${{ inputs.ref }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
node scripts/release-runner-artifacts.mjs assemble "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-inputs" "$RUNNER_TEMP/runner-release-assembled"
- name: Record run-bound assembled artifact
id: identity
env:
SOURCE_SHA: ${{ inputs.ref }}
run: echo "artifact_name=runner-release-assembled-$SOURCE_SHA-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ steps.identity.outputs.artifact_name }}
path: |
${{ runner.temp }}/runner-release-assembled/runner-release-assets.tar.gz
${{ runner.temp }}/runner-release-assembled/runner-release-assets.tar.gz.sha256
if-no-files-found: error
retention-days: 7
runner_release_request:
if: inputs.runner_assets_only
name: Validate immutable runner producer inputs
permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Reject mutable sources before any producer checkout
env:
SOURCE_SHA: ${{ inputs.ref }}
run: |
set -euo pipefail
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]]
+236 -8
View File
@@ -337,8 +337,11 @@ jobs:
run: node scripts/cloud-source-verification.mjs "$SOURCE_SHA"
publish_canary:
if: github.event_name == 'push'
needs: verify_canary
if: >-
!cancelled() && github.event_name == 'push' && needs.verify_canary.result == 'success' &&
needs.pin_runner_release_source.result == 'success' &&
(needs.pin_runner_release_source.outputs.required != 'true' || needs.release_runner_assets.result == 'success')
needs: [verify_canary, pin_runner_release_source, release_runner_assets]
runs-on: ubuntu-latest
timeout-minutes: 150
environment: npm-canary
@@ -350,10 +353,14 @@ jobs:
# For the explicit docker.yml dispatch below.
actions: write
defaults:
run:
working-directory: source
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
path: source
fetch-depth: 0
- name: Setup pnpm
@@ -366,6 +373,33 @@ jobs:
with:
node-version: 24
cache: pnpm
cache-dependency-path: source/pnpm-lock.yaml
- name: Checkout trusted release transfer tools
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.sha }}
path: trusted
persist-credentials: false
- name: Download this run's verified runner data
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ needs.release_runner_assets.outputs.runner_assets_artifact }}
path: ${{ runner.temp }}/runner-release-archive
- name: Validate runner data against the selected source before publishing
if: needs.pin_runner_release_source.outputs.required == 'true'
env:
SOURCE_SHA: ${{ needs.pin_runner_release_source.outputs.source_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
node "$GITHUB_WORKSPACE/trusted/scripts/release-runner-artifacts.mjs" unpack "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz.sha256" \
"$RUNNER_TEMP/runner-release-assets" "$GITHUB_WORKSPACE/source"
echo "PAPERCLIP_RELEASE_RUNNER_ASSETS=$RUNNER_TEMP/runner-release-assets" >> "$GITHUB_ENV"
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
@@ -587,11 +621,13 @@ jobs:
artifact_name: nightly-release-smoke
publish_nightly:
needs: [select_nightly, smoke_nightly]
needs: [select_nightly, smoke_nightly, pin_runner_release_source, release_runner_assets]
# Publish when smoke passed, or when smoke was deliberately skipped by a
# dry-run dispatch (the publish itself is a dry-run in that case).
if: >-
!cancelled() &&
needs.pin_runner_release_source.result == 'success' &&
(needs.pin_runner_release_source.outputs.required != 'true' || needs.release_runner_assets.result == 'success') &&
needs.select_nightly.outputs.proceed == 'true' &&
(needs.smoke_nightly.result == 'success' ||
(needs.smoke_nightly.result == 'skipped' && github.event_name == 'workflow_dispatch' && inputs.dry_run))
@@ -611,10 +647,14 @@ jobs:
id-token: write
actions: write
defaults:
run:
working-directory: source
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
path: source
ref: ${{ needs.select_nightly.outputs.sha }}
fetch-depth: 0
@@ -628,6 +668,33 @@ jobs:
with:
node-version: 24
cache: pnpm
cache-dependency-path: source/pnpm-lock.yaml
- name: Checkout trusted release transfer tools
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.sha }}
path: trusted
persist-credentials: false
- name: Download this run's verified runner data
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ needs.release_runner_assets.outputs.runner_assets_artifact }}
path: ${{ runner.temp }}/runner-release-archive
- name: Validate runner data against the selected source before publishing
if: needs.pin_runner_release_source.outputs.required == 'true'
env:
SOURCE_SHA: ${{ needs.pin_runner_release_source.outputs.source_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
node "$GITHUB_WORKSPACE/trusted/scripts/release-runner-artifacts.mjs" unpack "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz.sha256" \
"$RUNNER_TEMP/runner-release-assets" "$GITHUB_WORKSPACE/source"
echo "PAPERCLIP_RELEASE_RUNNER_ASSETS=$RUNNER_TEMP/runner-release-assets" >> "$GITHUB_ENV"
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
@@ -855,9 +922,11 @@ jobs:
ref: ${{ needs.select_beta.outputs.sha }}
publish_beta:
needs: [select_beta, verify_beta_candidate]
needs: [select_beta, verify_beta_candidate, pin_runner_release_source, release_runner_assets]
if: >-
!cancelled() &&
needs.pin_runner_release_source.result == 'success' &&
(needs.pin_runner_release_source.outputs.required != 'true' || needs.release_runner_assets.result == 'success') &&
needs.select_beta.result == 'success' &&
(needs.verify_beta_candidate.result == 'success' ||
(needs.verify_beta_candidate.result == 'skipped' && needs.select_beta.outputs.mode == 'promote'))
@@ -877,10 +946,14 @@ jobs:
outputs:
beta_version: ${{ steps.result.outputs.beta_version }}
defaults:
run:
working-directory: source
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
path: source
ref: ${{ needs.select_beta.outputs.sha }}
fetch-depth: 0
@@ -894,6 +967,33 @@ jobs:
with:
node-version: 24
cache: pnpm
cache-dependency-path: source/pnpm-lock.yaml
- name: Checkout trusted release transfer tools
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.sha }}
path: trusted
persist-credentials: false
- name: Download this run's verified runner data
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ needs.release_runner_assets.outputs.runner_assets_artifact }}
path: ${{ runner.temp }}/runner-release-archive
- name: Validate runner data against the selected source before publishing
if: needs.pin_runner_release_source.outputs.required == 'true'
env:
SOURCE_SHA: ${{ needs.pin_runner_release_source.outputs.source_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
node "$GITHUB_WORKSPACE/trusted/scripts/release-runner-artifacts.mjs" unpack "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz.sha256" \
"$RUNNER_TEMP/runner-release-assets" "$GITHUB_WORKSPACE/source"
echo "PAPERCLIP_RELEASE_RUNNER_ASSETS=$RUNNER_TEMP/runner-release-assets" >> "$GITHUB_ENV"
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
@@ -1240,17 +1340,24 @@ jobs:
ref: ${{ needs.preflight_stable.outputs.sha }}
preview_stable:
if: github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && inputs.dry_run
needs: [preflight_stable, verify_stable]
if: >-
!cancelled() && github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && inputs.dry_run &&
needs.verify_stable.result == 'success' && needs.pin_runner_release_source.result == 'success' &&
(needs.pin_runner_release_source.outputs.required != 'true' || needs.release_runner_assets.result == 'success')
needs: [preflight_stable, verify_stable, pin_runner_release_source, release_runner_assets]
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
defaults:
run:
working-directory: source
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
path: source
fetch-depth: 0
ref: ${{ needs.preflight_stable.outputs.sha }}
@@ -1264,6 +1371,33 @@ jobs:
with:
node-version: 24
cache: pnpm
cache-dependency-path: source/pnpm-lock.yaml
- name: Checkout trusted release transfer tools
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.sha }}
path: trusted
persist-credentials: false
- name: Download this run's verified runner data
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ needs.release_runner_assets.outputs.runner_assets_artifact }}
path: ${{ runner.temp }}/runner-release-archive
- name: Validate runner data against the selected source before publishing
if: needs.pin_runner_release_source.outputs.required == 'true'
env:
SOURCE_SHA: ${{ needs.pin_runner_release_source.outputs.source_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
node "$GITHUB_WORKSPACE/trusted/scripts/release-runner-artifacts.mjs" unpack "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz.sha256" \
"$RUNNER_TEMP/runner-release-assets" "$GITHUB_WORKSPACE/source"
echo "PAPERCLIP_RELEASE_RUNNER_ASSETS=$RUNNER_TEMP/runner-release-assets" >> "$GITHUB_ENV"
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
@@ -1282,8 +1416,11 @@ jobs:
./scripts/release.sh "${args[@]}"
publish_stable:
if: github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && !inputs.dry_run
needs: [preflight_stable, verify_stable]
if: >-
!cancelled() && github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && !inputs.dry_run &&
needs.verify_stable.result == 'success' && needs.pin_runner_release_source.result == 'success' &&
(needs.pin_runner_release_source.outputs.required != 'true' || needs.release_runner_assets.result == 'success')
needs: [preflight_stable, verify_stable, pin_runner_release_source, release_runner_assets]
runs-on: ubuntu-latest
timeout-minutes: 150
environment: npm-stable
@@ -1294,10 +1431,14 @@ jobs:
outputs:
stable_version: ${{ steps.tag.outputs.version }}
defaults:
run:
working-directory: source
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
path: source
fetch-depth: 0
ref: ${{ needs.preflight_stable.outputs.sha }}
@@ -1311,6 +1452,33 @@ jobs:
with:
node-version: 24
cache: pnpm
cache-dependency-path: source/pnpm-lock.yaml
- name: Checkout trusted release transfer tools
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.sha }}
path: trusted
persist-credentials: false
- name: Download this run's verified runner data
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ needs.release_runner_assets.outputs.runner_assets_artifact }}
path: ${{ runner.temp }}/runner-release-archive
- name: Validate runner data against the selected source before publishing
if: needs.pin_runner_release_source.outputs.required == 'true'
env:
SOURCE_SHA: ${{ needs.pin_runner_release_source.outputs.source_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
node "$GITHUB_WORKSPACE/trusted/scripts/release-runner-artifacts.mjs" unpack "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz.sha256" \
"$RUNNER_TEMP/runner-release-assets" "$GITHUB_WORKSPACE/source"
echo "PAPERCLIP_RELEASE_RUNNER_ASSETS=$RUNNER_TEMP/runner-release-assets" >> "$GITHUB_ENV"
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
@@ -1477,3 +1645,63 @@ jobs:
echo "- Open and merge the PR (a human opens it so CI runs):"
echo " https://github.com/${GITHUB_REPOSITORY}/compare/master...${branch}?expand=1"
} >> "$GITHUB_STEP_SUMMARY"
# Resolve one applicable release channel after its existing gate. Optional
# channels are deliberately skipped, so use explicit results, never an
# implicit all-needs-success dependency that would suppress promotions.
pin_runner_release_source:
needs: [verify_canary, select_nightly, select_beta, verify_beta_candidate, preflight_stable, verify_stable]
if: >-
!cancelled() &&
((github.event_name == 'push' && needs.verify_canary.result == 'success') ||
(needs.select_nightly.result == 'success' && needs.select_nightly.outputs.proceed == 'true') ||
(needs.select_beta.result == 'success' &&
(needs.verify_beta_candidate.result == 'success' || needs.select_beta.outputs.mode == 'promote')) ||
(needs.preflight_stable.result == 'success' && needs.verify_stable.result == 'success'))
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
source_sha: ${{ steps.pin.outputs.source_sha }}
required: ${{ steps.pin.outputs.required }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false
- name: Pin the gated source and identify its release assembly contract
id: pin
env:
EVENT_NAME: ${{ github.event_name }}
CANARY_SHA: ${{ github.sha }}
NIGHTLY_SHA: ${{ needs.select_nightly.outputs.sha }}
BETA_SHA: ${{ needs.select_beta.outputs.sha }}
STABLE_SHA: ${{ needs.preflight_stable.outputs.sha }}
run: |
set -euo pipefail
if [[ "$EVENT_NAME" == push ]]; then source_sha="$CANARY_SHA";
elif [[ -n "$NIGHTLY_SHA" ]]; then source_sha="$NIGHTLY_SHA";
elif [[ -n "$BETA_SHA" ]]; then source_sha="$BETA_SHA";
else source_sha="$STABLE_SHA"; fi
[[ "$source_sha" =~ ^[a-f0-9]{40}$ ]]
git cat-file -e "$source_sha^{commit}"
required=false
# Promoted older sources retain their original contract. New sources
# opt in with the checked-in hook and may never publish partial assets.
if git show "$source_sha:scripts/release.sh" | grep -F 'PAPERCLIP_RELEASE_RUNNER_ASSETS' >/dev/null; then
git cat-file -e "$source_sha:scripts/release-runner-artifacts.mjs"
required=true
fi
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
echo "required=$required" >> "$GITHUB_OUTPUT"
release_runner_assets:
needs: pin_runner_release_source
if: needs.pin_runner_release_source.outputs.required == 'true'
permissions:
contents: read
uses: ./.github/workflows/release-verify.yml
with:
ref: ${{ needs.pin_runner_release_source.outputs.source_sha }}
runner_assets_only: true