ci(runner): add bounded hosted E2E support recheck

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-01 17:45:19 -05:00
1 parent b58907578e
commit d0e835100d
2 files changed
+235 -14

No files matched your search

+99 -14
View File
@@ -19,6 +19,10 @@ on:
description: "With verify_source, run only the complete pnpm test:run suite (180-minute bound)"
type: boolean
default: false
source_e2e_support_only:
description: "Run only E2E support typecheck, complete unit suite and catalog on the exact reviewed coverage source"
type: boolean
default: false
verify_runner:
description: "Run the complete offline Runner verify command on GitHub-hosted Ubuntu without building an image"
type: boolean
@@ -32,11 +36,11 @@ on:
type: boolean
default: false
expected_source_sha:
description: "Require this immutable target commit (required for verify_runner or verify_source)"
description: "Require this immutable target commit (required for verify_runner, verify_source or source_e2e_support_only)"
type: string
required: false
expected_resolved_lock_sha256:
description: "Require this reviewed resolved dependency lock (required for verify_runner or verify_source)"
description: "Require this reviewed resolved dependency lock (required for verify_runner, verify_source or source_e2e_support_only)"
type: string
required: false
verify_public_install:
@@ -61,7 +65,7 @@ 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' }}
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_e2e_support_only && 'source-e2e-support' || 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:
@@ -113,10 +117,11 @@ jobs:
VERIFY_RUNNER: ${{ inputs.verify_runner }}
VERIFY_SOURCE: ${{ inputs.verify_source }}
SOURCE_ROOT_TESTS_ONLY: ${{ inputs.source_root_tests_only }}
SOURCE_E2E_SUPPORT_ONLY: ${{ inputs.source_e2e_support_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 }}
OTHER_MODE: ${{ inputs.publish_eval_image || inputs.verify_source || inputs.verify_public_install || inputs.build_eval_viewer || inputs.verify_pi_intel || inputs.source_e2e_support_only }}
run: |
set -euo pipefail
test "$REPOSITORY" = paperclipai/paperclip
@@ -131,10 +136,21 @@ jobs:
if [ "$SOURCE_ROOT_TESTS_ONLY" = true ]; then
test "$VERIFY_SOURCE" = true
fi
if [ "$VERIFY_RUNNER" = true ] || [ "$VERIFY_SOURCE" = true ]; then
if [ "$VERIFY_RUNNER" = true ] || [ "$VERIFY_SOURCE" = true ] || [ "$SOURCE_E2E_SUPPORT_ONLY" = true ]; then
[[ "$EXPECTED_SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$EXPECTED_LOCK_SHA256" =~ ^[0-9a-f]{64}$ ]]
fi
if [ "$SOURCE_E2E_SUPPORT_ONLY" = true ]; then
test "$VERIFY_SOURCE" != true
test "$SOURCE_ROOT_TESTS_ONLY" != true
test "$VERIFY_RUNNER" != true
test "$VERIFY_PI_INTEL" != true
test "$DIAGNOSE_AJV" != true
test "$IMAGE_MODE" != true
test "$EXPECTED_SOURCE_SHA" = 5cd6d3d5237e3e15394fcc1c7e754c30a63c5169
test "$EXPECTED_LOCK_SHA256" = 38338a6867358440c5ab5993eaeb85fb501bc7df65d24acf52fbd63c880ee4ba
test "$(gh api "repos/$REPOSITORY" --jq '.visibility')" = public
fi
if [ "$VERIFY_SOURCE" = true ]; then
test "$VERIFY_RUNNER" != true
test "$IMAGE_MODE" != true
@@ -296,11 +312,11 @@ jobs:
retention-days: 14
manual_source_verify:
name: Full source verification on GitHub-hosted Ubuntu
if: github.event_name == 'workflow_dispatch' && inputs.verify_source
name: ${{ inputs.source_e2e_support_only && 'Focused E2E support verification on GitHub-hosted Ubuntu' || 'Full source verification on GitHub-hosted Ubuntu' }}
if: github.event_name == 'workflow_dispatch' && (inputs.verify_source || inputs.source_e2e_support_only)
needs: authorize_manual
runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.source_root_tests_only && 200 || 295 }}
timeout-minutes: ${{ inputs.source_e2e_support_only && 45 || inputs.source_root_tests_only && 200 || 295 }}
permissions:
contents: read
env:
@@ -319,6 +335,7 @@ jobs:
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 }}
SOURCE_E2E_SUPPORT_ONLY: ${{ inputs.source_e2e_support_only }}
run: |
set -euo pipefail
mkdir -p remote-source-verification
@@ -339,6 +356,16 @@ jobs:
if [ "$SOURCE_ROOT_TESTS_ONLY" = true ]; then
printf '%s\n' 'pnpm test:run' > remote-source-verification/commands.txt
fi
if [ "$SOURCE_E2E_SUPPORT_ONLY" = true ]; then
printf '%s pnpm-lock.yaml\n' e11d69fa8702a906c293c1cb307c71df90d3b1f1617b3c23ebf17fa9a87fec79 | sha256sum --check --strict
git ls-tree -r -z HEAD > remote-source-verification/source-tree.zlist
git archive --format=tar HEAD | sha256sum > remote-source-verification/source-archive.sha256
printf '%s\n' \
'pnpm test:e2e:runner:typecheck' \
'pnpm test:e2e:runner:unit' \
'node cli/node_modules/tsx/dist/cli.mjs tests/runner-e2e/launch.ts --list' \
> remote-source-verification/commands.txt
fi
index=0
while IFS= read -r _check; do
index=$((index + 1))
@@ -389,12 +416,15 @@ jobs:
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 }}
timeout-minutes: ${{ inputs.source_e2e_support_only && 28 || inputs.source_root_tests_only && 182 || 273 }}
env:
SOURCE_E2E_SUPPORT_ONLY: ${{ inputs.source_e2e_support_only }}
run: |
set -uo pipefail
status=0
index=0
progress_pid=
checks_started_at=$(date +%s)
trap 'if [ -n "$progress_pid" ]; then kill "$progress_pid" 2>/dev/null || true; fi' EXIT
while IFS= read -r check; do
index=$((index + 1))
@@ -404,6 +434,12 @@ jobs:
# alone exceed 85 minutes; ordinary CI distributes them across shards.
check_timeout=15m
if [ "$check" = 'pnpm test:run' ]; then check_timeout=180m; fi
remaining=900
if [ "$SOURCE_E2E_SUPPORT_ONLY" = true ]; then
# Leave job time for final auditing and complete evidence upload.
remaining=$((1380 - $(date +%s) + checks_started_at))
if [ "$remaining" -lt 900 ] && [ "$remaining" -gt 0 ]; then check_timeout="${remaining}s"; fi
fi
date -u +%FT%TZ > "remote-source-verification/check-$index.started-at"
started_at=$(date +%s)
(
@@ -414,7 +450,12 @@ jobs:
) &
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=$?
if [ "$remaining" -gt 0 ]; then
timeout --verbose --signal=TERM --kill-after=15s "$check_timeout" bash -c "$check" > "remote-source-verification/check-$index.log" 2>&1 || check_status=$?
else
check_status=124
printf 'Not launched: focused command budget exhausted.\n' > "remote-source-verification/check-$index.log"
fi
kill "$progress_pid" 2>/dev/null || true
wait "$progress_pid" 2>/dev/null || true
progress_pid=
@@ -426,6 +467,26 @@ jobs:
echo "Finished $check (exit $check_status)"
done < remote-source-verification/commands.txt
exit "$status"
- name: Audit focused source and lock closure even on check failure
if: always() && inputs.source_e2e_support_only
timeout-minutes: 2
env:
SOURCE_SHA: ${{ needs.authorize_manual.outputs.target_sha }}
EXPECTED_LOCK_SHA256: ${{ inputs.expected_resolved_lock_sha256 }}
run: |
set -uo pipefail
mkdir -p remote-source-verification
status=0
date -u +%FT%TZ > remote-source-verification/final-audit.started-at
git rev-parse HEAD > remote-source-verification/final-source.txt
test "$(cat remote-source-verification/final-source.txt)" = "$SOURCE_SHA" || status=1
git status --porcelain=v1 --untracked-files=no > remote-source-verification/final-source-status.txt
git diff -- . ':(exclude)pnpm-lock.yaml' > remote-source-verification/final-source.diff
git diff --quiet -- . ':(exclude)pnpm-lock.yaml' || status=1
printf '%s pnpm-lock.yaml\n' "$EXPECTED_LOCK_SHA256" | sha256sum --check --strict > remote-source-verification/final-overlay-check.log 2>&1 || status=1
date -u +%FT%TZ > remote-source-verification/final-audit.finished-at
printf '%s\n' "$status" > remote-source-verification/final-audit.status
exit "$status"
- name: Capture the final lock state even on resolution failure
if: always()
run: |
@@ -435,13 +496,37 @@ jobs:
sha256sum pnpm-lock.yaml > remote-source-verification/final-lock.sha256
git diff -- pnpm-lock.yaml > remote-source-verification/final-lock.diff
fi
- name: Admit complete focused evidence within the storage bound
if: always() && inputs.source_e2e_support_only
id: source_e2e_evidence
timeout-minutes: 2
run: |
python3 - <<'PYTHON'
import hashlib, json, os, pathlib
root = pathlib.Path('remote-source-verification')
files, size, limit = [], 0, 256 * 1024 * 1024
for path in sorted(root.rglob('*')):
if path.is_symlink(): raise RuntimeError('Evidence symlink is not allowed')
if path.is_dir(): continue
if not path.is_file(): raise RuntimeError('Nonregular evidence is not allowed')
size += path.stat().st_size
if size > limit: raise RuntimeError('Complete evidence exceeds 256 MiB bound')
digest = hashlib.sha256()
with path.open('rb') as source:
for chunk in iter(lambda: source.read(1024 * 1024), b''): digest.update(chunk)
files.append({'path': str(path.relative_to(root)), 'bytes': path.stat().st_size, 'sha256': digest.hexdigest()})
inventory = json.dumps({'bytes': size, 'files': files}, indent=2) + '\n'
if not files or size + len(inventory.encode()) > limit: raise RuntimeError('Complete evidence exceeds bound or is absent')
(root / 'evidence-inventory.json').write_text(inventory)
with open(os.environ['GITHUB_OUTPUT'], 'a') as output: output.write('admitted=true\n')
PYTHON
- name: Retain source verification evidence even on failure
if: always()
if: always() && (!inputs.source_e2e_support_only || steps.source_e2e_evidence.outputs.admitted == 'true')
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ inputs.source_root_tests_only && 'source-root-tests' || 'source-full-verification' }}-${{ github.run_id }}
name: ${{ inputs.source_e2e_support_only && 'source-e2e-support' || inputs.source_root_tests_only && 'source-root-tests' || 'source-full-verification' }}-${{ github.run_id }}
path: remote-source-verification/
retention-days: 14
retention-days: ${{ inputs.source_e2e_support_only && 7 || 14 }}
manual_ajv_pack_diagnostic:
name: Pinned AJV directory-pack diagnostic on Linux
@@ -506,7 +591,7 @@ jobs:
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
if: github.event_name == 'workflow_dispatch' && !inputs.verify_runner && !inputs.verify_source && !inputs.diagnose_ajv_pack && !inputs.verify_pi_intel && !inputs.source_e2e_support_only
needs: authorize_manual
runs-on: runs-on/fleet=paperclip-public-pr-x64/env=public-ci
timeout-minutes: 90
+136
View File
@@ -0,0 +1,136 @@
"""Offline regressions for the reviewed, exclusive hosted support dispatch."""
import os
from pathlib import Path
import re
import shutil
import sys
import subprocess
import tempfile
import textwrap
import unittest
ROOT = Path(__file__).resolve().parents[2]
WORKFLOW = '.github/workflows/docker-runner-check.yml'
SOURCE = '5cd6d3d5237e3e15394fcc1c7e754c30a63c5169'
LOCK = '38338a6867358440c5ab5993eaeb85fb501bc7df65d24acf52fbd63c880ee4ba'
INTEL_SOURCE = '5eba61ece929dcfb2b0c1761825a2d9e557c2554'
BASELINE = 'b58907578'
def job(text, name):
return re.search(r'^ ' + name + r':\n.*?(?=^ [a-z_]+:\n|\Z)', text, re.M | re.S).group()
def script(block, name):
step = block.split(' - name: ' + name + '\n', 1)[1].split('\n - ', 1)[0]
return textwrap.dedent(step.split(' run: |\n', 1)[1]).rstrip() + '\n'
class SupportMode(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.workflow = (ROOT / WORKFLOW).read_text()
cls.authorize = script(job(cls.workflow, 'authorize_manual'), 'Authorize an explicit maintainer image build')
def admission(self, **changes):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
fake = root / 'gh'
fake.write_text('''#!/usr/bin/env python3
import os, sys
path = sys.argv[2]
if path == 'users/maintainer': print('1')
elif path == 'repos/paperclipai/paperclip/git/ref/heads/coverage': print(os.environ['FAKE_TARGET'])
elif path == 'repos/paperclipai/paperclip': print(os.environ['FAKE_VISIBILITY'])
else: raise RuntimeError('Unexpected offline GitHub request')
''')
fake.chmod(0o755)
env = dict(PATH=str(root) + os.pathsep + str(Path(shutil.which('jq')).parent) + os.pathsep + os.defpath,
REPOSITORY='paperclipai/paperclip', REPOSITORY_ID='1170821064', ACTOR_ID='1',
TRIGGERING_ACTOR='maintainer', ALLOWED_IDS='[1]', TARGET_BRANCH='coverage',
EXPECTED_SOURCE_SHA=SOURCE, EXPECTED_LOCK_SHA256=LOCK, FAKE_TARGET=SOURCE,
FAKE_VISIBILITY='public', VERIFY_RUNNER='false', VERIFY_SOURCE='false',
SOURCE_ROOT_TESTS_ONLY='false', SOURCE_E2E_SUPPORT_ONLY='true', IMAGE_MODE='false',
VERIFY_PI_INTEL='false', DIAGNOSE_AJV='false', OTHER_MODE='true',
GITHUB_OUTPUT=str(root / 'output'))
env.update(changes)
result = subprocess.run(['bash', '-c', self.authorize], env=env, capture_output=True, text=True, timeout=5)
return result.returncode, (root / 'output').read_text() if (root / 'output').exists() else ''
def test_exact_reviewed_source_and_overlay_are_admitted(self):
self.assertEqual(self.admission(), (0, 'target_sha=' + SOURCE + '\n'))
def test_every_other_execution_mode_conflicts(self):
for mode in ('VERIFY_RUNNER', 'VERIFY_SOURCE', 'SOURCE_ROOT_TESTS_ONLY', 'IMAGE_MODE', 'VERIFY_PI_INTEL', 'DIAGNOSE_AJV'):
with self.subTest(mode=mode):
status, output = self.admission(**{mode: 'true'})
self.assertNotEqual(status, 0); self.assertEqual(output, '')
def test_wrong_missing_or_moving_source_and_lock_fail(self):
for changes in ({'EXPECTED_SOURCE_SHA': ''}, {'EXPECTED_SOURCE_SHA': INTEL_SOURCE},
{'FAKE_TARGET': INTEL_SOURCE}, {'EXPECTED_LOCK_SHA256': ''},
{'EXPECTED_LOCK_SHA256': 'a' * 64}, {'FAKE_VISIBILITY': 'private'}, {'ACTOR_ID': '2'}):
with self.subTest(changes=changes):
status, output = self.admission(**changes)
self.assertNotEqual(status, 0); self.assertEqual(output, '')
def test_original_intel_admission_and_entire_job_are_unchanged(self):
self.assertEqual(self.admission(SOURCE_E2E_SUPPORT_ONLY='false', VERIFY_PI_INTEL='true',
EXPECTED_SOURCE_SHA=INTEL_SOURCE, FAKE_TARGET=INTEL_SOURCE),
(0, 'target_sha=' + INTEL_SOURCE + '\n'))
baseline = subprocess.check_output(['git', 'show', BASELINE + ':' + WORKFLOW], cwd=ROOT, text=True)
self.assertEqual(job(self.workflow, 'manual_pi_intel'), job(baseline, 'manual_pi_intel'))
for name in ('manual_runner_verify', 'manual_ajv_pack_diagnostic'):
self.assertEqual(job(self.workflow, name), job(baseline, name))
def test_focused_commands_are_complete_support_only_and_image_is_excluded(self):
source = job(self.workflow, 'manual_source_verify')
plan = script(source, 'Record immutable source and planned checks')
focused = plan.split('if [ "$SOURCE_E2E_SUPPORT_ONLY" = true ]; then', 1)[1].split('\nfi', 1)[0]
self.assertEqual(re.findall(r"^ '([^']+)'", focused, re.M), [
'pnpm test:e2e:runner:typecheck', 'pnpm test:e2e:runner:unit',
'node cli/node_modules/tsx/dist/cli.mjs tests/runner-e2e/launch.ts --list'])
self.assertIn('!inputs.source_e2e_support_only', job(self.workflow, 'manual_image'))
self.assertIn("inputs.source_e2e_support_only && 'source-e2e-support'", self.workflow)
self.assertNotIn('packages: write', source)
self.assertNotIn('secrets.', source)
self.assertIn('persist-credentials: false', source)
self.assertIn('1380 - $(date +%s) + checks_started_at', source)
self.assertIn('check_timeout=15m', source)
self.assertIn('inputs.source_e2e_support_only && 45', source)
self.assertIn('256 * 1024 * 1024', source)
self.assertIn('inputs.source_e2e_support_only && 7 || 14', source)
def test_complete_failure_evidence_is_admitted_and_unsafe_or_oversize_is_not(self):
source = script(job(self.workflow, 'manual_source_verify'), 'Admit complete focused evidence within the storage bound')
admission = source.split("<<'PYTHON'\n", 1)[1].rsplit('PYTHON', 1)[0]
for scenario in ('failed-check', 'oversize', 'symlink', 'absent'):
with self.subTest(scenario=scenario), tempfile.TemporaryDirectory() as temp:
root = Path(temp); evidence = root / 'remote-source-verification'; evidence.mkdir()
if scenario == 'failed-check':
(evidence / 'check-1.log').write_text('real failure output\n')
(evidence / 'check-1.status').write_text('1\n')
elif scenario == 'oversize':
with (evidence / 'large.log').open('wb') as output: output.truncate(256 * 1024 * 1024 + 1)
elif scenario == 'symlink': (evidence / 'link').symlink_to(root / 'outside')
output = root / 'output'
result = subprocess.run([sys.executable, '-c', admission], cwd=root,
env={'GITHUB_OUTPUT': str(output)}, capture_output=True, timeout=5)
if scenario == 'failed-check':
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(output.read_text(), 'admitted=true\n')
self.assertIn('real failure output', (evidence / 'check-1.log').read_text())
self.assertTrue((evidence / 'evidence-inventory.json').is_file())
else:
self.assertNotEqual(result.returncode, 0)
self.assertFalse(output.exists())
def test_all_embedded_shell_scripts_parse(self):
for number, match in enumerate(re.finditer(r'^ run: \|\n((?: .*\n|\n)+)', self.workflow, re.M)):
# The heredoc retains its Python as data for bash syntax validation.
with self.subTest(block=number):
subprocess.run(['bash', '-n'], input=textwrap.dedent(match.group(1)), text=True, check=True)
if __name__ == '__main__':
unittest.main()