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' 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