From ceb989606c96f987ab5f521ff724c5727c7b258b Mon Sep 17 00:00:00 2001 From: "radiusred-cody[bot]" <270330637+radiusred-cody[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:27:46 +0100 Subject: [PATCH 1/4] ci: add a shared commitlint action carrying the org config (#39) A composite action at .github/actions/commitlint runs wagoid/commitlint-github-action pinned to v6.2.1 with the commitlint config moved from the repository root into the action directory, so callers need no local config. wagoid is a Docker action and resolves configFile against the workspace inside its container, where the runner's _actions directory is not mounted; the action copies the config into the workspace, prints it, and removes the copy afterwards. --- .github/actions/commitlint/action.yml | 36 +++++++++++++++++++ .../actions/commitlint/commitlint.config.mjs | 0 2 files changed, 36 insertions(+) create mode 100644 .github/actions/commitlint/action.yml rename commitlint.config.mjs => .github/actions/commitlint/commitlint.config.mjs (100%) diff --git a/.github/actions/commitlint/action.yml b/.github/actions/commitlint/action.yml new file mode 100644 index 0000000..f6ed4b6 --- /dev/null +++ b/.github/actions/commitlint/action.yml @@ -0,0 +1,36 @@ +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 + 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 and print it, so the log shows what was linted + # against. + run: | + target="${GITHUB_WORKSPACE}/.radiusred-commitlint" + mkdir -p "${target}" + cp "${{ github.action_path }}/commitlint.config.mjs" "${target}/commitlint.config.mjs" + echo "commitlint config placed at ${target}/commitlint.config.mjs:" + cat "${target}/commitlint.config.mjs" + + - name: Lint the commit messages + uses: wagoid/commitlint-github-action@v6.2.1 + with: + configFile: .radiusred-commitlint/commitlint.config.mjs + + - name: Remove the copied config + if: always() + shell: bash + run: rm -rf "${GITHUB_WORKSPACE}/.radiusred-commitlint" 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 From d8ef10306b4a90b86834f5f00a978e35004f0255 Mon Sep 17 00:00:00 2001 From: "radiusred-cody[bot]" <270330637+radiusred-cody[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:27:54 +0100 Subject: [PATCH 2/4] ci: lint this repository's commits through the shared action (#39) The workflow becomes the reference caller: pull_request trigger, contents and pull-requests read, checkout with fetch-depth 0, then the action, from the job named "Lint commit messages" that the org ruleset require-lint pins. It calls the action by local path so a PR here exercises the version of the action it carries. --- .github/workflows/commitlint.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 From dfe8c50db6732ab3d24171d2a52d168685ca3b33 Mon Sep 17 00:00:00 2001 From: "radiusred-cody[bot]" <270330637+radiusred-cody[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:28:14 +0100 Subject: [PATCH 3/4] docs: document adopting the shared commitlint action (#39) CONTRIBUTING.md lists the action in the inventory and gains a section with the exact caller workflow to copy, the permissions a private repository needs, why the job name must stay "Lint commit messages" (the org ruleset require-lint pins that context), and that the caller checks out before the action. --- CONTRIBUTING.md | 64 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 4 deletions(-) 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. From c4d028635fe629b60b53c1a6b48ea0455a3ad221 Mon Sep 17 00:00:00 2001 From: "radiusred-cody[bot]" <270330637+radiusred-cody[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:34:16 +0100 Subject: [PATCH 4/4] fix: stage the commitlint config in a directory the action creates (#39) Review finding on the PR: the fixed workspace path .radiusred-commitlint/ is caller-owned territory, so an adopting repository that already had it would have a file overwritten and the whole directory removed by the cleanup. The staging step now creates a fresh, uniquely named directory with mktemp -d (run id, attempt and a random suffix), which never reuses an existing path, hands it to the lint step through a step output, and the cleanup removes only that directory. --- .github/actions/commitlint/action.yml | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/actions/commitlint/action.yml b/.github/actions/commitlint/action.yml index f6ed4b6..4d62a8e 100644 --- a/.github/actions/commitlint/action.yml +++ b/.github/actions/commitlint/action.yml @@ -10,27 +10,32 @@ 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 and print it, so the log shows what was linted - # against. + # 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: | - target="${GITHUB_WORKSPACE}/.radiusred-commitlint" - mkdir -p "${target}" - cp "${{ github.action_path }}/commitlint.config.mjs" "${target}/commitlint.config.mjs" - echo "commitlint config placed at ${target}/commitlint.config.mjs:" - cat "${target}/commitlint.config.mjs" + 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: .radiusred-commitlint/commitlint.config.mjs + configFile: ${{ steps.stage.outputs.dir }}/commitlint.config.mjs - name: Remove the copied config - if: always() + if: always() && steps.stage.outputs.dir != '' shell: bash - run: rm -rf "${GITHUB_WORKSPACE}/.radiusred-commitlint" + env: + STAGE_DIR: ${{ steps.stage.outputs.dir }} + run: rm -rf "${GITHUB_WORKSPACE}/${STAGE_DIR}"