f85f606c5a
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The PR pipeline runs a "Canary Dry Run" step that validates dependency PRs against a regenerated `pnpm-lock.yaml` before merge > - When a dependabot (or other) PR's regenerated lockfile is byte-identical to `HEAD`, `git commit` exits non-zero with "nothing to commit, working tree clean" > - The surrounding `bash -e` step propagates the non-zero exit, failing the Canary Dry Run for PRs that should be allowed through > - This pull request guards the commit with `git diff --cached --quiet` so the step only commits when there is a real staged change > - The benefit is that dependabot PRs (#7571 storybook bump, #7572) and any future PR where regen happens to match HEAD stop getting spuriously blocked by the canary step ## Linked Issues or Issue Description Refs #7571 Refs #7572 PR #7571 (dependabot: storybook 10.3.5 → 10.4.2) and PR #7572 both fail on the exact same line of the `Canary Dry Run` step: ``` + git -c user.email=ci@paperclip.local -c user.name=CI commit --no-verify -m 'ci(canary): stage regenerated lockfile' On branch master nothing to commit, working tree clean Error: Process completed with exit code 1. ``` The step regenerates `pnpm-lock.yaml`, stages it, and unconditionally commits. When the regenerated lockfile matches `HEAD` exactly (which happens for some dependabot bumps where the lockfile resolution did not actually change), `git commit` exits 1 and `bash -e` fails the entire step. ## What Changed - `.github/workflows/pr.yml`: Wrap the `git commit` inside the Canary Dry Run step in `if ! git diff --cached --quiet; then ... fi`, so the commit is skipped when there is no staged diff. ## Verification - Reproduced the failure on PR #7571 and PR #7572 (identical stack trace at the `git commit` line of the Canary Dry Run step). - Local sanity check: `git diff --cached --quiet` returns 0 (no diff) when the regen is a no-op and non-zero when there is a real change, which matches the intended branching. - Once merged, dependabot PRs that previously stalled on this step should re-run the Canary Dry Run cleanly. ## Risks - Low risk: the change only adds a guard before an existing `git commit`. The `else` branch (`git checkout -- pnpm-lock.yaml` when no artifact lockfile was used) is unchanged. No behavior change when there *is* a regen diff. ## Model Used - Claude (Anthropic) — `claude-opus-4-7`, extended thinking off, tool use enabled. ## 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 searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have run tests locally and they pass (workflow YAML change; verified via shell sanity check of the guard condition) - [ ] I have added or updated tests where applicable (no test harness for workflow YAML) - [x] If this change affects the UI, I have included before/after screenshots (n/a — CI workflow only) - [x] I have updated relevant documentation to reflect my changes (n/a) - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
397 lines
12 KiB
YAML
397 lines
12 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
|
|
outputs:
|
|
lockfile_regenerated: ${{ steps.regen_lockfile.outputs.regenerated }}
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v6
|
|
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@v6
|
|
with:
|
|
version: 9.15.4
|
|
run_install: false
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
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
|
|
id: regen_lockfile
|
|
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
|
|
echo "regenerated=1" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "regenerated=0" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# Manifest-only PRs (where pnpm-lock.yaml stays at base because the policy
|
|
# job above blocks committing it) need the regenerated lockfile for the
|
|
# downstream `pnpm install --frozen-lockfile` steps. Upload it here so
|
|
# every job consumes the same hash without recomputing.
|
|
- name: Upload regenerated lockfile for downstream jobs
|
|
if: steps.regen_lockfile.outputs.regenerated == '1'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: pr-lockfile
|
|
path: pnpm-lock.yaml
|
|
retention-days: 1
|
|
if-no-files-found: error
|
|
|
|
typecheck_release_registry:
|
|
name: Typecheck + Release Registry
|
|
needs: [policy]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Restore regenerated PR lockfile (if policy uploaded one)
|
|
uses: actions/download-artifact@v4
|
|
continue-on-error: true
|
|
with:
|
|
name: pr-lockfile
|
|
path: .
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
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@v6
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Restore regenerated PR lockfile (if policy uploaded one)
|
|
uses: actions/download-artifact@v4
|
|
continue-on-error: true
|
|
with:
|
|
name: pr-lockfile
|
|
path: .
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
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@v6
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Restore regenerated PR lockfile (if policy uploaded one)
|
|
uses: actions/download-artifact@v4
|
|
continue-on-error: true
|
|
with:
|
|
name: pr-lockfile
|
|
path: .
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
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@v6
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Restore regenerated PR lockfile (if policy uploaded one)
|
|
uses: actions/download-artifact@v4
|
|
continue-on-error: true
|
|
with:
|
|
name: pr-lockfile
|
|
path: .
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
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@v6
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Restore regenerated PR lockfile (if policy uploaded one)
|
|
uses: actions/download-artifact@v4
|
|
continue-on-error: true
|
|
with:
|
|
name: pr-lockfile
|
|
path: .
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
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. release.sh
|
|
# also requires a clean working tree, so any in-place lockfile churn
|
|
# from `pnpm install --frozen-lockfile` must be reverted first — unless
|
|
# the policy job uploaded a regenerated lockfile (manifest-changing
|
|
# PRs), in which case we stage the artifact-restored copy into an
|
|
# ephemeral local commit so release.sh sees a clean tree and its
|
|
# workspace build sees a lockfile that matches the manifest.
|
|
- name: Release canary dry run via release.sh internal build
|
|
env:
|
|
USED_ARTIFACT_LOCKFILE: ${{ needs.policy.outputs.lockfile_regenerated || '0' }}
|
|
run: |
|
|
git checkout -B master HEAD
|
|
if [ "$USED_ARTIFACT_LOCKFILE" = "1" ]; then
|
|
git add pnpm-lock.yaml
|
|
if ! git diff --cached --quiet; then
|
|
git -c user.email=ci@paperclip.local -c user.name=CI \
|
|
commit --no-verify -m "ci(canary): stage regenerated lockfile"
|
|
fi
|
|
else
|
|
git checkout -- pnpm-lock.yaml
|
|
fi
|
|
./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@v6
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9.15.4
|
|
|
|
- name: Restore regenerated PR lockfile (if policy uploaded one)
|
|
uses: actions/download-artifact@v4
|
|
continue-on-error: true
|
|
with:
|
|
name: pr-lockfile
|
|
path: .
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
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@v7
|
|
if: always()
|
|
with:
|
|
name: playwright-report
|
|
path: |
|
|
tests/e2e/playwright-report/
|
|
tests/e2e/test-results/
|
|
retention-days: 14
|