From 149baf67883e0ebcceda926c0bf2d9e2925f0f85 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 10 Jun 2026 00:50:48 +0000 Subject: [PATCH 1/2] 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 copied from operator -- no third-party action. --- .github/check-conventional-pr-title.py | 84 ++++++++++++++++++++++++ .github/workflows/validate-pr-title.yaml | 21 ++++++ 2 files changed, 105 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 0000000..793644a --- /dev/null +++ b/.github/check-conventional-pr-title.py @@ -0,0 +1,84 @@ +# 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 defines a restricted set of commit types and disallows scopes in PR titles. +""" + +from __future__ import annotations + +import os +import re +import sys + +_TYPES = frozenset({ + 'chore', + 'ci', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + '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: [!]: \n' + f'Got: {title!r}\n' + 'Read more: https://github.com/canonical/operator/blob/main/CONTRIBUTING.md#pull-requests', + file=sys.stderr, + ) + sys.exit(1) + + scope = match.group('scope') + if scope is not None: + print( + f'Scopes must not be used in PR titles.\n' + f'Got: {title!r}\n' + 'Read more: https://github.com/canonical/operator/blob/main/CONTRIBUTING.md#pull-requests', + 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/operator/blob/main/CONTRIBUTING.md#pull-requests', + 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 0000000..ec634f0 --- /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 }} From 858a5b2479e0563096e7d4e7315d8defce627921 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Wed, 10 Jun 2026 15:59:50 +1200 Subject: [PATCH 2/2] ci: point the PR-title check at this repo's contributing guide Co-Authored-By: Claude Fable 5 --- .github/check-conventional-pr-title.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/check-conventional-pr-title.py b/.github/check-conventional-pr-title.py index 793644a..1b97f44 100644 --- a/.github/check-conventional-pr-title.py +++ b/.github/check-conventional-pr-title.py @@ -51,7 +51,7 @@ def _main() -> None: f'PR title does not follow Conventional Commits format.\n' f'Expected: [!]: \n' f'Got: {title!r}\n' - 'Read more: https://github.com/canonical/operator/blob/main/CONTRIBUTING.md#pull-requests', + 'Read more: https://github.com/canonical/concierge/blob/main/CONTRIBUTING.md#pull-requests', file=sys.stderr, ) sys.exit(1) @@ -61,7 +61,7 @@ def _main() -> None: print( f'Scopes must not be used in PR titles.\n' f'Got: {title!r}\n' - 'Read more: https://github.com/canonical/operator/blob/main/CONTRIBUTING.md#pull-requests', + 'Read more: https://github.com/canonical/concierge/blob/main/CONTRIBUTING.md#pull-requests', file=sys.stderr, ) sys.exit(1) @@ -72,7 +72,7 @@ def _main() -> None: 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/operator/blob/main/CONTRIBUTING.md#pull-requests', + 'Read more: https://github.com/canonical/concierge/blob/main/CONTRIBUTING.md#pull-requests', file=sys.stderr, ) sys.exit(1)