fix: allow agents to clear orphaned execution locks #18406
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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@v7 | |
| 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@v7 | |
| 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: Test general-server shard partition | |
| run: node --test ./scripts/__tests__/run-vitest-stable-shard.test.mjs | |
| - name: Test e2e shard partition | |
| run: node --test ./scripts/__tests__/e2e-shard.test.mjs | |
| - name: Test release verify workflow wiring | |
| run: node --test ./scripts/__tests__/release-verify-workflow.test.mjs | |
| - name: Test standalone package build concurrency | |
| run: node --test ./scripts/__tests__/build-standalone-concurrency.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)$|^patches/' | |
| 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@v7 | |
| 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@v7 | |
| - 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@v8 | |
| continue-on-error: true | |
| with: | |
| name: pr-lockfile | |
| path: . | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| 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: | |
| # The server suite is pinned to maxWorkers=1 (server/vitest.config.ts), | |
| # so it can only be parallelized across runners. Shard it to keep this | |
| # lane off the PR critical path. Five shards because the suite has | |
| # grown to ~946s of serial vitest wall time (run 30930345729, | |
| # 2026-08-04): at four shards the worst shard ran 311s and was the | |
| # slowest check in the whole PR run; five brings each shard to ~196s | |
| # of suite time (~240s job), level with the other ~250-300s lanes. | |
| - group: general-server | |
| group_label: server (1/5) | |
| shard_index: 0 | |
| shard_count: 5 | |
| - group: general-server | |
| group_label: server (2/5) | |
| shard_index: 1 | |
| shard_count: 5 | |
| - group: general-server | |
| group_label: server (3/5) | |
| shard_index: 2 | |
| shard_count: 5 | |
| - group: general-server | |
| group_label: server (4/5) | |
| shard_index: 3 | |
| shard_count: 5 | |
| - group: general-server | |
| group_label: server (5/5) | |
| shard_index: 4 | |
| shard_count: 5 | |
| - group: general-workspaces-a | |
| group_label: workspaces-a | |
| - group: general-workspaces-b | |
| group_label: workspaces-b | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - 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@v8 | |
| continue-on-error: true | |
| with: | |
| name: pr-lockfile | |
| path: . | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Run grouped general test suites | |
| run: | | |
| if [ -n "${{ matrix.shard_count }}" ]; then | |
| pnpm test:run:general -- --group '${{ matrix.group }}' \ | |
| --shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }} | |
| else | |
| pnpm test:run:general -- --group '${{ matrix.group }}' | |
| fi | |
| 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@v7 | |
| - 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@v8 | |
| continue-on-error: true | |
| with: | |
| name: pr-lockfile | |
| path: . | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| 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: | |
| # A successful PR run on 2026-08-04 (30876682788) spent 256s in | |
| # serialized shard 2/4, making its 305s job the run's slowest check. | |
| # Five shards reduce the measured 739s suite total to about 148s per | |
| # runner before setup overhead. | |
| - shard_index: 0 | |
| shard_count: 5 | |
| shard_label: 1/5 | |
| - shard_index: 1 | |
| shard_count: 5 | |
| shard_label: 2/5 | |
| - shard_index: 2 | |
| shard_count: 5 | |
| shard_label: 3/5 | |
| - shard_index: 3 | |
| shard_count: 5 | |
| shard_label: 4/5 | |
| - shard_index: 4 | |
| shard_count: 5 | |
| shard_label: 5/5 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - 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@v8 | |
| continue-on-error: true | |
| with: | |
| name: pr-lockfile | |
| path: . | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| 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@v7 | |
| - 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@v8 | |
| continue-on-error: true | |
| with: | |
| name: pr-lockfile | |
| path: . | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| 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_shards: | |
| name: e2e shard (${{ matrix.shard_label }}) | |
| needs: [policy] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # The Playwright lane is pinned to workers=1 (tests/e2e/playwright.config.ts) | |
| # because every spec shares one throwaway server and some toggle | |
| # instance-level flags, so it can only be parallelized across runners. | |
| # Each shard boots its own server, which keeps that isolation intact. | |
| # Three shards let the ~3min smoke-lab spec ride alone while the rest | |
| # of the catalog splits evenly, pulling this lane off the PR critical | |
| # path (it was the slowest check at ~8min20s with two shards). | |
| - shard_index: 0 | |
| shard_count: 3 | |
| shard_label: 1/3 | |
| - shard_index: 1 | |
| shard_count: 3 | |
| shard_label: 2/3 | |
| - shard_index: 2 | |
| shard_count: 3 | |
| shard_label: 3/3 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - 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@v8 | |
| continue-on-error: true | |
| with: | |
| name: pr-lockfile | |
| path: . | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| 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: | | |
| # Playwright's own --shard balances by test count, and one spec | |
| # (smoke-lab) is ~40% of the lane's wall clock. Partition by recorded | |
| # spec duration instead so both runners finish together. | |
| specs="$(node ./scripts/e2e-shard.mjs \ | |
| --shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }})" | |
| echo "shard ${{ matrix.shard_label }} specs: $specs" | |
| pnpm run test:e2e $specs | |
| - name: Upload Playwright report | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: playwright-report-${{ matrix.shard_index }} | |
| path: | | |
| tests/e2e/playwright-report/ | |
| tests/e2e/test-results/ | |
| retention-days: 14 | |
| e2e: | |
| # Preserve the legacy required-check name while the specs run sharded | |
| # across the matrix above (same pattern as the `verify` aggregate). | |
| name: e2e | |
| if: ${{ always() }} | |
| needs: [e2e_shards] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Fail if any e2e shard failed | |
| env: | |
| E2E_SHARDS_RESULT: ${{ needs.e2e_shards.result }} | |
| run: test "$E2E_SHARDS_RESULT" = "success" |