DEVX-31: Use branch name as sole task ID source #52

Closed
emil wants to merge 2 commits from DEVX-31-taskid-fix into master
5 changed files with 28 additions and 35 deletions
+1 -1
View File
@@ -120,7 +120,7 @@ jobs:
auto-merge:
# Auto-merge runs after all CI checks pass. It reads the task ID
# from .taskid file, validates the PR title, and squash-merges.
# from the branch name, validates the PR title, and squash-merges.
# No manual label or review needed — CI is the quality gate.
needs: [quality, detect-changes, pr-review]
if: github.event_name == 'pull_request'
+11 -19
View File
@@ -1,12 +1,11 @@
#!/usr/bin/env python3
"""Auto-merge PR when all CI checks pass.
Runs as the final job in ci.yml. Reads the task ID from ``.taskid`` file
(falling back to branch name extraction for backwards compatibility),
validates the PR title, and squash-merges with a conventional commit
message prefixed by the task ID.
Runs as the final job in ci.yml. Extracts the task ID from the branch name
(e.g., ``DEVX-31-fix-bug`` → ``DEVX-31``), validates the PR title, and
squash-merges with a conventional commit message prefixed by the task ID.
PR title format: ``{PREFIX}-N: <vikunja task title>``
PR title format: ``{PREFIX}-N: <vikunja task title>``
Merge commit format: ``{PREFIX}-N <conventional commit message>``
The ``{PREFIX}`` is determined by ``DEVX_TASK_PREFIX`` (default: ``DEVX``).
@@ -23,7 +22,6 @@ Usage:
import os
import re
import subprocess # nosec B404
from pathlib import Path
from typing import Any
import click
@@ -63,20 +61,14 @@ def run_cmd(args: list[str], check: bool = True) -> subprocess.CompletedProcess[
def read_taskid(branch: str) -> str:
"""Read task ID from .taskid file, falling back to branch name extraction.
"""Extract task ID from branch name.
The .taskid file is a simple text file containing just the task ID
(e.g., ``DEVX-60``). If the file doesn't exist, extract from the
branch name as a backwards-compatibility fallback.
The branch name is the sole source of truth for the task ID
(e.g., ``DEVX-31-fix-foo`` → ``DEVX-31``). The ``.taskid`` file
is no longer read — it caused merge conflicts on every PR because
master's ``.taskid`` always differs from the branch's.
"""
path = Path(TASKID_FILE)
if path.exists():
task_id = path.read_text(encoding="utf-8").strip()
if task_id:
return task_id
# Fallback: extract from branch name
match = TASK_ID_RE.search(branch)
return match.group(0) if match else ""
return extract_task_id(branch)
def extract_task_id(branch: str) -> str:
@@ -204,7 +196,7 @@ def main(branch: str, pr_title: str, repo: str, pr_number: str) -> None:
if not task_id:
raise click.ClickException(
_(
"Oops! No task ID found in .taskid file or branch name '{branch}'.",
"Oops! No task ID found in branch name '{branch}'.",
branch=branch,
)
)
+2
View File
@@ -547,6 +547,8 @@ def main(dry_run: bool, skip_tests: bool, verify: bool) -> None:
# Ensure we're on master (skip this check in dry-run mode for PR validation)
branch = run_cmd(["git", "rev-parse", "--abbrev-ref", "HEAD"]).stdout.strip()
# Some git versions return "heads/master" instead of "master"
branch = branch.removeprefix("heads/")
if branch != "master" and not dry_run:
raise click.ClickException(_("Release must be run on master, currently on '{branch}'.", branch=branch))
if branch != "master" and dry_run:
+6 -6
View File
@@ -776,12 +776,12 @@
"ru": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
"zh": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}"
},
"Oops! No task ID found in .taskid file or branch name '{branch}'.": {
"bg": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
"de": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
"en": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
"ru": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
"zh": "Oops! No task ID found in .taskid file or branch name '{branch}'."
"Oops! No task ID found in branch name '{branch}'.": {
"bg": "Oops! No task ID found in branch name '{branch}'.",
"de": "Oops! No task ID found in branch name '{branch}'.",
"en": "Oops! No task ID found in branch name '{branch}'.",
"ru": "Oops! No task ID found in branch name '{branch}'.",
"zh": "Oops! No task ID found in branch name '{branch}'."
},
"Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}": {
"bg": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
+8 -9
View File
@@ -21,23 +21,22 @@ from devx.exceptions import APIError
class TestReadTaskid:
def test_reads_from_file(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("DEVX-60\n")
assert read_taskid("some-branch") == "DEVX-60"
def test_falls_back_to_branch_name(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
def test_extracts_from_branch_name(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path)
assert read_taskid("DEVX-19-fix-bug") == "DEVX-19"
def test_returns_empty_when_no_file_no_match(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
def test_returns_empty_when_no_match(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path)
assert read_taskid("feature-branch") == ""
def test_empty_file_falls_back_to_branch(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
def test_ignores_taskid_file(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
""".taskid file is no longer used — branch name is the sole source."""
monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("\n")
(tmp_path / ".taskid").write_text("DEVX-60\n")
# Even with .taskid present, branch name takes priority
assert read_taskid("DEVX-42-test") == "DEVX-42"
# And branches without task ID return empty even if .taskid exists
assert read_taskid("some-branch") == ""
# -- extract_task_id (legacy fallback) --