diff --git a/.taskid b/.taskid index b053605..272a5b4 100644 --- a/.taskid +++ b/.taskid @@ -1 +1 @@ -DEVX-8 +DEVX-9 diff --git a/src/devx/ci/sync_wiki.py b/src/devx/ci/sync_wiki.py index 7623a5d..e66fa45 100644 --- a/src/devx/ci/sync_wiki.py +++ b/src/devx/ci/sync_wiki.py @@ -34,7 +34,13 @@ from devx.i18n import _ load_dotenv() -DOCS_DIR = Path(__file__).resolve().parent.parent.parent.parent / "docs" +# DOCS_DIR is the repo's docs/ directory. When devx is installed as a +# package (e.g., in .venv/lib/python3.12/site-packages/devx/), the +# __file__-relative path would point inside the venv, not the repo. +# Use DEVX_DOCS_DIR env var if set, otherwise fall back to ./docs +# (relative to the current working directory, which is the repo root +# in CI and local development). +DOCS_DIR = Path(os.environ.get("DEVX_DOCS_DIR", "docs")) MAPPING_FILE = DOCS_DIR / "mapping.json" diff --git a/src/devx/ci/validate_commit_msg.py b/src/devx/ci/validate_commit_msg.py index 7b0ddd8..b0f4a0a 100644 --- a/src/devx/ci/validate_commit_msg.py +++ b/src/devx/ci/validate_commit_msg.py @@ -2,9 +2,14 @@ """Validate commit messages for devx. Rules: -- On feature branches: conventional commits ONLY, must NOT include DEVX-N prefix. +- On feature branches: conventional commits ONLY, must NOT include -N prefix. - On master branch: must follow ': ' pattern, e.g. 'DEVX-24: fix: resolve timeout'. + +The task ID prefix is configurable via the ``DEVX_TASK_PREFIX`` environment +variable (default: ``DEVX``). Projects consuming devx (e.g., GRM) set +their own prefix (e.g., ``GRM``) so the validator enforces the correct +task ID format for each project. """ import re @@ -12,10 +17,10 @@ import subprocess # nosec B404 import click -from devx.config import CONVENTIONAL_RE +from devx.config import CONVENTIONAL_RE, TASK_PREFIX from devx.i18n import _ -MASTER_TASK_ID_RE = re.compile(r"^DEVX-\d+:") +MASTER_TASK_ID_RE = re.compile(rf"^{TASK_PREFIX}-\d+:") def first_line(text: str) -> str: @@ -51,8 +56,9 @@ def main(commit_msg_file: str, branch: str | None) -> None: raise click.ClickException( _( "Oops! Master branch commits must start with a task ID.\n" - " Expected: DEVX-N: \n" + " Expected: {prefix}-N: \n" " Got: {subject}", + prefix=TASK_PREFIX, subject=subject, ) ) @@ -61,8 +67,9 @@ def main(commit_msg_file: str, branch: str | None) -> None: raise click.ClickException( _( "Oops! Master branch commit must follow conventional format after task ID.\n" - " Expected: DEVX-N: : \n" + " Expected: {prefix}-N: : \n" " Got: {subject}", + prefix=TASK_PREFIX, subject=subject, ) ) @@ -71,8 +78,9 @@ def main(commit_msg_file: str, branch: str | None) -> None: if MASTER_TASK_ID_RE.match(subject): raise click.ClickException( _( - "Oops! Do not include task ID (DEVX-N) in feature branch commits.\n" - " The task ID will be added automatically on merge via CI." + "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n" + " The task ID will be added automatically on merge via CI.", + prefix=TASK_PREFIX, ) ) diff --git a/src/devx/translations.json b/src/devx/translations.json index bb29b30..c37afef 100644 --- a/src/devx/translations.json +++ b/src/devx/translations.json @@ -559,13 +559,6 @@ "ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: : \n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: : \n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE" }, - "Oops! Do not include task ID (DEVX-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.": { - "en": "Oops! Do not include task ID (DEVX-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.", - "bg": "Опа! Не включвайте идентификатор на задача (DEVX-N) в commit-и от feature клонове.\n Идентификаторът ще бъде добавен автоматично при сливане чрез CI.", - "de": "Ups! Keine Task-ID (DEVX-N) in Feature-Branch-Commits einfügen.\n Die Task-ID wird beim Merge automatisch über CI hinzugefügt.", - "ru": "Ой! Не включайте ID задачи (DEVX-N) в коммиты feature-веток.\n ID задачи будет добавлен автоматически при слиянии через CI.", - "zh": "哎呀!不要在 feature 分支的提交中包含任务 ID (DEVX-N)。\n 任务 ID 将在通过 CI 合并时自动添加。" - }, "Oops! Gitea PyPI registry publish failed:\n{stderr}": { "en": "Oops! Gitea PyPI registry publish failed:\n{stderr}", "bg": "Опа! Публикуването в Gitea PyPI registry неуспешно:\n{stderr}", @@ -573,20 +566,6 @@ "ru": "Ой! Публикация в Gitea PyPI registry не удалась:\n{stderr}", "zh": "哎呀!Gitea PyPI registry 发布失败:\n{stderr}" }, - "Oops! Master branch commit must follow conventional format after task ID.\n Expected: DEVX-N: : \n Got: {subject}": { - "en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: DEVX-N: : \n Got: {subject}", - "bg": "Опа! Commit-ът в клона master трябва да следва конвенционален формат след идентификатора.\n Очаква се: DEVX-N: : \n Получено: {subject}", - "de": "Ups! Master-Branch-Commit muss nach der Task-ID dem konventionellen Format folgen.\n Erwartet: DEVX-N: : \n Erhalten: {subject}", - "ru": "Ой! Коммит в ветку master после ID задачи должен соответствовать conventional формату.\n Ожидается: DEVX-N: : \n Получено: {subject}", - "zh": "哎呀!master 分支提交在任务 ID 后必须遵循 conventional commit 格式。\n 预期格式: DEVX-N: : \n 实际: {subject}" - }, - "Oops! Master branch commits must start with a task ID.\n Expected: DEVX-N: \n Got: {subject}": { - "en": "Oops! Master branch commits must start with a task ID.\n Expected: DEVX-N: \n Got: {subject}", - "bg": "Опа! Commit-ите в клона master трябва да започват с идентификатор на задача.\n Очаква се: DEVX-N: \n Получено: {subject}", - "de": "Ups! Master-Branch-Commits müssen mit einer Task-ID beginnen.\n Erwartet: DEVX-N: \n Erhalten: {subject}", - "ru": "Ой! Коммиты в ветку master должны начинаться с ID задачи.\n Ожидается: DEVX-N: \n Получено: {subject}", - "zh": "哎呀!master 分支的提交必须以任务 ID 开头。\n 预期格式: DEVX-N: \n 实际: {subject}" - }, "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}'.", "bg": "Oops! No task ID found in .taskid file or branch name '{branch}'.", @@ -1041,5 +1020,26 @@ "de": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).", "ru": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).", "zh": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1)." + }, + "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.": { + "en": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.", + "bg": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.", + "de": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.", + "ru": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.", + "zh": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI." + }, + "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: : \n Got: {subject}": { + "en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: : \n Got: {subject}", + "bg": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: : \n Got: {subject}", + "de": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: : \n Got: {subject}", + "ru": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: : \n Got: {subject}", + "zh": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: : \n Got: {subject}" + }, + "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: \n Got: {subject}": { + "en": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: \n Got: {subject}", + "bg": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: \n Got: {subject}", + "de": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: \n Got: {subject}", + "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}" } } diff --git a/tests/unit/test_validate_commit_msg.py b/tests/unit/test_validate_commit_msg.py index b4fa859..b2ec8ee 100644 --- a/tests/unit/test_validate_commit_msg.py +++ b/tests/unit/test_validate_commit_msg.py @@ -152,6 +152,89 @@ class TestMain: assert "task ID" in result.output +class TestCustomPrefix: + """Tests for custom task ID prefix (e.g., GRM-N instead of DEVX-N). + + The prefix is configured via the DEVX_TASK_PREFIX environment variable. + This is critical for consumer projects like GRM that use their own + Vikunja project with a different identifier prefix. + """ + + def _write_msg(self, content: str) -> str: + fd, path = tempfile.mkstemp() + with os.fdopen(fd, "w") as f: + f.write(content) + return path + + @patch.dict("os.environ", {"DEVX_TASK_PREFIX": "GRM"}) + def test_master_accepts_grm_prefix(self) -> None: + """Master branch accepts GRM-N: prefix when DEVX_TASK_PREFIX=GRM.""" + import importlib + + import devx.ci.validate_commit_msg as vcm + import devx.config + + importlib.reload(devx.config) + importlib.reload(vcm) + try: + msg_path = self._write_msg("GRM-66: fix: add scripts/** to infrastructure") + with patch("devx.ci.validate_commit_msg.get_branch", return_value="master"): + runner = CliRunner() + result = runner.invoke(vcm.main, [msg_path]) + assert result.exit_code == 0 + os.unlink(msg_path) + finally: + os.environ.pop("DEVX_TASK_PREFIX", None) + importlib.reload(devx.config) + importlib.reload(vcm) + + @patch.dict("os.environ", {"DEVX_TASK_PREFIX": "GRM"}) + def test_master_rejects_devx_prefix_when_grm_configured(self) -> None: + """Master branch rejects DEVX-N: prefix when DEVX_TASK_PREFIX=GRM.""" + import importlib + + import devx.ci.validate_commit_msg as vcm + import devx.config + + importlib.reload(devx.config) + importlib.reload(vcm) + try: + msg_path = self._write_msg("DEVX-8: fix: wrong prefix") + with patch("devx.ci.validate_commit_msg.get_branch", return_value="master"): + runner = CliRunner() + result = runner.invoke(vcm.main, [msg_path]) + assert result.exit_code == 1 + assert "GRM-N" in result.output + os.unlink(msg_path) + finally: + os.environ.pop("DEVX_TASK_PREFIX", None) + importlib.reload(devx.config) + importlib.reload(vcm) + + @patch.dict("os.environ", {"DEVX_TASK_PREFIX": "GRM"}) + def test_feature_branch_rejects_grm_prefix(self) -> None: + """Feature branch rejects GRM-N: prefix when DEVX_TASK_PREFIX=GRM.""" + import importlib + + import devx.ci.validate_commit_msg as vcm + import devx.config + + importlib.reload(devx.config) + importlib.reload(vcm) + try: + msg_path = self._write_msg("GRM-66: fix: should not have prefix on branch") + with patch("devx.ci.validate_commit_msg.get_branch", return_value="GRM-66-fix"): + runner = CliRunner() + result = runner.invoke(vcm.main, [msg_path]) + assert result.exit_code == 1 + assert "task ID" in result.output + os.unlink(msg_path) + finally: + os.environ.pop("DEVX_TASK_PREFIX", None) + importlib.reload(devx.config) + importlib.reload(vcm) + + def test_main_module_block() -> None: import tempfile