diff --git a/.github/workflows/ci-build-artifact-gate.yml b/.github/workflows/ci-build-artifact-gate.yml index d648994..ff75af0 100644 --- a/.github/workflows/ci-build-artifact-gate.yml +++ b/.github/workflows/ci-build-artifact-gate.yml @@ -21,7 +21,7 @@ permissions: jobs: build-artifact-gate: - name: Block committed build artifacts + name: Check gated paths runs-on: ${{ fromJSON(inputs.runs-on) }} steps: - name: Checkout repository diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index ae2f855..f83ad0a 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -59,7 +59,7 @@ permissions: jobs: build: - name: Build + name: Production build runs-on: ${{ fromJSON(inputs.runs-on) }} defaults: run: diff --git a/.github/workflows/ci-detect-changes.yml b/.github/workflows/ci-detect-changes.yml index 5585c56..3c57efe 100644 --- a/.github/workflows/ci-detect-changes.yml +++ b/.github/workflows/ci-detect-changes.yml @@ -70,7 +70,7 @@ permissions: jobs: detect: - name: Detect changes + name: Bucket changed files runs-on: ${{ fromJSON(inputs.runs-on) }} outputs: # css/js/php come from the scope step; total/ignored/gha stay repo-wide on detect. diff --git a/.github/workflows/ci-lint-php.yml b/.github/workflows/ci-lint-php.yml index 31201cf..048b5f8 100644 --- a/.github/workflows/ci-lint-php.yml +++ b/.github/workflows/ci-lint-php.yml @@ -51,7 +51,7 @@ permissions: jobs: lint-php: - name: Lint PHP + name: ${{ inputs.enable-phpstan && 'PHPCS + PHPStan' || 'PHPCS' }} runs-on: ${{ fromJSON(inputs.runs-on) }} defaults: run: diff --git a/.github/workflows/wp-ci.yml b/.github/workflows/wp-ci.yml index 073a50f..8715e9b 100644 --- a/.github/workflows/wp-ci.yml +++ b/.github/workflows/wp-ci.yml @@ -323,7 +323,8 @@ jobs: working-dir: ${{ needs.validate-inputs.outputs.working-dir }} test-php: - name: Test PHP + # Named explicitly: GitHub's default suffix for this matrix renders as "(8.3, )". + name: Test PHP ${{ matrix.php }}${{ matrix.wp != '' && format(', WP {0}', matrix.wp) || '' }} needs: [validate-inputs, detect-changes] if: >- (needs.detect-changes.outputs.php-count > 0 || needs.detect-changes.outputs.gha-count > 0) @@ -421,3 +422,42 @@ jobs: runs-on: ${{ inputs.runs-on }} gated-paths: ${{ inputs.gated-paths }} working-dir: ${{ needs.validate-inputs.outputs.working-dir }} + + # The one check to require in branch protection: " / CI result". The other check + # names and their count change with project-type, skip, what changed and the PHP matrix, and a + # skipped required check counts as passing. Runs on always(), so a failed, cancelled or + # never-started job fails it, while a job skipped by its own gate does not. + ci-result: + name: CI result + needs: + - validate-inputs + - detect-changes + - lint-css + - lint-js + - lint-php + - test-js + - test-php + - build + - build-artifact + - a11y + - artifact-gate + if: always() + runs-on: ${{ fromJSON(inputs.runs-on) }} + steps: + - name: Check every job's result + shell: bash + env: + NEEDS: ${{ toJSON(needs) }} + run: | + set -euo pipefail + { + echo "| Job | Result |" + echo "|---|---|" + jq -r 'to_entries[] | "| `\(.key)` | \(.value.result) |"' <<< "$NEEDS" + } >> "$GITHUB_STEP_SUMMARY" + bad="$(jq -r 'to_entries[] | select(.value.result != "success" and .value.result != "skipped") | "\(.key) (\(.value.result))"' <<< "$NEEDS")" + if [ -n "$bad" ]; then + echo "::error::CI did not pass: $(printf '%s' "$bad" | paste -sd ',' - | sed 's/,/, /g')." + exit 1 + fi + echo "Every job passed or was skipped by its own gate." diff --git a/AGENTS.md b/AGENTS.md index 5576e20..5fe6e14 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,6 +129,6 @@ Use `bash -eo pipefail -c`, never bare `bash -c`: `-e` is not inherited, so a ba ## PR instructions - GitHub flow: branch off `main`, PR back into `main`. A release is a tag on `main` that release-please cuts. Name branches `/` (`fix/runs-on-input`). Never commit to `main` directly. -- [Conventional Commits](https://www.conventionalcommits.org/) for the PR title, typed by what changes for callers: `fix:` or `feat:` for any change to a reusable workflow, and `feat!:` for anything a caller has to change for (a removed, renamed or newly required input, a new `permissions:` entry, a renamed job `name:`). `docs:` and `chore:` (e.g. `chore(self-check): ...`) are only for this repo's own files and don't cut a release. `ci:` isn't accepted here: nearly everything in this repo is CI, so the title check makes the choice explicit. Example: `feat(lint-css): Add lint-css workflow`. +- [Conventional Commits](https://www.conventionalcommits.org/) for the PR title, typed by what changes for callers: `fix:` or `feat:` for any change to a reusable workflow, and `feat!:` for anything a caller has to change for (a removed, renamed or newly required input, a new `permissions:` entry). A renamed job `name:` changes a check name callers may require: it is `feat:` when a code search finds no caller requiring the old name, with that evidence in the PR body and the old and new names in the README's "Renamed checks" list, and `feat!:` otherwise. `docs:` and `chore:` (e.g. `chore(self-check): ...`) are only for this repo's own files and don't cut a release. `ci:` isn't accepted here: nearly everything in this repo is CI, so the title check makes the choice explicit. Example: `feat(lint-css): Add lint-css workflow`. - Ensure the Code quality commands pass, the caller example is updated alongside the workflow, and the PR title says what changed for a consumer — see `CONTRIBUTING.md` for the full checklist. - Ask first before removing or renaming an input, adding a deploy target, or granting a job `contents: write` it does not already need. diff --git a/README.md b/README.md index f3f8a2b..58b9689 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,35 @@ Node comes from `.nvmrc` in `working-dir`, then the repository root, falling bac `node-version` to override it. `wp-ci.yml` also forwards `php-version`, the Composer settings and a `*-command` input per job, each falling back to that job's default when left empty. +### Branch protection + +Require one check per `wp-ci.yml` call: ` / CI result`, for example +`CI / CI result`, or `plugins/acme-blocks / CI result` for each unit of the matrix in +[`examples/wp-ci.yml`](examples/wp-ci.yml). It fails when any job failed, was cancelled or never +started, and passes when every job passed or was skipped by its own gate. Don't require the other +checks: which of them run, and their names, change with `project-type`, `skip`, the PHP matrix and +what a pull request touched, and a required check that never reports leaves the pull request +waiting forever. + +`CI result` only exists when the calling job runs. A calling job with its own `if:` that skips +reports no `CI result`, so a repository that gates the call needs its own always-running aggregate +job, or should not require `CI result`. + +#### Renamed checks + +Check names come from job names, so a rename changes what a ruleset must require. Since v1.2.0: + +| Workflow | Old check name | New check name | +|---|---|---| +| `ci-lint-php.yml` | `Lint PHP` | `PHPCS`, or `PHPCS + PHPStan` with `enable-phpstan` | +| `ci-build.yml` | `Build` | `Production build` | +| `ci-detect-changes.yml` | `Detect changes` | `Bucket changed files` | +| `ci-build-artifact-gate.yml` | `Block committed build artifacts` | `Check gated paths` | +| `wp-ci.yml` (`test-php` matrix) | `Test PHP (, )`, GitHub's default, e.g. `Test PHP (8.3, )` | `Test PHP `, or `Test PHP , WP ` | + +Called through `wp-ci.yml`, each appears after the `wp-ci.yml` job, for example +`CI / Lint PHP / PHPCS`; called directly, after the caller's job, for example `lint / PHPCS`. + ## Workflows Each workflow declares its inputs, secrets and outputs in its own `workflow_call` block, and GitHub