diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c7e00fd..ed9076b 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -94,6 +94,33 @@ jobs: echo "No user-facing files changed — skipping release dry-run." 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 : (squash-merge) + # release: vX.Y.Z (release commits) + # GRM-N : (#M) (squash-merge with PR ref) + if echo "$MSG" | grep -qE '^GRM-[0-9]+ [a-z]+: .+'; then + echo "OK: GRM-N 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 : or release: vX.Y.Z" + echo "Got: $MSG" + exit 1 + fi + discover-runners: needs: [detect-changes] if: needs.detect-changes.outputs.ansible-changed == 'true' diff --git a/AGENTS.md b/AGENTS.md index f7d76c7..a102cd3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,13 +103,19 @@ REPO_TOKEN= python3 scripts/ci/review_pr.py \ ``` 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: `) and match against Vikunja task title 2. **Check** that at least one APPROVE review exists 3. Wait for all CI checks to pass -4. Squash-merge with title: `GRM-N ` (space-separated) +4. Squash-merge with title: `GRM-N ` (space-separated, no colon after GRM-N) 5. The post-merge workflow marks the Vikunja task as done 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 ` 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 : ` or `release: vX.Y.Z`. + ### CI Path Filtering The CI workflow includes a `detect-changes` job that checks whether any files diff --git a/scripts/ci/post_merge.py b/scripts/ci/post_merge.py index e761611..ca46dee 100644 --- a/scripts/ci/post_merge.py +++ b/scripts/ci/post_merge.py @@ -78,8 +78,19 @@ def main(commit_msg: str, commit_sha: str) -> None: task_id = extract_task_id(commit_msg) if not task_id: - click.echo(_("No task ID in commit message, skipping Vikunja update. All good — nothing to do here!")) - return + # Allow release commits without GRM-N prefix + 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 : ", + msg=first_line, + ) + ) client = VikunjaClient(VIKUNJA_API_URL, token) vikunja_task_id = 0 diff --git a/tests/unit/test_post_merge.py b/tests/unit/test_post_merge.py index a22fd3f..d1a2585 100644 --- a/tests/unit/test_post_merge.py +++ b/tests/unit/test_post_merge.py @@ -130,10 +130,20 @@ class TestMain: assert "VIKUNJA_TOKEN" in result.output @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() 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 "Release commit" in result.output assert "skipping" in result.output @patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})