From e9ccdf5b101072343be2d01977b71a06f672533e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 10 Jun 2026 00:50:50 +0000 Subject: [PATCH] ci: enforce Conventional Commits on PR titles Port canonical/operator's validate-pr-title.yaml workflow, which validates that pull request titles follow the Conventional Commits specification. PR titles become the squash-merge commit subject, so this keeps the merged history Conventional-Commits clean. The workflow uses actions/checkout@v6.0.2 (the only action) and runs the local .github/check-conventional-pr-title.py script -- no third-party action. The script's type list and optional component scopes are adapted to match this repo's documented convention in HACKING.md rather than operator's stricter no-scopes set. --- .github/check-conventional-pr-title.py | 75 ++++++++++++++++++++++++ .github/workflows/validate-pr-title.yaml | 21 +++++++ 2 files changed, 96 insertions(+) create mode 100644 .github/check-conventional-pr-title.py create mode 100644 .github/workflows/validate-pr-title.yaml diff --git a/.github/check-conventional-pr-title.py b/.github/check-conventional-pr-title.py new file mode 100644 index 000000000..402c164fb --- /dev/null +++ b/.github/check-conventional-pr-title.py @@ -0,0 +1,75 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Check that a PR title follows the Conventional Commits specification. + +Reads the PR title from the PR_TITLE environment variable. +Exits with a non-zero status and prints an error message if the title is invalid. + +Reference: https://www.conventionalcommits.org/en/v1.0.0/ + +This repo's commit types and optional component scopes follow HACKING.md. +""" + +from __future__ import annotations + +import os +import re +import sys + +_TYPES = frozenset({ + 'build', + 'chore', + 'ci', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'style', + 'test', +}) + +# [optional scope][optional !]: +_PATTERN = re.compile( + r'^(?P[A-Za-z]+)' # lower-case only, but let this be validated by _TYPES + r'(?:\((?P[^()]+)\))?' + r'(?P!)?' + r': ' + r'(?P.+)$' +) + + +def _main() -> None: + title = os.environ.get('PR_TITLE', '').strip() + if not title: + print('PR_TITLE environment variable is not set or empty.', file=sys.stderr) + sys.exit(1) + + match = _PATTERN.match(title) + if not match: + print( + f'PR title does not follow Conventional Commits format.\n' + f'Expected: [(scope)][!]: \n' + f'Got: {title!r}\n' + 'Read more: https://github.com/canonical/pebble/blob/master/HACKING.md#commits', + file=sys.stderr, + ) + sys.exit(1) + + commit_type = match.group('type') + if commit_type not in _TYPES: + print( + f'Invalid type {commit_type!r} in PR title.\n' + f'Valid types: {", ".join(sorted(_TYPES))}\n' + f'Got: {title!r}\n' + 'Read more: https://github.com/canonical/pebble/blob/master/HACKING.md#commits', + file=sys.stderr, + ) + sys.exit(1) + + print(f'OK: {title!r}') + + +if __name__ == '__main__': + _main() diff --git a/.github/workflows/validate-pr-title.yaml b/.github/workflows/validate-pr-title.yaml new file mode 100644 index 000000000..ec634f059 --- /dev/null +++ b/.github/workflows/validate-pr-title.yaml @@ -0,0 +1,21 @@ +--- +name: "Validate PR Title" +# Ensure that the PR title conforms to the Conventional Commits and our choice of types and scopes, so that library version bumps can be detected automatically + +on: + pull_request: + types: [opened, edited, synchronize] + +permissions: {} + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + with: + persist-credentials: false + - run: python3 .github/check-conventional-pr-title.py + env: + PR_TITLE: ${{ github.event.pull_request.title }}