Public Access
DEVX-31: Use branch name as sole task ID source #52
@@ -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
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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) --
|
||||
|
||||
Reference in New Issue
Block a user