GRM-41: feat: enforce commit naming conventions and workflow discipline
This commit is contained in:
@@ -94,6 +94,33 @@ jobs:
|
|||||||
echo "No user-facing files changed — skipping release dry-run."
|
echo "No user-facing files changed — skipping release dry-run."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
validate-merge:
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
- name: Validate commit message format
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
MSG=$(git log -1 --pretty=%s)
|
||||||
|
echo "Commit message: $MSG"
|
||||||
|
# Allowed formats:
|
||||||
|
# GRM-N <type>: <description> (squash-merge)
|
||||||
|
# release: vX.Y.Z (release commits)
|
||||||
|
# GRM-N <type>: <description> (#M) (squash-merge with PR ref)
|
||||||
|
if echo "$MSG" | grep -qE '^GRM-[0-9]+ [a-z]+: .+'; then
|
||||||
|
echo "OK: GRM-N <conventional> format"
|
||||||
|
elif echo "$MSG" | grep -qE '^release: v[0-9]+\.[0-9]+\.[0-9]+'; then
|
||||||
|
echo "OK: release commit format"
|
||||||
|
else
|
||||||
|
echo "FAIL: commit message does not follow naming convention"
|
||||||
|
echo "Expected: GRM-N <type>: <description> or release: vX.Y.Z"
|
||||||
|
echo "Got: $MSG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
discover-runners:
|
discover-runners:
|
||||||
needs: [detect-changes]
|
needs: [detect-changes]
|
||||||
if: needs.detect-changes.outputs.ansible-changed == 'true'
|
if: needs.detect-changes.outputs.ansible-changed == 'true'
|
||||||
|
|||||||
@@ -103,13 +103,19 @@ REPO_TOKEN=<token> python3 scripts/ci/review_pr.py <pr_number> <owner/repo> \
|
|||||||
```
|
```
|
||||||
|
|
||||||
Then add the `ready-to-merge` label. The auto-merge workflow will:
|
Then add the `ready-to-merge` label. The auto-merge workflow will:
|
||||||
1. **Validate** PR title format and match against Vikunja task title
|
1. **Validate** PR title format (`GRM-N: <vikunja task title>`) and match against Vikunja task title
|
||||||
2. **Check** that at least one APPROVE review exists
|
2. **Check** that at least one APPROVE review exists
|
||||||
3. Wait for all CI checks to pass
|
3. Wait for all CI checks to pass
|
||||||
4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated)
|
4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated, no colon after GRM-N)
|
||||||
5. The post-merge workflow marks the Vikunja task as done
|
5. The post-merge workflow marks the Vikunja task as done
|
||||||
6. The release workflow automatically versions, tags, and publishes (see below)
|
6. The release workflow automatically versions, tags, and publishes (see below)
|
||||||
|
|
||||||
|
> **IMPORTANT**: Never manually merge PRs via the API. Always use the auto-merge
|
||||||
|
> workflow by adding the `ready-to-merge` label. Manual merges bypass the
|
||||||
|
> `GRM-N <conventional>` format enforcement, producing incorrectly named commits.
|
||||||
|
> The CI `validate-merge` job checks every push to master and will fail if a
|
||||||
|
> commit message doesn't match `GRM-N <type>: <description>` or `release: vX.Y.Z`.
|
||||||
|
|
||||||
### CI Path Filtering
|
### CI Path Filtering
|
||||||
|
|
||||||
The CI workflow includes a `detect-changes` job that checks whether any files
|
The CI workflow includes a `detect-changes` job that checks whether any files
|
||||||
|
|||||||
@@ -78,8 +78,19 @@ def main(commit_msg: str, commit_sha: str) -> None:
|
|||||||
|
|
||||||
task_id = extract_task_id(commit_msg)
|
task_id = extract_task_id(commit_msg)
|
||||||
if not task_id:
|
if not task_id:
|
||||||
click.echo(_("No task ID in commit message, skipping Vikunja update. All good — nothing to do here!"))
|
# Allow release commits without GRM-N prefix
|
||||||
return
|
first_line = commit_msg.split("\n")[0]
|
||||||
|
if re.match(r"^release: v\d+\.\d+\.\d+", first_line):
|
||||||
|
click.echo(_("Release commit without task ID, skipping Vikunja update."))
|
||||||
|
return
|
||||||
|
# Non-release commits must have GRM-N prefix — fail loudly
|
||||||
|
raise click.ClickException(
|
||||||
|
_(
|
||||||
|
"No task ID (GRM-N) found in commit message: {msg}\n"
|
||||||
|
"All non-release commits on master must follow format: GRM-N <type>: <description>",
|
||||||
|
msg=first_line,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
client = VikunjaClient(VIKUNJA_API_URL, token)
|
client = VikunjaClient(VIKUNJA_API_URL, token)
|
||||||
vikunja_task_id = 0
|
vikunja_task_id = 0
|
||||||
|
|||||||
@@ -130,10 +130,20 @@ class TestMain:
|
|||||||
assert "VIKUNJA_TOKEN" in result.output
|
assert "VIKUNJA_TOKEN" in result.output
|
||||||
|
|
||||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||||
def test_no_task_id_skips(self) -> None:
|
def test_no_task_id_non_release_fails(self) -> None:
|
||||||
|
"""Non-release commits without GRM-N prefix should fail."""
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(main, ["fix: resolve bug"])
|
result = runner.invoke(main, ["fix: resolve bug"])
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "No task ID" in result.output
|
||||||
|
|
||||||
|
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||||
|
def test_release_commit_without_task_id_skips(self) -> None:
|
||||||
|
"""Release commits without GRM-N prefix should skip gracefully."""
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(main, ["release: v0.3.2"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
assert "Release commit" in result.output
|
||||||
assert "skipping" in result.output
|
assert "skipping" in result.output
|
||||||
|
|
||||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||||
|
|||||||
Reference in New Issue
Block a user