GRM-20: fix: enforce GRM-N: conventional on master commits and PR titles
This commit is contained in:
@@ -21,6 +21,15 @@ jobs:
|
||||
fi
|
||||
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
|
||||
# Validate PR title follows conventional commits so squash merge message is valid
|
||||
if ! echo "$PR_TITLE" | grep -qE '^(feat|fix|chore|docs|style|refactor|perf|test|ci|build|revert|BREAKING CHANGE)(\(.+\))?: .+'; then
|
||||
echo "ERROR: PR title must follow conventional commit format."
|
||||
echo " Expected: <type>: <description>"
|
||||
echo " Got: $PR_TITLE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MERGE_TITLE="${TASK_ID}: ${PR_TITLE}"
|
||||
|
||||
curl -X POST \
|
||||
|
||||
+11
-9
@@ -14,6 +14,7 @@ The `GRM-N` prefix is mandatory — CI extracts it for merge messages and Vikunj
|
||||
|
||||
## Commit Format
|
||||
|
||||
### Feature branches
|
||||
Use **conventional commits** on feature branches:
|
||||
|
||||
```
|
||||
@@ -25,17 +26,18 @@ docs: improve README
|
||||
|
||||
Allowed types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `perf`, `test`, `ci`, `build`, `revert`, `BREAKING CHANGE`.
|
||||
|
||||
**Do NOT** include the `GRM-N:` prefix in commit messages on feature branches — it is added automatically during squash merge.
|
||||
**Do NOT** include the `GRM-N:` prefix in commit messages on feature branches.
|
||||
|
||||
## PR Workflow
|
||||
### Master branch (squash merges)
|
||||
Squash commits on `master` must follow:
|
||||
|
||||
1. Create a branch `GRM-N` or `GRM-N-brief-description` from `master`
|
||||
2. Make changes, committing with conventional format
|
||||
3. Push and open a PR against `master`
|
||||
4. CI runs lint, unit tests, and molecule tests
|
||||
5. When all checks pass, add the `ready-to-merge` label
|
||||
6. The auto-merge workflow squash-merges with message `GRM-N: <PR title>`
|
||||
7. The post-merge workflow updates the Vikunja task
|
||||
```
|
||||
GRM-N: <conventional commit message>
|
||||
```
|
||||
|
||||
Example: `GRM-24: fix: resolve molecule idempotence`.
|
||||
|
||||
This format is enforced by the auto-merge workflow, which validates the PR title is a conventional commit before squash-merging and prepending the task ID.
|
||||
|
||||
## Local Testing
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
"""Validate commit messages for GRM.
|
||||
|
||||
Rules:
|
||||
- On master branch: allow anything (merge commits already have task ID).
|
||||
- On feature branches: must use conventional commit format, must NOT include GRM-N prefix.
|
||||
- On feature branches: conventional commits ONLY, must NOT include GRM-N prefix.
|
||||
- On master branch: must follow '<task-id>: <conventional commit>' pattern,
|
||||
e.g. 'GRM-24: fix: resolve timeout'.
|
||||
"""
|
||||
import re
|
||||
import subprocess
|
||||
@@ -41,11 +42,23 @@ def main(args=None) -> None:
|
||||
|
||||
branch = get_branch()
|
||||
|
||||
if branch == "master":
|
||||
return
|
||||
|
||||
subject = first_line(msg)
|
||||
|
||||
if branch == "master":
|
||||
if not TASK_ID_RE.match(subject):
|
||||
print("ERROR: Master branch commits must start with a task ID.")
|
||||
print(" Expected: GRM-N: <conventional commit message>")
|
||||
print(f" Got: {subject}")
|
||||
sys.exit(1)
|
||||
# Strip task-id prefix and validate the remainder as conventional
|
||||
remainder = TASK_ID_RE.sub("", subject).strip()
|
||||
if not CONVENTIONAL_RE.match(remainder):
|
||||
print("ERROR: Master branch commit message must follow conventional format after task ID.")
|
||||
print(" Expected: GRM-N: <type>: <description>")
|
||||
print(f" Got: {subject}")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
if TASK_ID_RE.match(subject):
|
||||
print("ERROR: Do not include task ID (GRM-N) in feature branch commits.")
|
||||
print(" Task ID will be added automatically on merge via CI.")
|
||||
|
||||
@@ -82,11 +82,25 @@ class TestMain:
|
||||
with patch("scripts.validate_commit_msg.get_branch", return_value="GRM-19"):
|
||||
main(["validate_commit_msg.py", msg_path])
|
||||
|
||||
def test_accepts_anything_on_master(self) -> None:
|
||||
def test_accepts_valid_master_commit(self) -> None:
|
||||
msg_path = self._write_msg("GRM-19: feat: add feature")
|
||||
with patch("scripts.validate_commit_msg.get_branch", return_value="master"):
|
||||
main(["validate_commit_msg.py", msg_path])
|
||||
|
||||
def test_rejects_master_without_task_id(self) -> None:
|
||||
msg_path = self._write_msg("feat: add feature")
|
||||
with patch("scripts.validate_commit_msg.get_branch", return_value="master"):
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
main(["validate_commit_msg.py", msg_path])
|
||||
assert exc.value.code == 1
|
||||
|
||||
def test_rejects_master_with_non_conventional_after_task_id(self) -> None:
|
||||
msg_path = self._write_msg("GRM-19: random message")
|
||||
with patch("scripts.validate_commit_msg.get_branch", return_value="master"):
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
main(["validate_commit_msg.py", msg_path])
|
||||
assert exc.value.code == 1
|
||||
|
||||
def test_rejects_non_conventional_on_feature_branch(self) -> None:
|
||||
msg_path = self._write_msg("random message")
|
||||
with patch("scripts.validate_commit_msg.get_branch", return_value="feature"):
|
||||
@@ -106,6 +120,12 @@ class TestMain:
|
||||
|
||||
|
||||
def test_main_module_block() -> None:
|
||||
import tempfile
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
|
||||
f.write("GRM-1: feat: test")
|
||||
msg_path = f.name
|
||||
|
||||
with patch("scripts.validate_commit_msg.get_branch", return_value="master"):
|
||||
import scripts.validate_commit_msg as vcm
|
||||
|
||||
@@ -115,4 +135,6 @@ def test_main_module_block() -> None:
|
||||
source = source.replace('if __name__ == "__main__":\n main()\n', "")
|
||||
namespace = dict(vcm.__dict__)
|
||||
exec(compile(source, vcm.__file__, "exec"), namespace)
|
||||
namespace["main"](["validate_commit_msg.py", "/dev/null"])
|
||||
namespace["main"](["validate_commit_msg.py", msg_path])
|
||||
|
||||
os.unlink(msg_path)
|
||||
|
||||
Reference in New Issue
Block a user