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: | 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: | missing=0 # Extract only the deps stage from the Dockerfile deps_stage="$(awk '/^FROM .* AS deps$/{found=1; next} found && /^FROM /{exit} found{print}' Dockerfile)" if [ -z "$deps_stage" ]; then echo "::error::Could not extract deps stage from Dockerfile (expected 'FROM ... AS deps')" exit 1 fi # Derive workspace search roots from pnpm-workspace.yaml (exclude dev-only packages) search_roots="$(grep '^ *- ' pnpm-workspace.yaml | sed 's/^ *- //' | sed 's/\*$//' | grep -v 'examples' | grep -v 'create-paperclip-plugin' | tr '\n' ' ')" if [ -z "$search_roots" ]; then echo "::error::Could not derive workspace roots from pnpm-workspace.yaml" exit 1 fi # Check all workspace package.json files are copied in the deps stage for pkg in $(find $search_roots -maxdepth 2 -name package.json -not -path '*/examples/*' -not -path '*/create-paperclip-plugin/*' -not -path '*/node_modules/*' 2>/dev/null | sort -u); do dir="$(dirname "$pkg")" if ! echo "$deps_stage" | grep -q "^COPY ${dir}/package.json"; then echo "::error::Dockerfile deps stage missing: COPY ${pkg} ${dir}/" missing=1 fi done # Check patches directory is copied if it exists if [ -d patches ] && ! echo "$deps_stage" | grep -q '^COPY patches/'; then echo "::error::Dockerfile deps stage missing: COPY patches/ patches/" missing=1 fi if [ "$missing" -eq 1 ]; then echo "Dockerfile deps stage is out of sync. Update it to include the missing files." exit 1 fi - 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 verify: 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 run: pnpm -r typecheck - name: Run tests run: pnpm test:run - name: Build run: pnpm build - name: Release canary dry run 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: Build run: pnpm build - name: Install Playwright run: npx playwright install --with-deps chromium - 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" 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