Files
PaperClipAI/.github/workflows/docker-runner-check.yml
T
DottaandPaperclip 2f90cb31ae ci(runner): retain Intel pack and stop on uncertain cleanup
Archive and verify the complete tested pack before startup checks. Make cleanup uncertainty sticky across test cases, retain scratch after interrupted launches, and enforce the reviewed 16 GiB seven-day artifact storage bound without truncation.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-01 16:21:11 -05:00

604 lines
29 KiB
YAML

name: Docker Runner check
on:
workflow_dispatch:
inputs:
target_branch:
description: "Branch in this repository to build; resolved to one immutable commit before checkout"
type: string
required: false
publish_eval_image:
description: "Publish an immutable Daytona qualification image on the EC2 fleet (no provider credentials)"
type: boolean
default: false
verify_source:
description: "Run broad source checks on GitHub-hosted Ubuntu without building an image"
type: boolean
default: false
source_root_tests_only:
description: "With verify_source, run only the complete pnpm test:run suite (180-minute bound)"
type: boolean
default: false
verify_runner:
description: "Run the complete offline Runner verify command on GitHub-hosted Ubuntu without building an image"
type: boolean
default: false
verify_pi_intel:
description: "Verify frozen Pi 1.0 closed startup and SDK contracts on native Intel macOS (no provider prompts)"
type: boolean
default: false
diagnose_ajv_pack:
description: "Diagnose only pinned AJV npm directory packing on Linux (no Runner build/tests)"
type: boolean
default: false
expected_source_sha:
description: "Require this immutable target commit (required for verify_runner or verify_source)"
type: string
required: false
expected_resolved_lock_sha256:
description: "Require this reviewed resolved dependency lock (required for verify_runner or verify_source)"
type: string
required: false
verify_public_install:
description: "Build and verify clean public npm installation on EC2 (no provider credentials)"
type: boolean
default: false
build_eval_viewer:
description: "Build the canonical eval report viewer on EC2"
type: boolean
default: false
pull_request:
paths:
- .github/workflows/docker-runner-check.yml
- Dockerfile
- .dockerignore
- scripts/check-docker-runner-cache.sh
- packages/paperclip-runner/rust-toolchain.toml
- packages/paperclip-runner/runner/**
- packages/paperclip-runner/protocol/**
permissions: {}
concurrency:
# Publishing a newer image must not discard an earlier verification's evidence.
group: docker-runner-check-${{ github.event.pull_request.number || github.ref }}-${{ inputs.verify_pi_intel && 'pi-native-intel' || inputs.diagnose_ajv_pack && 'ajv-diagnostic' || inputs.verify_runner && 'runner-verification' || inputs.source_root_tests_only && 'source-root-tests' || inputs.verify_source && 'verification' || inputs.verify_public_install && 'public-install' || inputs.build_eval_viewer && 'viewer' || 'image' }}
cancel-in-progress: true
jobs:
runner:
if: github.event_name == 'pull_request'
name: Compile isolated native Runner
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
# Compile the real target, then change source in a disposable context.
# A fresh builder must import dependencies and produce changed binary metadata.
# The baseline build anonymously seeds from the public BuildKit cache at
# ghcr.io/paperclipai/paperclip:buildcache-{amd64,arm64}; the verification
# build imports only this run's locally exported cache on a fresh builder.
# No registry credentials or image publication.
- name: Verify native build and dependency cache reuse
run: bash scripts/check-docker-runner-cache.sh
authorize_manual:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 5
outputs:
target_sha: ${{ steps.authorize.outputs.target_sha }}
steps:
- name: Authorize an explicit maintainer image build
id: authorize
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
REPOSITORY_ID: ${{ github.repository_id }}
ACTOR_ID: ${{ github.actor_id }}
TRIGGERING_ACTOR: ${{ github.triggering_actor }}
ALLOWED_IDS: ${{ vars.AWS_CI_TRUSTED_USER_IDS }}
TARGET_BRANCH: ${{ inputs.target_branch || github.ref_name }}
EXPECTED_SOURCE_SHA: ${{ inputs.expected_source_sha }}
EXPECTED_LOCK_SHA256: ${{ inputs.expected_resolved_lock_sha256 }}
VERIFY_RUNNER: ${{ inputs.verify_runner }}
VERIFY_SOURCE: ${{ inputs.verify_source }}
SOURCE_ROOT_TESTS_ONLY: ${{ inputs.source_root_tests_only }}
IMAGE_MODE: ${{ inputs.publish_eval_image || inputs.verify_public_install || inputs.build_eval_viewer }}
VERIFY_PI_INTEL: ${{ inputs.verify_pi_intel }}
DIAGNOSE_AJV: ${{ inputs.diagnose_ajv_pack }}
OTHER_MODE: ${{ inputs.publish_eval_image || inputs.verify_source || inputs.verify_public_install || inputs.build_eval_viewer || inputs.verify_pi_intel }}
run: |
set -euo pipefail
test "$REPOSITORY" = paperclipai/paperclip
test "$REPOSITORY_ID" = 1170821064
jq -e 'type == "array" and length > 0 and all(.[]; type == "number")' <<< "$ALLOWED_IDS" >/dev/null
triggering_id="$(gh api "users/$TRIGGERING_ACTOR" --jq .id)"
for id in "$ACTOR_ID" "$triggering_id"; do
jq -e --argjson id "$id" 'index($id) != null' <<< "$ALLOWED_IDS" >/dev/null
done
target_sha="$(gh api "repos/$REPOSITORY/git/ref/heads/$TARGET_BRANCH" --jq '.object.sha')"
[[ "$target_sha" =~ ^[0-9a-f]{40}$ ]]
if [ "$SOURCE_ROOT_TESTS_ONLY" = true ]; then
test "$VERIFY_SOURCE" = true
fi
if [ "$VERIFY_RUNNER" = true ] || [ "$VERIFY_SOURCE" = true ]; then
[[ "$EXPECTED_SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$EXPECTED_LOCK_SHA256" =~ ^[0-9a-f]{64}$ ]]
fi
if [ "$VERIFY_SOURCE" = true ]; then
test "$VERIFY_RUNNER" != true
test "$IMAGE_MODE" != true
fi
if [ "$VERIFY_RUNNER" = true ]; then
test "$OTHER_MODE" != true
fi
if [ "$DIAGNOSE_AJV" = true ]; then
test "$VERIFY_RUNNER" != true
test "$OTHER_MODE" != true
test "$EXPECTED_SOURCE_SHA" = 34f49a201a804564cfae81e425266b3f9f987e30
test "$EXPECTED_LOCK_SHA256" = 5ae57d1ddd475691dac1f1cfbd4836e54f7da1956b47e6e705b218ae3070ae2e
fi
if [ "$VERIFY_PI_INTEL" = true ]; then
test "$VERIFY_RUNNER" != true
test "$VERIFY_SOURCE" != true
test "$SOURCE_ROOT_TESTS_ONLY" != true
test "$DIAGNOSE_AJV" != true
test "$IMAGE_MODE" != true
test "$EXPECTED_SOURCE_SHA" = 5eba61ece929dcfb2b0c1761825a2d9e557c2554
test "$EXPECTED_LOCK_SHA256" = 38338a6867358440c5ab5993eaeb85fb501bc7df65d24acf52fbd63c880ee4ba
# Standard hosted runner minutes are free for this public repository.
# Artifact storage and unrelated resource costs are not inferred here.
test "$(gh api "repos/$REPOSITORY" --jq '.visibility')" = public
fi
if [ -n "$EXPECTED_SOURCE_SHA" ]; then
test "$target_sha" = "$EXPECTED_SOURCE_SHA"
fi
echo "target_sha=$target_sha" >> "$GITHUB_OUTPUT"
manual_pi_intel:
name: Frozen Pi 1.0 native Intel startup and SDK contracts
if: github.event_name == 'workflow_dispatch' && inputs.verify_pi_intel
needs: authorize_manual
runs-on: macos-15-intel
timeout-minutes: 60
permissions:
contents: read
steps:
- name: Checkout trusted CI helpers independently of candidate source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.workflow_sha }}
path: trusted-ci
persist-credentials: false
- name: Checkout exact frozen production source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.authorize_manual.outputs.target_sha }}
path: candidate
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24.21.0
architecture: x64
package-manager-cache: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
with:
version: 9.15.4
package_json_file: candidate/package.json
- name: Build frozen pack and run unchanged no-provider contracts once
timeout-minutes: 55
run: |
set -euo pipefail
python3 -B trusted-ci/scripts/ci/pi-intel/verify.py \
--source "$GITHUB_WORKSPACE/candidate" \
--output "$GITHUB_WORKSPACE/pi-intel-evidence"
- name: Admit complete evidence within the reserved storage bound
if: always()
id: pi_intel_evidence
timeout-minutes: 2
run: |
python3 -B trusted-ci/scripts/ci/pi-intel/admit_evidence.py \
--evidence "$GITHUB_WORKSPACE/pi-intel-evidence" \
--github-output "$GITHUB_OUTPUT"
- name: Retain exact input, independent output, test, and cleanup evidence
if: always() && steps.pi_intel_evidence.outputs.admitted == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: pi-native-intel-${{ github.run_id }}
include-hidden-files: true
path: pi-intel-evidence/
retention-days: 7
if-no-files-found: error
manual_runner_verify:
name: Full offline Runner verification on GitHub-hosted Ubuntu
if: github.event_name == 'workflow_dispatch' && inputs.verify_runner
needs: authorize_manual
runs-on: ubuntu-latest
timeout-minutes: 55
permissions:
contents: read
env:
CI: "true"
PAPERCLIP_TELEMETRY_ENABLED: "false"
NPM_CONFIG_AUDIT: "false"
NPM_CONFIG_FUND: "false"
NPM_CONFIG_UPDATE_NOTIFIER: "false"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.authorize_manual.outputs.target_sha }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24.21.0
package-manager-cache: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
with:
version: 9.15.4
- name: Record exact source and install dependencies
timeout-minutes: 8
env:
SOURCE_SHA: ${{ needs.authorize_manual.outputs.target_sha }}
EXPECTED_LOCK_SHA256: ${{ inputs.expected_resolved_lock_sha256 }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
mkdir -p remote-runner-verification
git rev-parse HEAD > remote-runner-verification/source.txt
cp pnpm-lock.yaml remote-runner-verification/original-pnpm-lock.yaml
sha256sum pnpm-lock.yaml > remote-runner-verification/original-lock.sha256
# A stale stacked-branch lock may be resolved only to the reviewed overlay.
pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile
cp pnpm-lock.yaml remote-runner-verification/resolved-pnpm-lock.yaml
sha256sum pnpm-lock.yaml > remote-runner-verification/resolved-lock.sha256
git diff -- pnpm-lock.yaml > remote-runner-verification/lock.diff
printf '%s pnpm-lock.yaml\n' "$EXPECTED_LOCK_SHA256" | sha256sum --check --strict
pnpm install --frozen-lockfile --ignore-scripts
- name: Select pinned Rust and install browser dependencies
timeout-minutes: 15
run: |
set -euo pipefail
cd packages/paperclip-runner
rustup show
rustc --version --verbose > ../../remote-runner-verification/rust.txt
pnpm exec playwright install --with-deps chromium
- name: Run the complete offline Runner verification
timeout-minutes: 35
run: |
set -uo pipefail
status=0
timeout --signal=TERM --kill-after=20s 34m \
pnpm --filter @paperclipai/paperclip-runner verify \
> remote-runner-verification/verify.log 2>&1 || status=$?
printf '%s\n' "$status" > remote-runner-verification/exit-code.txt
tail -n 100 remote-runner-verification/verify.log
exit "$status"
- name: Retain Runner verification evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: runner-full-verification-${{ github.run_id }}
path: |
remote-runner-verification/
packages/paperclip-runner/**/playwright-report/
packages/paperclip-runner/**/test-results/
retention-days: 14
manual_source_verify:
name: Full source verification on GitHub-hosted Ubuntu
if: github.event_name == 'workflow_dispatch' && inputs.verify_source
needs: authorize_manual
runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.source_root_tests_only && 200 || 295 }}
permissions:
contents: read
env:
CI: "true"
PAPERCLIP_TELEMETRY_ENABLED: "false"
NPM_CONFIG_AUDIT: "false"
NPM_CONFIG_FUND: "false"
NPM_CONFIG_UPDATE_NOTIFIER: "false"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.authorize_manual.outputs.target_sha }}
persist-credentials: false
- name: Record immutable source and planned checks
env:
SOURCE_SHA: ${{ needs.authorize_manual.outputs.target_sha }}
EXPECTED_LOCK_SHA256: ${{ inputs.expected_resolved_lock_sha256 }}
SOURCE_ROOT_TESTS_ONLY: ${{ inputs.source_root_tests_only }}
run: |
set -euo pipefail
mkdir -p remote-source-verification
git rev-parse HEAD > remote-source-verification/source.txt
test "$(cat remote-source-verification/source.txt)" = "$SOURCE_SHA"
printf '%s\n' "$EXPECTED_LOCK_SHA256" > remote-source-verification/expected-resolved-lock.sha256
cp pnpm-lock.yaml remote-source-verification/original-pnpm-lock.yaml
sha256sum pnpm-lock.yaml > remote-source-verification/original-lock.sha256
printf '%s\n' \
'pnpm -r typecheck' \
'pnpm test:run' \
'pnpm check:token-gates' \
'pnpm test:e2e:runner:typecheck' \
'pnpm test:e2e:runner:unit' \
'pnpm build' \
'if [ -f scripts/verify-grok-npm-install.mjs ]; then node scripts/verify-grok-npm-install.mjs; fi' \
> remote-source-verification/commands.txt
if [ "$SOURCE_ROOT_TESTS_ONLY" = true ]; then
printf '%s\n' 'pnpm test:run' > remote-source-verification/commands.txt
fi
index=0
while IFS= read -r _check; do
index=$((index + 1))
printf 'not-run\n' > "remote-source-verification/check-$index.status"
: > "remote-source-verification/check-$index.log"
done < remote-source-verification/commands.txt
for phase in resolution lock-check install test-build-deps; do
printf 'not-run\n' > "remote-source-verification/$phase.status"
: > "remote-source-verification/$phase.log"
done
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24.21.0
package-manager-cache: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
with:
version: 9.15.4
- name: Install the exact reviewed dependency graph without lifecycle scripts
timeout-minutes: 10
env:
EXPECTED_LOCK_SHA256: ${{ inputs.expected_resolved_lock_sha256 }}
run: |
set -euo pipefail
node --version > remote-source-verification/node.txt
pnpm --version > remote-source-verification/pnpm.txt
test "$(cat remote-source-verification/node.txt)" = v24.21.0
test "$(cat remote-source-verification/pnpm.txt)" = 9.15.4
phase=resolution
trap 'printf "%s\n" "$?" > "remote-source-verification/$phase.status"' EXIT
pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile > remote-source-verification/resolution.log 2>&1
printf '0\n' > remote-source-verification/resolution.status
cp pnpm-lock.yaml remote-source-verification/resolved-pnpm-lock.yaml
sha256sum pnpm-lock.yaml > remote-source-verification/resolved-lock.sha256
git diff -- pnpm-lock.yaml > remote-source-verification/lock.diff
phase=lock-check
printf '%s pnpm-lock.yaml\n' "$EXPECTED_LOCK_SHA256" | sha256sum --check --strict > remote-source-verification/lock-check.log 2>&1
printf '0\n' > remote-source-verification/lock-check.status
phase=install
pnpm install --frozen-lockfile --ignore-scripts > remote-source-verification/install.log 2>&1
- name: Build workspace prerequisites used by the root tests
timeout-minutes: 5
run: |
set -euo pipefail
# Match test:run:general and test:run:serialized on a clean checkout.
# The SDK exports dist files, which an ignore-scripts install cannot provide.
trap 'printf "%s\n" "$?" > remote-source-verification/test-build-deps.status' EXIT
timeout --verbose --signal=TERM --kill-after=15s 4m \
pnpm --filter @paperclipai/plugin-sdk ensure-build-deps \
> remote-source-verification/test-build-deps.log 2>&1
- name: Run every source check and retain each result
timeout-minutes: ${{ inputs.source_root_tests_only && 182 || 273 }}
run: |
set -uo pipefail
status=0
index=0
progress_pid=
trap 'if [ -n "$progress_pid" ]; then kill "$progress_pid" 2>/dev/null || true; fi' EXIT
while IFS= read -r check; do
index=$((index + 1))
echo "Starting $check"
printf 'running\n' > "remote-source-verification/check-$index.status"
# test:run executes all groups serially. Recorded server durations
# alone exceed 85 minutes; ordinary CI distributes them across shards.
check_timeout=15m
if [ "$check" = 'pnpm test:run' ]; then check_timeout=180m; fi
date -u +%FT%TZ > "remote-source-verification/check-$index.started-at"
started_at=$(date +%s)
(
while sleep 60; do
echo "Still running $check ($(( $(date +%s) - started_at ))s elapsed)"
tail -n 2 "remote-source-verification/check-$index.log"
done
) &
progress_pid=$!
check_status=0
timeout --verbose --signal=TERM --kill-after=15s "$check_timeout" bash -c "$check" > "remote-source-verification/check-$index.log" 2>&1 || check_status=$?
kill "$progress_pid" 2>/dev/null || true
wait "$progress_pid" 2>/dev/null || true
progress_pid=
printf '%s\n' "$(( $(date +%s) - started_at ))" > "remote-source-verification/check-$index.elapsed-seconds"
date -u +%FT%TZ > "remote-source-verification/check-$index.finished-at"
printf '%s\n' "$check_status" > "remote-source-verification/check-$index.status"
printf '%s\t%s\n' "$check_status" "$check" >> remote-source-verification/check-status.tsv
if [ "$check_status" -ne 0 ]; then status=1; fi
echo "Finished $check (exit $check_status)"
done < remote-source-verification/commands.txt
exit "$status"
- name: Capture the final lock state even on resolution failure
if: always()
run: |
set -euo pipefail
if [ -d remote-source-verification ] && [ -f pnpm-lock.yaml ]; then
cp pnpm-lock.yaml remote-source-verification/final-pnpm-lock.yaml
sha256sum pnpm-lock.yaml > remote-source-verification/final-lock.sha256
git diff -- pnpm-lock.yaml > remote-source-verification/final-lock.diff
fi
- name: Retain source verification evidence even on failure
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ inputs.source_root_tests_only && 'source-root-tests' || 'source-full-verification' }}-${{ github.run_id }}
path: remote-source-verification/
retention-days: 14
manual_ajv_pack_diagnostic:
name: Pinned AJV directory-pack diagnostic on Linux
if: github.event_name == 'workflow_dispatch' && inputs.diagnose_ajv_pack
needs: authorize_manual
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.authorize_manual.outputs.target_sha }}
persist-credentials: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.workflow_sha }}
path: .ajv-diagnostic-driver
sparse-checkout: .github/scripts/diagnose-ajv-pack.py
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24.21.0
package-manager-cache: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
with:
version: 9.15.4
- name: Install only exact reviewed dependency graph without scripts
timeout-minutes: 6
env:
EXPECTED_LOCK_SHA256: ${{ inputs.expected_resolved_lock_sha256 }}
run: |
set -euo pipefail
umask 077
mkdir -p ajv-pack-diagnostic "$RUNNER_TEMP/ajv-install-home"
export HOME="$RUNNER_TEMP/ajv-install-home"
export NPM_CONFIG_USERCONFIG="$HOME/user.npmrc"
export NPM_CONFIG_GLOBALCONFIG="$HOME/global.npmrc"
touch "$NPM_CONFIG_USERCONFIG" "$NPM_CONFIG_GLOBALCONFIG"
test "$(git rev-parse HEAD)" = 34f49a201a804564cfae81e425266b3f9f987e30
test "$(node --version)" = v24.21.0
test "$(npm --version)" = 11.19.0
test "$(pnpm --version)" = 9.15.4
git rev-parse HEAD > ajv-pack-diagnostic/source.txt
git -C .ajv-diagnostic-driver rev-parse HEAD > ajv-pack-diagnostic/driver-source.txt
cp pnpm-lock.yaml ajv-pack-diagnostic/original-pnpm-lock.yaml
pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile > ajv-pack-diagnostic/resolution.log 2>&1
cp pnpm-lock.yaml ajv-pack-diagnostic/resolved-pnpm-lock.yaml
printf '%s pnpm-lock.yaml\n' "$EXPECTED_LOCK_SHA256" | sha256sum --check --strict
pnpm install --frozen-lockfile --ignore-scripts > ajv-pack-diagnostic/install.log 2>&1
- name: Compare installed-layout and identical plain-package packing
timeout-minutes: 3
run: python3 .ajv-diagnostic-driver/.github/scripts/diagnose-ajv-pack.py
- name: Retain diagnostic evidence even on npm failure
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ajv-pack-diagnostic-${{ github.run_id }}
path: ajv-pack-diagnostic/
retention-days: 14
manual_image:
name: Build and verify on EC2
if: github.event_name == 'workflow_dispatch' && !inputs.verify_runner && !inputs.verify_source && !inputs.diagnose_ajv_pack && !inputs.verify_pi_intel
needs: authorize_manual
runs-on: runs-on/fleet=paperclip-public-pr-x64/env=public-ci
timeout-minutes: 90
permissions:
contents: read
packages: write
steps:
- name: Authorize an explicit maintainer image build
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
REPOSITORY_ID: ${{ github.repository_id }}
ACTOR_ID: ${{ github.actor_id }}
TRIGGERING_ACTOR: ${{ github.triggering_actor }}
ALLOWED_IDS: ${{ vars.AWS_CI_TRUSTED_USER_IDS }}
run: |
set -euo pipefail
test "$REPOSITORY" = paperclipai/paperclip
test "$REPOSITORY_ID" = 1170821064
jq -e 'type == "array" and length > 0 and all(.[]; type == "number")' <<< "$ALLOWED_IDS" >/dev/null
triggering_id="$(gh api "users/$TRIGGERING_ACTOR" --jq .id)"
for id in "$ACTOR_ID" "$triggering_id"; do
jq -e --argjson id "$id" 'index($id) != null' <<< "$ALLOWED_IDS" >/dev/null
done
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.authorize_manual.outputs.target_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
package-manager-cache: false
- name: Resolve source dependencies without lifecycle scripts
run: |
pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile
mkdir -p remote-verification
sha256sum pnpm-lock.yaml > remote-verification/lock.sha256
git rev-parse HEAD > remote-verification/source.txt
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
- if: inputs.publish_eval_image
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build checksum-verified Grok provider image
if: inputs.publish_eval_image
env:
SOURCE_SHA: ${{ needs.authorize_manual.outputs.target_sha }}
run: |
set -euo pipefail
image="ghcr.io/paperclipai/paperclip-daytona-runner:qualification-${SOURCE_SHA}-${GITHUB_RUN_ID}"
lock_sha="$(sha256sum pnpm-lock.yaml | cut -d ' ' -f 1)"
docker buildx build --platform linux/amd64 \
--file docker/daytona-runner/Dockerfile \
--build-arg "PAPERCLIP_RUNNER_SOURCE_REVISION=$SOURCE_SHA" \
--build-arg "PAPERCLIP_RUNNER_CONTENT_ID=qualification-$SOURCE_SHA" \
--build-arg "PAPERCLIP_RUNNER_LOCK_SHA256=$lock_sha" \
--cache-from type=registry,ref=ghcr.io/paperclipai/paperclip-daytona-runner:e2e-buildcache-amd64 \
--tag "$image" --metadata-file remote-verification/image.json --push .
digest="$(jq -r '."containerimage.digest"' remote-verification/image.json)"
[[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]
immutable="${image%:*}@$digest"
docker logout ghcr.io
docker buildx imagetools inspect "$immutable" >/dev/null
echo "$immutable" > remote-verification/image.txt
echo "Image: $immutable" >> "$GITHUB_STEP_SUMMARY"
- name: Verify clean public npm installation
if: inputs.verify_public_install
timeout-minutes: 35
run: |
set -euo pipefail
test -f scripts/verify-grok-npm-install.mjs || { echo "Selected source does not provide the public-install verifier"; exit 1; }
pnpm install --frozen-lockfile --ignore-scripts > remote-verification/npm-setup.log 2>&1
pnpm build > remote-verification/npm-build.log 2>&1
node scripts/verify-grok-npm-install.mjs > remote-verification/public-npm-install.log 2>&1
- name: Build canonical eval report viewer
if: always() && inputs.build_eval_viewer
run: |
set -euo pipefail
pnpm install --frozen-lockfile --ignore-scripts --filter '@paperclipai/paperclip-runner...'
pnpm --filter @paperclipai/paperclip-runner build:issue-thread > remote-verification/viewer-build.log 2>&1
tar -czf remote-verification/eval-viewer.tar.gz -C packages/paperclip-runner dist-issue-thread
sha256sum remote-verification/eval-viewer.tar.gz > remote-verification/eval-viewer.sha256
- name: Retain source, image and verification evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: grok-remote-verification-${{ github.run_id }}
path: remote-verification/
retention-days: 7