name: CI on: pull_request: branches: [main] paths-ignore: ['**.md', 'docs/**', 'LICENSE'] workflow_call: permissions: contents: read # Concurrency convention: see CONTRIBUTING.md → "GitHub Actions — Concurrency Convention". # Hardcoded `CI-` prefix (not `${{ github.workflow }}`) because this workflow is # invoked as a reusable workflow from publish.yml. In called-workflow context # `github.workflow` evaluation is ambiguous across GitHub Actions versions, and a # prefix that could resolve to the caller's name would share a concurrency group # with the caller → deadlock. A literal prefix is immune. Direct `pull_request` # invocations use `CI-`; invocations from a reusable-workflow caller fall # into a per-run-unique group that never serializes with the caller. `push` to # main is handled by publish.yml (RC mode), which calls this workflow once # before publishing. concurrency: group: ${{ github.event_name == 'pull_request' && format('CI-{0}', github.ref) || format('CI-nested-{0}', github.run_id) }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} # ── Reusable workflow orchestration ───────────────────────────────── # Each concern lives in its own workflow file for maintainability: # ci-quality.yml — typecheck (tsc --noEmit) # ci-tests.yml — unit + integration tests with coverage + cross-platform # (includes the scope-resolution resolver tests) # ci-e2e.yml — E2E tests (only when gitnexus-web/ changes) # # Shared setup is DRY via .github/actions/setup-gitnexus composite action. jobs: quality: uses: ./.github/workflows/ci-quality.yml permissions: contents: read tests: uses: ./.github/workflows/ci-tests.yml permissions: contents: read e2e: uses: ./.github/workflows/ci-e2e.yml permissions: contents: read # ── Save PR metadata for the reporting workflow ───────────────── # The ci-report.yml workflow (triggered by workflow_run) needs the # PR number and job results to post a comment. We save them as an # artifact because workflow_run context doesn't reliably carry PR # info for fork PRs. save-pr-meta: name: Save PR Metadata if: always() && github.event_name == 'pull_request' needs: [quality, tests, e2e] runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Write metadata shell: bash env: PR_NUMBER: ${{ github.event.number }} QUALITY: ${{ needs.quality.result }} TESTS: ${{ needs.tests.result }} E2E: ${{ needs.e2e.result }} run: | mkdir -p pr-meta echo "$PR_NUMBER" > pr-meta/pr_number echo "$QUALITY" > pr-meta/quality_result echo "$TESTS" > pr-meta/tests_result echo "$E2E" > pr-meta/e2e_result # TODO(post-merge): remove backward-compat copies once ci-report.yml # on main reads underscore names. # Backward-compat: ci-report.yml on main still reads hyphenated # names. workflow_run always executes from the default branch, so # the main-branch reader won't find the underscore variants until # this PR is merged. Write both until then. cp pr-meta/pr_number pr-meta/pr-number cp pr-meta/quality_result pr-meta/quality-result cp pr-meta/tests_result pr-meta/tests-result cp pr-meta/e2e_result pr-meta/e2e-result - name: Upload PR metadata uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: pr-meta path: pr-meta/ retention-days: 1 # ── Unified CI gate ────────────────────────────────────────────── # Single required check for branch protection. ci-status: name: CI Gate needs: [quality, tests, e2e] if: always() runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Check all jobs passed shell: bash env: QUALITY: ${{ needs.quality.result }} # The tree-sitter ABI gate (#1922) runs as the `abi-assert` job # inside the `tests` reusable workflow. A failed job fails the # reusable workflow, so `needs.tests.result` below blocks the merge # on an ABI mismatch. (`jobs..result` cannot be exposed as a # workflow_call output, so the gate is enforced transitively here.) # The scope-resolution resolver tests also run inside the `tests` # workflow (RING4-1 #942 removed the separate scope-parity gate), # so a resolver regression makes TESTS != success and blocks here. TESTS: ${{ needs.tests.result }} E2E: ${{ needs.e2e.result }} run: | echo "Quality: $QUALITY" echo "Tests: $TESTS" echo "E2E: $E2E" # A failed `abi-assert` job (#1922) inside the tests reusable # workflow makes TESTS != success, so this clause also blocks the # merge on a tree-sitter ABI mismatch. if [[ "$QUALITY" != "success" ]] || [[ "$TESTS" != "success" ]]; then echo "::error::Quality or test jobs failed (includes the tree-sitter ABI gate, #1922)" exit 1 fi if [[ "$E2E" != "success" && "$E2E" != "skipped" ]]; then echo "::error::E2E job failed" exit 1 fi