diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f13c16ec..96d3b668 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -178,42 +178,44 @@ jobs: exit 1 fi - # Path-filter no-op shadow for Canvas (Next.js). + # Canvas (Next.js) is a required check on staging branch protection. + # The job ALWAYS runs (no `if:` gate at job level) so branch protection + # always sees a SUCCESS conclusion. Inside, every meaningful step is + # gated on `needs.changes.outputs.canvas == 'true'` — when the PR + # doesn't touch canvas/**, the job spins up, skips all real work, and + # exits clean in ~10s. # - # Branch protection on staging requires a "Canvas (Next.js)" check. - # When a PR doesn't touch canvas/** paths, the real canvas-build job - # below is skipped via `if:`, and GitHub reports its conclusion as - # SKIPPED — which branch protection treats as not-passed → merge - # BLOCKED on every workspace-server-only or migration-only PR. - # - # Pattern (per durable feedback memory: branch_protection_check_name_parity): - # split into a real job + a no-op shadow that share the same `name:`. - # Exactly one runs per PR; both report the same check context, and at - # least one always reports SUCCESS, satisfying the required check. - canvas-build-noop: - name: Canvas (Next.js) - needs: changes - if: needs.changes.outputs.canvas != 'true' - runs-on: ubuntu-latest - steps: - - run: echo "No canvas/** changes in this PR — Canvas (Next.js) skip is intentional, satisfying required-check via this no-op." - + # Why not a job-level `if:` + a no-op shadow: + # An earlier version (PR #2321) tried two jobs sharing the same `name:` + # — one with `if: == 'true'`, the other with `if: != 'true'`. Branch + # protection sees BOTH check runs and treats the SKIPPED one as + # not-passed even when the other reports SUCCESS — so the merge stays + # BLOCKED. Single-job-with-conditional-steps is the only shape that + # produces exactly one Canvas (Next.js) check run per commit AND + # always SUCCEEDS, regardless of which paths changed. canvas-build: name: Canvas (Next.js) needs: changes - if: needs.changes.outputs.canvas == 'true' runs-on: ubuntu-latest defaults: run: working-directory: canvas steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - if: needs.changes.outputs.canvas != 'true' + working-directory: . + run: echo "No canvas/** changes — skipping real build steps; this job always runs to satisfy the required-check name on branch protection." + - if: needs.changes.outputs.canvas == 'true' + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - if: needs.changes.outputs.canvas == 'true' + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '22' - - run: rm -f package-lock.json && npm install - - run: npm run build - - name: Run tests with coverage + - if: needs.changes.outputs.canvas == 'true' + run: rm -f package-lock.json && npm install + - if: needs.changes.outputs.canvas == 'true' + run: npm run build + - if: needs.changes.outputs.canvas == 'true' + name: Run tests with coverage # Coverage instrumentation is configured in canvas/vitest.config.ts # (provider: v8, reporters: text + html + json-summary). Step 2 of # #1815 — wires coverage into CI so we get a baseline visible on @@ -224,7 +226,7 @@ jobs: # thresholds + a hard gate" — this PR ships the observability half. run: npx vitest run --coverage - name: Upload coverage summary as artifact - if: always() + if: needs.changes.outputs.canvas == 'true' && always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: canvas-coverage-${{ github.run_id }}