mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-08 00:54:38 +02:00
ci: isolate pinned AJV npm packaging diagnosis
This commit is contained in:
1 parent
1cfb366607
commit
56d4ece67f
2 files changed
+203
-2
No files matched your search
@@ -0,0 +1,129 @@
|
||||
"""Two bounded, credential-free npm directory-pack probes; never a verify substitute."""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import resource
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
SOURCE = "34f49a201a804564cfae81e425266b3f9f987e30"
|
||||
LOCK = "5ae57d1ddd475691dac1f1cfbd4836e54f7da1956b47e6e705b218ae3070ae2e"
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def inventory(root):
|
||||
rows = {}
|
||||
for path in sorted(root.rglob("*")):
|
||||
assert not path.is_symlink(), "Unexpected AJV package symlink"
|
||||
rows[str(path.relative_to(root))] = {
|
||||
"mode": path.stat().st_mode & 0o777,
|
||||
**({"sha256": digest(path)} if path.is_file() else {"directory": True}),
|
||||
}
|
||||
return rows
|
||||
|
||||
|
||||
def bound_output_files():
|
||||
resource.setrlimit(resource.RLIMIT_FSIZE, (16 * 1024 * 1024, 16 * 1024 * 1024))
|
||||
|
||||
|
||||
def run_probe(node, npm, package, root, evidence):
|
||||
root.mkdir()
|
||||
for name in ("home", "cache", "tmp", "artifacts"):
|
||||
(root / name).mkdir()
|
||||
for name in ("user.npmrc", "global.npmrc"):
|
||||
(root / name).write_text("")
|
||||
env = {
|
||||
"PATH": str(node.parent) + ":/usr/bin:/bin", "CI": "true",
|
||||
"HOME": str(root / "home"), "TMPDIR": str(root / "tmp"),
|
||||
"NPM_CONFIG_USERCONFIG": str(root / "user.npmrc"),
|
||||
"NPM_CONFIG_GLOBALCONFIG": str(root / "global.npmrc"),
|
||||
"NPM_CONFIG_CACHE": str(root / "cache"),
|
||||
"NPM_CONFIG_UPDATE_NOTIFIER": "false", "NPM_CONFIG_AUDIT": "false",
|
||||
"NPM_CONFIG_FUND": "false",
|
||||
}
|
||||
argv = [str(node), str(npm), "pack", str(package), "--ignore-scripts",
|
||||
"--pack-destination", str(root / "artifacts"), "--loglevel=verbose"]
|
||||
started = time.monotonic()
|
||||
with (evidence / (root.name + ".stdout.log")).open("wb") as stdout, \
|
||||
(evidence / (root.name + ".stderr.log")).open("wb") as stderr:
|
||||
process = subprocess.Popen(argv, cwd=root / "artifacts", env=env,
|
||||
stdout=stdout, stderr=stderr, start_new_session=True,
|
||||
preexec_fn=bound_output_files)
|
||||
timed_out = False
|
||||
try:
|
||||
process.wait(timeout=60)
|
||||
except subprocess.TimeoutExpired:
|
||||
timed_out = True
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
process.wait(timeout=5)
|
||||
# The clean environment has no tokens/config credentials. Do not dump ambient env.
|
||||
for label, folder in (("npm-logs", root / "cache/_logs"), ("tarballs", root / "artifacts")):
|
||||
target = evidence / (root.name + "-" + label)
|
||||
target.mkdir()
|
||||
if folder.exists():
|
||||
files = list(folder.iterdir())
|
||||
assert len(files) <= 32 and sum(p.stat().st_size for p in files) <= 16 * 1024 * 1024
|
||||
for path in files:
|
||||
assert path.is_file() and not path.is_symlink()
|
||||
shutil.copyfile(path, target / path.name)
|
||||
return {"argv": argv, "cwd": str(root / "artifacts"), "pid": process.pid,
|
||||
"exitCode": process.returncode, "timedOut": timed_out,
|
||||
"elapsedSeconds": time.monotonic() - started, "processReaped": True,
|
||||
"environment": env}
|
||||
|
||||
|
||||
def main():
|
||||
os.umask(0o077)
|
||||
repo = Path.cwd().resolve()
|
||||
evidence = repo / "ajv-pack-diagnostic"
|
||||
evidence.mkdir(exist_ok=True)
|
||||
node = Path(shutil.which("node")).resolve()
|
||||
npm = Path(shutil.which("npm")).resolve()
|
||||
assert sys.platform == "linux" and os.uname().machine == "x86_64"
|
||||
assert subprocess.check_output(["git", "rev-parse", "HEAD"], text=True, timeout=10).strip() == SOURCE
|
||||
assert digest(repo / "pnpm-lock.yaml") == LOCK
|
||||
assert subprocess.check_output([node, "--version"], text=True, timeout=10).strip() == "v24.21.0"
|
||||
assert json.loads((npm.parent.parent / "package.json").read_text())["version"] == "11.19.0"
|
||||
source = (repo / "node_modules/.pnpm/ajv@8.20.0/node_modules/ajv").resolve()
|
||||
assert source.is_relative_to(repo / "node_modules/.pnpm")
|
||||
assert json.loads((source / "package.json").read_text())["version"] == "8.20.0"
|
||||
before = inventory(source)
|
||||
(evidence / "ajv-input-inventory.json").write_text(json.dumps(before, indent=2) + "\n")
|
||||
work = Path(tempfile.mkdtemp(prefix="pc-ajv-pack-"))
|
||||
receipt = {"source": SOURCE, "lockSha256": LOCK, "platform": os.uname().sysname,
|
||||
"architecture": os.uname().machine, "node": {"version": "24.21.0", "path": str(node), "sha256": digest(node)},
|
||||
"npm": {"version": "11.19.0", "path": str(npm), "sha256": digest(npm),
|
||||
"manifestSha256": digest(npm.parent.parent / "package.json")},
|
||||
"driverSha256": digest(Path(__file__)), "workRoot": str(work), "results": []}
|
||||
try:
|
||||
plain = work / "plain-ajv"
|
||||
shutil.copytree(source, plain)
|
||||
assert inventory(plain) == before
|
||||
for label, package in (("pnpm-layout", source), ("plain-copy", plain)):
|
||||
receipt["results"].append(run_probe(node, npm, package, work / label, evidence))
|
||||
assert inventory(source) == before
|
||||
receipt["sourceUnchanged"] = True
|
||||
finally:
|
||||
shutil.rmtree(work)
|
||||
receipt["privateRootRemoved"] = not work.exists()
|
||||
(evidence / "receipt.json").write_text(json.dumps(receipt, indent=2) + "\n")
|
||||
hashes = {str(p.relative_to(evidence)): digest(p) for p in sorted(evidence.rglob("*")) if p.is_file()}
|
||||
(evidence / "artifact-hashes.json").write_text(json.dumps(hashes, indent=2) + "\n")
|
||||
# Preserve failed probes as failures, while still collecting the comparison.
|
||||
return 0 if all(row["exitCode"] == 0 for row in receipt["results"]) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -19,6 +19,10 @@ on:
|
||||
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
|
||||
@@ -49,7 +53,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_runner && 'runner-verification' || 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.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:
|
||||
@@ -99,6 +103,7 @@ jobs:
|
||||
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
|
||||
@@ -116,6 +121,12 @@ jobs:
|
||||
[[ "$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
|
||||
@@ -196,9 +207,70 @@ jobs:
|
||||
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
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user