diff --git a/.github/actions/commitlint/action.yml b/.github/actions/commitlint/action.yml new file mode 100644 index 0000000..4d62a8e --- /dev/null +++ b/.github/actions/commitlint/action.yml @@ -0,0 +1,41 @@ +name: Lint commit messages +description: >- + Lints a pull request's commit messages against the Radius Red commitlint + config shipped in this directory (Conventional Commits). The action does + not check out: the caller runs actions/checkout with fetch-depth 0 first, + from a job named exactly "Lint commit messages" (the org ruleset + require-lint pins that check context). See CONTRIBUTING.md. + +runs: + using: composite + steps: + - name: Place the shared commitlint config in the workspace + id: stage + shell: bash + # wagoid/commitlint-github-action is a Docker action. It resolves + # configFile against GITHUB_WORKSPACE inside its container, and the + # runner's _actions directory (where this action is checked out) is + # not mounted there, so a path into the action directory would fall + # back silently to bare config-conventional. Copy the config into the + # workspace instead, into a directory this action creates itself: + # mktemp -d makes a fresh, uniquely named directory and fails rather + # than reuse one, so nothing the caller owns is overwritten, and the + # cleanup below removes only that directory. + run: | + dir="$(mktemp -d "${GITHUB_WORKSPACE}/.radiusred-commitlint-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.XXXXXXXX")" + cp "${{ github.action_path }}/commitlint.config.mjs" "${dir}/commitlint.config.mjs" + echo "dir=${dir#"${GITHUB_WORKSPACE}/"}" >> "${GITHUB_OUTPUT}" + echo "commitlint config placed at ${dir}/commitlint.config.mjs:" + cat "${dir}/commitlint.config.mjs" + + - name: Lint the commit messages + uses: wagoid/commitlint-github-action@v6.2.1 + with: + configFile: ${{ steps.stage.outputs.dir }}/commitlint.config.mjs + + - name: Remove the copied config + if: always() && steps.stage.outputs.dir != '' + shell: bash + env: + STAGE_DIR: ${{ steps.stage.outputs.dir }} + run: rm -rf "${GITHUB_WORKSPACE}/${STAGE_DIR}" diff --git a/commitlint.config.mjs b/.github/actions/commitlint/commitlint.config.mjs similarity index 100% rename from commitlint.config.mjs rename to .github/actions/commitlint/commitlint.config.mjs diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 3207229..67af18e 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -1,3 +1,8 @@ +# Reference caller for the shared commitlint action. Other repositories copy +# this file and replace the local `uses:` path with +# `radiusred/.github/.github/actions/commitlint@main`; this repository calls +# the action by path so a PR here tests the version of the action it carries. +# The job name is the check context the org ruleset require-lint requires. name: Lint commit messages on: @@ -5,6 +10,7 @@ on: permissions: contents: read + pull-requests: read jobs: commitlint: @@ -14,4 +20,4 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: wagoid/commitlint-github-action@v6 + - uses: ./.github/actions/commitlint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4823ea5..4ee4d03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,12 +17,19 @@ other repositories consume: from their `prepare-release` and `finalize-release` workflows. - `.github/actions/calculate_version` — the version-calculation action the release workflow runs. +- `.github/actions/commitlint` — the shared commit-message lint action, with + the organisation's `commitlint.config.mjs` inside it. Any repository that + lints commit messages calls it from a thin workflow (see + [Linting commit messages in a repository](#linting-commit-messages-in-a-repository)); + this repository's own `.github/workflows/commitlint.yml` is the reference + caller. - `profile/` — the organisation profile shown at [github.com/radiusred](https://github.com/radiusred). Beyond those, the repository carries only its own housekeeping: this file, -`.gitignore`, and `.codecrew.yml`, which points CodeCrew coordination for this -repository at the company hub. Each project's documentation lives with the +`.gitignore`, `.codecrew.yml`, which points CodeCrew coordination for this +repository at the company hub, and `.github/workflows/commitlint.yml`, its own +call of the shared commit lint. Each project's documentation lives with the project. ## Contributing to a project @@ -38,16 +45,65 @@ is its contributing guide.) If you are unsure whether a change is wanted, open an issue in the repository concerned describing the goal before writing the code. +## Linting commit messages in a repository + +Every Radius Red repository lints pull-request commit messages against +[Conventional Commits](https://www.conventionalcommits.org/) through the +shared action in this repository, so the config and the pinned +`wagoid/commitlint-github-action` version change in one place. Do not add a +`commitlint.config.mjs` to the calling repository; the action carries it. + +Copy this file to `.github/workflows/commitlint.yml` in the repository: + +```yaml +name: Lint commit messages + +on: + pull_request: + +permissions: + contents: read + pull-requests: read + +jobs: + commitlint: + name: Lint commit messages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: radiusred/.github/.github/actions/commitlint@main +``` + +- **The job name must be exactly `Lint commit messages`.** It is the check + context the organisation ruleset `require-lint` requires on the default + branch of every public repository; a job named anything else reports a + different context, and pull requests cannot merge until a check with the + required one passes. (This is why the shared piece is a composite action + rather than a reusable workflow: GitHub reports a called workflow's job as + `caller job / called job`, which can never equal the required context.) +- **Permissions.** `contents: read` is for the checkout. `pull-requests: read` + lets the action list the pull request's commits; a private repository + fails without it, a public one merely gets it for free. Keep both. +- **The action does not check out.** Run `actions/checkout` with + `fetch-depth: 0` before it, as above. +- This repository calls the action by local path (`./.github/actions/commitlint`) + instead of `@main`, so a pull request here tests the version of the action + it carries. Every other repository uses `@main`. + ## Changing this repository `main` is protected: changes arrive by pull request, history is linear (rebase only, no merge commits), and commit messages follow [Conventional Commits](https://www.conventionalcommits.org/). -- Callers reference the reusable workflows and the action at `@main`, so a +- Callers reference the reusable workflows and the actions at `@main`, so a merge here is live for every caller at once. Say in the PR which callers you checked, and keep workflow inputs, secrets and outputs backward compatible - or land the callers' change alongside. + or land the callers' change alongside. A change to the commitlint config or + to the pinned `wagoid/commitlint-github-action` version is proved by this + repository's own `Lint commit messages` check before it can merge. - Keep the profile accurate; it is what a visitor to the organisation sees first. - Never include secrets or internal data in a public PR.