Files
PaperClipAI/.github/workflows/docker-runner-check.yml
T

386 lines
18 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 EC2"
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
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)"
type: string
required: false
expected_resolved_lock_sha256:
description: "Require this reviewed resolved dependency lock (required for verify_runner)"
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.diagnose_ajv_pack && 'ajv-diagnostic' || inputs.verify_runner && 'runner-verification' || 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 }}
DIAGNOSE_AJV: ${{ inputs.diagnose_ajv_pack }}
OTHER_MODE: ${{ inputs.publish_eval_image || inputs.verify_source || inputs.verify_public_install || inputs.build_eval_viewer }}
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 [ "$VERIFY_RUNNER" = true ]; then
[[ "$EXPECTED_SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$EXPECTED_LOCK_SHA256" =~ ^[0-9a-f]{64}$ ]]
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 [ -n "$EXPECTED_SOURCE_SHA" ]; then
test "$target_sha" = "$EXPECTED_SOURCE_SHA"
fi
echo "target_sha=$target_sha" >> "$GITHUB_OUTPUT"
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: 8
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_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.diagnose_ajv_pack
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 repository on EC2
if: inputs.verify_source
# Leave time for artifact retention before the fleet's one-hour lifetime.
timeout-minutes: 38
run: |
set -uo pipefail
pnpm install --frozen-lockfile --ignore-scripts
status=0
for check in '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'; do
label="$(echo "$check" | tr -cs 'a-zA-Z0-9' '-')"
echo "Starting $check"
check_status=0
timeout --signal=TERM --kill-after=15s 15m bash -c "$check" > "remote-verification/$label.log" 2>&1 || check_status=$?
printf '%s\t%s\n' "$check_status" "$check" >> remote-verification/check-status.tsv
if [ "$check_status" -ne 0 ]; then status=1; fi
echo "Finished $check (exit $check_status)"
done
exit "$status"
- name: Verify clean public npm installation
if: inputs.verify_public_install && !inputs.verify_source
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.verify_source || 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