Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37772f21a9 | ||
|
|
b07132e3c6 |
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.4.4] - 2026-06-22
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Configurable task prefix and CWD-relative DOCS_DIR
|
||||||
|
|
||||||
## [0.4.3] - 2026-06-22
|
## [0.4.3] - 2026-06-22
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
||||||
|
|
||||||
__version__ = "0.4.3"
|
__version__ = "0.4.4"
|
||||||
|
|||||||
@@ -34,7 +34,13 @@ from devx.i18n import _
|
|||||||
|
|
||||||
load_dotenv()
|
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"
|
MAPPING_FILE = DOCS_DIR / "mapping.json"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,14 @@
|
|||||||
"""Validate commit messages for devx.
|
"""Validate commit messages for devx.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- On feature branches: conventional commits ONLY, must NOT include DEVX-N prefix.
|
- On feature branches: conventional commits ONLY, must NOT include <PREFIX>-N prefix.
|
||||||
- On master branch: must follow '<task-id>: <conventional commit>' pattern,
|
- On master branch: must follow '<task-id>: <conventional commit>' pattern,
|
||||||
e.g. 'DEVX-24: fix: resolve timeout'.
|
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
|
import re
|
||||||
@@ -12,10 +17,10 @@ import subprocess # nosec B404
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from devx.config import CONVENTIONAL_RE
|
from devx.config import CONVENTIONAL_RE, TASK_PREFIX
|
||||||
from devx.i18n import _
|
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:
|
def first_line(text: str) -> str:
|
||||||
@@ -51,8 +56,9 @@ def main(commit_msg_file: str, branch: str | None) -> None:
|
|||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
_(
|
_(
|
||||||
"Oops! Master branch commits must start with a task ID.\n"
|
"Oops! Master branch commits must start with a task ID.\n"
|
||||||
" Expected: DEVX-N: <conventional commit message>\n"
|
" Expected: {prefix}-N: <conventional commit message>\n"
|
||||||
" Got: {subject}",
|
" Got: {subject}",
|
||||||
|
prefix=TASK_PREFIX,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -61,8 +67,9 @@ def main(commit_msg_file: str, branch: str | None) -> None:
|
|||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
_(
|
_(
|
||||||
"Oops! Master branch commit must follow conventional format after task ID.\n"
|
"Oops! Master branch commit must follow conventional format after task ID.\n"
|
||||||
" Expected: DEVX-N: <type>: <description>\n"
|
" Expected: {prefix}-N: <type>: <description>\n"
|
||||||
" Got: {subject}",
|
" Got: {subject}",
|
||||||
|
prefix=TASK_PREFIX,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -71,8 +78,9 @@ def main(commit_msg_file: str, branch: str | None) -> None:
|
|||||||
if MASTER_TASK_ID_RE.match(subject):
|
if MASTER_TASK_ID_RE.match(subject):
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
_(
|
_(
|
||||||
"Oops! Do not include task ID (DEVX-N) in feature branch commits.\n"
|
"Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n"
|
||||||
" The task ID will be added automatically on merge via CI."
|
" The task ID will be added automatically on merge via CI.",
|
||||||
|
prefix=TASK_PREFIX,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+21
-21
@@ -559,13 +559,6 @@
|
|||||||
"ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
"ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
||||||
"zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE"
|
"zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\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}": {
|
"Oops! Gitea PyPI registry publish failed:\n{stderr}": {
|
||||||
"en": "Oops! Gitea PyPI registry publish failed:\n{stderr}",
|
"en": "Oops! Gitea PyPI registry publish failed:\n{stderr}",
|
||||||
"bg": "Опа! Публикуването в Gitea PyPI registry неуспешно:\n{stderr}",
|
"bg": "Опа! Публикуването в Gitea PyPI registry неуспешно:\n{stderr}",
|
||||||
@@ -573,20 +566,6 @@
|
|||||||
"ru": "Ой! Публикация в Gitea PyPI registry не удалась:\n{stderr}",
|
"ru": "Ой! Публикация в Gitea PyPI registry не удалась:\n{stderr}",
|
||||||
"zh": "哎呀!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: <type>: <description>\n Got: {subject}": {
|
|
||||||
"en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: DEVX-N: <type>: <description>\n Got: {subject}",
|
|
||||||
"bg": "Опа! Commit-ът в клона master трябва да следва конвенционален формат след идентификатора.\n Очаква се: DEVX-N: <type>: <description>\n Получено: {subject}",
|
|
||||||
"de": "Ups! Master-Branch-Commit muss nach der Task-ID dem konventionellen Format folgen.\n Erwartet: DEVX-N: <type>: <description>\n Erhalten: {subject}",
|
|
||||||
"ru": "Ой! Коммит в ветку master после ID задачи должен соответствовать conventional формату.\n Ожидается: DEVX-N: <type>: <description>\n Получено: {subject}",
|
|
||||||
"zh": "哎呀!master 分支提交在任务 ID 后必须遵循 conventional commit 格式。\n 预期格式: DEVX-N: <type>: <description>\n 实际: {subject}"
|
|
||||||
},
|
|
||||||
"Oops! Master branch commits must start with a task ID.\n Expected: DEVX-N: <conventional commit message>\n Got: {subject}": {
|
|
||||||
"en": "Oops! Master branch commits must start with a task ID.\n Expected: DEVX-N: <conventional commit message>\n Got: {subject}",
|
|
||||||
"bg": "Опа! Commit-ите в клона master трябва да започват с идентификатор на задача.\n Очаква се: DEVX-N: <conventional commit message>\n Получено: {subject}",
|
|
||||||
"de": "Ups! Master-Branch-Commits müssen mit einer Task-ID beginnen.\n Erwartet: DEVX-N: <conventional commit message>\n Erhalten: {subject}",
|
|
||||||
"ru": "Ой! Коммиты в ветку master должны начинаться с ID задачи.\n Ожидается: DEVX-N: <conventional commit message>\n Получено: {subject}",
|
|
||||||
"zh": "哎呀!master 分支的提交必须以任务 ID 开头。\n 预期格式: DEVX-N: <conventional commit message>\n 实际: {subject}"
|
|
||||||
},
|
|
||||||
"Oops! No task ID found in .taskid file or branch name '{branch}'.": {
|
"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}'.",
|
"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}'.",
|
"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).",
|
"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).",
|
"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)."
|
"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: <type>: <description>\n Got: {subject}": {
|
||||||
|
"en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
|
||||||
|
"bg": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
|
||||||
|
"de": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
|
||||||
|
"ru": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
|
||||||
|
"zh": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}"
|
||||||
|
},
|
||||||
|
"Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}": {
|
||||||
|
"en": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
|
||||||
|
"bg": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
|
||||||
|
"de": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
|
||||||
|
"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}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,89 @@ class TestMain:
|
|||||||
assert "task ID" in result.output
|
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:
|
def test_main_module_block() -> None:
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user