From 59589cd0a694bac5308088e17fcde72fd6468e5b Mon Sep 17 00:00:00 2001 From: emil Date: Wed, 24 Jun 2026 13:00:42 +0200 Subject: [PATCH 1/2] fix: use branch name as sole task ID source, strip heads/ prefix in release The .taskid file caused merge conflicts on every PR because master's .taskid always differs from the branch's. Branch names already contain the task ID (e.g., DEVX-31-fix-foo), so .taskid is redundant. read_taskid() now only extracts from branch name, ignoring .taskid. Also fix release.py: some git versions return 'heads/master' instead of 'master' from git rev-parse --abbrev-ref HEAD. Closes DEVX-31 --- .gitea/workflows/ci.yml | 2 +- .gitignore | 3 +++ src/devx/ci/auto_merge.py | 30 +++++++++++------------------- src/devx/ci/release.py | 2 ++ src/devx/translations.json | 12 ++++++------ tests/unit/test_auto_merge.py | 17 ++++++++--------- 6 files changed, 31 insertions(+), 35 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 9133910..2bc488f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -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' diff --git a/.gitignore b/.gitignore index 7dcbcc9..80c4140 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ Thumbs.db # Badges .badges/ + +# CI task tracking (derived from branch name, not committed) +.taskid diff --git a/src/devx/ci/auto_merge.py b/src/devx/ci/auto_merge.py index 45d051e..5235cf9 100644 --- a/src/devx/ci/auto_merge.py +++ b/src/devx/ci/auto_merge.py @@ -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: `` +PR title format: ``{PREFIX}-N: `` Merge commit format: ``{PREFIX}-N `` 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, ) ) diff --git a/src/devx/ci/release.py b/src/devx/ci/release.py index 9227957..1146941 100644 --- a/src/devx/ci/release.py +++ b/src/devx/ci/release.py @@ -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: diff --git a/src/devx/translations.json b/src/devx/translations.json index 0081307..fa6c889 100644 --- a/src/devx/translations.json +++ b/src/devx/translations.json @@ -776,12 +776,12 @@ "ru": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: \n Got: {subject}", "zh": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: \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: '.\n Expected: {task_id}: \n Got: {pr_title}": { "bg": "Oops! PR title must follow format '{prefix}-N: '.\n Expected: {task_id}: \n Got: {pr_title}", diff --git a/tests/unit/test_auto_merge.py b/tests/unit/test_auto_merge.py index 1a8fdd9..79794f2 100644 --- a/tests/unit/test_auto_merge.py +++ b/tests/unit/test_auto_merge.py @@ -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) -- -- 2.54.0 From 55a12fb9ad658bf67362f8a3eb627819c28374e9 Mon Sep 17 00:00:00 2001 From: emil Date: Wed, 24 Jun 2026 13:01:51 +0200 Subject: [PATCH 2/2] fix: use branch name as sole task ID source, strip heads/ prefix in release The .taskid file caused merge conflicts on every PR because master's .taskid always differs from the branch's. Branch names already contain the task ID (e.g., DEVX-31-fix-foo), so .taskid is redundant. read_taskid() now only extracts from branch name, ignoring .taskid. Also fix release.py: some git versions return 'heads/master' instead of 'master' from git rev-parse --abbrev-ref HEAD. Closes DEVX-31 --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 80c4140..7dcbcc9 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,3 @@ Thumbs.db # Badges .badges/ - -# CI task tracking (derived from branch name, not committed) -.taskid -- 2.54.0