mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-15 12:28:10 +02:00
feat(runner): qualify native Codex platforms and cache hosted builds
Extract the tested runtime and installed-package prerequisite without changing agent creation defaults or experimental gates. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
7d9b93aee2
commit
ddcd746eb6
40 files changed
+3474
-565
No files matched your search
@@ -23,6 +23,11 @@ on:
|
||||
required: false
|
||||
default: release-smoke
|
||||
type: string
|
||||
qualification_source_sha:
|
||||
description: Optional immutable source SHA for provider-free native package qualification
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
paperclip_version:
|
||||
@@ -36,6 +41,13 @@ on:
|
||||
required: false
|
||||
default: release-smoke
|
||||
type: string
|
||||
qualification_source_sha:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# The Docker smoke below can never exercise the background-service leg of
|
||||
@@ -45,6 +57,7 @@ jobs:
|
||||
# on the runner VM's systemd and require the installed service to end up
|
||||
# serving.
|
||||
smoke_service:
|
||||
if: inputs.qualification_source_sha == ''
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
@@ -98,6 +111,7 @@ jobs:
|
||||
retention-days: 14
|
||||
|
||||
smoke:
|
||||
if: inputs.qualification_source_sha == ''
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
|
||||
@@ -198,3 +212,163 @@ jobs:
|
||||
- name: Stop Docker smoke container
|
||||
if: always()
|
||||
run: docker rm -f "$SMOKE_CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
qualification_daemons:
|
||||
if: inputs.qualification_source_sha != ''
|
||||
uses: ./.github/workflows/release-verify.yml
|
||||
with:
|
||||
ref: ${{ inputs.qualification_source_sha }}
|
||||
runner_binaries_only: true
|
||||
|
||||
qualification_pack:
|
||||
if: inputs.qualification_source_sha != ''
|
||||
needs: qualification_daemons
|
||||
name: Assemble and verify public Linux packages
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ inputs.qualification_source_sha }}
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
||||
with:
|
||||
version: 9.15.4
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: ${{ needs.qualification_daemons.outputs.runner_artifact_prefix }}-*
|
||||
path: ${{ runner.temp }}/release-runner-inputs
|
||||
merge-multiple: true
|
||||
- name: Bind the immutable Linux producer source
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.qualification_source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1
|
||||
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
|
||||
- name: Build normal workspace outputs and public CLI
|
||||
run: |
|
||||
pnpm install --no-frozen-lockfile
|
||||
pnpm build
|
||||
bash scripts/build-npm.sh --skip-checks --skip-typecheck
|
||||
mv cli/package.dev.json cli/package.json
|
||||
- name: Assemble exact-source daemons before server packing
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.qualification_source_sha }}
|
||||
run: |
|
||||
node packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs \
|
||||
assemble-daemons "$RUNNER_TEMP/release-runner-inputs" "$SOURCE_SHA"
|
||||
cp -R packages/paperclip-runner/dist/bin/. server/dist/vendor/paperclip-runner/bin/
|
||||
- name: Verify clean public installation and retain the same package bytes
|
||||
env:
|
||||
PAPERCLIP_RUNNER_PUBLIC_PACKS_DIR: ${{ runner.temp }}/public-runner-packs
|
||||
run: node scripts/verify-grok-npm-install.mjs
|
||||
- name: Locate completed package assembly
|
||||
id: pack_identity
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
if [[ -s "$RUNNER_TEMP/public-runner-packs/manifest.json" ]]; then
|
||||
echo "available=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Retain completed package assembly even after a later Linux failure
|
||||
if: ${{ !cancelled() && steps.pack_identity.outputs.available == 'true' }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}-public-packages
|
||||
path: ${{ runner.temp }}/public-runner-packs/
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
qualification_native:
|
||||
if: ${{ !cancelled() && inputs.qualification_source_sha != '' && needs.qualification_daemons.result == 'success' }}
|
||||
needs: [qualification_daemons, qualification_pack]
|
||||
name: Clean installed package (${{ matrix.target }})
|
||||
runs-on: ${{ matrix.host }}
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.qualification_daemons.outputs.native_consumer_matrix) }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ inputs.qualification_source_sha }}
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
||||
with:
|
||||
version: 9.15.4
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}-public-packages
|
||||
path: ${{ runner.temp }}/public-runner-packs
|
||||
- name: Install only the existing verifier tooling
|
||||
run: pnpm install --ignore-scripts --no-frozen-lockfile
|
||||
- name: Checkout immutable ARM qualification tooling
|
||||
if: ${{ matrix.target == 'linux-arm64' }}
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
# job.workflow_* identifies this reusable workflow, not its caller.
|
||||
repository: ${{ job.workflow_repository }}
|
||||
ref: ${{ job.workflow_sha }}
|
||||
path: .qualification-tooling
|
||||
sparse-checkout: scripts/prepare-linux-arm64-qualification.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
- name: Prepare Linux ARM sandbox prerequisites
|
||||
if: ${{ matrix.target == 'linux-arm64' }}
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.qualification_source_sha }}
|
||||
TOOLING_SHA: ${{ job.workflow_sha }}
|
||||
TOOLING_REPOSITORY: ${{ job.workflow_repository }}
|
||||
TOOLING_WORKFLOW_REF: ${{ job.workflow_ref }}
|
||||
TOOLING_WORKFLOW_PATH: ${{ job.workflow_file_path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1
|
||||
[[ "$TOOLING_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1
|
||||
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
|
||||
[[ "$(git -C .qualification-tooling rev-parse HEAD)" == "$TOOLING_SHA" ]] || exit 1
|
||||
node --input-type=module <<'NODE'
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
const directory = join(process.env.RUNNER_TEMP, 'paperclip-arm-bwrap-prerequisite');
|
||||
mkdirSync(directory, { recursive: true });
|
||||
const helper = '.qualification-tooling/scripts/prepare-linux-arm64-qualification.sh';
|
||||
writeFileSync(join(directory, 'qualification-identity.json'), JSON.stringify({
|
||||
productSourceSha: process.env.SOURCE_SHA,
|
||||
toolingSourceSha: process.env.TOOLING_SHA,
|
||||
toolingRepository: process.env.TOOLING_REPOSITORY,
|
||||
toolingWorkflowRef: process.env.TOOLING_WORKFLOW_REF,
|
||||
toolingWorkflowPath: process.env.TOOLING_WORKFLOW_PATH,
|
||||
helperSha256: createHash('sha256').update(readFileSync(helper)).digest('hex'),
|
||||
}, null, 2) + '\n');
|
||||
NODE
|
||||
bash .qualification-tooling/scripts/prepare-linux-arm64-qualification.sh
|
||||
- name: Verify host identity and actual clean npm consumer
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.qualification_source_sha }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
|
||||
[[ "$(node -p 'process.platform + "-" + process.arch')" == "$TARGET" ]] || exit 1
|
||||
mode=--mac-consumer
|
||||
if [[ "$TARGET" == linux-arm64 ]]; then mode=--linux-arm64-consumer; fi
|
||||
node scripts/verify-grok-npm-install.mjs "$mode" \
|
||||
"$RUNNER_TEMP/public-runner-packs" "$SOURCE_SHA" "$RUNNER_TEMP/native-public-install.json"
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
if: always()
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}-${{ matrix.target }}
|
||||
path: |
|
||||
${{ runner.temp }}/native-public-install.json
|
||||
${{ runner.temp }}/paperclip-arm-bwrap-prerequisite/
|
||||
if-no-files-found: warn
|
||||
retention-days: 14
|
||||
@@ -7,18 +7,30 @@ on:
|
||||
description: Commit SHA, branch, or tag to verify
|
||||
required: true
|
||||
type: string
|
||||
runner_binaries_only:
|
||||
description: Build exact-source release daemons without publishing or provider calls
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
outputs:
|
||||
runner_artifact_prefix:
|
||||
value: ${{ jobs.runner_binaries.outputs.artifact_prefix }}
|
||||
native_consumer_matrix:
|
||||
value: ${{ jobs.runner_binary_targets.outputs.native_consumers }}
|
||||
|
||||
# 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_binaries_only }}
|
||||
name: Pre-release Runner chaos evals
|
||||
uses: ./.github/workflows/runner-chaos-evals.yml
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
typecheck:
|
||||
if: ${{ !inputs.runner_binaries_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 +109,7 @@ jobs:
|
||||
run: pnpm -r typecheck
|
||||
|
||||
general_tests:
|
||||
if: ${{ !inputs.runner_binaries_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
|
||||
@@ -208,6 +221,7 @@ jobs:
|
||||
fi
|
||||
|
||||
serialized_tests:
|
||||
if: ${{ !inputs.runner_binaries_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
|
||||
@@ -257,6 +271,7 @@ jobs:
|
||||
run: pnpm test:run:serialized -- --shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }}
|
||||
|
||||
runner_workflow_evals:
|
||||
if: ${{ !inputs.runner_binaries_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
|
||||
@@ -287,6 +302,7 @@ jobs:
|
||||
run: pnpm test:runner-workflow-evals
|
||||
|
||||
verify_paperclip_runner:
|
||||
if: ${{ !inputs.runner_binaries_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
|
||||
@@ -470,6 +486,7 @@ jobs:
|
||||
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
|
||||
|
||||
build:
|
||||
if: ${{ !inputs.runner_binaries_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
|
||||
@@ -498,3 +515,161 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
runner_binary_targets:
|
||||
if: inputs.runner_binaries_only
|
||||
name: Read exact-source daemon targets
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
matrix: ${{ steps.targets.outputs.matrix }}
|
||||
native_consumers: ${{ steps.targets.outputs.native_consumers }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Preserve the immutable source's platform contract
|
||||
id: targets
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1
|
||||
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
|
||||
node --input-type=module <<'NODE'
|
||||
import assert from 'node:assert/strict';
|
||||
import { appendFileSync } from 'node:fs';
|
||||
import { RELEASE_RUNNER_TARGETS } from './packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs';
|
||||
const historical = ['darwin-arm64', 'darwin-x64', 'linux-x64'];
|
||||
const current = [...historical, 'linux-arm64'];
|
||||
const targets = [...RELEASE_RUNNER_TARGETS].sort();
|
||||
assert.ok(JSON.stringify(targets) === JSON.stringify(historical.sort()) || JSON.stringify(targets) === JSON.stringify(current.sort()), 'Unknown exact-source daemon platform contract');
|
||||
const hosts = { 'linux-x64': targets.length === current.length ? 'ubuntu-24.04' : 'ubuntu-latest', 'linux-arm64': 'ubuntu-24.04-arm', 'darwin-arm64': 'macos-15', 'darwin-x64': 'macos-15-intel' };
|
||||
const include = RELEASE_RUNNER_TARGETS.map(target => ({ target, host: hosts[target] }));
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `matrix=${JSON.stringify({ include })}\nnative_consumers=${JSON.stringify({ include: include.filter(row => row.target !== 'linux-x64') })}\n`);
|
||||
NODE
|
||||
|
||||
runner_binaries:
|
||||
if: inputs.runner_binaries_only
|
||||
needs: runner_binary_targets
|
||||
name: Release daemon (${{ matrix.target }})
|
||||
runs-on: ${{ matrix.host }}
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.runner_binary_targets.outputs.matrix) }}
|
||||
outputs:
|
||||
artifact_prefix: ${{ steps.identity.outputs.artifact_prefix }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
||||
with:
|
||||
version: 9.15.4
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Bind immutable producer source and target
|
||||
id: identity
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.ref }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1
|
||||
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
|
||||
[[ "$(node -p 'process.platform + "-" + process.arch')" == "$TARGET" ]] || exit 1
|
||||
echo "artifact_prefix=runner-release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
|
||||
- name: Pin native Cargo workspace and toolchain
|
||||
id: native_rust_workspace
|
||||
working-directory: packages/paperclip-runner
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pinned="$HOME/paperclip-runner-rust"
|
||||
if [[ -e "$pinned" && ! -L "$pinned" ]]; then
|
||||
echo '::error::Native Cargo cache path must not replace a real directory'
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$pinned"
|
||||
ln -s "$GITHUB_WORKSPACE/packages/paperclip-runner/runner" "$pinned"
|
||||
echo "path=$pinned" >> "$GITHUB_OUTPUT"
|
||||
rustup show
|
||||
toolchain="$(rustup show active-toolchain | awk '{print $1}')"
|
||||
echo "RUSTUP_TOOLCHAIN=$toolchain" >> "$GITHUB_ENV"
|
||||
extra_toolchains="$(rustup toolchain list | awk '{print $1}' | grep -vx "$toolchain" || true)"
|
||||
if [[ -n "$extra_toolchains" ]]; then
|
||||
echo "$extra_toolchains" | xargs -n1 rustup toolchain uninstall \
|
||||
|| echo '::notice title=Native Rust cache::could not remove an extra toolchain; the cache key may not match'
|
||||
fi
|
||||
rustup toolchain list
|
||||
- name: Cache native Rust dependencies
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
workspaces: ${{ steps.native_rust_workspace.outputs.path }} -> target
|
||||
# Compiler/environment and Cargo lock hashes are added by the action.
|
||||
# Also bind generated contracts outside the Rust workspace, while
|
||||
# always recompiling workspace code and recording actual new bytes.
|
||||
shared-key: release-native-v2-${{ matrix.host }}-${{ matrix.target }}-${{ hashFiles('packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/src/**', 'packages/paperclip-runner/*.json') }}
|
||||
cache-workspace-crates: false
|
||||
cache-bin: false
|
||||
cache-on-failure: false
|
||||
# A master workflow can verify an unmerged preview ref. Those jobs
|
||||
# restore only; they never write master's trusted dependency cache.
|
||||
save-if: ${{ github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: Build TypeScript contract and host daemon concurrently
|
||||
run: |
|
||||
node --input-type=module <<'NODE'
|
||||
import { mkdirSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { startPrefetch, stopPrefetches } from './scripts/prefetch-child.mjs';
|
||||
const root = process.cwd(), logs = join(process.env.RUNNER_TEMP, 'native-build-preparation');
|
||||
mkdirSync(logs, { recursive: true });
|
||||
const builds = [];
|
||||
let failure;
|
||||
try {
|
||||
for (const [name, args, cwd] of [
|
||||
['typescript', ['--filter', '@paperclipai/paperclip-runner', 'build:typescript'], root],
|
||||
['native', ['build:binary'], join(root, 'packages/paperclip-runner')],
|
||||
]) {
|
||||
builds.push(startPrefetch({ name, cmd: 'pnpm', args, cwd, env: process.env, logPath: join(logs, name + '.log') }));
|
||||
}
|
||||
await Promise.all(builds.map(build => build.done));
|
||||
} catch (error) { failure = error; }
|
||||
finally {
|
||||
await stopPrefetches(builds);
|
||||
const results = await Promise.allSettled(builds.map(build => build.done));
|
||||
builds.forEach((build, index) => {
|
||||
console.log('::group::' + build.name + ' build');
|
||||
process.stdout.write(readFileSync(join(logs, build.name + '.log'), 'utf8'));
|
||||
console.log('::endgroup::');
|
||||
console.log(JSON.stringify({ build: build.name, status: results[index].status,
|
||||
exitCode: build.child.exitCode, signal: build.child.signalCode, durationMs: build.durationMs }));
|
||||
});
|
||||
}
|
||||
if (failure) throw failure;
|
||||
NODE
|
||||
- name: Record actual daemon metadata and immutable bytes
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.ref }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
run: |
|
||||
node packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs record "$SOURCE_SHA" "$TARGET" \
|
||||
packages/paperclip-runner/dist/bin/paperclip-runnerd "$RUNNER_TEMP/runner-release/$TARGET"
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: ${{ steps.identity.outputs.artifact_prefix }}-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/runner-release/
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
@@ -340,9 +340,54 @@ jobs:
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
run: node scripts/cloud-source-verification.mjs "$SOURCE_SHA"
|
||||
|
||||
release_runner_source:
|
||||
# Old promotions retain their already-shipped packaging contract. New
|
||||
# sources opt into source-declared native target assembly in their release.sh.
|
||||
needs: [verify_canary, select_nightly, select_beta, preflight_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.preflight_stable.result == 'success')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
sha: ${{ steps.source.outputs.sha }}
|
||||
required: ${{ steps.source.outputs.required }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ needs.preflight_stable.outputs.sha || needs.select_beta.outputs.sha || needs.select_nightly.outputs.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
- name: Read the exact source packaging contract
|
||||
id: source
|
||||
env:
|
||||
SOURCE_SHA: ${{ needs.preflight_stable.outputs.sha || needs.select_beta.outputs.sha || needs.select_nightly.outputs.sha || github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1
|
||||
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
|
||||
echo "sha=$SOURCE_SHA" >> "$GITHUB_OUTPUT"
|
||||
source_script="$(git show "$SOURCE_SHA:scripts/release.sh")" || exit 1
|
||||
if printf '%s\n' "$source_script" | grep -F 'assemble-daemons "$PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR"' > /dev/null; then
|
||||
echo "required=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "required=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
release_runner_binaries:
|
||||
needs: release_runner_source
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
uses: ./.github/workflows/release-verify.yml
|
||||
with:
|
||||
ref: ${{ needs.release_runner_source.outputs.sha }}
|
||||
runner_binaries_only: true
|
||||
|
||||
publish_canary:
|
||||
if: github.event_name == 'push'
|
||||
needs: verify_canary
|
||||
if: ${{ !cancelled() && github.event_name == 'push' && needs.verify_canary.result == 'success' && needs.release_runner_source.result == 'success' && (needs.release_runner_source.outputs.required == 'false' || needs.release_runner_binaries.result == 'success') }}
|
||||
needs: [verify_canary, release_runner_source, release_runner_binaries]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 150
|
||||
environment: npm-canary
|
||||
@@ -380,6 +425,17 @@ jobs:
|
||||
git checkout -B master "$SOURCE_SHA"
|
||||
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
|
||||
|
||||
- name: Download exact-source release daemon receipts
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
|
||||
path: ${{ runner.temp }}/release-runner-inputs
|
||||
merge-multiple: true
|
||||
- name: Bind daemon inputs for release assembly
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
@@ -633,12 +689,14 @@ jobs:
|
||||
artifact_name: nightly-release-smoke
|
||||
|
||||
publish_nightly:
|
||||
needs: [select_nightly, smoke_nightly]
|
||||
needs: [select_nightly, smoke_nightly, release_runner_source, release_runner_binaries]
|
||||
# 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.select_nightly.outputs.proceed == 'true' &&
|
||||
needs.release_runner_source.result == 'success' &&
|
||||
(needs.release_runner_source.outputs.required == 'false' || needs.release_runner_binaries.result == 'success') &&
|
||||
(needs.smoke_nightly.result == 'success' ||
|
||||
(needs.smoke_nightly.result == 'skipped' && github.event_name == 'workflow_dispatch' && inputs.dry_run))
|
||||
runs-on: ubuntu-latest
|
||||
@@ -664,6 +722,17 @@ jobs:
|
||||
ref: ${{ needs.select_nightly.outputs.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download exact-source release daemon receipts
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
|
||||
path: ${{ runner.temp }}/release-runner-inputs
|
||||
merge-multiple: true
|
||||
- name: Bind daemon inputs for release assembly
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
@@ -901,10 +970,12 @@ jobs:
|
||||
ref: ${{ needs.select_beta.outputs.sha }}
|
||||
|
||||
publish_beta:
|
||||
needs: [select_beta, verify_beta_candidate]
|
||||
needs: [select_beta, verify_beta_candidate, release_runner_source, release_runner_binaries]
|
||||
if: >-
|
||||
!cancelled() &&
|
||||
needs.select_beta.result == 'success' &&
|
||||
needs.release_runner_source.result == 'success' &&
|
||||
(needs.release_runner_source.outputs.required == 'false' || needs.release_runner_binaries.result == 'success') &&
|
||||
(needs.verify_beta_candidate.result == 'success' ||
|
||||
(needs.verify_beta_candidate.result == 'skipped' && needs.select_beta.outputs.mode == 'promote'))
|
||||
runs-on: ubuntu-latest
|
||||
@@ -930,6 +1001,17 @@ jobs:
|
||||
ref: ${{ needs.select_beta.outputs.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download exact-source release daemon receipts
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
|
||||
path: ${{ runner.temp }}/release-runner-inputs
|
||||
merge-multiple: true
|
||||
- name: Bind daemon inputs for release assembly
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
@@ -1286,8 +1368,8 @@ 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.preflight_stable.result == 'success' && needs.verify_stable.result == 'success' && needs.release_runner_source.result == 'success' && (needs.release_runner_source.outputs.required == 'false' || needs.release_runner_binaries.result == 'success') }}
|
||||
needs: [preflight_stable, verify_stable, release_runner_source, release_runner_binaries]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
@@ -1300,6 +1382,17 @@ jobs:
|
||||
fetch-depth: 0
|
||||
ref: ${{ needs.preflight_stable.outputs.sha }}
|
||||
|
||||
- name: Download exact-source release daemon receipts
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
|
||||
path: ${{ runner.temp }}/release-runner-inputs
|
||||
merge-multiple: true
|
||||
- name: Bind daemon inputs for release assembly
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
@@ -1328,8 +1421,8 @@ 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.preflight_stable.result == 'success' && needs.verify_stable.result == 'success' && needs.release_runner_source.result == 'success' && (needs.release_runner_source.outputs.required == 'false' || needs.release_runner_binaries.result == 'success') }}
|
||||
needs: [preflight_stable, verify_stable, release_runner_source, release_runner_binaries]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 150
|
||||
environment: npm-stable
|
||||
@@ -1347,6 +1440,17 @@ jobs:
|
||||
fetch-depth: 0
|
||||
ref: ${{ needs.preflight_stable.outputs.sha }}
|
||||
|
||||
- name: Download exact-source release daemon receipts
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
|
||||
path: ${{ runner.temp }}/release-runner-inputs
|
||||
merge-multiple: true
|
||||
- name: Bind daemon inputs for release assembly
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
|
||||
Reference in new issue
Block a user