Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/check-conventional-pr-title.py
Original file line number Diff line number Diff line change
@@ -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',
})

# <type>[optional scope][optional !]: <description>
_PATTERN = re.compile(
r'^(?p<type>[a-za-z]+)' # lower-case only, but let this be validated by _TYPES
r'(?:\((?P<scope>[^()]+)\))?'
r'(?P<breaking>!)?'
r': '
r'(?P<description>.+)$'
)


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: <type>[!]: <description>\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 are not 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()
24 changes: 7 additions & 17 deletions .github/workflows/validate-pr-title.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ 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_target: # zizmor: ignore[dangerous-triggers] Doesn't touch code - pull_request results in cancellation errors.
pull_request:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd be back to cancellation errors, unless the concurrency group is removed or changed.

Maybe it's cleaner to the remove concurrency seeting, given how simple the job is.

Copy link
Contributor Author

@james-garner-canonical james-garner-canonical Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the cancelation errors are only due to there being two workflows running right now, the pull_request trigger one from this branch, and the pull_request_target one from main.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt that. We used to have on: pull_request originally, and had those cancellation errors. I think it's because pushing the branch and creating a PR triggers both "pr created" and "code pushed" events. TBH I never found the root cause.

Copy link
Contributor Author

@james-garner-canonical james-garner-canonical Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm certain that the cancellations we see in this PR are because both the pull_request trigger from this branch, and the pull_request_target trigger from main are running here, and they define the same concurrency group.

I know those aren't the errors you're talking about, just to be clear.

I suggest we merge this PR and keep an eye out for cancellation errors thereafter. If we see the issue you remember, then let's remove the concurrency. I believe the idea with concurrency for this job is to avoid multiple quick PR edits resulting in spurious failures if an intermediate edit made a mistake.

types: [opened, edited, synchronize]

permissions:
pull-requests: read
permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
Expand All @@ -18,18 +17,9 @@ jobs:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v6
with:
types: |
chore
ci
docs
feat
fix
perf
refactor
revert
test
disallowScopes: ".*"
persist-credentials: false
- run: python3 .github/check-conventional-pr-title.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about attempting a super-compact inline Python block instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love the idea, I think that will be harder to maintain in future

env:
PR_TITLE: ${{ github.event.pull_request.title }}
Loading