diff --git a/.github/workflows/auto-promote-staging.yml b/.github/workflows/auto-promote-staging.yml index c3427787..118d0c83 100644 --- a/.github/workflows/auto-promote-staging.yml +++ b/.github/workflows/auto-promote-staging.yml @@ -61,13 +61,30 @@ jobs: run: | set -euo pipefail - # Required gate workflow names. Must match the `name:` field - # in the respective .github/workflows/*.yml files. + # Required gate workflow files. Use file paths (relative to + # .github/workflows/) rather than display names because: + # + # 1. `gh run list --workflow=` is ambiguous when two + # workflows have the same `name:` — observed 2026-04-28 + # with "CodeQL" matching both `codeql.yml` (explicit) and + # GitHub's UI-configured Code-quality default setup + # (internal "codeql"). gh CLI returns "could not resolve + # to a unique workflow" → empty result → gate evaluated + # as missing/none → auto-promote dead-locked despite all + # checks actually passing. + # + # 2. File paths are the unique identifier for workflows; + # `name:` is just a display string and can collide. + # + # When adding/removing a gate, update this list AND the + # branch-protection required-checks list (which uses check-run + # display names, not workflow names; the two are decoupled and + # should be kept in sync manually). GATES=( - "CI" - "E2E Staging Canvas (Playwright)" - "E2E API Smoke Test" - "CodeQL" + "ci.yml" + "e2e-staging-canvas.yml" + "e2e-api.yml" + "codeql.yml" ) echo "head_sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/e2e-api.yml b/.github/workflows/e2e-api.yml index 89c69b88..d7d6ea09 100644 --- a/.github/workflows/e2e-api.yml +++ b/.github/workflows/e2e-api.yml @@ -1,27 +1,73 @@ name: E2E API Smoke Test # Extracted from ci.yml so workflow-level concurrency can protect this job # from run-level cancellation (issue #458). +# +# Trigger model (changed 2026-04-28 — see auto-promote gap below): +# +# This workflow always FIRES on push/pull_request to staging+main, but +# only does real work when paths under `workspace-server/`, +# `tests/e2e/`, or this workflow file changed. The detect-changes job +# uses dorny/paths-filter to decide; the e2e-api job runs only if +# changes match. Otherwise the no-op job emits success so the workflow +# always produces a `completed/success` run record. +# +# Why: auto-promote-staging.yml's gate-check (line 99) treats "workflow +# didn't run" as failure, which dead-locked any platform-only or +# test-only push to staging that didn't touch workspace-server paths. +# Dropping the path filter on the trigger and gating real work +# internally guarantees the workflow always emits a result that the +# auto-promote chain can read. Same pattern applied to +# e2e-staging-canvas.yml in the same PR. on: push: branches: [main, staging] - paths: - - 'workspace-server/**' - - 'tests/e2e/**' - - '.github/workflows/e2e-api.yml' pull_request: branches: [main, staging] - paths: - - 'workspace-server/**' - - 'tests/e2e/**' - - '.github/workflows/e2e-api.yml' + workflow_dispatch: concurrency: group: e2e-api-${{ github.ref }} cancel-in-progress: false jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + api: ${{ steps.decide.outputs.api }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + api: + - 'workspace-server/**' + - 'tests/e2e/**' + - '.github/workflows/e2e-api.yml' + - id: decide + # Always run real work for manual dispatch — no diff context to + # filter against and ops dispatching this expects the suite to + # actually exercise the platform. + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "api=true" >> "$GITHUB_OUTPUT" + else + echo "api=${{ steps.filter.outputs.api }}" >> "$GITHUB_OUTPUT" + fi + + no-op: + needs: detect-changes + if: needs.detect-changes.outputs.api != 'true' + runs-on: ubuntu-latest + steps: + - run: | + echo "No workspace-server / tests/e2e / workflow changes — E2E API gate satisfied without running tests." + echo "::notice::E2E API Smoke Test no-op pass (paths filter excluded this commit)." + e2e-api: + needs: detect-changes + if: needs.detect-changes.outputs.api == 'true' name: E2E API Smoke Test runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/.github/workflows/e2e-staging-canvas.yml b/.github/workflows/e2e-staging-canvas.yml index 143d9469..310e16f3 100644 --- a/.github/workflows/e2e-staging-canvas.yml +++ b/.github/workflows/e2e-staging-canvas.yml @@ -13,16 +13,23 @@ name: E2E Staging Canvas (Playwright) # workflow — mirrors what PR #1891 does for e2e-api.yml. on: + # Trigger model (changed 2026-04-28 — see auto-promote gap below): + # + # Always fires on push/pull_request; only does real work when canvas/ + # or this workflow file changed. The detect-changes job uses + # dorny/paths-filter to decide; the playwright job runs only if + # changes match. Otherwise no-op emits success so the workflow always + # produces a `completed/success` run record. + # + # Why: auto-promote-staging.yml's gate-check (line 99) treats + # "workflow didn't run" as failure, which dead-locked platform-only + # pushes to staging. Dropping the trigger path filter and gating real + # work internally guarantees a result the auto-promote chain can + # read. Same pattern applied to e2e-api.yml in the same PR. push: branches: [main, staging] - paths: - - 'canvas/**' - - '.github/workflows/e2e-staging-canvas.yml' pull_request: branches: [main, staging] - paths: - - 'canvas/**' - - '.github/workflows/e2e-staging-canvas.yml' workflow_dispatch: schedule: # Weekly on Sunday 08:00 UTC — catches Chrome / Playwright / Next.js @@ -34,7 +41,41 @@ concurrency: cancel-in-progress: false jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + canvas: ${{ steps.decide.outputs.canvas }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + canvas: + - 'canvas/**' + - '.github/workflows/e2e-staging-canvas.yml' + - id: decide + # Always run real tests for manual dispatch and the weekly cron — + # both exist precisely to exercise the suite, regardless of diff. + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "schedule" ]; then + echo "canvas=true" >> "$GITHUB_OUTPUT" + else + echo "canvas=${{ steps.filter.outputs.canvas }}" >> "$GITHUB_OUTPUT" + fi + + no-op: + needs: detect-changes + if: needs.detect-changes.outputs.canvas != 'true' + runs-on: ubuntu-latest + steps: + - run: | + echo "No canvas / workflow changes — E2E Staging Canvas gate satisfied without running tests." + echo "::notice::E2E Staging Canvas no-op pass (paths filter excluded this commit)." + playwright: + needs: detect-changes + if: needs.detect-changes.outputs.canvas == 'true' name: Canvas tabs E2E runs-on: ubuntu-latest timeout-minutes: 40