paperclip/.github/workflows/pr.yml
Devin Foley c369d3d357
fix: exempt Dependabot PRs from manual-lockfile block and quality gates (#7457)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies, including
how we ship the public `paperclip` repo itself
> - The `PR` and `commitperclip PR Review` workflows are the CI gating
layer that decides whether any pull request — human or bot — can be
merged to `master`
> - Dependabot opens dependency PRs that always carry a `pnpm-lock.yaml`
diff and an auto-generated PR body, but our `policy` job hard-fails any
non-`chore/refresh-lockfile` lockfile change, and our `commitperclip`
quality gate requires a Thinking-Path / What-Changed / Verification /
Risks / Model template Dependabot can't produce
> - Because `policy` fails first, every downstream lane (`Build`,
`Typecheck + Release Registry`, `General tests`, `Verify serialized
server`, `Canary Dry Run`, `e2e`, and the required `verify` check) skips
and `verify` fails — so we never see whether the upgrade is actually
safe
> - Socket.dev (PR Alerts + Project Report) and Snyk already run on
every dependency PR and are the supply-chain compensating control
against malicious upgrades; the missing piece is just letting our own
build/test signal run so a human can merge with confidence
> - This pull request adds a narrow Dependabot bypass to the two gates
that block on lockfile diffs and PR-template prose, while leaving every
other policy and security check active
> - The benefit is that Dependabot PRs like #7331 will now run the full
PR matrix, giving reviewers real evidence to approve or reject — without
weakening any check that targets supply-chain or build-correctness risk

## What Changed

- `.github/workflows/pr.yml` — extended the existing
`chore/refresh-lockfile` bypass on the `policy` job's "Block manual
lockfile edits" step to also skip when `github.actor ==
'dependabot[bot]'`. Every other policy step (Dockerfile deps stage
validation, `no-git-push` enforcement, release-package map check,
release bootstrap, manifest-driven `pnpm install --lockfile-only`
resolution) keeps running on Dependabot PRs.
- `.github/workflows/commitperclip-review.yml` — gated the `Run quality
gates` step and the dependent `Fail if quality gates failed` step on
`github.event.pull_request.user.login != 'dependabot[bot]'`. `Run
security gates` (`check-pr-security.mjs`) stays unconditional so
supply-chain visibility into Dependabot lockfile churn is preserved.

No changes to `.github/scripts/*.mjs` — keeping the bypass at the
workflow level avoids churning unit-tested code.

## Verification

- CI on this PR: `policy` should pass and the downstream lanes (`Build`,
`Typecheck + Release Registry`, `General tests`, `Verify serialized
server`, `Canary Dry Run`, `e2e`, `verify`) should all run normally
(this PR isn't from Dependabot, so the bypass condition is false —
proves we didn't accidentally widen the exemption).
- After merge, ask Dependabot to rebase #7331 (`@dependabot rebase`) and
confirm:
- `PR / policy` → `success` (lockfile step now `skipped`, other policy
steps `success`)
- `PR / Build`, `PR / Typecheck + Release Registry`, `PR / General tests
(server|workspaces-a|workspaces-b)`, `PR / Verify serialized server
(1/4..4/4)`, `PR / Canary Dry Run`, `PR / e2e` → all execute (none
`skipped`)
  - `PR / verify` → `success` once the matrix passes
- `commitperclip PR Review / review` → `success` (quality-gates steps
`skipped` for Dependabot; security gates ran)
  - Socket and Snyk checks unchanged
- Local sanity-check: `git diff origin/master..HEAD` shows only the two
workflow files, 7 added / 2 removed lines.

## Risks

- **Auto-merging a poisoned dep.** Mitigated by Socket.dev + Snyk +
human merge approval. This change only affects CI gating, not who clicks
"Merge".
- **Spoofing `github.actor` as `dependabot[bot]`.** GitHub sets
`github.actor` from the push actor; spoofing requires a compromised
Dependabot install token, which is the same threat model that already
lets an attacker push anything to a Dependabot-controlled branch — not a
new risk surface.
- **Policy "Validate dependency resolution when manifests change" step
running `pnpm install --lockfile-only --no-frozen-lockfile` on a
Dependabot lockfile.** That step intentionally uses `--lockfile-only`,
so it only verifies the manifest resolves and does not push or commit
the result. Existing behavior is unchanged.
- Low overall: the diff is two workflow-level `if:` conditions in steps
that already had bypasses.

## Model Used

- Provider: Anthropic Claude (via Claude Code in the Paperclip executor)
- Model ID: claude-opus-4-7
- Context window: 200K
- Reasoning mode: standard tool-use; no extended thinking required for
this change
- Capabilities used: file edit, bash, GraphQL/REST API calls
- Plan was drafted, approved by board, and split into child issues
before implementation; see
[PAPA-490](https://paperclip.ing/PAPA/issues/PAPA-490) for the planning
thread.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass (this change is
workflow-only — no code under test; lint via `yamllint` clean)
- [x] I have added or updated tests where applicable (workflow gating;
no script changes, no unit-testable surface)
- [x] If this change affects the UI, I have included before/after
screenshots (no UI changes)
- [x] I have updated relevant documentation to reflect my changes (no
docs reference these gates)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-06-03 16:10:03 -07:00

319 lines
9.1 KiB
YAML

name: PR
on:
pull_request:
branches:
- master
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
policy:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Block manual lockfile edits
if: >-
github.head_ref != 'chore/refresh-lockfile' &&
github.event.pull_request.user.login != 'dependabot[bot]'
run: |
# Diff the PR branch against its merge base so recent base-branch commits
# do not masquerade as changes made by the PR itself.
changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}")"
if printf '%s\n' "$changed" | grep -qx 'pnpm-lock.yaml'; then
echo "Do not commit pnpm-lock.yaml in pull requests. CI owns lockfile updates."
exit 1
fi
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
- name: Validate Dockerfile deps stage
run: node ./scripts/check-docker-deps-stage.mjs
- name: Reject git push in adapter/runtime code
run: node ./scripts/check-no-git-push.mjs
- name: Test no-git-push check
run: node --test ./scripts/check-no-git-push.test.mjs
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
- name: Verify release package bootstrap for changed manifests
run: |
mapfile -t changed_paths < <(git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}")
PAPERCLIP_RELEASE_BOOTSTRAP_BASE_SHA="${{ github.event.pull_request.base.sha }}" \
node ./scripts/check-release-package-bootstrap.mjs "${changed_paths[@]}"
- name: Validate dependency resolution when manifests change
run: |
changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}")"
manifest_pattern='(^|/)package\.json$|^pnpm-workspace\.yaml$|^\.npmrc$|^pnpmfile\.(cjs|js|mjs)$'
if printf '%s\n' "$changed" | grep -Eq "$manifest_pattern"; then
pnpm install --lockfile-only --ignore-scripts --no-frozen-lockfile
fi
typecheck_release_registry:
name: Typecheck + Release Registry
needs: [policy]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Typecheck workspaces whose build scripts skip TypeScript
run: pnpm run typecheck:build-gaps
- name: Verify release registry test coverage
run: pnpm run test:release-registry
general_tests:
name: General tests (${{ matrix.group_label }})
needs: [policy]
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- group: general-server
group_label: server
- group: general-workspaces-a
group_label: workspaces-a
- group: general-workspaces-b
group_label: workspaces-b
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run grouped general test suites
run: pnpm test:run:general -- --group '${{ matrix.group }}'
verify:
# Preserve the legacy required-check name while the underlying work runs in parallel.
name: verify
if: ${{ always() }}
needs: [typecheck_release_registry, general_tests, build]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Fail if any split verify lane failed
env:
TYPECHECK_RELEASE_REGISTRY_RESULT: ${{ needs.typecheck_release_registry.result }}
GENERAL_TESTS_RESULT: ${{ needs.general_tests.result }}
BUILD_RESULT: ${{ needs.build.result }}
run: |
test "$TYPECHECK_RELEASE_REGISTRY_RESULT" = "success"
test "$GENERAL_TESTS_RESULT" = "success"
test "$BUILD_RESULT" = "success"
build:
name: Build
needs: [policy]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
verify_serialized_server:
name: Verify serialized server suites (${{ matrix.shard_label }})
needs: [policy]
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- shard_index: 0
shard_count: 4
shard_label: 1/4
- shard_index: 1
shard_count: 4
shard_label: 2/4
- shard_index: 2
shard_count: 4
shard_label: 3/4
- shard_index: 3
shard_count: 4
shard_label: 4/4
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run serialized server test shard
run: pnpm test:run:serialized -- --shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }}
canary_dry_run:
name: Canary Dry Run
needs: [policy]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
# `release.sh` always executes its Step 2/7 workspace build, even when
# `--skip-verify` bypasses the initial verification gate.
- name: Release canary dry run via release.sh internal build
run: |
git checkout -B master HEAD
git checkout -- pnpm-lock.yaml
./scripts/release.sh canary --skip-verify --dry-run
e2e:
needs: [policy]
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Verify runner Chrome
# GitHub's Ubuntu runner image already ships Google Chrome, so use that
# directly for the headless e2e lane instead of downloading Playwright
# browser bundles inside the 30 minute job budget.
run: google-chrome --version
- name: Generate Paperclip config
run: |
mkdir -p ~/.paperclip/instances/default
cat > ~/.paperclip/instances/default/config.json << 'CONF'
{
"$meta": { "version": 1, "updatedAt": "2026-01-01T00:00:00.000Z", "source": "onboard" },
"database": { "mode": "embedded-postgres" },
"logging": { "mode": "file" },
"server": { "deploymentMode": "local_trusted", "host": "127.0.0.1", "port": 3100 },
"auth": { "baseUrlMode": "auto" },
"storage": { "provider": "local_disk" },
"secrets": { "provider": "local_encrypted", "strictMode": false }
}
CONF
- name: Run e2e tests
env:
PAPERCLIP_E2E_SKIP_LLM: "true"
PAPERCLIP_PLAYWRIGHT_CHANNEL: "chrome"
run: pnpm run test:e2e
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: |
tests/e2e/playwright-report/
tests/e2e/test-results/
retention-days: 14