From 45ffe5aa6ad1d5f5c4be45ccb7b338b937c73a1f Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 5 May 2026 19:24:09 +0200 Subject: [PATCH] ci: lint PR titles with commitlint (#37498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Enforce **Conventional Commits** on PR titles (PRs are squash-merged, so the PR title becomes the final commit message). - Add a local `make lint-pr-title` target so contributors can validate titles before pushing. ## Why We squash-merge PRs, which means the final repository history is largely shaped by **PR titles**. Enforcing a consistent Conventional Commits format makes: - **Release notes & changelogs easier to generate** (types like `feat` / `fix` can be grouped automatically). - **History easier to scan** (uniform structure, optional scopes, explicit breaking changes via `!`). - **Automation more reliable** (future tooling can infer category and scope from the title). ## PR title format ```text type(scope)!: subject type: one of build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test scope: optional (e.g. web, api, actions, repo, …) !: optional, indicates a breaking change subject: short, imperative, no trailing period ``` ## Examples ```text feat(web): add dark mode toggle fix(api): avoid panic when repo is missing chore(ci): lint PR titles with commitlint refactor(templates): reduce duplication in repo list rendering feat!: remove legacy OAuth endpoint ``` ## Local testing ```text make deps-frontend make lint-pr-title PR_TITLE="feat(web): add dark mode toggle" ``` --------- Signed-off-by: Nicolas Co-authored-by: nb Co-authored-by: GPT-5.2 --- .github/pull_request_template.md | 13 +++++++------ .github/workflows/pull-pr-title.yml | 28 ++++++++++++++++++++++++++++ AGENTS.md | 1 + CONTRIBUTING.md | 16 ++++++++++++++++ Makefile | 4 ++++ tools/lint-pr-title.js | 19 +++++++++++++++++++ 6 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/pull-pr-title.yml create mode 100644 tools/lint-pr-title.js diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b7594a1ba7..91d001e078 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,10 +1,11 @@ Please check the following: 1. Make sure you are targeting the `main` branch, pull requests on release branches are only allowed for backports. -2. Make sure you have read contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md . -3. For documentations contribution, please go to https://gitea.com/gitea/docs -4. Describe what your pull request does and which issue you're targeting (if any). -5. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily. -6. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`. -7. Delete all these tips before posting. +2. Use a Conventional Commits PR title, for example `fix(repo): handle empty branch names`. +3. Make sure you have read contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md . +4. For documentations contribution, please go to https://gitea.com/gitea/docs +5. Describe what your pull request does and which issue you're targeting (if any). +6. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily. +7. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`. +8. Delete all these tips before posting. diff --git a/.github/workflows/pull-pr-title.yml b/.github/workflows/pull-pr-title.yml new file mode 100644 index 0000000000..59b0e78c40 --- /dev/null +++ b/.github/workflows/pull-pr-title.yml @@ -0,0 +1,28 @@ +name: pr-title + +on: + pull_request: + types: + - opened + - edited + - reopened + - synchronize + - ready_for_review + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint-pr-title: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - run: make lint-pr-title + env: + PR_TITLE: ${{ github.event.pull_request.title }} diff --git a/AGENTS.md b/AGENTS.md index fd87f432b7..276023f234 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,7 @@ - Run single playwright e2e test files with `GITEA_TEST_E2E_FLAGS='' make test-e2e` - Add the current year into the copyright header of new `.go` files - Ensure no trailing whitespace in edited files +- Use Conventional Commits format for commit messages and PR titles (e.g. `type(scope): subject`) - Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates - Preserve existing code comments, do not remove or rewrite comments that are still relevant - In TypeScript, use `!` (non-null assertion) instead of `?.`/`??` when a value is known to always exist diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1871f1470..c62950d84b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -189,6 +189,22 @@ In the PR title, describe the problem you are fixing, not how you are fixing it. Use the first comment as a summary of your PR. \ In the PR summary, you can describe exactly how you are fixing this problem. +PR titles must follow the [Conventional Commits](https://www.conventionalcommits.org/) format, because PRs are squash-merged and the PR title becomes the resulting commit message: + +```text +type(scope)!: subject +``` + +The allowed types are `build`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, and `test`. The generic `chore` type is intentionally not accepted; pick a more descriptive type instead. + +Examples: + +```text +fix(web): prevent avatar upload crash on empty file +feat(api): add pagination to repo hooks list +ci(workflows): lint PR titles with commitlint +``` + Keep this summary up-to-date as the PR evolves. \ If your PR changes the UI, you must add **after** screenshots in the PR summary. \ If you are not implementing a new feature, you should also post **before** screenshots for comparison. diff --git a/Makefile b/Makefile index 2466435b17..b6aa9371e5 100644 --- a/Makefile +++ b/Makefile @@ -321,6 +321,10 @@ lint-md: node_modules ## lint markdown files lint-md-fix: node_modules ## lint markdown files and fix issues pnpm exec markdownlint --fix *.md +.PHONY: lint-pr-title +lint-pr-title: ## lint PR title against Conventional Commits (set PR_TITLE=...) + @node ./tools/lint-pr-title.js + .PHONY: lint-spell lint-spell: ## lint spelling @git ls-files $(SPELLCHECK_FILES) | xargs go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -error diff --git a/tools/lint-pr-title.js b/tools/lint-pr-title.js new file mode 100644 index 0000000000..2df340f186 --- /dev/null +++ b/tools/lint-pr-title.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node +import {env, exit} from 'node:process'; + +const allowedTypes = 'build, ci, docs, feat, fix, perf, refactor, revert, style, test'; +const title = env.PR_TITLE; + +if (!title) { + console.error('Missing PR_TITLE'); + exit(1); +} + +const validTitlePattern = new RegExp(`^(${allowedTypes.replaceAll(', ', '|')})(\\([\\w.-]+\\))?(!)?: .+$`); + +if (!validTitlePattern.test(title)) { + console.error(`Invalid PR title: ${title}`); + console.error('Expected format: type(scope): subject'); + console.error(`Allowed types: ${allowedTypes}`); + exit(1); +}