Compare commits

...
19 Commits
Author SHA1 Message Date
devx-ci-bot 6053fb9fba release: v0.9.1 [skip ci] 2026-06-24 01:50:03 +02:00
emil e76741bfad DEVX-21: fix: always start dockerd in CI runner for molecule tests
Post-merge / detect-type (push) Successful in 6s
Post-merge / validate-commit-msg (push) Successful in 14s
Post-merge / configure-repo (push) Successful in 14s
Post-merge / release (push) Successful in 40s
Post-merge / vikunja (push) Successful in 15s
Post-merge / sync-wiki (push) Successful in 51s
Post-merge / badges (push) Successful in 59s
2026-06-23 23:49:13 +00:00
devx-ci-bot f206a9cd8d release: v0.9.0 [skip ci] 2026-06-24 01:29:24 +02:00
emil b4b7428f9c DEVX-20: feat: extract Docker daemon start to tested Python module
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / release (push) Successful in 51s
Post-merge / vikunja (push) Successful in 18s
Post-merge / badges (push) Successful in 45s
Post-merge / sync-wiki (push) Successful in 47s
2026-06-23 23:28:20 +00:00
devx-ci-bot 0d9e76a838 release: v0.8.5 [skip ci] 2026-06-24 01:02:21 +02:00
emil 034cbde2f7 DEVX-19: fix: retry pip install with --ignore-installed only on failure
Post-merge / detect-type (push) Successful in 6s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / configure-repo (push) Successful in 14s
Post-merge / release (push) Successful in 39s
Post-merge / vikunja (push) Successful in 13s
Post-merge / sync-wiki (push) Successful in 36s
Post-merge / badges (push) Successful in 41s
2026-06-23 23:01:32 +00:00
devx-ci-bot e0abe6f176 release: v0.8.4 [skip ci] 2026-06-23 22:28:29 +00:00
emil 15f6837dc2 DEVX-18: fix: add --ignore-installed to pip in CI to bypass debian packages
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 13s
Post-merge / release (push) Successful in 45s
Post-merge / vikunja (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 39s
Post-merge / badges (push) Successful in 42s
2026-06-23 22:27:35 +00:00
devx-ci-bot b4dda91e24 release: v0.8.3 [skip ci] 2026-06-23 21:58:33 +00:00
emil 3e21e774f7 DEVX-17: fix: pass --break-system-packages to pip in CI environments
Post-merge / detect-type (push) Successful in 9s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / release (push) Successful in 45s
Post-merge / sync-wiki (push) Successful in 38s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / vikunja (push) Successful in 14s
Post-merge / badges (push) Successful in 41s
2026-06-23 21:57:37 +00:00
emil 7c11215e57 DEVX-16: fix: lower check_test_speed threshold to 4 seconds
Post-merge / detect-type (push) Successful in 23s
Post-merge / configure-repo (push) Successful in 13s
Post-merge / release (push) Successful in 51s
Post-merge / validate-commit-msg (push) Successful in 12s
Post-merge / badges (push) Successful in 57s
Post-merge / vikunja (push) Successful in 19s
Post-merge / sync-wiki (push) Successful in 1m29s
2026-06-23 20:35:53 +00:00
devx-ci-bot 5384269c83 release: v0.8.2 [skip ci] 2026-06-23 21:44:03 +02:00
emil b3d0dd8ca7 DEVX-15: fix: encode spaces in pair commands to survive shell word-splitting
Post-merge / detect-type (push) Successful in 15s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / vikunja (push) Successful in 31s
Post-merge / sync-wiki (push) Successful in 47s
Post-merge / badges (push) Successful in 1m3s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / release (push) Successful in 47s
2026-06-23 19:42:56 +00:00
devx-ci-bot a7dcaee5c6 release: v0.8.1 [skip ci] 2026-06-23 20:45:02 +02:00
emil 02f8d3757b DEVX-14: fix: set fresh MOLECULE_HOME per pair to avoid stale config cache
Post-merge / detect-type (push) Successful in 14s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 20s
Post-merge / sync-wiki (push) Successful in 58s
Post-merge / badges (push) Successful in 1m6s
Post-merge / release (push) Successful in 48s
Post-merge / vikunja (push) Successful in 21s
2026-06-23 18:43:58 +00:00
devx-ci-bot 4311fb7648 release: v0.8.0 [skip ci] 2026-06-23 20:11:18 +02:00
emil 2ead959fcf DEVX-14: feat: fix molecule platforms to use sleep infinity, add --platforms-file
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 20s
Post-merge / release (push) Successful in 47s
Post-merge / vikunja (push) Successful in 20s
Post-merge / sync-wiki (push) Successful in 50s
Post-merge / badges (push) Successful in 1m0s
2026-06-23 18:10:19 +00:00
devx-ci-bot 9a60009d29 release: v0.7.0 [skip ci] 2026-06-23 18:26:54 +02:00
emil c20dfd185a DEVX-13: feat: add per-test timing quality gate to check_test_speed
Post-merge / detect-type (push) Successful in 12s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 26s
Post-merge / release (push) Successful in 43s
Post-merge / vikunja (push) Successful in 16s
Post-merge / sync-wiki (push) Successful in 48s
Post-merge / badges (push) Successful in 58s
2026-06-23 16:25:50 +00:00
20 changed files with 1937 additions and 1270 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
PYTHONPATH: src PYTHONPATH: src
run: | run: |
. .venv/bin/activate . .venv/bin/activate
python3 -m devx.tools.check_test_speed --max-seconds 60 python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
- name: Documentation coverage check - name: Documentation coverage check
env: env:
PYTHONPATH: src PYTHONPATH: src
+1 -1
View File
@@ -1 +1 @@
DEVX-12 DEVX-21
+55
View File
@@ -2,6 +2,61 @@
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.9.1] - 2026-06-23
### Bug Fixes
- Always start dockerd in CI runner for molecule tests
## [0.9.0] - 2026-06-23
### Features
- Extract Docker daemon start to tested Python module
## [0.8.5] - 2026-06-23
### Bug Fixes
- Retry pip install with --ignore-installed only on failure
## [0.8.4] - 2026-06-23
### Bug Fixes
- Add --ignore-installed to pip in CI to bypass debian packages
## [0.8.3] - 2026-06-23
### Bug Fixes
- Lower check_test_speed threshold to 4 seconds
- Pass --break-system-packages to pip in CI environments
## [0.8.2] - 2026-06-23
### Bug Fixes
- Encode spaces in pair commands to survive shell word-splitting
## [0.8.1] - 2026-06-23
### Bug Fixes
- Set fresh MOLECULE_HOME per pair to avoid stale config cache
## [0.8.0] - 2026-06-23
### Features
- Fix molecule platforms to use sleep infinity, add --platforms-file
## [0.7.0] - 2026-06-23
### Features
- Add per-test timing quality gate to check_test_speed
## [0.6.0] - 2026-06-23 ## [0.6.0] - 2026-06-23
### Features ### Features
+7 -1
View File
@@ -76,7 +76,13 @@ Validate commit messages for conventional commit format.
### `devx tools check-test-speed` ### `devx tools check-test-speed`
Run unit tests and enforce a maximum execution-time budget. Run unit tests and enforce execution-time budgets:
- **Total suite time** must not exceed `--max-seconds` (default: 10s).
- **Per-test time** — no individual test may exceed `--max-single-seconds` (default: 0.5s, 0 to disable).
```bash
python3 -m devx.tools.check_test_speed --max-seconds 10 --max-single-seconds 0.5
```
### `devx tools configure-repo` ### `devx tools configure-repo`
+4 -3
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# pre-commit hook: fail if unit tests take longer than 10 seconds. # pre-commit hook: fail if unit tests are too slow.
# Aligned with CI timeout (ci.yml uses --max-seconds 10). # Checks both total suite time (10s) and per-test time (0.5s).
# Aligned with CI (ci.yml uses same thresholds).
set -e set -e
export PYTHONPATH=src export PYTHONPATH=src
python3 -m devx.tools.check_test_speed --max-seconds 10 python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
+1 -1
View File
@@ -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.6.0" __version__ = "0.9.1"
+20 -11
View File
@@ -25,7 +25,7 @@ from pathlib import Path
import click import click
from devx.i18n import _ from devx.i18n import _
from devx.molecule.platforms import PLATFORMS from devx.molecule.platforms import PLATFORMS, load_platforms
DEFAULT_MAX_RUNNERS = 3 DEFAULT_MAX_RUNNERS = 3
MOLECULE_ROOT = Path("ansible/roles/gitea-runner/molecule") MOLECULE_ROOT = Path("ansible/roles/gitea-runner/molecule")
@@ -41,7 +41,8 @@ class TestPair:
def encode(self) -> str: def encode(self) -> str:
"""Serialize to a pipe-delimited string for CI consumption.""" """Serialize to a pipe-delimited string for CI consumption."""
return f"{self.scenario}|{self.platform['name']}|{self.platform['image']}|{self.platform['command']}" cmd = self.platform["command"].replace(" ", "__SPACE__")
return f"{self.scenario}|{self.platform['name']}|{self.platform['image']}|{cmd}"
@staticmethod @staticmethod
def decode(encoded: str) -> TestPair: def decode(encoded: str) -> TestPair:
@@ -49,7 +50,7 @@ class TestPair:
parts = encoded.split("|") parts = encoded.split("|")
return TestPair( return TestPair(
scenario=parts[0], scenario=parts[0],
platform={"name": parts[1], "image": parts[2], "command": parts[3]}, platform={"name": parts[1], "image": parts[2], "command": parts[3].replace("__SPACE__", " ")},
) )
@@ -63,9 +64,8 @@ class MultiRoleTestPair:
def encode(self) -> str: def encode(self) -> str:
"""Serialize to a pipe-delimited string: ``role|scenario|platform_name|image|command``.""" """Serialize to a pipe-delimited string: ``role|scenario|platform_name|image|command``."""
return ( cmd = self.platform["command"].replace(" ", "__SPACE__")
f"{self.role}|{self.scenario}|{self.platform['name']}|{self.platform['image']}|{self.platform['command']}" return f"{self.role}|{self.scenario}|{self.platform['name']}|{self.platform['image']}|{cmd}"
)
@staticmethod @staticmethod
def decode(encoded: str) -> MultiRoleTestPair: def decode(encoded: str) -> MultiRoleTestPair:
@@ -74,7 +74,7 @@ class MultiRoleTestPair:
return MultiRoleTestPair( return MultiRoleTestPair(
role=parts[0], role=parts[0],
scenario=parts[1], scenario=parts[1],
platform={"name": parts[2], "image": parts[3], "command": parts[4]}, platform={"name": parts[2], "image": parts[3], "command": parts[4].replace("__SPACE__", " ")},
) )
@@ -238,6 +238,13 @@ def _write_github_env(key: str, value: str) -> None:
help="Roles directory for multi-role discovery (scans */molecule/*/). " help="Roles directory for multi-role discovery (scans */molecule/*/). "
"Use this for projects with multiple Ansible roles. Default: disabled (single-role mode).", "Use this for projects with multiple Ansible roles. Default: disabled (single-role mode).",
) )
@click.option(
"--platforms-file",
type=click.Path(exists=True, file_okay=True, path_type=Path),
default=None,
help="JSON file with custom platform list (each entry: name, image, command). "
"Overrides the default platform matrix. Useful for projects with custom test images.",
)
def cli( def cli(
runner_index: int | None, runner_index: int | None,
max_runners: int, max_runners: int,
@@ -247,7 +254,9 @@ def cli(
skip_if_excess: bool, skip_if_excess: bool,
molecule_root: Path | None, molecule_root: Path | None,
roles_root: Path | None, roles_root: Path | None,
platforms_file: Path | None,
) -> None: ) -> None:
platforms = load_platforms(platforms_file)
# Multi-role mode: discover (role, scenario) pairs across all roles # Multi-role mode: discover (role, scenario) pairs across all roles
if roles_root is not None: if roles_root is not None:
role_scenarios = discover_multi_role_scenarios(roles_root) role_scenarios = discover_multi_role_scenarios(roles_root)
@@ -256,10 +265,10 @@ def cli(
click.echo(f"{role}|{scenario}") click.echo(f"{role}|{scenario}")
return return
if list_platforms: if list_platforms:
for p in PLATFORMS: for p in platforms:
click.echo(f"{p['name']}|{p['image']}|{p['command']}") click.echo(f"{p['name']}|{p['image']}|{p['command']}")
return return
pairs_mr = build_multi_role_pairs(role_scenarios) pairs_mr = build_multi_role_pairs(role_scenarios, platforms)
if runner_index is None: if runner_index is None:
groups = distribute_multi_role(pairs_mr, max_runners) groups = distribute_multi_role(pairs_mr, max_runners)
for i, group in enumerate(groups): for i, group in enumerate(groups):
@@ -291,10 +300,10 @@ def cli(
click.echo(s) click.echo(s)
return return
if list_platforms: if list_platforms:
for p in PLATFORMS: for p in platforms:
click.echo(f"{p['name']}|{p['image']}|{p['command']}") click.echo(f"{p['name']}|{p['image']}|{p['command']}")
return return
pairs = build_pairs(scenarios) pairs = build_pairs(scenarios, platforms)
if runner_index is None: if runner_index is None:
groups = distribute(pairs, max_runners) groups = distribute(pairs, max_runners)
for i, group in enumerate(groups): for i, group in enumerate(groups):
+10 -2
View File
@@ -114,12 +114,14 @@ def parse_pair(pair: str) -> tuple[str, str, str, str, str]:
Supports both 4-part (single-role) and 5-part (multi-role) formats. Supports both 4-part (single-role) and 5-part (multi-role) formats.
For 4-part pairs, role is empty (caller uses default role dir). For 4-part pairs, role is empty (caller uses default role dir).
Spaces in the command field are encoded as ``__SPACE__`` to survive
shell word-splitting when ``$TEST_PAIRS`` is expanded unquoted.
""" """
parts = pair.split("|") parts = pair.split("|")
if len(parts) == 4: if len(parts) == 4:
return "", parts[0], parts[1], parts[2], parts[3] return "", parts[0], parts[1], parts[2], parts[3].replace("__SPACE__", " ")
if len(parts) == 5: if len(parts) == 5:
return parts[0], parts[1], parts[2], parts[3], parts[4] return parts[0], parts[1], parts[2], parts[3], parts[4].replace("__SPACE__", " ")
raise click.ClickException(f"Invalid pair format: {pair!r} (expected 4 or 5 pipe-delimited parts)") raise click.ClickException(f"Invalid pair format: {pair!r} (expected 4 or 5 pipe-delimited parts)")
@@ -134,6 +136,12 @@ def build_env_for_pair(pair: str, base_env: dict[str, str]) -> dict[str, str]:
elif "MOLECULE_PLATFORM_COMMAND" in env: elif "MOLECULE_PLATFORM_COMMAND" in env:
del env["MOLECULE_PLATFORM_COMMAND"] del env["MOLECULE_PLATFORM_COMMAND"]
env["ANSIBLE_ALLOW_BROKEN_CONDITIONALS"] = "true" env["ANSIBLE_ALLOW_BROKEN_CONDITIONALS"] = "true"
# Use a fresh MOLECULE_HOME per pair to avoid stale config cache
# from previous CI runs (causes "Instances missing" errors).
if "MOLECULE_HOME" not in env:
import tempfile
env["MOLECULE_HOME"] = tempfile.mkdtemp(prefix="molecule-ci-")
return env return env
+33 -7
View File
@@ -10,13 +10,39 @@ dev tools and CI scripts.
from __future__ import annotations from __future__ import annotations
#: Supported OS platform matrix. import json
from pathlib import Path
#: Default supported OS platform matrix.
#: Each entry maps a short name to (image, command). #: Each entry maps a short name to (image, command).
#: The command must be systemd since rootless Docker requires #: Uses the project's pre-built molecule-test-base image with
#: loginctl/systemctl --user. #: ``sleep infinity`` (NOT systemd) to avoid cgroup v2 failures.
PLATFORMS: list[dict[str, str]] = [ PLATFORMS: list[dict[str, str]] = [
{"name": "ubuntu-2204", "image": "geerlingguy/docker-ubuntu2204-ansible:latest", "command": "/lib/systemd/systemd"}, {
{"name": "ubuntu-2404", "image": "geerlingguy/docker-ubuntu2404-ansible:latest", "command": "/lib/systemd/systemd"}, "name": "ubuntu-2604",
{"name": "debian-12", "image": "geerlingguy/docker-debian12-ansible:latest", "command": "/lib/systemd/systemd"}, "image": "git.oblachno.oblachno.fyi/oblachno/molecule-test-base:latest",
{"name": "archlinux", "image": "marcstraube/archlinux-ansible:latest", "command": "/usr/lib/systemd/systemd"}, "command": "sleep infinity",
},
] ]
def load_platforms(platforms_file: str | Path | None = None) -> list[dict[str, str]]:
"""Load platforms from a JSON file, falling back to PLATFORMS.
Args:
platforms_file: Path to a JSON file with a list of platform dicts.
Each dict must have ``name``, ``image``, and ``command`` keys.
Returns:
List of platform dictionaries.
"""
if platforms_file is None:
return PLATFORMS
path = Path(platforms_file)
if not path.is_file():
return PLATFORMS
with path.open() as f:
data = json.load(f)
if not isinstance(data, list) or not data:
return PLATFORMS
return data
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Start a Docker daemon inside a CI runner container (Docker-in-Docker).
CI runners (e.g. ``gitea/runner-images:ubuntu-latest``) may have the host's
Docker socket mounted, but molecule needs a local Docker daemon to create
nested containers. This module always starts ``dockerd`` in the background
and waits for it to become ready.
Usage::
python3 -m devx.molecule.start_docker [--timeout 30]
"""
from __future__ import annotations
import subprocess # nosec B404
import sys
import time
import click
from devx.i18n import _
DEFAULT_TIMEOUT = 30
DOCKERD_LOG = "/var/log/dockerd.log"
def is_docker_ready() -> bool:
"""Check if the Docker daemon is responding."""
result = subprocess.run( # nosec B603 B607
["docker", "info"],
capture_output=True,
check=False,
)
return result.returncode == 0
def start_docker_daemon(timeout: int = DEFAULT_TIMEOUT) -> bool:
"""Start dockerd in the background and wait for it to be ready.
Always starts a local dockerd even if ``docker info`` succeeds,
because the host socket may be mounted but not suitable for
molecule's nested container creation.
Returns ``True`` if Docker is ready, ``False`` if it failed to
start within the timeout.
"""
click.echo(_("Starting Docker daemon..."))
log_file = open(DOCKERD_LOG, "w") # noqa: SIM115
subprocess.Popen( # nosec B603 B607
["dockerd"],
stdout=log_file,
stderr=subprocess.STDOUT,
start_new_session=True,
)
for _i in range(timeout):
if is_docker_ready():
click.echo(_("Docker daemon started"))
return True
time.sleep(1)
click.echo(_("Docker daemon failed to start"))
return False
@click.command()
@click.option(
"--timeout",
default=DEFAULT_TIMEOUT,
type=int,
help="Seconds to wait for Docker daemon to start (default: 30).",
)
def main(timeout: int) -> None:
"""Start Docker daemon for CI molecule tests."""
if start_docker_daemon(timeout):
sys.exit(0)
sys.exit(1)
if __name__ == "__main__": # pragma: no cover
main()
+95 -11
View File
@@ -1,12 +1,21 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Run unit tests and enforce a maximum execution-time budget. """Run unit tests and enforce execution-time budgets.
Checks two quality gates:
1. **Total suite time** must not exceed ``--max-seconds``.
2. **Per-test time** — no individual test may exceed ``--max-single-seconds``.
Usage: Usage:
python3 -m devx.tools.check_test_speed [--max-seconds N] python3 -m devx.tools.check_test_speed [--max-seconds N] [--max-single-seconds S]
The module runs ``make test-unit`` with ``PYTEST_ADDOPTS=--durations=0`` so
that pytest emits per-test timing lines alongside the summary. Both the
total wall-clock time and individual test durations are parsed and validated.
""" """
from __future__ import annotations from __future__ import annotations
import os
import re import re
import subprocess # nosec B404 import subprocess # nosec B404
@@ -14,18 +23,32 @@ import click
from devx.i18n import _ from devx.i18n import _
DEFAULT_MAX_SECONDS = 2.0 DEFAULT_MAX_SECONDS = 10.0
DEFAULT_MAX_SINGLE_SECONDS = 0.5
TEST_COMMAND = ["make", "test-unit"] TEST_COMMAND = ["make", "test-unit"]
# Matches pytest summary line: "234 passed in 0.70s"
_TIMING_RE = re.compile(r"(\d+) passed.* in ([0-9.]+)s") _TIMING_RE = re.compile(r"(\d+) passed.* in ([0-9.]+)s")
# Matches per-test duration lines from --durations=0:
# 0.51s call tests/test_foo.py::test_bar
_DURATION_LINE_RE = re.compile(r"^(\d+\.?\d*)s\s+(?:setup|call|teardown)\s+(.+)$")
def run_tests() -> tuple[str, str]: def run_tests() -> tuple[str, str]:
"""Execute the unit-test suite and return (stdout, stderr).""" """Execute the unit-test suite and return (stdout, stderr).
Sets ``PYTEST_ADDOPTS=--durations=0`` so pytest emits per-test timings.
"""
env = os.environ.copy()
existing = env.get("PYTEST_ADDOPTS", "")
env["PYTEST_ADDOPTS"] = f"--durations=0 {existing}".strip()
result = subprocess.run( # nosec B603 result = subprocess.run( # nosec B603
TEST_COMMAND, TEST_COMMAND,
capture_output=True, capture_output=True,
text=True, text=True,
check=False, check=False,
env=env,
) )
return result.stdout, result.stderr return result.stdout, result.stderr
@@ -43,8 +66,23 @@ def parse_duration(output: str) -> float:
raise click.ClickException(_("Could not parse test execution time from output.")) raise click.ClickException(_("Could not parse test execution time from output."))
def parse_per_test_durations(output: str) -> list[tuple[str, float]]:
"""Extract per-test timings from ``--durations=0`` output.
Returns a list of ``(test_name, seconds)`` tuples sorted by duration
(slowest first).
"""
durations: list[tuple[str, float]] = []
for line in output.splitlines():
match = _DURATION_LINE_RE.match(line.strip())
if match:
durations.append((match.group(2).strip(), float(match.group(1))))
durations.sort(key=lambda x: x[1], reverse=True)
return durations
def check_speed(duration: float, max_seconds: float) -> None: def check_speed(duration: float, max_seconds: float) -> None:
"""Validate duration is within budget; raise on violation.""" """Validate total duration is within budget; raise on violation."""
if duration > max_seconds: if duration > max_seconds:
raise click.ClickException( raise click.ClickException(
_( _(
@@ -57,19 +95,58 @@ def check_speed(duration: float, max_seconds: float) -> None:
) )
def main(max_seconds: float) -> None: def check_per_test_speed(
"""Run tests, parse timing, and enforce the budget.""" durations: list[tuple[str, float]],
max_single_seconds: float,
) -> list[str]:
"""Return a list of violation messages for tests exceeding the per-test limit.
An empty list means all tests are within budget.
"""
violations: list[str] = []
for name, elapsed in durations:
if elapsed > max_single_seconds:
violations.append(
_(
"Test '{name}' took {elapsed:.2f}s (limit: {limit}s). "
"Optimise: use lighter fixtures, reduce I/O, or mock external calls.",
name=name,
elapsed=elapsed,
limit=max_single_seconds,
)
)
return violations
def main(max_seconds: float, max_single_seconds: float) -> None:
"""Run tests, parse timings, and enforce both budgets."""
stdout, stderr = run_tests() stdout, stderr = run_tests()
combined = stdout + "\n" + stderr combined = stdout + "\n" + stderr
click.echo(combined, err=False) click.echo(combined, err=False)
duration = parse_duration(combined) duration = parse_duration(combined)
check_speed(duration, max_seconds) check_speed(duration, max_seconds)
if max_single_seconds > 0:
per_test = parse_per_test_durations(combined)
violations = check_per_test_speed(per_test, max_single_seconds)
if violations:
msg = _(
"Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
count=len(violations),
limit=max_single_seconds,
)
click.echo(f"\n{msg}", err=True)
for v in violations:
click.echo(f" - {v}", err=True)
raise click.ClickException(msg)
click.echo( click.echo(
_( _(
"Unit tests passed in {duration:.2f}s (under {max}s limit).", "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).",
duration=duration, duration=duration,
max=max_seconds, max=max_seconds,
single=max_single_seconds,
) )
) )
@@ -80,10 +157,17 @@ def main(max_seconds: float) -> None:
type=float, type=float,
default=DEFAULT_MAX_SECONDS, default=DEFAULT_MAX_SECONDS,
show_default=True, show_default=True,
help="Maximum allowed execution time in seconds.", help="Maximum allowed total execution time in seconds.",
) )
def cli(max_seconds: float) -> None: @click.option(
main(max_seconds) "--max-single-seconds",
type=float,
default=DEFAULT_MAX_SINGLE_SECONDS,
show_default=True,
help="Maximum allowed per-test time in seconds (0 to disable).",
)
def cli(max_seconds: float, max_single_seconds: float) -> None:
main(max_seconds, max_single_seconds)
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover
+18 -3
View File
@@ -26,9 +26,24 @@ def _run(cmd: list[str]) -> None:
def _install_python_deps(bin_dir: str, extras: str = "dev") -> None: def _install_python_deps(bin_dir: str, extras: str = "dev") -> None:
"""Install the project with the specified extras in editable mode.""" """Install the project with the specified extras in editable mode.
In CI (system Python with PIP_BREAK_SYSTEM_PACKAGES=1), a first attempt
uses --break-system-packages. If that fails (e.g. debian-installed
packages without RECORD files), retry with --ignore-installed to skip
uninstalling system packages entirely.
"""
pip = str(Path(bin_dir) / "pip") pip = str(Path(bin_dir) / "pip")
_run([pip, "install", "-e", f".[{extras}]"]) cmd = [pip, "install", "-e", f".[{extras}]"]
if os.environ.get("PIP_BREAK_SYSTEM_PACKAGES") == "1":
cmd.append("--break-system-packages")
result = subprocess.run(cmd, check=False) # nosec B603
if result.returncode != 0 and os.environ.get("PIP_BREAK_SYSTEM_PACKAGES") == "1":
click.echo(" Retrying with --ignore-installed to bypass system packages...")
cmd.append("--ignore-installed")
_run(cmd)
elif result.returncode != 0:
raise subprocess.CalledProcessError(result.returncode, cmd)
def _install_pre_commit_hooks(bin_dir: str) -> None: def _install_pre_commit_hooks(bin_dir: str) -> None:
@@ -40,7 +55,7 @@ def _install_pre_commit_hooks(bin_dir: str) -> None:
def _install_ansible_collections(bin_dir: str) -> None: def _install_ansible_collections(bin_dir: str) -> None:
"""Install required Ansible Galaxy collections if requirements exist.""" """Install required Ansible Galaxy collections if requirements exist."""
galaxy = str(Path(bin_dir) / "ansible-galaxy") galaxy = shutil.which("ansible-galaxy") or str(Path(bin_dir) / "ansible-galaxy")
requirements = Path("ansible/requirements.yml") requirements = Path("ansible/requirements.yml")
if not requirements.exists(): if not requirements.exists():
click.echo(" ansible/requirements.yml not found — skipping collections.") click.echo(" ansible/requirements.yml not found — skipping collections.")
+1232 -1197
View File
@@ -1,1199 +1,1234 @@
{ {
"\n=== Summary ===": { "\n=== Summary ===": {
"en": "\n=== Summary ===", "bg": "\n=== Summary ===",
"bg": "\n=== Summary ===", "de": "\n=== Summary ===",
"de": "\n=== Summary ===", "en": "\n=== Summary ===",
"ru": "\n=== Summary ===", "ru": "\n=== Summary ===",
"zh": "\n=== Summary ===" "zh": "\n=== Summary ==="
}, },
"\nAll documentation coverage checks passed!": { "\nAll documentation coverage checks passed!": {
"en": "\nAll documentation coverage checks passed!", "bg": "\nAll documentation coverage checks passed!",
"bg": "\nAll documentation coverage checks passed!", "de": "\nAll documentation coverage checks passed!",
"de": "\nAll documentation coverage checks passed!", "en": "\nAll documentation coverage checks passed!",
"ru": "\nAll documentation coverage checks passed!", "ru": "\nAll documentation coverage checks passed!",
"zh": "\nAll documentation coverage checks passed!" "zh": "\nAll documentation coverage checks passed!"
}, },
"\nCHANGELOG version ordering:": { "\nCHANGELOG version ordering:": {
"en": "\nCHANGELOG version ordering:", "bg": "\nCHANGELOG version ordering:",
"bg": "\nCHANGELOG version ordering:", "de": "\nCHANGELOG version ordering:",
"de": "\nCHANGELOG version ordering:", "en": "\nCHANGELOG version ordering:",
"ru": "\nCHANGELOG version ordering:", "ru": "\nCHANGELOG version ordering:",
"zh": "\nCHANGELOG version ordering:" "zh": "\nCHANGELOG version ordering:"
}, },
"\nChecking CI script documentation in ci-cd-workflow.md...": { "\nChecking CI script documentation in ci-cd-workflow.md...": {
"en": "\nChecking CI script documentation in ci-cd-workflow.md...", "bg": "\nChecking CI script documentation in ci-cd-workflow.md...",
"bg": "\nChecking CI script documentation in ci-cd-workflow.md...", "de": "\nChecking CI script documentation in ci-cd-workflow.md...",
"de": "\nChecking CI script documentation in ci-cd-workflow.md...", "en": "\nChecking CI script documentation in ci-cd-workflow.md...",
"ru": "\nChecking CI script documentation in ci-cd-workflow.md...", "ru": "\nChecking CI script documentation in ci-cd-workflow.md...",
"zh": "\nChecking CI script documentation in ci-cd-workflow.md..." "zh": "\nChecking CI script documentation in ci-cd-workflow.md..."
}, },
"\nChecking module documentation in architecture.md...": { "\nChecking module documentation in architecture.md...": {
"en": "\nChecking module documentation in architecture.md...", "bg": "\nChecking module documentation in architecture.md...",
"bg": "\nChecking module documentation in architecture.md...", "de": "\nChecking module documentation in architecture.md...",
"de": "\nChecking module documentation in architecture.md...", "en": "\nChecking module documentation in architecture.md...",
"ru": "\nChecking module documentation in architecture.md...", "ru": "\nChecking module documentation in architecture.md...",
"zh": "\nChecking module documentation in architecture.md..." "zh": "\nChecking module documentation in architecture.md..."
}, },
"\nDoc coverage: {covered}/{total} ({pct}%)": { "\nDoc coverage: {covered}/{total} ({pct}%)": {
"en": "\nDoc coverage: {covered}/{total} ({pct}%)", "bg": "\nDoc coverage: {covered}/{total} ({pct}%)",
"bg": "\nDoc coverage: {covered}/{total} ({pct}%)", "de": "\nDoc coverage: {covered}/{total} ({pct}%)",
"de": "\nDoc coverage: {covered}/{total} ({pct}%)", "en": "\nDoc coverage: {covered}/{total} ({pct}%)",
"ru": "\nDoc coverage: {covered}/{total} ({pct}%)", "ru": "\nDoc coverage: {covered}/{total} ({pct}%)",
"zh": "\nDoc coverage: {covered}/{total} ({pct}%)" "zh": "\nDoc coverage: {covered}/{total} ({pct}%)"
}, },
"\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}": { "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}": {
"en": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}", "bg": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
"bg": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}", "de": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
"de": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}", "en": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
"ru": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}", "ru": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
"zh": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}" "zh": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}"
}, },
"\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.": { "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.": {
"en": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.", "bg": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.",
"bg": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.", "de": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.",
"de": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.", "en": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.",
"ru": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.", "ru": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.",
"zh": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce." "zh": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce."
}, },
"\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.": { "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.": {
"en": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.", "bg": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
"bg": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.", "de": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
"de": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.", "en": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
"ru": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.", "ru": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
"zh": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report." "zh": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report."
}, },
"\nIntegrity check FAILED ({count} issues):": { "\nIntegrity check FAILED ({count} issues):": {
"en": "\nIntegrity check FAILED ({count} issues):", "bg": "\nIntegrity check FAILED ({count} issues):",
"bg": "\nIntegrity check FAILED ({count} issues):", "de": "\nIntegrity check FAILED ({count} issues):",
"de": "\nIntegrity check FAILED ({count} issues):", "en": "\nIntegrity check FAILED ({count} issues):",
"ru": "\nIntegrity check FAILED ({count} issues):", "ru": "\nIntegrity check FAILED ({count} issues):",
"zh": "\nIntegrity check FAILED ({count} issues):" "zh": "\nIntegrity check FAILED ({count} issues):"
}, },
"\nIntegrity check passed — all {count} pages verified.": { "\nIntegrity check passed — all {count} pages verified.": {
"en": "\nIntegrity check passed — all {count} pages verified.", "bg": "\nIntegrity check passed — all {count} pages verified.",
"bg": "\nIntegrity check passed — all {count} pages verified.", "de": "\nIntegrity check passed — all {count} pages verified.",
"de": "\nIntegrity check passed — all {count} pages verified.", "en": "\nIntegrity check passed — all {count} pages verified.",
"ru": "\nIntegrity check passed — all {count} pages verified.", "ru": "\nIntegrity check passed — all {count} pages verified.",
"zh": "\nIntegrity check passed — all {count} pages verified." "zh": "\nIntegrity check passed — all {count} pages verified."
}, },
"\nLatest tag: {tag}": { "\nLatest tag: {tag}": {
"en": "\nLatest tag: {tag}", "bg": "\nLatest tag: {tag}",
"bg": "\nLatest tag: {tag}", "de": "\nLatest tag: {tag}",
"de": "\nLatest tag: {tag}", "en": "\nLatest tag: {tag}",
"ru": "\nLatest tag: {tag}", "ru": "\nLatest tag: {tag}",
"zh": "\nLatest tag: {tag}" "zh": "\nLatest tag: {tag}"
}, },
"\nMissing documentation:": { "\nMissing documentation:": {
"en": "\nMissing documentation:", "bg": "\nMissing documentation:",
"bg": "\nMissing documentation:", "de": "\nMissing documentation:",
"de": "\nMissing documentation:", "en": "\nMissing documentation:",
"ru": "\nMissing documentation:", "ru": "\nMissing documentation:",
"zh": "\nMissing documentation:" "zh": "\nMissing documentation:"
}, },
"\nResult: {status}": { "\nResult: {status}": {
"en": "\nResult: {status}", "bg": "\nResult: {status}",
"bg": "\nResult: {status}", "de": "\nResult: {status}",
"de": "\nResult: {status}", "en": "\nResult: {status}",
"ru": "\nResult: {status}", "ru": "\nResult: {status}",
"zh": "\nResult: {status}" "zh": "\nResult: {status}"
}, },
"\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).": { "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).": {
"en": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).", "bg": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).",
"bg": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).", "de": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).",
"de": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).", "en": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).",
"ru": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).", "ru": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).",
"zh": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments)." "zh": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments)."
}, },
"\nRunning full wiki integrity check...": { "\nRunning full wiki integrity check...": {
"en": "\nRunning full wiki integrity check...", "bg": "\nRunning full wiki integrity check...",
"bg": "\nRunning full wiki integrity check...", "de": "\nRunning full wiki integrity check...",
"de": "\nRunning full wiki integrity check...", "en": "\nRunning full wiki integrity check...",
"ru": "\nRunning full wiki integrity check...", "ru": "\nRunning full wiki integrity check...",
"zh": "\nRunning full wiki integrity check..." "zh": "\nRunning full wiki integrity check..."
}, },
"\nTag → Commit alignment:": { "\nTag → Commit alignment:": {
"en": "\nTag → Commit alignment:", "bg": "\nTag → Commit alignment:",
"bg": "\nTag → Commit alignment:", "de": "\nTag → Commit alignment:",
"de": "\nTag → Commit alignment:", "en": "\nTag → Commit alignment:",
"ru": "\nTag → Commit alignment:", "ru": "\nTag → Commit alignment:",
"zh": "\nTag → Commit alignment:" "zh": "\nTag → Commit alignment:"
}, },
"\nUntagged release commits:": { "\nUntagged release commits:": {
"en": "\nUntagged release commits:", "bg": "\nUntagged release commits:",
"bg": "\nUntagged release commits:", "de": "\nUntagged release commits:",
"de": "\nUntagged release commits:", "en": "\nUntagged release commits:",
"ru": "\nUntagged release commits:", "ru": "\nUntagged release commits:",
"zh": "\nUntagged release commits:" "zh": "\nUntagged release commits:"
}, },
"\nUser-facing changes ({count}):": { "\nUser-facing changes ({count}):": {
"en": "\nUser-facing changes ({count}):", "bg": "\nUser-facing changes ({count}):",
"bg": "\nUser-facing changes ({count}):", "de": "\nUser-facing changes ({count}):",
"de": "\nUser-facing changes ({count}):", "en": "\nUser-facing changes ({count}):",
"ru": "\nUser-facing changes ({count}):", "ru": "\nUser-facing changes ({count}):",
"zh": "\nUser-facing changes ({count}):" "zh": "\nUser-facing changes ({count}):"
}, },
"\nVerification FAILED: {failures} page(s) have empty or mismatched content!": { "\nVerification FAILED: {failures} page(s) have empty or mismatched content!": {
"en": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!", "bg": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!",
"bg": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!", "de": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!",
"de": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!", "en": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!",
"ru": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!", "ru": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!",
"zh": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!" "zh": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!"
}, },
"\nVerification passed — all wiki pages have correct content.": { "\nVerification passed — all wiki pages have correct content.": {
"en": "\nVerification passed — all wiki pages have correct content.", "bg": "\nVerification passed — all wiki pages have correct content.",
"bg": "\nVerification passed — all wiki pages have correct content.", "de": "\nVerification passed — all wiki pages have correct content.",
"de": "\nVerification passed — all wiki pages have correct content.", "en": "\nVerification passed — all wiki pages have correct content.",
"ru": "\nVerification passed — all wiki pages have correct content.", "ru": "\nVerification passed — all wiki pages have correct content.",
"zh": "\nVerification passed — all wiki pages have correct content." "zh": "\nVerification passed — all wiki pages have correct content."
}, },
"\nVerifying wiki pages have content...": { "\nVerifying wiki pages have content...": {
"en": "\nVerifying wiki pages have content...", "bg": "\nVerifying wiki pages have content...",
"bg": "\nVerifying wiki pages have content...", "de": "\nVerifying wiki pages have content...",
"de": "\nVerifying wiki pages have content...", "en": "\nVerifying wiki pages have content...",
"ru": "\nVerifying wiki pages have content...", "ru": "\nVerifying wiki pages have content...",
"zh": "\nVerifying wiki pages have content..." "zh": "\nVerifying wiki pages have content..."
}, },
"\nWorkflow-only changes ({count}):": { "\nWorkflow-only changes ({count}):": {
"en": "\nWorkflow-only changes ({count}):", "bg": "\nWorkflow-only changes ({count}):",
"bg": "\nWorkflow-only changes ({count}):", "de": "\nWorkflow-only changes ({count}):",
"de": "\nWorkflow-only changes ({count}):", "en": "\nWorkflow-only changes ({count}):",
"ru": "\nWorkflow-only changes ({count}):", "ru": "\nWorkflow-only changes ({count}):",
"zh": "\nWorkflow-only changes ({count}):" "zh": "\nWorkflow-only changes ({count}):"
}, },
"\n[dry-run] Changelog:\n{changelog}": { "\n[dry-run] Changelog:\n{changelog}": {
"en": "\n[dry-run] Changelog:\n{changelog}", "bg": "\n[dry-run] Changelog:\n{changelog}",
"bg": "\n[dry-run] Changelog:\n{changelog}", "de": "\n[dry-run] Changelog:\n{changelog}",
"de": "\n[dry-run] Changelog:\n{changelog}", "en": "\n[dry-run] Changelog:\n{changelog}",
"ru": "\n[dry-run] Changelog:\n{changelog}", "ru": "\n[dry-run] Changelog:\n{changelog}",
"zh": "\n[dry-run] Changelog:\n{changelog}" "zh": "\n[dry-run] Changelog:\n{changelog}"
}, },
"\n{label} files changed ({count}):": { "\n{label} files changed ({count}):": {
"en": "\n{label} files changed ({count}):", "bg": "\n{label} files changed ({count}):",
"bg": "\n{label} files changed ({count}):", "de": "\n{label} files changed ({count}):",
"de": "\n{label} files changed ({count}):", "en": "\n{label} files changed ({count}):",
"ru": "\n{label} files changed ({count}):", "ru": "\n{label} files changed ({count}):",
"zh": "\n{label} files changed ({count}):" "zh": "\n{label} files changed ({count}):"
}, },
"\n{tag} files ({count}):": { "\n{tag} files ({count}):": {
"en": "\n{tag} files ({count}):", "bg": "\n{tag} files ({count}):",
"bg": "\n{tag} files ({count}):", "de": "\n{tag} files ({count}):",
"de": "\n{tag} files ({count}):", "en": "\n{tag} files ({count}):",
"ru": "\n{tag} files ({count}):", "ru": "\n{tag} files ({count}):",
"zh": "\n{tag} files ({count}):" "zh": "\n{tag} files ({count}):"
}, },
" - Auto-delete branch after merge: yes": { " - Auto-delete branch after merge: yes": {
"en": " - Auto-delete branch after merge: yes", "bg": " - Автоматично изтриване на клон след сливане: да",
"bg": " - Автоматично изтриване на клон след сливане: да", "de": " - Branch nach Merge automatisch löschen: ja",
"de": " - Branch nach Merge automatisch löschen: ja", "en": " - Auto-delete branch after merge: yes",
"ru": " - Автоудаление ветки после слияния: да", "ru": " - Автоудаление ветки после слияния: да",
"zh": " - 合并后自动删除分支: 是" "zh": " - 合并后自动删除分支: 是"
}, },
" - Block outdated branches: yes": { " - Block outdated branches: yes": {
"en": " - Block outdated branches: yes", "bg": " - Блокиране на остарели клонове: да",
"bg": " - Блокиране на остарели клонове: да", "de": " - Veraltete Branches blockieren: ja",
"de": " - Veraltete Branches blockieren: ja", "en": " - Block outdated branches: yes",
"ru": " - Блокировать устаревшие ветки: да", "ru": " - Блокировать устаревшие ветки: да",
"zh": " - 阻止过时分支: 是" "zh": " - 阻止过时分支: 是"
}, },
" - Block rejected reviews: yes": { " - Block rejected reviews: yes": {
"en": " - Block rejected reviews: yes", "bg": " - Блокиране на отхвърлени рецензии: да",
"bg": " - Блокиране на отхвърлени рецензии: да", "de": " - Abgelehnte Reviews blockieren: ja",
"de": " - Abgelehnte Reviews blockieren: ja", "en": " - Block rejected reviews: yes",
"ru": " - Блокировать отклонённые ревью: да", "ru": " - Блокировать отклонённые ревью: да",
"zh": " - 阻止被拒绝的审查: 是" "zh": " - 阻止被拒绝的审查: 是"
}, },
" - Direct pushes: BLOCKED (require PR, whitelisted users can push)": { " - Direct pushes: BLOCKED (require PR, whitelisted users can push)": {
"en": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)", "bg": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)",
"bg": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)", "de": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)",
"de": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)", "en": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)",
"ru": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)", "ru": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)",
"zh": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)" "zh": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)"
}, },
" - Dismiss stale approvals: yes": { " - Dismiss stale approvals: yes": {
"en": " - Dismiss stale approvals: yes", "bg": " - Анулиране на остарели одобрения: да",
"bg": " - Анулиране на остарели одобрения: да", "de": " - Veraltete Genehmigungen ablehnen: ja",
"de": " - Veraltete Genehmigungen ablehnen: ja", "en": " - Dismiss stale approvals: yes",
"ru": " - Отклонять устаревшие одобрения: да", "ru": " - Отклонять устаревшие одобрения: да",
"zh": " - 忽略过时审批: 是" "zh": " - 忽略过时审批: 是"
}, },
" - Required approvals: {count}": { " - Required approvals: {count}": {
"en": " - Required approvals: {count}", "bg": " - Необходими одобрения: {count}",
"bg": " - Необходими одобрения: {count}", "de": " - Erforderliche Genehmigungen: {count}",
"de": " - Erforderliche Genehmigungen: {count}", "en": " - Required approvals: {count}",
"ru": " - Требуемые одобрения: {count}", "ru": " - Требуемые одобрения: {count}",
"zh": " - 必需审批数: {count}" "zh": " - 必需审批数: {count}"
}, },
" - Required status checks: {checks}": { " - Required status checks: {checks}": {
"en": " - Required status checks: {checks}", "bg": " - Необходими проверки на състоянието: {checks}",
"bg": " - Необходими проверки на състоянието: {checks}", "de": " - Erforderliche Status-Checks: {checks}",
"de": " - Erforderliche Status-Checks: {checks}", "en": " - Required status checks: {checks}",
"ru": " - Требуемые проверки статуса: {checks}", "ru": " - Требуемые проверки статуса: {checks}",
"zh": " - 必需状态检查: {checks}" "zh": " - 必需状态检查: {checks}"
}, },
" Created: {title}": { " Created: {title}": {
"en": " Created: {title}", "bg": " Created: {title}",
"bg": " Created: {title}", "de": " Created: {title}",
"de": " Created: {title}", "en": " Created: {title}",
"ru": " Created: {title}", "ru": " Created: {title}",
"zh": " Created: {title}" "zh": " Created: {title}"
}, },
" FAIL: {title} — content mismatch or empty!": { " FAIL: {title} — content mismatch or empty!": {
"en": " FAIL: {title} — content mismatch or empty!", "bg": " FAIL: {title} — content mismatch or empty!",
"bg": " FAIL: {title} — content mismatch or empty!", "de": " FAIL: {title} — content mismatch or empty!",
"de": " FAIL: {title} — content mismatch or empty!", "en": " FAIL: {title} — content mismatch or empty!",
"ru": " FAIL: {title} — content mismatch or empty!", "ru": " FAIL: {title} — content mismatch or empty!",
"zh": " FAIL: {title} — content mismatch or empty!" "zh": " FAIL: {title} — content mismatch or empty!"
}, },
" MISSING: devx {cmd}": { " MISSING: devx {cmd}": {
"en": " MISSING: devx {cmd}", "bg": " ЛИПСВА: devx {cmd}",
"bg": " ЛИПСВА: devx {cmd}", "de": " FEHLT: devx {cmd}",
"de": " FEHLT: devx {cmd}", "en": " MISSING: devx {cmd}",
"ru": " ОТСУТСТВУЕТ: devx {cmd}", "ru": " ОТСУТСТВУЕТ: devx {cmd}",
"zh": " 缺失: devx {cmd}" "zh": " 缺失: devx {cmd}"
}, },
" MISSING: {module}": { " MISSING: {module}": {
"en": " MISSING: {module}", "bg": " MISSING: {module}",
"bg": " MISSING: {module}", "de": " MISSING: {module}",
"de": " MISSING: {module}", "en": " MISSING: {module}",
"ru": " MISSING: {module}", "ru": " MISSING: {module}",
"zh": " MISSING: {module}" "zh": " MISSING: {module}"
}, },
" MISSING: {script}": { " MISSING: {script}": {
"en": " MISSING: {script}", "bg": " MISSING: {script}",
"bg": " MISSING: {script}", "de": " MISSING: {script}",
"de": " MISSING: {script}", "en": " MISSING: {script}",
"ru": " MISSING: {script}", "ru": " MISSING: {script}",
"zh": " MISSING: {script}" "zh": " MISSING: {script}"
}, },
" OK: devx {cmd}": { " OK: devx {cmd}": {
"en": " OK: devx {cmd}", "bg": " ОК: devx {cmd}",
"bg": " ОК: devx {cmd}", "de": " OK: devx {cmd}",
"de": " OK: devx {cmd}", "en": " OK: devx {cmd}",
"ru": " ОК: devx {cmd}", "ru": " ОК: devx {cmd}",
"zh": " 正常: devx {cmd}" "zh": " 正常: devx {cmd}"
}, },
" OK: {module}": { " OK: {module}": {
"en": " OK: {module}", "bg": " OK: {module}",
"bg": " OK: {module}", "de": " OK: {module}",
"de": " OK: {module}", "en": " OK: {module}",
"ru": " OK: {module}", "ru": " OK: {module}",
"zh": " OK: {module}" "zh": " OK: {module}"
}, },
" OK: {script}": { " OK: {script}": {
"en": " OK: {script}", "bg": " OK: {script}",
"bg": " OK: {script}", "de": " OK: {script}",
"de": " OK: {script}", "en": " OK: {script}",
"ru": " OK: {script}", "ru": " OK: {script}",
"zh": " OK: {script}" "zh": " OK: {script}"
}, },
" OK: {title} ({chars} chars)": { " OK: {title} ({chars} chars)": {
"en": " OK: {title} ({chars} chars)", "bg": " OK: {title} ({chars} chars)",
"bg": " OK: {title} ({chars} chars)", "de": " OK: {title} ({chars} chars)",
"de": " OK: {title} ({chars} chars)", "en": " OK: {title} ({chars} chars)",
"ru": " OK: {title} ({chars} chars)", "ru": " OK: {title} ({chars} chars)",
"zh": " OK: {title} ({chars} chars)" "zh": " OK: {title} ({chars} chars)"
}, },
" Updated: {title}": { " Updated: {title}": {
"en": " Updated: {title}", "bg": " Updated: {title}",
"bg": " Updated: {title}", "de": " Updated: {title}",
"de": " Updated: {title}", "en": " Updated: {title}",
"ru": " Updated: {title}", "ru": " Updated: {title}",
"zh": " Updated: {title}" "zh": " Updated: {title}"
}, },
"=== Release Alignment Verification ===\n": { "--skip-build: skipping package build and PyPI publish.": {
"en": "=== Release Alignment Verification ===\n", "bg": "--skip-build: skipping package build and PyPI publish.",
"bg": "=== Release Alignment Verification ===\n", "de": "--skip-build: skipping package build and PyPI publish.",
"de": "=== Release Alignment Verification ===\n", "en": "--skip-build: skipping package build and PyPI publish.",
"ru": "=== Release Alignment Verification ===\n", "ru": "--skip-build: skipping package build and PyPI publish.",
"zh": "=== Release Alignment Verification ===\n" "zh": "--skip-build: skipping package build and PyPI publish."
}, },
"API poll warning: {exc}": { "=== Release Alignment Verification ===\n": {
"en": "API poll warning: {exc}", "bg": "=== Release Alignment Verification ===\n",
"bg": "API poll warning: {exc}", "de": "=== Release Alignment Verification ===\n",
"de": "API poll warning: {exc}", "en": "=== Release Alignment Verification ===\n",
"ru": "API poll warning: {exc}", "ru": "=== Release Alignment Verification ===\n",
"zh": "API poll warning: {exc}" "zh": "=== Release Alignment Verification ===\n"
}, },
"All molecule tests passed.": { "API poll warning: {exc}": {
"en": "All molecule tests passed.", "bg": "API poll warning: {exc}",
"bg": "All molecule tests passed.", "de": "API poll warning: {exc}",
"de": "All molecule tests passed.", "en": "API poll warning: {exc}",
"ru": "All molecule tests passed.", "ru": "API poll warning: {exc}",
"zh": "All molecule tests passed." "zh": "API poll warning: {exc}"
}, },
"Another molecule runner failed. Stopping this runner early.": { "All molecule tests passed.": {
"en": "Another molecule runner failed. Stopping this runner early.", "bg": "All molecule tests passed.",
"bg": "Another molecule runner failed. Stopping this runner early.", "de": "All molecule tests passed.",
"de": "Another molecule runner failed. Stopping this runner early.", "en": "All molecule tests passed.",
"ru": "Another molecule runner failed. Stopping this runner early.", "ru": "All molecule tests passed.",
"zh": "Another molecule runner failed. Stopping this runner early." "zh": "All molecule tests passed."
}, },
"Bumping version: {current} -> v{new_version}": { "Another molecule runner failed. Stopping this runner early.": {
"en": "Bumping version: {current} -> v{new_version}", "bg": "Another molecule runner failed. Stopping this runner early.",
"bg": "Bumping version: {current} -> v{new_version}", "de": "Another molecule runner failed. Stopping this runner early.",
"de": "Bumping version: {current} -> v{new_version}", "en": "Another molecule runner failed. Stopping this runner early.",
"ru": "Bumping version: {current} -> v{new_version}", "ru": "Another molecule runner failed. Stopping this runner early.",
"zh": "Bumping version: {current} -> v{new_version}" "zh": "Another molecule runner failed. Stopping this runner early."
}, },
"Checking CLI command documentation...": { "Bumping version: {current} -> v{new_version}": {
"en": "Checking CLI command documentation...", "bg": "Bumping version: {current} -> v{new_version}",
"bg": "Checking CLI command documentation...", "de": "Bumping version: {current} -> v{new_version}",
"de": "Checking CLI command documentation...", "en": "Bumping version: {current} -> v{new_version}",
"ru": "Checking CLI command documentation...", "ru": "Bumping version: {current} -> v{new_version}",
"zh": "Checking CLI command documentation..." "zh": "Bumping version: {current} -> v{new_version}"
}, },
"Command failed ({cmd}): {stderr}": { "Checking CLI command documentation...": {
"en": "Command failed ({cmd}): {stderr}", "bg": "Checking CLI command documentation...",
"bg": "Command failed ({cmd}): {stderr}", "de": "Checking CLI command documentation...",
"de": "Command failed ({cmd}): {stderr}", "en": "Checking CLI command documentation...",
"ru": "Command failed ({cmd}): {stderr}", "ru": "Checking CLI command documentation...",
"zh": "Command failed ({cmd}): {stderr}" "zh": "Checking CLI command documentation..."
}, },
"Comparing {base}..{head} ({count} files changed)": { "Command failed ({cmd}): {stderr}": {
"en": "Comparing {base}..{head} ({count} files changed)", "bg": "Command failed ({cmd}): {stderr}",
"bg": "Comparing {base}..{head} ({count} files changed)", "de": "Command failed ({cmd}): {stderr}",
"de": "Comparing {base}..{head} ({count} files changed)", "en": "Command failed ({cmd}): {stderr}",
"ru": "Comparing {base}..{head} ({count} files changed)", "ru": "Command failed ({cmd}): {stderr}",
"zh": "Comparing {base}..{head} ({count} files changed)" "zh": "Command failed ({cmd}): {stderr}"
}, },
"Configuring branch protection for {branch}...": { "Comparing {base}..{head} ({count} files changed)": {
"en": "Configuring branch protection for {branch}...", "bg": "Comparing {base}..{head} ({count} files changed)",
"bg": "Конфигуриране на защита на клона {branch}...", "de": "Comparing {base}..{head} ({count} files changed)",
"de": "Konfiguriere Branch-Schutz für {branch}...", "en": "Comparing {base}..{head} ({count} files changed)",
"ru": "Настройка защиты ветки {branch}...", "ru": "Comparing {base}..{head} ({count} files changed)",
"zh": "正在配置 {branch} 的分支保护..." "zh": "Comparing {base}..{head} ({count} files changed)"
}, },
"Configuring repository settings...": { "Configuring branch protection for {branch}...": {
"en": "Configuring repository settings...", "bg": "Конфигуриране на защита на клона {branch}...",
"bg": "Конфигуриране на настройките на хранилището...", "de": "Konfiguriere Branch-Schutz für {branch}...",
"de": "Repository-Einstellungen konfigurieren...", "en": "Configuring branch protection for {branch}...",
"ru": "Настройка параметров репозитория...", "ru": "Настройка защиты ветки {branch}...",
"zh": "正在配置仓库设置..." "zh": "正在配置 {branch} 的分支保护..."
}, },
"Could not extract conventional commit message from PR commits.": { "Configuring repository settings...": {
"en": "Could not extract conventional commit message from PR commits.", "bg": "Конфигуриране на настройките на хранилището...",
"bg": "Could not extract conventional commit message from PR commits.", "de": "Repository-Einstellungen konfigurieren...",
"de": "Could not extract conventional commit message from PR commits.", "en": "Configuring repository settings...",
"ru": "Could not extract conventional commit message from PR commits.", "ru": "Настройка параметров репозитория...",
"zh": "Could not extract conventional commit message from PR commits." "zh": "正在配置仓库设置..."
}, },
"Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.": { "Could not extract conventional commit message from PR commits.": {
"en": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.", "bg": "Could not extract conventional commit message from PR commits.",
"bg": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.", "de": "Could not extract conventional commit message from PR commits.",
"de": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.", "en": "Could not extract conventional commit message from PR commits.",
"ru": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.", "ru": "Could not extract conventional commit message from PR commits.",
"zh": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task." "zh": "Could not extract conventional commit message from PR commits."
}, },
"Could not find __version__ in {file}": { "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.": {
"en": "Could not find __version__ in {file}", "bg": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
"bg": "Could not find __version__ in {file}", "de": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
"de": "Could not find __version__ in {file}", "en": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
"ru": "Could not find __version__ in {file}", "ru": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
"zh": "Could not find __version__ in {file}" "zh": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task."
}, },
"Could not parse test execution time from output.": { "Could not find __version__ in {file}": {
"en": "Could not parse test execution time from output.", "bg": "Could not find __version__ in {file}",
"bg": "Could not parse test execution time from output.", "de": "Could not find __version__ in {file}",
"de": "Could not parse test execution time from output.", "en": "Could not find __version__ in {file}",
"ru": "Could not parse test execution time from output.", "ru": "Could not find __version__ in {file}",
"zh": "Could not parse test execution time from output." "zh": "Could not find __version__ in {file}"
}, },
"Created issue #{issue_id}: {title}": { "Could not parse test execution time from output.": {
"en": "Created issue #{issue_id}: {title}", "bg": "Could not parse test execution time from output.",
"bg": "Created issue #{issue_id}: {title}", "de": "Could not parse test execution time from output.",
"de": "Created issue #{issue_id}: {title}", "en": "Could not parse test execution time from output.",
"ru": "Created issue #{issue_id}: {title}", "ru": "Could not parse test execution time from output.",
"zh": "Created issue #{issue_id}: {title}" "zh": "Could not parse test execution time from output."
}, },
"Created release commit.": { "Created issue #{issue_id}: {title}": {
"en": "Created release commit.", "bg": "Created issue #{issue_id}: {title}",
"bg": "Created release commit.", "de": "Created issue #{issue_id}: {title}",
"de": "Created release commit.", "en": "Created issue #{issue_id}: {title}",
"ru": "Created release commit.", "ru": "Created issue #{issue_id}: {title}",
"zh": "Created release commit." "zh": "Created issue #{issue_id}: {title}"
}, },
"Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.": { "Created release commit.": {
"en": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.", "bg": "Created release commit.",
"bg": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.", "de": "Created release commit.",
"de": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.", "en": "Created release commit.",
"ru": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.", "ru": "Created release commit.",
"zh": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently." "zh": "Created release commit."
}, },
"ERROR: REPO_TOKEN is not set.": { "Docker daemon failed to start": {
"en": "ERROR: REPO_TOKEN is not set.", "bg": "Docker daemon failed to start",
"bg": "ГРЕШКА: REPO_TOKEN не е зададен.", "de": "Docker-Daemon konnte nicht gestartet werden",
"de": "FEHLER: REPO_TOKEN ist nicht gesetzt.", "en": "Docker daemon failed to start",
"ru": "ОШИБКА: REPO_TOKEN не задан.", "ru": "Не удалось запустить Docker-демон",
"zh": "错误:未设置 REPO_TOKEN。" "zh": "Docker 守护进程启动失败"
}, },
"ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME.": { "Docker daemon started": {
"en": "ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME.", "bg": "Docker daemon started",
"bg": "ГРЕШКА: Името на хранилището не е указано. Използвайте --repo или задайте DEVX_REPO_NAME.", "de": "Docker-Daemon gestartet",
"de": "FEHLER: Repository-Name nicht angegeben. Verwenden Sie --repo oder setzen Sie DEVX_REPO_NAME.", "en": "Docker daemon started",
"ru": "ОШИБКА: Имя репозитория не указано. Используйте --repo или задайте DEVX_REPO_NAME.", "ru": "Docker-демон запущен",
"zh": "错误:未指定仓库名称。请使用 --repo 或设置 DEVX_REPO_NAME。" "zh": "Docker 守护进程已启动"
}, },
"ERROR: Tag consistency check failed. Existing tags are misaligned:": { "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.": {
"en": "ERROR: Tag consistency check failed. Existing tags are misaligned:", "bg": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
"bg": "ERROR: Tag consistency check failed. Existing tags are misaligned:", "de": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
"de": "ERROR: Tag consistency check failed. Existing tags are misaligned:", "en": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
"ru": "ERROR: Tag consistency check failed. Existing tags are misaligned:", "ru": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
"zh": "ERROR: Tag consistency check failed. Existing tags are misaligned:" "zh": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently."
}, },
"ERROR: VIKUNJA_TOKEN is not set.": { "ERROR: REPO_TOKEN is not set.": {
"en": "ERROR: VIKUNJA_TOKEN is not set.", "bg": "ГРЕШКА: REPO_TOKEN не е зададен.",
"bg": "ГРЕШКА: VIKUNJA_TOKEN не е зададен.", "de": "FEHLER: REPO_TOKEN ist nicht gesetzt.",
"de": "FEHLER: VIKUNJA_TOKEN ist nicht gesetzt.", "en": "ERROR: REPO_TOKEN is not set.",
"ru": "ОШИБКА: VIKUNJA_TOKEN не задан.", "ru": "ОШИБКА: REPO_TOKEN не задан.",
"zh": "错误:未设置 VIKUNJA_TOKEN。" "zh": "错误:未设置 REPO_TOKEN。"
}, },
"ERROR: mapping.json not found at {path}": { "ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME.": {
"en": "ERROR: mapping.json not found at {path}", "bg": "ГРЕШКА: Името на хранилището не е указано. Използвайте --repo или задайте DEVX_REPO_NAME.",
"bg": "ERROR: mapping.json not found at {path}", "de": "FEHLER: Repository-Name nicht angegeben. Verwenden Sie --repo oder setzen Sie DEVX_REPO_NAME.",
"de": "ERROR: mapping.json not found at {path}", "en": "ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME.",
"ru": "ERROR: mapping.json not found at {path}", "ru": "ОШИБКА: Имя репозитория не указано. Используйте --repo или задайте DEVX_REPO_NAME.",
"zh": "ERROR: mapping.json not found at {path}" "zh": "错误:未指定仓库名称。请使用 --repo 或设置 DEVX_REPO_NAME。"
}, },
"FAILED: {pair} exited with code {code}": { "ERROR: Tag consistency check failed. Existing tags are misaligned:": {
"en": "FAILED: {pair} exited with code {code}", "bg": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
"bg": "FAILED: {pair} exited with code {code}", "de": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
"de": "FAILED: {pair} exited with code {code}", "en": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
"ru": "FAILED: {pair} exited with code {code}", "ru": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
"zh": "FAILED: {pair} exited with code {code}" "zh": "ERROR: Tag consistency check failed. Existing tags are misaligned:"
}, },
"Failed to create issue via tea: {error}": { "ERROR: VIKUNJA_TOKEN is not set.": {
"en": "Failed to create issue via tea: {error}", "bg": "ГРЕШКА: VIKUNJA_TOKEN не е зададен.",
"bg": "Failed to create issue via tea: {error}", "de": "FEHLER: VIKUNJA_TOKEN ist nicht gesetzt.",
"de": "Failed to create issue via tea: {error}", "en": "ERROR: VIKUNJA_TOKEN is not set.",
"ru": "Failed to create issue via tea: {error}", "ru": "ОШИБКА: VIKUNJA_TOKEN не задан.",
"zh": "Failed to create issue via tea: {error}" "zh": "错误:未设置 VIKUNJA_TOKEN。"
}, },
"Found {count} existing wiki pages.": { "ERROR: mapping.json not found at {path}": {
"en": "Found {count} existing wiki pages.", "bg": "ERROR: mapping.json not found at {path}",
"bg": "Found {count} existing wiki pages.", "de": "ERROR: mapping.json not found at {path}",
"de": "Found {count} existing wiki pages.", "en": "ERROR: mapping.json not found at {path}",
"ru": "Found {count} existing wiki pages.", "ru": "ERROR: mapping.json not found at {path}",
"zh": "Found {count} existing wiki pages." "zh": "ERROR: mapping.json not found at {path}"
}, },
"GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.": { "FAILED: {pair} exited with code {code}": {
"en": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.", "bg": "FAILED: {pair} exited with code {code}",
"bg": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.", "de": "FAILED: {pair} exited with code {code}",
"de": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.", "en": "FAILED: {pair} exited with code {code}",
"ru": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.", "ru": "FAILED: {pair} exited with code {code}",
"zh": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation." "zh": "FAILED: {pair} exited with code {code}"
}, },
"Generated {file} with prefix '{prefix}'.": { "Failed to create issue via tea: {error}": {
"en": "Generated {file} with prefix '{prefix}'.", "bg": "Failed to create issue via tea: {error}",
"bg": "Generated {file} with prefix '{prefix}'.", "de": "Failed to create issue via tea: {error}",
"de": "Generated {file} with prefix '{prefix}'.", "en": "Failed to create issue via tea: {error}",
"ru": "Generated {file} with prefix '{prefix}'.", "ru": "Failed to create issue via tea: {error}",
"zh": "Generated {file} with prefix '{prefix}'." "zh": "Failed to create issue via tea: {error}"
}, },
"HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.": { "Found {count} existing wiki pages.": {
"en": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.", "bg": "Found {count} existing wiki pages.",
"bg": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.", "de": "Found {count} existing wiki pages.",
"de": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.", "en": "Found {count} existing wiki pages.",
"ru": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.", "ru": "Found {count} existing wiki pages.",
"zh": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag." "zh": "Found {count} existing wiki pages."
}, },
"HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.": { "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.": {
"en": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.", "bg": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"bg": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.", "de": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"de": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.", "en": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"ru": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.", "ru": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"zh": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment." "zh": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation."
}, },
"HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.": { "Generated {file} with prefix '{prefix}'.": {
"en": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.", "bg": "Generated {file} with prefix '{prefix}'.",
"bg": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.", "de": "Generated {file} with prefix '{prefix}'.",
"de": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.", "en": "Generated {file} with prefix '{prefix}'.",
"ru": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.", "ru": "Generated {file} with prefix '{prefix}'.",
"zh": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping." "zh": "Generated {file} with prefix '{prefix}'."
}, },
"HTTP error: {status} — {message}": { "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.": {
"en": "HTTP error: {status} — {message}", "bg": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
"bg": "HTTP грешка: {status} — {message}", "de": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
"de": "HTTP-Fehler: {status} — {message}", "en": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
"ru": "Ошибка HTTP: {status} — {message}", "ru": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
"zh": "HTTP 错误: {status} — {message}" "zh": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag."
}, },
"HTTP {status} Forbidden — your token lacks admin rights.\nMake sure the token belongs to a repo owner or organisation admin.\nAlternatively, configure branch protection manually in Settings → Branches.": { "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.": {
"en": "HTTP {status} Forbidden — your token lacks admin rights.\nMake sure the token belongs to a repo owner or organisation admin.\nAlternatively, configure branch protection manually in Settings → Branches.", "bg": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.",
"bg": "HTTP {status} Забранено — вашият токен няма администраторски права.\nУверете се, че токенът принадлежи на собственик на хранилище или администратор на организация.\nАлтернативно, конфигурирайте защитата на клона ръчно в Настройки → Клонове.", "de": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.",
"de": "HTTP {status} Verboten — Ihr Token hat keine Admin-Rechte.\nStellen Sie sicher, dass das Token einem Repository-Besitzer oder Organisations-Admin gehört.\nAlternativ können Sie den Branch-Schutz manuell unter Einstellungen → Branches konfigurieren.", "en": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.",
"ru": "HTTP {status} Запрещено — у вашего токена нет прав администратора.\nУбедитесь, что токен принадлежит владельцу репозитория или администратору организации.\nЛибо настройте защиту ветки вручную в разделе Настройки → Ветки.", "ru": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.",
"zh": "HTTP {status} 禁止访问 — 您的令牌缺少管理员权限。\n请确保令牌属于仓库所有者或组织管理员。\n或者,您可以在 设置 → 分支 中手动配置分支保护。" "zh": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment."
}, },
"Head branch is behind master. Pulling and rebasing...": { "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.": {
"en": "Head branch is behind master. Pulling and rebasing...", "bg": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
"bg": "Head branch is behind master. Pulling and rebasing...", "de": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
"de": "Head branch is behind master. Pulling and rebasing...", "en": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
"ru": "Head branch is behind master. Pulling and rebasing...", "ru": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
"zh": "Head branch is behind master. Pulling and rebasing..." "zh": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping."
}, },
"Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}": { "HTTP error: {status} — {message}": {
"en": "Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}", "bg": "HTTP грешка: {status} — {message}",
"bg": "Инфраструктурен commit (без идентификатор на задача DEVX-N), пропускаме обновяването на Vikunja: {msg}", "de": "HTTP-Fehler: {status} — {message}",
"de": "Infrastruktur-Commit (keine DEVX-N Task-ID), Vikunja-Update wird übersprungen: {msg}", "en": "HTTP error: {status} — {message}",
"ru": "Инфраструктурный коммит (без ID задачи DEVX-N), пропуск обновления Vikunja: {msg}", "ru": "Ошибка HTTP: {status} — {message}",
"zh": "基础设施提交(无 DEVX-N 任务 ID),跳过 Vikunja 更新: {msg}" "zh": "HTTP 错误: {status} — {message}"
}, },
"Lint failed — refusing to release. Fix lint errors first.\n{stderr}": { "HTTP {status} Forbidden — your token lacks admin rights.\nMake sure the token belongs to a repo owner or organisation admin.\nAlternatively, configure branch protection manually in Settings → Branches.": {
"en": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}", "bg": "HTTP {status} Забранено — вашият токен няма администраторски права.\nУверете се, че токенът принадлежи на собственик на хранилище или администратор на организация.\nАлтернативно, конфигурирайте защитата на клона ръчно в Настройки → Клонове.",
"bg": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}", "de": "HTTP {status} Verboten — Ihr Token hat keine Admin-Rechte.\nStellen Sie sicher, dass das Token einem Repository-Besitzer oder Organisations-Admin gehört.\nAlternativ können Sie den Branch-Schutz manuell unter Einstellungen → Branches konfigurieren.",
"de": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}", "en": "HTTP {status} Forbidden — your token lacks admin rights.\nMake sure the token belongs to a repo owner or organisation admin.\nAlternatively, configure branch protection manually in Settings → Branches.",
"ru": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}", "ru": "HTTP {status} Запрещено — у вашего токена нет прав администратора.\nУбедитесь, что токен принадлежит владельцу репозитория или администратору организации.\nЛибо настройте защиту ветки вручную в разделе Настройки → Ветки.",
"zh": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}" "zh": "HTTP {status} 禁止访问 — 您的令牌缺少管理员权限。\n请确保令牌属于仓库所有者或组织管理员。\n或者,您可以在 设置 → 分支 中手动配置分支保护。"
}, },
"Lint passed.": { "Head branch is behind master. Pulling and rebasing...": {
"en": "Lint passed.", "bg": "Head branch is behind master. Pulling and rebasing...",
"bg": "Lint passed.", "de": "Head branch is behind master. Pulling and rebasing...",
"de": "Lint passed.", "en": "Head branch is behind master. Pulling and rebasing...",
"ru": "Lint passed.", "ru": "Head branch is behind master. Pulling and rebasing...",
"zh": "Lint passed." "zh": "Head branch is behind master. Pulling and rebasing..."
}, },
"Mapped file {file} is empty. Update the content or remove from mapping.json.": { "Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}": {
"en": "Mapped file {file} is empty. Update the content or remove from mapping.json.", "bg": "Инфраструктурен commit (без идентификатор на задача DEVX-N), пропускаме обновяването на Vikunja: {msg}",
"bg": "Mapped file {file} is empty. Update the content or remove from mapping.json.", "de": "Infrastruktur-Commit (keine DEVX-N Task-ID), Vikunja-Update wird übersprungen: {msg}",
"de": "Mapped file {file} is empty. Update the content or remove from mapping.json.", "en": "Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}",
"ru": "Mapped file {file} is empty. Update the content or remove from mapping.json.", "ru": "Инфраструктурный коммит (без ID задачи DEVX-N), пропуск обновления Vikunja: {msg}",
"zh": "Mapped file {file} is empty. Update the content or remove from mapping.json." "zh": "基础设施提交(无 DEVX-N 任务 ID),跳过 Vikunja 更新: {msg}"
}, },
"Mapped file {file} not found. Update mapping.json or create the file.": { "Integration tests cancelled — another runner failed.": {
"en": "Mapped file {file} not found. Update mapping.json or create the file.", "bg": "Integration tests cancelled — another runner failed.",
"bg": "Mapped file {file} not found. Update mapping.json or create the file.", "de": "Integration tests cancelled — another runner failed.",
"de": "Mapped file {file} not found. Update mapping.json or create the file.", "en": "Integration tests cancelled — another runner failed.",
"ru": "Mapped file {file} not found. Update mapping.json or create the file.", "ru": "Integration tests cancelled — another runner failed.",
"zh": "Mapped file {file} not found. Update mapping.json or create the file." "zh": "Integration tests cancelled — another runner failed."
}, },
"Merge failed after rebase retry: {error}\nPlease rebase the PR manually.": { "Integration tests failed with exit code {code}": {
"en": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.", "bg": "Integration tests failed with exit code {code}",
"bg": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.", "de": "Integration tests failed with exit code {code}",
"de": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.", "en": "Integration tests failed with exit code {code}",
"ru": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.", "ru": "Integration tests failed with exit code {code}",
"zh": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually." "zh": "Integration tests failed with exit code {code}"
}, },
"Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.": { "Integration tests passed.": {
"en": "Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.", "bg": "Integration tests passed.",
"bg": "Сливането неуспешно с HTTP {status}: {message}\nПроверете дали PR е готов и имате права за сливане.", "de": "Integration tests passed.",
"de": "Merge fehlgeschlagen mit HTTP {status}: {message}\nBitte prüfen Sie, ob der PR bereit ist und Sie Merge-Rechte haben.", "en": "Integration tests passed.",
"ru": "Слияние не удалось: HTTP {status}: {message}\nПроверьте, что PR готов и у вас есть права на слияние.", "ru": "Integration tests passed.",
"zh": "合并失败: HTTP {status}: {message}\n请检查 PR 是否准备就绪且您具有合并权限。" "zh": "Integration tests passed."
}, },
"Module {mod} has no main() function": { "Lint failed — refusing to release. Fix lint errors first.\n{stderr}": {
"en": "Module {mod} has no main() function", "bg": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
"bg": "Модул {mod} няма функция main()", "de": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
"de": "Modul {mod} hat keine main()-Funktion", "en": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
"ru": "Модуль {mod} не имеет функции main()", "ru": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
"zh": "模块 {mod} 没有 main() 函数" "zh": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}"
}, },
"Molecule directory not found: {path}": { "Lint passed.": {
"en": "Molecule directory not found: {path}", "bg": "Lint passed.",
"bg": "Директорията на molecule не е намерена: {path}", "de": "Lint passed.",
"de": "Molecule-Verzeichnis nicht gefunden: {path}", "en": "Lint passed.",
"ru": "Директория molecule не найдена: {path}", "ru": "Lint passed.",
"zh": "未找到 molecule 目录: {path}" "zh": "Lint passed."
}, },
"Nice! Gitea release {tag} created.": { "Mapped file {file} is empty. Update the content or remove from mapping.json.": {
"en": "Nice! Gitea release {tag} created.", "bg": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
"bg": "Отлично! Gitea release {tag} е създаден.", "de": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
"de": "Prima! Gitea-Release {tag} erstellt.", "en": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
"ru": "Отлично! Gitea release {tag} создан.", "ru": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
"zh": "不错!Gitea release {tag} 已创建。" "zh": "Mapped file {file} is empty. Update the content or remove from mapping.json."
}, },
"Nice! PR #{pr_number} squash-merged with title: {merge_title}": { "Mapped file {file} not found. Update mapping.json or create the file.": {
"en": "Nice! PR #{pr_number} squash-merged with title: {merge_title}", "bg": "Mapped file {file} not found. Update mapping.json or create the file.",
"bg": "Отлично! PR #{pr_number} е squash-merge-нат със заглавие: {merge_title}", "de": "Mapped file {file} not found. Update mapping.json or create the file.",
"de": "Prima! PR #{pr_number} wurde mit Titel {merge_title} squash-gemergt.", "en": "Mapped file {file} not found. Update mapping.json or create the file.",
"ru": "Отлично! PR #{pr_number} squash-merge с заголовком: {merge_title}", "ru": "Mapped file {file} not found. Update mapping.json or create the file.",
"zh": "不错!PR #{pr_number} 已 squash 合并,标题: {merge_title}" "zh": "Mapped file {file} not found. Update mapping.json or create the file."
}, },
"Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.": { "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.": {
"en": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.", "bg": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
"bg": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.", "de": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
"de": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.", "en": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
"ru": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.", "ru": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
"zh": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered." "zh": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually."
}, },
"Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.": { "Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.": {
"en": "Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.", "bg": "Сливането неуспешно с HTTP {status}: {message}\nПроверете дали PR е готов и имате права за сливане.",
"bg": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) е обновена и маркирана като готова.", "de": "Merge fehlgeschlagen mit HTTP {status}: {message}\nBitte prüfen Sie, ob der PR bereit ist und Sie Merge-Rechte haben.",
"de": "Prima! Vikunja-Aufgabe {task_id} (ID {vikunja_id}) aktualisiert und als erledigt markiert.", "en": "Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.",
"ru": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) обновлена и отмечена как выполненная.", "ru": "Слияние не удалось: HTTP {status}: {message}\nПроверьте, что PR готов и у вас есть права на слияние.",
"zh": "不错!Vikunja 任务 {task_id} (ID {vikunja_id}) 已更新并标记为完成。" "zh": "合并失败: HTTP {status}: {message}\n请检查 PR 是否准备就绪且您具有合并权限。"
}, },
"No changes between {base} and {head}.": { "Merged {count} reports: {tests} tests, {failures} failures → {output}": {
"en": "No changes between {base} and {head}.", "bg": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
"bg": "No changes between {base} and {head}.", "de": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
"de": "No changes between {base} and {head}.", "en": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
"ru": "No changes between {base} and {head}.", "ru": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
"zh": "No changes between {base} and {head}." "zh": "Merged {count} reports: {tests} tests, {failures} failures → {output}"
}, },
"No staged changes — version and changelog already up to date.": { "Module {mod} has no main() function": {
"en": "No staged changes — version and changelog already up to date.", "bg": "Модул {mod} няма функция main()",
"bg": "No staged changes — version and changelog already up to date.", "de": "Modul {mod} hat keine main()-Funktion",
"de": "No staged changes — version and changelog already up to date.", "en": "Module {mod} has no main() function",
"ru": "No staged changes — version and changelog already up to date.", "ru": "Модуль {mod} не имеет функции main()",
"zh": "No staged changes — version and changelog already up to date." "zh": "模块 {mod} 没有 main() 函数"
}, },
"No tags found — treating all changes as user-facing.": { "Molecule directory not found: {path}": {
"en": "No tags found — treating all changes as user-facing.", "bg": "Директорията на molecule не е намерена: {path}",
"bg": "No tags found — treating all changes as user-facing.", "de": "Molecule-Verzeichnis nicht gefunden: {path}",
"de": "No tags found — treating all changes as user-facing.", "en": "Molecule directory not found: {path}",
"ru": "No tags found — treating all changes as user-facing.", "ru": "Директория molecule не найдена: {path}",
"zh": "No tags found — treating all changes as user-facing." "zh": "未找到 molecule 目录: {path}"
}, },
"No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.": { "Nice! Gitea release {tag} created.": {
"en": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.", "bg": "Отлично! Gitea release {tag} е създаден.",
"bg": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.", "de": "Prima! Gitea-Release {tag} erstellt.",
"de": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.", "en": "Nice! Gitea release {tag} created.",
"ru": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.", "ru": "Отлично! Gitea release {tag} создан.",
"zh": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID." "zh": "不错!Gitea release {tag} 已创建。"
}, },
"No unreleased changes found. Nothing to release.": { "Nice! PR #{pr_number} squash-merged with title: {merge_title}": {
"en": "No unreleased changes found. Nothing to release.", "bg": "Отлично! PR #{pr_number} е squash-merge-нат със заглавие: {merge_title}",
"bg": "No unreleased changes found. Nothing to release.", "de": "Prima! PR #{pr_number} wurde mit Titel {merge_title} squash-gemergt.",
"de": "No unreleased changes found. Nothing to release.", "en": "Nice! PR #{pr_number} squash-merged with title: {merge_title}",
"ru": "No unreleased changes found. Nothing to release.", "ru": "Отлично! PR #{pr_number} squash-merge с заголовком: {merge_title}",
"zh": "No unreleased changes found. Nothing to release." "zh": "不错!PR #{pr_number} 已 squash 合并,标题: {merge_title}"
}, },
"No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.": { "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.": {
"en": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.", "bg": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
"bg": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.", "de": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
"de": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.", "en": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
"ru": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.", "ru": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
"zh": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release." "zh": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered."
}, },
"Note: Self-approval not allowed. Posting COMMENT instead.": { "Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.": {
"en": "Note: Self-approval not allowed. Posting COMMENT instead.", "bg": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) е обновена и маркирана като готова.",
"bg": "Note: Self-approval not allowed. Posting COMMENT instead.", "de": "Prima! Vikunja-Aufgabe {task_id} (ID {vikunja_id}) aktualisiert und als erledigt markiert.",
"de": "Note: Self-approval not allowed. Posting COMMENT instead.", "en": "Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.",
"ru": "Note: Self-approval not allowed. Posting COMMENT instead.", "ru": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) обновлена и отмечена как выполненная.",
"zh": "Note: Self-approval not allowed. Posting COMMENT instead." "zh": "不错!Vikunja 任务 {task_id} (ID {vikunja_id}) 已更新并标记为完成。"
}, },
"Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE": { "No JUnit reports found matching {pattern} — skipping merge.": {
"en": "Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "bg": "No JUnit reports found matching {pattern} — skipping merge.",
"bg": "Опа! Съобщението за commit трябва да следва конвенционален формат.\n Очаква се: <type>: <description>\n Получено: {subject}\n Разрешени типове: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "de": "No JUnit reports found matching {pattern} — skipping merge.",
"de": "Ups! Commit-Nachricht muss dem konventionellen Commit-Format folgen.\n Erwartet: <type>: <description>\n Erhalten: {subject}\n Erlaubte Typen: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "en": "No JUnit reports found matching {pattern} — skipping merge.",
"ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "ru": "No JUnit reports found matching {pattern} — skipping merge.",
"zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE" "zh": "No JUnit reports found matching {pattern} — skipping merge."
}, },
"Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.": { "No changes between {base} and {head}.": {
"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": "No changes between {base} and {head}.",
"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": "No changes between {base} and {head}.",
"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.", "en": "No changes between {base} and {head}.",
"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.", "ru": "No changes between {base} and {head}.",
"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." "zh": "No changes between {base} and {head}."
}, },
"Oops! Gitea PyPI registry publish failed:\n{stderr}": { "No staged changes — version and changelog already up to date.": {
"en": "Oops! Gitea PyPI registry publish failed:\n{stderr}", "bg": "No staged changes — version and changelog already up to date.",
"bg": "Опа! Публикуването в Gitea PyPI registry неуспешно:\n{stderr}", "de": "No staged changes — version and changelog already up to date.",
"de": "Ups! Veröffentlichung in der Gitea PyPI-Registry fehlgeschlagen:\n{stderr}", "en": "No staged changes — version and changelog already up to date.",
"ru": "Ой! Публикация в Gitea PyPI registry не удалась:\n{stderr}", "ru": "No staged changes — version and changelog already up to date.",
"zh": "哎呀!Gitea PyPI registry 发布失败:\n{stderr}" "zh": "No staged changes — version and changelog already up to date."
}, },
"Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}": { "No tags found — treating all changes as user-facing.": {
"en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}", "bg": "No tags found — treating all changes as user-facing.",
"bg": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}", "de": "No tags found — treating all changes as user-facing.",
"de": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}", "en": "No tags found — treating all changes as user-facing.",
"ru": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}", "ru": "No tags found — treating all changes as user-facing.",
"zh": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}" "zh": "No tags found — treating all changes as user-facing."
}, },
"Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}": { "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.": {
"en": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}", "bg": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
"bg": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}", "de": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
"de": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}", "en": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
"ru": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}", "ru": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
"zh": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}" "zh": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID."
}, },
"Oops! No task ID found in .taskid file or branch name '{branch}'.": { "No unreleased changes found. Nothing to release.": {
"en": "Oops! No task ID found in .taskid file or branch name '{branch}'.", "bg": "No unreleased changes found. Nothing to release.",
"bg": "Oops! No task ID found in .taskid file or branch name '{branch}'.", "de": "No unreleased changes found. Nothing to release.",
"de": "Oops! No task ID found in .taskid file or branch name '{branch}'.", "en": "No unreleased changes found. Nothing to release.",
"ru": "Oops! No task ID found in .taskid file or branch name '{branch}'.", "ru": "No unreleased changes found. Nothing to release.",
"zh": "Oops! No task ID found in .taskid file or branch name '{branch}'." "zh": "No unreleased changes found. Nothing to release."
}, },
"Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}": { "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.": {
"en": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}", "bg": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
"bg": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}", "de": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
"de": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}", "en": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
"ru": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}", "ru": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
"zh": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}" "zh": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release."
}, },
"Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}": { "Note: Self-approval not allowed. Posting COMMENT instead.": {
"en": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}", "bg": "Note: Self-approval not allowed. Posting COMMENT instead.",
"bg": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}", "de": "Note: Self-approval not allowed. Posting COMMENT instead.",
"de": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}", "en": "Note: Self-approval not allowed. Posting COMMENT instead.",
"ru": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}", "ru": "Note: Self-approval not allowed. Posting COMMENT instead.",
"zh": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}" "zh": "Note: Self-approval not allowed. Posting COMMENT instead."
}, },
"Oops! Package build failed:\n{stderr}": { "Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE": {
"en": "Oops! Package build failed:\n{stderr}", "bg": "Опа! Съобщението за commit трябва да следва конвенционален формат.\n Очаква се: <type>: <description>\n Получено: {subject}\n Разрешени типове: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
"bg": "Опа! Сборката на пакета неуспешна:\n{stderr}", "de": "Ups! Commit-Nachricht muss dem konventionellen Commit-Format folgen.\n Erwartet: <type>: <description>\n Erhalten: {subject}\n Erlaubte Typen: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
"de": "Ups! Paket-Build fehlgeschlagen:\n{stderr}", "en": "Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
"ru": "Ой! Сборка пакета не удалась:\n{stderr}", "ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
"zh": "哎呀!包构建失败:\n{stderr}" "zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE"
}, },
"Oops! PyPI publish failed:\n{stderr}": { "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! PyPI publish failed:\n{stderr}", "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.",
"bg": "Опа! Публикуването в PyPI неуспешно:\n{stderr}", "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.",
"de": "Ups! PyPI-Veröffentlichung fehlgeschlagen:\n{stderr}", "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.",
"ru": "Ой! Публикация в PyPI не удалась:\n{stderr}", "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": "哎呀!PyPI 发布失败:\n{stderr}" "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."
}, },
"PASSED: {pair}": { "Oops! Gitea PyPI registry publish failed:\n{stderr}": {
"en": "PASSED: {pair}", "bg": "Опа! Публикуването в Gitea PyPI registry неуспешно:\n{stderr}",
"bg": "PASSED: {pair}", "de": "Ups! Veröffentlichung in der Gitea PyPI-Registry fehlgeschlagen:\n{stderr}",
"de": "PASSED: {pair}", "en": "Oops! Gitea PyPI registry publish failed:\n{stderr}",
"ru": "PASSED: {pair}", "ru": "Ой! Публикация в Gitea PyPI registry не удалась:\n{stderr}",
"zh": "PASSED: {pair}" "zh": "哎呀!Gitea PyPI registry 发布失败:\n{stderr}"
}, },
"PR number must be an integer, got: {pr_number}": { "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}": {
"en": "PR number must be an integer, got: {pr_number}", "bg": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
"bg": "PR number must be an integer, got: {pr_number}", "de": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
"de": "PR number must be an integer, got: {pr_number}", "en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
"ru": "PR number must be an integer, got: {pr_number}", "ru": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
"zh": "PR number must be an integer, got: {pr_number}" "zh": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}"
}, },
"PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}": { "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}": {
"en": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}", "bg": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
"bg": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}", "de": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
"de": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}", "en": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
"ru": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}", "ru": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
"zh": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}" "zh": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}"
}, },
"PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.": { "Oops! No task ID found in .taskid file or branch name '{branch}'.": {
"en": "PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.", "bg": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
"bg": "PYPI_TOKEN не е зададен и няма конфигуриран URL на registry — пропускаме публикуването в PyPI. Без притеснения, просто ще създадем Gitea release.", "de": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
"de": "PYPI_TOKEN nicht gesetzt und keine Registry-URL konfiguriert — PyPI-Veröffentlichung wird übersprungen. Keine Sorge, wir erstellen einfach das Gitea-Release.", "en": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
"ru": "PYPI_TOKEN не задан и URL registry не настроен — пропускаем публикацию в PyPI. Не беспокойтесь, мы просто создадим Gitea release.", "ru": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
"zh": "未设置 PYPI_TOKEN 且未配置 registry URL — 跳过 PyPI 发布。别担心,我们直接创建 Gitea release。" "zh": "Oops! No task ID found in .taskid file or branch name '{branch}'."
}, },
"Published to Gitea PyPI registry.": { "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}": {
"en": "Published to Gitea PyPI registry.", "bg": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
"bg": "Публикувано в Gitea PyPI registry.", "de": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
"de": "In der Gitea PyPI-Registry veröffentlicht.", "en": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
"ru": "Опубликовано в Gitea PyPI registry.", "ru": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
"zh": "已发布到 Gitea PyPI registry。" "zh": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}"
}, },
"Published to PyPI.": { "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}": {
"en": "Published to PyPI.", "bg": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
"bg": "Публикувано в PyPI.", "de": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
"de": "In PyPI veröffentlicht.", "en": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
"ru": "Опубликовано в PyPI.", "ru": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
"zh": "已发布到 PyPI。" "zh": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}"
}, },
"Pushed release commit to master.": { "Oops! Package build failed:\n{stderr}": {
"en": "Pushed release commit to master.", "bg": "Опа! Сборката на пакета неуспешна:\n{stderr}",
"bg": "Pushed release commit to master.", "de": "Ups! Paket-Build fehlgeschlagen:\n{stderr}",
"de": "Pushed release commit to master.", "en": "Oops! Package build failed:\n{stderr}",
"ru": "Pushed release commit to master.", "ru": "Ой! Сборка пакета не удалась:\n{stderr}",
"zh": "Pushed release commit to master." "zh": "哎呀!包构建失败:\n{stderr}"
}, },
"Rebased and pushed. Retrying merge...": { "Oops! PyPI publish failed:\n{stderr}": {
"en": "Rebased and pushed. Retrying merge...", "bg": "Опа! Публикуването в PyPI неуспешно:\n{stderr}",
"bg": "Rebased and pushed. Retrying merge...", "de": "Ups! PyPI-Veröffentlichung fehlgeschlagen:\n{stderr}",
"de": "Rebased and pushed. Retrying merge...", "en": "Oops! PyPI publish failed:\n{stderr}",
"ru": "Rebased and pushed. Retrying merge...", "ru": "Ой! Публикация в PyPI не удалась:\n{stderr}",
"zh": "Rebased and pushed. Retrying merge..." "zh": "哎呀!PyPI 发布失败:\n{stderr}"
}, },
"Release creation failed: {error}": { "PASSED: {pair}": {
"en": "Release creation failed: {error}", "bg": "PASSED: {pair}",
"bg": "Release creation failed: {error}", "de": "PASSED: {pair}",
"de": "Release creation failed: {error}", "en": "PASSED: {pair}",
"ru": "Release creation failed: {error}", "ru": "PASSED: {pair}",
"zh": "Release creation failed: {error}" "zh": "PASSED: {pair}"
}, },
"Release must be run on master, currently on '{branch}'.": { "PR number must be an integer, got: {pr_number}": {
"en": "Release must be run on master, currently on '{branch}'.", "bg": "PR number must be an integer, got: {pr_number}",
"bg": "Release must be run on master, currently on '{branch}'.", "de": "PR number must be an integer, got: {pr_number}",
"de": "Release must be run on master, currently on '{branch}'.", "en": "PR number must be an integer, got: {pr_number}",
"ru": "Release must be run on master, currently on '{branch}'.", "ru": "PR number must be an integer, got: {pr_number}",
"zh": "Release must be run on master, currently on '{branch}'." "zh": "PR number must be an integer, got: {pr_number}"
}, },
"Repo must be in 'owner/name' format, got: {repo}": { "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}": {
"en": "Repo must be in 'owner/name' format, got: {repo}", "bg": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
"bg": "Repo must be in 'owner/name' format, got: {repo}", "de": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
"de": "Repo must be in 'owner/name' format, got: {repo}", "en": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
"ru": "Repo must be in 'owner/name' format, got: {repo}", "ru": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
"zh": "Repo must be in 'owner/name' format, got: {repo}" "zh": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}"
}, },
"Repository configuration complete.": { "PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.": {
"en": "Repository configuration complete.", "bg": "PYPI_TOKEN не е зададен и няма конфигуриран URL на registry — пропускаме публикуването в PyPI. Без притеснения, просто ще създадем Gitea release.",
"bg": "Конфигурирането на хранилището е завършено.", "de": "PYPI_TOKEN nicht gesetzt und keine Registry-URL konfiguriert — PyPI-Veröffentlichung wird übersprungen. Keine Sorge, wir erstellen einfach das Gitea-Release.",
"de": "Repository-Konfiguration abgeschlossen.", "en": "PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.",
"ru": "Конфигурация репозитория завершена.", "ru": "PYPI_TOKEN не задан и URL registry не настроен — пропускаем публикацию в PyPI. Не беспокойтесь, мы просто создадим Gitea release.",
"zh": "仓库配置完成。" "zh": "未设置 PYPI_TOKEN 且未配置 registry URL — 跳过 PyPI 发布。别担心,我们直接创建 Gitea release。"
}, },
"Runner index {index} out of range (0..{max})": { "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.": {
"en": "Runner index {index} out of range (0..{max})", "bg": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
"bg": "Индексът на runner {index} е извън диапазона (0..{max})", "de": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
"de": "Runner-Index {index} außerhalb des Bereichs (0..{max})", "en": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
"ru": "Индекс runner {index} вне диапазона (0..{max})", "ru": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
"zh": "Runner 索引 {index} 超出范围 (0..{max})" "zh": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit."
}, },
"Running lint checks...": { "Published to Gitea PyPI registry.": {
"en": "Running lint checks...", "bg": "Публикувано в Gitea PyPI registry.",
"bg": "Running lint checks...", "de": "In der Gitea PyPI-Registry veröffentlicht.",
"de": "Running lint checks...", "en": "Published to Gitea PyPI registry.",
"ru": "Running lint checks...", "ru": "Опубликовано в Gitea PyPI registry.",
"zh": "Running lint checks..." "zh": "已发布到 Gitea PyPI registry。"
}, },
"Running tests...": { "Published to PyPI.": {
"en": "Running tests...", "bg": "Публикувано в PyPI.",
"bg": "Running tests...", "de": "In PyPI veröffentlicht.",
"de": "Running tests...", "en": "Published to PyPI.",
"ru": "Running tests...", "ru": "Опубликовано в PyPI.",
"zh": "Running tests..." "zh": "已发布到 PyPI。"
}, },
"Running: {scenario} on {platform}": { "Pushed release commit to master.": {
"en": "Running: {scenario} on {platform}", "bg": "Pushed release commit to master.",
"bg": "Running: {scenario} on {platform}", "de": "Pushed release commit to master.",
"de": "Running: {scenario} on {platform}", "en": "Pushed release commit to master.",
"ru": "Running: {scenario} on {platform}", "ru": "Pushed release commit to master.",
"zh": "Running: {scenario} on {platform}" "zh": "Pushed release commit to master."
}, },
"Skipping commit push — no staged changes.": { "Rebased and pushed. Retrying merge...": {
"en": "Skipping commit push — no staged changes.", "bg": "Rebased and pushed. Retrying merge...",
"bg": "Skipping commit push — no staged changes.", "de": "Rebased and pushed. Retrying merge...",
"de": "Skipping commit push — no staged changes.", "en": "Rebased and pushed. Retrying merge...",
"ru": "Skipping commit push — no staged changes.", "ru": "Rebased and pushed. Retrying merge...",
"zh": "Skipping commit push — no staged changes." "zh": "Rebased and pushed. Retrying merge..."
}, },
"Syncing {count} documentation pages to wiki...": { "Release creation failed: {error}": {
"en": "Syncing {count} documentation pages to wiki...", "bg": "Release creation failed: {error}",
"bg": "Syncing {count} documentation pages to wiki...", "de": "Release creation failed: {error}",
"de": "Syncing {count} documentation pages to wiki...", "en": "Release creation failed: {error}",
"ru": "Syncing {count} documentation pages to wiki...", "ru": "Release creation failed: {error}",
"zh": "Syncing {count} documentation pages to wiki..." "zh": "Release creation failed: {error}"
}, },
"Tag consistency check failed.": { "Release must be run on master, currently on '{branch}'.": {
"en": "Tag consistency check failed.", "bg": "Release must be run on master, currently on '{branch}'.",
"bg": "Tag consistency check failed.", "de": "Release must be run on master, currently on '{branch}'.",
"de": "Tag consistency check failed.", "en": "Release must be run on master, currently on '{branch}'.",
"ru": "Tag consistency check failed.", "ru": "Release must be run on master, currently on '{branch}'.",
"zh": "Tag consistency check failed." "zh": "Release must be run on master, currently on '{branch}'."
}, },
"Tag v{version} already existed. Publish workflow should already have been triggered.": { "Repo must be in 'owner/name' format, got: {repo}": {
"en": "Tag v{version} already existed. Publish workflow should already have been triggered.", "bg": "Repo must be in 'owner/name' format, got: {repo}",
"bg": "Tag v{version} already existed. Publish workflow should already have been triggered.", "de": "Repo must be in 'owner/name' format, got: {repo}",
"de": "Tag v{version} already existed. Publish workflow should already have been triggered.", "en": "Repo must be in 'owner/name' format, got: {repo}",
"ru": "Tag v{version} already existed. Publish workflow should already have been triggered.", "ru": "Repo must be in 'owner/name' format, got: {repo}",
"zh": "Tag v{version} already existed. Publish workflow should already have been triggered." "zh": "Repo must be in 'owner/name' format, got: {repo}"
}, },
"Tag {tag} already exists and points to HEAD. Skipping creation.": { "Repository configuration complete.": {
"en": "Tag {tag} already exists and points to HEAD. Skipping creation.", "bg": "Конфигурирането на хранилището е завършено.",
"bg": "Tag {tag} already exists and points to HEAD. Skipping creation.", "de": "Repository-Konfiguration abgeschlossen.",
"de": "Tag {tag} already exists and points to HEAD. Skipping creation.", "en": "Repository configuration complete.",
"ru": "Tag {tag} already exists and points to HEAD. Skipping creation.", "ru": "Конфигурация репозитория завершена.",
"zh": "Tag {tag} already exists and points to HEAD. Skipping creation." "zh": "仓库配置完成。"
}, },
"Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.": { "Roles directory not found: {path}": {
"en": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.", "bg": "Roles directory not found: {path}",
"bg": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.", "de": "Roles directory not found: {path}",
"de": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.", "en": "Roles directory not found: {path}",
"ru": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.", "ru": "Roles directory not found: {path}",
"zh": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details." "zh": "Roles directory not found: {path}"
}, },
"Task ID: {task_id}": { "Runner index {index} out of range (0..{max})": {
"en": "Task ID: {task_id}", "bg": "Индексът на runner {index} е извън диапазона (0..{max})",
"bg": "Task ID: {task_id}", "de": "Runner-Index {index} außerhalb des Bereichs (0..{max})",
"de": "Task ID: {task_id}", "en": "Runner index {index} out of range (0..{max})",
"ru": "Task ID: {task_id}", "ru": "Индекс runner {index} вне диапазона (0..{max})",
"zh": "Task ID: {task_id}" "zh": "Runner 索引 {index} 超出范围 (0..{max})"
}, },
"Tests failed — refusing to release. Fix test failures first.\n{stderr}": { "Running lint checks...": {
"en": "Tests failed — refusing to release. Fix test failures first.\n{stderr}", "bg": "Running lint checks...",
"bg": "Tests failed — refusing to release. Fix test failures first.\n{stderr}", "de": "Running lint checks...",
"de": "Tests failed — refusing to release. Fix test failures first.\n{stderr}", "en": "Running lint checks...",
"ru": "Tests failed — refusing to release. Fix test failures first.\n{stderr}", "ru": "Running lint checks...",
"zh": "Tests failed — refusing to release. Fix test failures first.\n{stderr}" "zh": "Running lint checks..."
}, },
"Tests passed.": { "Running tests...": {
"en": "Tests passed.", "bg": "Running tests...",
"bg": "Tests passed.", "de": "Running tests...",
"de": "Tests passed.", "en": "Running tests...",
"ru": "Tests passed.", "ru": "Running tests...",
"zh": "Tests passed." "zh": "Running tests..."
}, },
"Unit tests passed in {duration:.2f}s (under {max}s limit).": { "Running: {scenario} on {platform}": {
"en": "Unit tests passed in {duration:.2f}s (under {max}s limit).", "bg": "Running: {scenario} on {platform}",
"bg": "Unit tests passed in {duration:.2f}s (under {max}s limit).", "de": "Running: {scenario} on {platform}",
"de": "Unit tests passed in {duration:.2f}s (under {max}s limit).", "en": "Running: {scenario} on {platform}",
"ru": "Unit tests passed in {duration:.2f}s (under {max}s limit).", "ru": "Running: {scenario} on {platform}",
"zh": "Unit tests passed in {duration:.2f}s (under {max}s limit)." "zh": "Running: {scenario} on {platform}"
}, },
"Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.": { "Skipping commit push — no staged changes.": {
"en": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.", "bg": "Skipping commit push — no staged changes.",
"bg": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.", "de": "Skipping commit push — no staged changes.",
"de": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.", "en": "Skipping commit push — no staged changes.",
"ru": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.", "ru": "Skipping commit push — no staged changes.",
"zh": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures." "zh": "Skipping commit push — no staged changes."
}, },
"Unknown check category '{check}'. Available: all, user-facing{tags}": { "Starting Docker daemon...": {
"en": "Unknown check category '{check}'. Available: all, user-facing{tags}", "bg": "Starting Docker daemon...",
"bg": "Unknown check category '{check}'. Available: all, user-facing{tags}", "de": "Docker-Daemon wird gestartet...",
"de": "Unknown check category '{check}'. Available: all, user-facing{tags}", "en": "Starting Docker daemon...",
"ru": "Unknown check category '{check}'. Available: all, user-facing{tags}", "ru": "Запуск Docker-демона...",
"zh": "Unknown check category '{check}'. Available: all, user-facing{tags}" "zh": "正在启动 Docker 守护进程..."
}, },
"Updated version in {init}": { "Syncing {count} documentation pages to wiki...": {
"en": "Updated version in {init}", "bg": "Syncing {count} documentation pages to wiki...",
"bg": "Updated version in {init}", "de": "Syncing {count} documentation pages to wiki...",
"de": "Updated version in {init}", "en": "Syncing {count} documentation pages to wiki...",
"ru": "Updated version in {init}", "ru": "Syncing {count} documentation pages to wiki...",
"zh": "Updated version in {init}" "zh": "Syncing {count} documentation pages to wiki..."
}, },
"Updated {changelog_file}": { "Tag consistency check failed.": {
"en": "Updated {changelog_file}", "bg": "Tag consistency check failed.",
"bg": "Updated {changelog_file}", "de": "Tag consistency check failed.",
"de": "Updated {changelog_file}", "en": "Tag consistency check failed.",
"ru": "Updated {changelog_file}", "ru": "Tag consistency check failed.",
"zh": "Updated {changelog_file}" "zh": "Tag consistency check failed."
}, },
"VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.": { "Tag v{version} already existed. Publish workflow should already have been triggered.": {
"en": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.", "bg": "Tag v{version} already existed. Publish workflow should already have been triggered.",
"bg": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.", "de": "Tag v{version} already existed. Publish workflow should already have been triggered.",
"de": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.", "en": "Tag v{version} already existed. Publish workflow should already have been triggered.",
"ru": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.", "ru": "Tag v{version} already existed. Publish workflow should already have been triggered.",
"zh": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles." "zh": "Tag v{version} already existed. Publish workflow should already have been triggered."
}, },
"Version file: {file}": { "Tag {tag} already exists and points to HEAD. Skipping creation.": {
"en": "Version file: {file}", "bg": "Tag {tag} already exists and points to HEAD. Skipping creation.",
"bg": "Version file: {file}", "de": "Tag {tag} already exists and points to HEAD. Skipping creation.",
"de": "Version file: {file}", "en": "Tag {tag} already exists and points to HEAD. Skipping creation.",
"ru": "Version file: {file}", "ru": "Tag {tag} already exists and points to HEAD. Skipping creation.",
"zh": "Version file: {file}" "zh": "Tag {tag} already exists and points to HEAD. Skipping creation."
}, },
"Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.": { "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.": {
"en": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.", "bg": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.",
"bg": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.", "de": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.",
"de": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.", "en": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.",
"ru": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.", "ru": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.",
"zh": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update." "zh": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details."
}, },
"WARNING: --skip-tests passed — skipping test verification.": { "Task ID: {task_id}": {
"en": "WARNING: --skip-tests passed — skipping test verification.", "bg": "Task ID: {task_id}",
"bg": "WARNING: --skip-tests passed — skipping test verification.", "de": "Task ID: {task_id}",
"de": "WARNING: --skip-tests passed — skipping test verification.", "en": "Task ID: {task_id}",
"ru": "WARNING: --skip-tests passed — skipping test verification.", "ru": "Task ID: {task_id}",
"zh": "WARNING: --skip-tests passed — skipping test verification." "zh": "Task ID: {task_id}"
}, },
"Warning: could not fetch tags from origin.": { "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.": {
"en": "Warning: could not fetch tags from origin.", "bg": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.",
"bg": "Warning: could not fetch tags from origin.", "de": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.",
"de": "Warning: could not fetch tags from origin.", "en": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.",
"ru": "Warning: could not fetch tags from origin.", "ru": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.",
"zh": "Warning: could not fetch tags from origin." "zh": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls."
}, },
"Wiki integrity check failed — {count} issue(s)": { "Tests failed — refusing to release. Fix test failures first.\n{stderr}": {
"en": "Wiki integrity check failed — {count} issue(s)", "bg": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
"bg": "Wiki integrity check failed — {count} issue(s)", "de": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
"de": "Wiki integrity check failed — {count} issue(s)", "en": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
"ru": "Wiki integrity check failed — {count} issue(s)", "ru": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
"zh": "Wiki integrity check failed — {count} issue(s)" "zh": "Tests failed — refusing to release. Fix test failures first.\n{stderr}"
}, },
"Wiki verification failed — {failures} page(s) empty or mismatched": { "Tests passed.": {
"en": "Wiki verification failed — {failures} page(s) empty or mismatched", "bg": "Tests passed.",
"bg": "Wiki verification failed — {failures} page(s) empty or mismatched", "de": "Tests passed.",
"de": "Wiki verification failed — {failures} page(s) empty or mismatched", "en": "Tests passed.",
"ru": "Wiki verification failed — {failures} page(s) empty or mismatched", "ru": "Tests passed.",
"zh": "Wiki verification failed — {failures} page(s) empty or mismatched" "zh": "Tests passed."
}, },
"[dry-run] Would commit: release: v{version}": { "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).": {
"en": "[dry-run] Would commit: release: v{version}", "bg": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).",
"bg": "[dry-run] Would commit: release: v{version}", "de": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).",
"de": "[dry-run] Would commit: release: v{version}", "en": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).",
"ru": "[dry-run] Would commit: release: v{version}", "ru": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).",
"zh": "[dry-run] Would commit: release: v{version}" "zh": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit)."
}, },
"[dry-run] Would create tag: v{version}": { "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.": {
"en": "[dry-run] Would create tag: v{version}", "bg": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.",
"bg": "[dry-run] Would create tag: v{version}", "de": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.",
"de": "[dry-run] Would create tag: v{version}", "en": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.",
"ru": "[dry-run] Would create tag: v{version}", "ru": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.",
"zh": "[dry-run] Would create tag: v{version}" "zh": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures."
}, },
"[dry-run] Would create tag: {tag}": { "Unknown check category '{check}'. Available: all, user-facing{tags}": {
"en": "[dry-run] Would create tag: {tag}", "bg": "Unknown check category '{check}'. Available: all, user-facing{tags}",
"bg": "[dry-run] Would create tag: {tag}", "de": "Unknown check category '{check}'. Available: all, user-facing{tags}",
"de": "[dry-run] Would create tag: {tag}", "en": "Unknown check category '{check}'. Available: all, user-facing{tags}",
"ru": "[dry-run] Would create tag: {tag}", "ru": "Unknown check category '{check}'. Available: all, user-facing{tags}",
"zh": "[dry-run] Would create tag: {tag}" "zh": "Unknown check category '{check}'. Available: all, user-facing{tags}"
}, },
"[dry-run] Would push commit to master": { "Updated version in {init}": {
"en": "[dry-run] Would push commit to master", "bg": "Updated version in {init}",
"bg": "[dry-run] Would push commit to master", "de": "Updated version in {init}",
"de": "[dry-run] Would push commit to master", "en": "Updated version in {init}",
"ru": "[dry-run] Would push commit to master", "ru": "Updated version in {init}",
"zh": "[dry-run] Would push commit to master" "zh": "Updated version in {init}"
}, },
"[dry-run] Would sync page: {title} ({chars} chars)": { "Updated {changelog_file}": {
"en": "[dry-run] Would sync page: {title} ({chars} chars)", "bg": "Updated {changelog_file}",
"bg": "[dry-run] Would sync page: {title} ({chars} chars)", "de": "Updated {changelog_file}",
"de": "[dry-run] Would sync page: {title} ({chars} chars)", "en": "Updated {changelog_file}",
"ru": "[dry-run] Would sync page: {title} ({chars} chars)", "ru": "Updated {changelog_file}",
"zh": "[dry-run] Would sync page: {title} ({chars} chars)" "zh": "Updated {changelog_file}"
}, },
"[dry-run] Would update {changelog_file}": { "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.": {
"en": "[dry-run] Would update {changelog_file}", "bg": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
"bg": "[dry-run] Would update {changelog_file}", "de": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
"de": "[dry-run] Would update {changelog_file}", "en": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
"ru": "[dry-run] Would update {changelog_file}", "ru": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
"zh": "[dry-run] Would update {changelog_file}" "zh": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles."
}, },
"[dry-run] Would update {init}": { "Version file: {file}": {
"en": "[dry-run] Would update {init}", "bg": "Version file: {file}",
"bg": "[dry-run] Would update {init}", "de": "Version file: {file}",
"de": "[dry-run] Would update {init}", "en": "Version file: {file}",
"ru": "[dry-run] Would update {init}", "ru": "Version file: {file}",
"zh": "[dry-run] Would update {init}" "zh": "Version file: {file}"
}, },
"active": { "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.": {
"en": "active", "bg": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.",
"bg": "активен", "de": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.",
"de": "aktiv", "en": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.",
"ru": "активен", "ru": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.",
"zh": "活跃" "zh": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update."
}, },
"completed": { "WARNING: --skip-tests passed — skipping test verification.": {
"en": "completed", "bg": "WARNING: --skip-tests passed — skipping test verification.",
"bg": "завършен", "de": "WARNING: --skip-tests passed — skipping test verification.",
"de": "abgeschlossen", "en": "WARNING: --skip-tests passed — skipping test verification.",
"ru": "завершён", "ru": "WARNING: --skip-tests passed — skipping test verification.",
"zh": "已完成" "zh": "WARNING: --skip-tests passed — skipping test verification."
}, },
"failed": { "Warning: could not fetch tags from origin.": {
"en": "failed", "bg": "Warning: could not fetch tags from origin.",
"bg": "неуспешен", "de": "Warning: could not fetch tags from origin.",
"de": "fehlgeschlagen", "en": "Warning: could not fetch tags from origin.",
"ru": "неудачный", "ru": "Warning: could not fetch tags from origin.",
"zh": "失败" "zh": "Warning: could not fetch tags from origin."
}, },
"git command failed ({cmd}): {stderr}": { "Wiki integrity check failed — {count} issue(s)": {
"en": "git command failed ({cmd}): {stderr}", "bg": "Wiki integrity check failed — {count} issue(s)",
"bg": "git command failed ({cmd}): {stderr}", "de": "Wiki integrity check failed — {count} issue(s)",
"de": "git command failed ({cmd}): {stderr}", "en": "Wiki integrity check failed — {count} issue(s)",
"ru": "git command failed ({cmd}): {stderr}", "ru": "Wiki integrity check failed — {count} issue(s)",
"zh": "git command failed ({cmd}): {stderr}" "zh": "Wiki integrity check failed — {count} issue(s)"
}, },
"git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.": { "Wiki verification failed — {failures} page(s) empty or mismatched": {
"en": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.", "bg": "Wiki verification failed — {failures} page(s) empty or mismatched",
"bg": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.", "de": "Wiki verification failed — {failures} page(s) empty or mismatched",
"de": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.", "en": "Wiki verification failed — {failures} page(s) empty or mismatched",
"ru": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.", "ru": "Wiki verification failed — {failures} page(s) empty or mismatched",
"zh": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history." "zh": "Wiki verification failed — {failures} page(s) empty or mismatched"
}, },
"git-cliff returned empty version.": { "[dry-run] Would commit: release: v{version}": {
"en": "git-cliff returned empty version.", "bg": "[dry-run] Would commit: release: v{version}",
"bg": "git-cliff returned empty version.", "de": "[dry-run] Would commit: release: v{version}",
"de": "git-cliff returned empty version.", "en": "[dry-run] Would commit: release: v{version}",
"ru": "git-cliff returned empty version.", "ru": "[dry-run] Would commit: release: v{version}",
"zh": "git-cliff returned empty version." "zh": "[dry-run] Would commit: release: v{version}"
}, },
"git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).": { "[dry-run] Would create tag: v{version}": {
"en": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).", "bg": "[dry-run] Would create tag: v{version}",
"bg": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).", "de": "[dry-run] Would create tag: v{version}",
"de": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).", "en": "[dry-run] Would create tag: v{version}",
"ru": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).", "ru": "[dry-run] Would create tag: v{version}",
"zh": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1)." "zh": "[dry-run] Would create tag: v{version}"
}, },
"in_progress": { "[dry-run] Would create tag: {tag}": {
"en": "in progress", "bg": "[dry-run] Would create tag: {tag}",
"bg": "в процес", "de": "[dry-run] Would create tag: {tag}",
"de": "in Bearbeitung", "en": "[dry-run] Would create tag: {tag}",
"ru": "в процессе", "ru": "[dry-run] Would create tag: {tag}",
"zh": "进行中" "zh": "[dry-run] Would create tag: {tag}"
}, },
"inactive": { "[dry-run] Would push commit to master": {
"en": "inactive", "bg": "[dry-run] Would push commit to master",
"bg": "неактивен", "de": "[dry-run] Would push commit to master",
"de": "inaktiv", "en": "[dry-run] Would push commit to master",
"ru": "неактивен", "ru": "[dry-run] Would push commit to master",
"zh": "未激活" "zh": "[dry-run] Would push commit to master"
}, },
"mapping.json keys and values must be strings, got {k}={v}": { "[dry-run] Would sync page: {title} ({chars} chars)": {
"en": "mapping.json keys and values must be strings, got {k}={v}", "bg": "[dry-run] Would sync page: {title} ({chars} chars)",
"bg": "mapping.json keys and values must be strings, got {k}={v}", "de": "[dry-run] Would sync page: {title} ({chars} chars)",
"de": "mapping.json keys and values must be strings, got {k}={v}", "en": "[dry-run] Would sync page: {title} ({chars} chars)",
"ru": "mapping.json keys and values must be strings, got {k}={v}", "ru": "[dry-run] Would sync page: {title} ({chars} chars)",
"zh": "mapping.json keys and values must be strings, got {k}={v}" "zh": "[dry-run] Would sync page: {title} ({chars} chars)"
}, },
"mapping.json must be a dict of file-path -> page-title, got {type}": { "[dry-run] Would update {changelog_file}": {
"en": "mapping.json must be a dict of file-path -> page-title, got {type}", "bg": "[dry-run] Would update {changelog_file}",
"bg": "mapping.json must be a dict of file-path -> page-title, got {type}", "de": "[dry-run] Would update {changelog_file}",
"de": "mapping.json must be a dict of file-path -> page-title, got {type}", "en": "[dry-run] Would update {changelog_file}",
"ru": "mapping.json must be a dict of file-path -> page-title, got {type}", "ru": "[dry-run] Would update {changelog_file}",
"zh": "mapping.json must be a dict of file-path -> page-title, got {type}" "zh": "[dry-run] Would update {changelog_file}"
}, },
"pending": { "[dry-run] Would update {init}": {
"en": "pending", "bg": "[dry-run] Would update {init}",
"bg": "в очакване", "de": "[dry-run] Would update {init}",
"de": "ausstehend", "en": "[dry-run] Would update {init}",
"ru": "ожидает", "ru": "[dry-run] Would update {init}",
"zh": "待处理" "zh": "[dry-run] Would update {init}"
}, },
"unknown": { "active": {
"en": "unknown", "bg": "активен",
"bg": "неизвестен", "de": "aktiv",
"de": "unbekannt", "en": "active",
"ru": "неизвестно", "ru": "активен",
"zh": "未知" "zh": "活跃"
}, },
"{file} already exists. Use --force to overwrite.": { "completed": {
"en": "{file} already exists. Use --force to overwrite.", "bg": "завършен",
"bg": "{file} already exists. Use --force to overwrite.", "de": "abgeschlossen",
"de": "{file} already exists. Use --force to overwrite.", "en": "completed",
"ru": "{file} already exists. Use --force to overwrite.", "ru": "завершён",
"zh": "{file} already exists. Use --force to overwrite." "zh": "已完成"
}, },
"--skip-build: skipping package build and PyPI publish.": { "failed": {
"en": "--skip-build: skipping package build and PyPI publish.", "bg": "неуспешен",
"bg": "--skip-build: skipping package build and PyPI publish.", "de": "fehlgeschlagen",
"de": "--skip-build: skipping package build and PyPI publish.", "en": "failed",
"ru": "--skip-build: skipping package build and PyPI publish.", "ru": "неудачный",
"zh": "--skip-build: skipping package build and PyPI publish." "zh": "失败"
}, },
"Integration tests cancelled — another runner failed.": { "git command failed ({cmd}): {stderr}": {
"en": "Integration tests cancelled — another runner failed.", "bg": "git command failed ({cmd}): {stderr}",
"bg": "Integration tests cancelled — another runner failed.", "de": "git command failed ({cmd}): {stderr}",
"de": "Integration tests cancelled — another runner failed.", "en": "git command failed ({cmd}): {stderr}",
"ru": "Integration tests cancelled — another runner failed.", "ru": "git command failed ({cmd}): {stderr}",
"zh": "Integration tests cancelled — another runner failed." "zh": "git command failed ({cmd}): {stderr}"
}, },
"Integration tests failed with exit code {code}": { "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.": {
"en": "Integration tests failed with exit code {code}", "bg": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
"bg": "Integration tests failed with exit code {code}", "de": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
"de": "Integration tests failed with exit code {code}", "en": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
"ru": "Integration tests failed with exit code {code}", "ru": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
"zh": "Integration tests failed with exit code {code}" "zh": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history."
}, },
"Integration tests passed.": { "git-cliff returned empty version.": {
"en": "Integration tests passed.", "bg": "git-cliff returned empty version.",
"bg": "Integration tests passed.", "de": "git-cliff returned empty version.",
"de": "Integration tests passed.", "en": "git-cliff returned empty version.",
"ru": "Integration tests passed.", "ru": "git-cliff returned empty version.",
"zh": "Integration tests passed." "zh": "git-cliff returned empty version."
}, },
"Merged {count} reports: {tests} tests, {failures} failures → {output}": { "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).": {
"en": "Merged {count} reports: {tests} tests, {failures} failures → {output}", "bg": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
"bg": "Merged {count} reports: {tests} tests, {failures} failures → {output}", "de": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
"de": "Merged {count} reports: {tests} tests, {failures} failures → {output}", "en": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
"ru": "Merged {count} reports: {tests} tests, {failures} failures → {output}", "ru": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
"zh": "Merged {count} reports: {tests} tests, {failures} failures → {output}" "zh": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1)."
}, },
"No JUnit reports found matching {pattern} — skipping merge.": { "in_progress": {
"en": "No JUnit reports found matching {pattern} — skipping merge.", "bg": "в процес",
"bg": "No JUnit reports found matching {pattern} — skipping merge.", "de": "in Bearbeitung",
"de": "No JUnit reports found matching {pattern} — skipping merge.", "en": "in progress",
"ru": "No JUnit reports found matching {pattern} — skipping merge.", "ru": "в процессе",
"zh": "No JUnit reports found matching {pattern} — skipping merge." "zh": "进行中"
}, },
"Roles directory not found: {path}": { "inactive": {
"en": "Roles directory not found: {path}", "bg": "неактивен",
"bg": "Roles directory not found: {path}", "de": "inaktiv",
"de": "Roles directory not found: {path}", "en": "inactive",
"ru": "Roles directory not found: {path}", "ru": "неактивен",
"zh": "Roles directory not found: {path}" "zh": "未激活"
} },
"mapping.json keys and values must be strings, got {k}={v}": {
"bg": "mapping.json keys and values must be strings, got {k}={v}",
"de": "mapping.json keys and values must be strings, got {k}={v}",
"en": "mapping.json keys and values must be strings, got {k}={v}",
"ru": "mapping.json keys and values must be strings, got {k}={v}",
"zh": "mapping.json keys and values must be strings, got {k}={v}"
},
"mapping.json must be a dict of file-path -> page-title, got {type}": {
"bg": "mapping.json must be a dict of file-path -> page-title, got {type}",
"de": "mapping.json must be a dict of file-path -> page-title, got {type}",
"en": "mapping.json must be a dict of file-path -> page-title, got {type}",
"ru": "mapping.json must be a dict of file-path -> page-title, got {type}",
"zh": "mapping.json must be a dict of file-path -> page-title, got {type}"
},
"pending": {
"bg": "в очакване",
"de": "ausstehend",
"en": "pending",
"ru": "ожидает",
"zh": "待处理"
},
"unknown": {
"bg": "неизвестен",
"de": "unbekannt",
"en": "unknown",
"ru": "неизвестно",
"zh": "未知"
},
"{file} already exists. Use --force to overwrite.": {
"bg": "{file} already exists. Use --force to overwrite.",
"de": "{file} already exists. Use --force to overwrite.",
"en": "{file} already exists. Use --force to overwrite.",
"ru": "{file} already exists. Use --force to overwrite.",
"zh": "{file} already exists. Use --force to overwrite."
}
} }
+2 -1
View File
@@ -566,7 +566,8 @@ class TestVikunjaClient:
json={"done": True}, json={"done": True},
) )
def test_http_error_raises_api_error(self) -> None: @patch("devx.api_clients.time.sleep")
def test_http_error_raises_api_error(self, mock_sleep: MagicMock) -> None:
client = VikunjaClient("https://work.example.com", "tok") client = VikunjaClient("https://work.example.com", "tok")
mock_resp = MagicMock() mock_resp = MagicMock()
mock_resp.raise_for_status.side_effect = _mock_http_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, "server error") mock_resp.raise_for_status.side_effect = _mock_http_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, "server error")
+164 -10
View File
@@ -1,4 +1,4 @@
"""Unit tests for scripts/check_test_speed.py.""" """Unit tests for devx.tools.check_test_speed."""
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@@ -8,10 +8,13 @@ from click.testing import CliRunner
from devx.tools.check_test_speed import ( from devx.tools.check_test_speed import (
DEFAULT_MAX_SECONDS, DEFAULT_MAX_SECONDS,
DEFAULT_MAX_SINGLE_SECONDS,
TEST_COMMAND, TEST_COMMAND,
check_per_test_speed,
check_speed, check_speed,
cli, cli,
parse_duration, parse_duration,
parse_per_test_durations,
run_tests, run_tests,
) )
@@ -23,12 +26,23 @@ class TestRunTests:
stdout, stderr = run_tests() stdout, stderr = run_tests()
assert stdout == "out" assert stdout == "out"
assert stderr == "err" assert stderr == "err"
mock_run.assert_called_once_with( mock_run.assert_called_once()
TEST_COMMAND, call_kwargs = mock_run.call_args
capture_output=True, assert call_kwargs.args[0] == TEST_COMMAND
text=True, assert call_kwargs.kwargs["capture_output"] is True
check=False, assert call_kwargs.kwargs["text"] is True
) assert call_kwargs.kwargs["check"] is False
env = call_kwargs.kwargs["env"]
assert "--durations=0" in env["PYTEST_ADDOPTS"]
@patch("devx.tools.check_test_speed.subprocess.run")
def test_run_tests_preserves_existing_pytest_addopts(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(stdout="out", stderr="err", returncode=0)
with patch.dict("os.environ", {"PYTEST_ADDOPTS": "-x"}, clear=False):
run_tests()
env = mock_run.call_args.kwargs["env"]
assert "--durations=0" in env["PYTEST_ADDOPTS"]
assert "-x" in env["PYTEST_ADDOPTS"]
class TestParseDuration: class TestParseDuration:
@@ -48,6 +62,38 @@ class TestParseDuration:
assert "Could not parse" in str(exc.value) assert "Could not parse" in str(exc.value)
class TestParsePerTestDurations:
def test_parses_call_lines(self) -> None:
output = "0.01s call tests/test_foo.py::test_bar\n"
durations = parse_per_test_durations(output)
assert len(durations) == 1
assert durations[0] == ("tests/test_foo.py::test_bar", 0.01)
def test_parses_setup_and_teardown(self) -> None:
output = (
"0.02s setup tests/test_foo.py::test_bar\n"
"0.01s call tests/test_foo.py::test_bar\n"
"0.00s teardown tests/test_foo.py::test_bar\n"
)
durations = parse_per_test_durations(output)
assert len(durations) == 3
names = [d[0] for d in durations]
assert "tests/test_foo.py::test_bar" in names
def test_sorted_slowest_first(self) -> None:
output = "0.01s call tests/test_a.py::test_slow\n0.50s call tests/test_b.py::test_fast\n"
durations = parse_per_test_durations(output)
assert durations[0][1] >= durations[1][1]
assert durations[0][1] == 0.50
def test_empty_output(self) -> None:
assert parse_per_test_durations("") == []
def test_ignores_non_duration_lines(self) -> None:
output = "Some random line\n234 passed in 0.70s\n"
assert parse_per_test_durations(output) == []
class TestCheckSpeed: class TestCheckSpeed:
def test_under_budget_passes(self) -> None: def test_under_budget_passes(self) -> None:
check_speed(1.0, 2.0) # should not raise check_speed(1.0, 2.0) # should not raise
@@ -64,6 +110,31 @@ class TestCheckSpeed:
assert "max allowed: 2.0s" in msg assert "max allowed: 2.0s" in msg
class TestCheckPerTestSpeed:
def test_no_violations_when_all_fast(self) -> None:
durations = [("test_a", 0.1), ("test_b", 0.2)]
assert check_per_test_speed(durations, 0.5) == []
def test_violation_when_test_exceeds_limit(self) -> None:
durations = [("test_slow", 0.6), ("test_fast", 0.1)]
violations = check_per_test_speed(durations, 0.5)
assert len(violations) == 1
assert "test_slow" in violations[0]
assert "0.60s" in violations[0]
def test_multiple_violations(self) -> None:
durations = [("test_a", 0.7), ("test_b", 0.6), ("test_c", 0.1)]
violations = check_per_test_speed(durations, 0.5)
assert len(violations) == 2
def test_exact_limit_passes(self) -> None:
durations = [("test_a", 0.5)]
assert check_per_test_speed(durations, 0.5) == []
def test_empty_durations(self) -> None:
assert check_per_test_speed([], 0.5) == []
def test_main_module_block() -> None: def test_main_module_block() -> None:
import devx.tools.check_test_speed as cts import devx.tools.check_test_speed as cts
@@ -77,39 +148,71 @@ class TestMain:
@patch("devx.tools.check_test_speed.run_tests") @patch("devx.tools.check_test_speed.run_tests")
@patch("devx.tools.check_test_speed.parse_duration") @patch("devx.tools.check_test_speed.parse_duration")
@patch("devx.tools.check_test_speed.check_speed") @patch("devx.tools.check_test_speed.check_speed")
@patch("devx.tools.check_test_speed.parse_per_test_durations")
@patch("devx.tools.check_test_speed.check_per_test_speed")
def test_successful_run( def test_successful_run(
self, self,
mock_check_per: MagicMock,
mock_parse_per: MagicMock,
mock_check: MagicMock, mock_check: MagicMock,
mock_parse: MagicMock, mock_parse: MagicMock,
mock_run: MagicMock, mock_run: MagicMock,
) -> None: ) -> None:
mock_run.return_value = ("stdout\n", "stderr\n") mock_run.return_value = ("stdout\n", "stderr\n")
mock_parse.return_value = 1.5 mock_parse.return_value = 1.5
mock_parse_per.return_value = []
mock_check_per.return_value = []
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert result.exit_code == 0 assert result.exit_code == 0
assert "1.50s" in result.output assert "1.50s" in result.output
assert "under 2.0s limit" in result.output assert "under 10.0s limit" in result.output
mock_run.assert_called_once() mock_run.assert_called_once()
mock_parse.assert_called_once_with("stdout\n\nstderr\n") mock_parse.assert_called_once_with("stdout\n\nstderr\n")
mock_check.assert_called_once_with(1.5, DEFAULT_MAX_SECONDS) mock_check.assert_called_once_with(1.5, DEFAULT_MAX_SECONDS)
mock_parse_per.assert_called_once()
mock_check_per.assert_called_once_with([], DEFAULT_MAX_SINGLE_SECONDS)
@patch("devx.tools.check_test_speed.run_tests") @patch("devx.tools.check_test_speed.run_tests")
@patch("devx.tools.check_test_speed.parse_duration") @patch("devx.tools.check_test_speed.parse_duration")
def test_slow_tests_exit( def test_slow_total_exits(
self, self,
mock_parse: MagicMock, mock_parse: MagicMock,
mock_run: MagicMock, mock_run: MagicMock,
) -> None: ) -> None:
mock_run.return_value = ("out\n", "err\n") mock_run.return_value = ("out\n", "err\n")
mock_parse.return_value = 3.0 mock_parse.return_value = 15.0
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert result.exit_code == 1 assert result.exit_code == 1
assert "too slow" in result.output.lower() assert "too slow" in result.output.lower()
@patch("devx.tools.check_test_speed.run_tests")
@patch("devx.tools.check_test_speed.parse_duration")
@patch("devx.tools.check_test_speed.check_speed")
@patch("devx.tools.check_test_speed.parse_per_test_durations")
@patch("devx.tools.check_test_speed.check_per_test_speed")
def test_per_test_violation_exits(
self,
mock_check_per: MagicMock,
mock_parse_per: MagicMock,
mock_check: MagicMock,
mock_parse: MagicMock,
mock_run: MagicMock,
) -> None:
mock_run.return_value = ("out\n", "err\n")
mock_parse.return_value = 3.0
mock_parse_per.return_value = [("test_slow", 0.8)]
mock_check_per.return_value = ["Test 'test_slow' took 0.80s (limit: 0.5s)."]
runner = CliRunner()
result = runner.invoke(cli, [])
assert result.exit_code == 1
assert "Per-test speed check FAILED" in result.output
assert "test_slow" in result.output
@patch("devx.tools.check_test_speed.run_tests") @patch("devx.tools.check_test_speed.run_tests")
def test_parse_failure_exits( def test_parse_failure_exits(
self, self,
@@ -125,16 +228,67 @@ class TestMain:
@patch("devx.tools.check_test_speed.run_tests") @patch("devx.tools.check_test_speed.run_tests")
@patch("devx.tools.check_test_speed.parse_duration") @patch("devx.tools.check_test_speed.parse_duration")
@patch("devx.tools.check_test_speed.check_speed") @patch("devx.tools.check_test_speed.check_speed")
@patch("devx.tools.check_test_speed.parse_per_test_durations")
@patch("devx.tools.check_test_speed.check_per_test_speed")
def test_custom_max_seconds( def test_custom_max_seconds(
self, self,
mock_check_per: MagicMock,
mock_parse_per: MagicMock,
mock_check: MagicMock, mock_check: MagicMock,
mock_parse: MagicMock, mock_parse: MagicMock,
mock_run: MagicMock, mock_run: MagicMock,
) -> None: ) -> None:
mock_run.return_value = ("out\n", "err\n") mock_run.return_value = ("out\n", "err\n")
mock_parse.return_value = 0.5 mock_parse.return_value = 0.5
mock_parse_per.return_value = []
mock_check_per.return_value = []
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli, ["--max-seconds", "1.5"]) result = runner.invoke(cli, ["--max-seconds", "1.5"])
assert result.exit_code == 0 assert result.exit_code == 0
mock_check.assert_called_once_with(0.5, 1.5) mock_check.assert_called_once_with(0.5, 1.5)
@patch("devx.tools.check_test_speed.run_tests")
@patch("devx.tools.check_test_speed.parse_duration")
@patch("devx.tools.check_test_speed.check_speed")
@patch("devx.tools.check_test_speed.parse_per_test_durations")
@patch("devx.tools.check_test_speed.check_per_test_speed")
def test_disable_per_test_check(
self,
mock_check_per: MagicMock,
mock_parse_per: MagicMock,
mock_check: MagicMock,
mock_parse: MagicMock,
mock_run: MagicMock,
) -> None:
mock_run.return_value = ("out\n", "err\n")
mock_parse.return_value = 1.0
runner = CliRunner()
result = runner.invoke(cli, ["--max-single-seconds", "0"])
assert result.exit_code == 0
mock_parse_per.assert_not_called()
mock_check_per.assert_not_called()
@patch("devx.tools.check_test_speed.run_tests")
@patch("devx.tools.check_test_speed.parse_duration")
@patch("devx.tools.check_test_speed.check_speed")
@patch("devx.tools.check_test_speed.parse_per_test_durations")
@patch("devx.tools.check_test_speed.check_per_test_speed")
def test_custom_max_single_seconds(
self,
mock_check_per: MagicMock,
mock_parse_per: MagicMock,
mock_check: MagicMock,
mock_parse: MagicMock,
mock_run: MagicMock,
) -> None:
mock_run.return_value = ("out\n", "err\n")
mock_parse.return_value = 1.0
mock_parse_per.return_value = []
mock_check_per.return_value = []
runner = CliRunner()
result = runner.invoke(cli, ["--max-single-seconds", "1.0"])
assert result.exit_code == 0
mock_check_per.assert_called_once_with([], 1.0)
+24 -6
View File
@@ -163,10 +163,7 @@ class TestCli:
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli, ["--list-platforms"]) result = runner.invoke(cli, ["--list-platforms"])
assert result.exit_code == 0 assert result.exit_code == 0
assert "ubuntu-2204" in result.output assert "ubuntu-2604" in result.output
assert "ubuntu-2404" in result.output
assert "debian-12" in result.output
assert "archlinux" in result.output
def test_no_runner_index_prints_all_groups(self, tmp_path: Path) -> None: def test_no_runner_index_prints_all_groups(self, tmp_path: Path) -> None:
from click.testing import CliRunner from click.testing import CliRunner
@@ -198,7 +195,7 @@ class TestCli:
assert result.exit_code == 0 assert result.exit_code == 0
# Output should contain encoded pairs with platform info # Output should contain encoded pairs with platform info
assert "alpha|" in result.output assert "alpha|" in result.output
assert "ubuntu-2204" in result.output assert "ubuntu-2604" in result.output
class TestGithubEnv: class TestGithubEnv:
@@ -409,7 +406,7 @@ class TestCliMultiRole:
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli, ["--roles-root", str(roles), "--list-platforms"]) result = runner.invoke(cli, ["--roles-root", str(roles), "--list-platforms"])
assert result.exit_code == 0 assert result.exit_code == 0
assert "ubuntu-2204" in result.output assert "ubuntu-2604" in result.output
def test_roles_root_no_runner_index_prints_groups(self, tmp_path: Path) -> None: def test_roles_root_no_runner_index_prints_groups(self, tmp_path: Path) -> None:
"""--roles-root without --runner-index prints all groups.""" """--roles-root without --runner-index prints all groups."""
@@ -422,6 +419,27 @@ class TestCliMultiRole:
assert "Runner 0:" in result.output assert "Runner 0:" in result.output
assert "Runner 1:" in result.output assert "Runner 1:" in result.output
def test_platforms_file_overrides_default(self, tmp_path: Path) -> None:
"""--platforms-file loads custom platforms from JSON."""
import json
from click.testing import CliRunner
from devx.molecule.distribute_molecule import cli
roles = tmp_path / "roles"
(roles / "role-a" / "molecule" / "default").mkdir(parents=True)
platforms_file = tmp_path / "platforms.json"
custom = [{"name": "custom-os", "image": "custom:latest", "command": "sleep infinity"}]
platforms_file.write_text(json.dumps(custom))
runner = CliRunner()
result = runner.invoke(
cli, ["--roles-root", str(roles), "--platforms-file", str(platforms_file), "--list-platforms"]
)
assert result.exit_code == 0
assert "custom-os" in result.output
assert "custom:latest" in result.output
def test_roles_root_skips_non_dir_role(self, tmp_path: Path) -> None: def test_roles_root_skips_non_dir_role(self, tmp_path: Path) -> None:
"""Non-directory entries in roles root are skipped.""" """Non-directory entries in roles root are skipped."""
roles = tmp_path / "roles" roles = tmp_path / "roles"
+3
View File
@@ -131,6 +131,7 @@ class TestCli:
clear=True, clear=True,
), ),
patch("devx.ci.integration_guard.POLL_INTERVAL", 0.01), patch("devx.ci.integration_guard.POLL_INTERVAL", 0.01),
patch("devx.molecule.molecule_ci_guard.POLL_INTERVAL", 0.01),
patch("devx.ci.integration_guard.subprocess.Popen") as mock_popen, patch("devx.ci.integration_guard.subprocess.Popen") as mock_popen,
patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect), patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect),
patch("os.killpg") as mock_killpg, patch("os.killpg") as mock_killpg,
@@ -177,6 +178,7 @@ class TestCli:
clear=True, clear=True,
), ),
patch("devx.ci.integration_guard.POLL_INTERVAL", 0.01), patch("devx.ci.integration_guard.POLL_INTERVAL", 0.01),
patch("devx.molecule.molecule_ci_guard.POLL_INTERVAL", 0.01),
patch("devx.ci.integration_guard.subprocess.Popen") as mock_popen, patch("devx.ci.integration_guard.subprocess.Popen") as mock_popen,
patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect), patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect),
patch("os.killpg", side_effect=ProcessLookupError("no such process")), patch("os.killpg", side_effect=ProcessLookupError("no such process")),
@@ -221,6 +223,7 @@ class TestCli:
clear=True, clear=True,
), ),
patch("devx.ci.integration_guard.POLL_INTERVAL", 0.01), patch("devx.ci.integration_guard.POLL_INTERVAL", 0.01),
patch("devx.molecule.molecule_ci_guard.POLL_INTERVAL", 0.01),
patch("devx.ci.integration_guard.subprocess.Popen") as mock_popen, patch("devx.ci.integration_guard.subprocess.Popen") as mock_popen,
patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect), patch("devx.molecule.molecule_ci_guard.get_running_jobs", side_effect=get_jobs_side_effect),
patch("os.killpg") as mock_killpg, patch("os.killpg") as mock_killpg,
+40 -7
View File
@@ -1,11 +1,13 @@
"""Unit tests for scripts/ci/platforms.py.""" """Unit tests for devx.molecule.platforms."""
from devx.molecule.platforms import PLATFORMS import json
from devx.molecule.platforms import PLATFORMS, load_platforms
class TestPlatforms: class TestPlatforms:
def test_platforms_not_empty(self) -> None: def test_platforms_not_empty(self) -> None:
assert len(PLATFORMS) >= 4 assert len(PLATFORMS) >= 1
def test_each_platform_has_required_keys(self) -> None: def test_each_platform_has_required_keys(self) -> None:
for p in PLATFORMS: for p in PLATFORMS:
@@ -17,9 +19,40 @@ class TestPlatforms:
names = [p["name"] for p in PLATFORMS] names = [p["name"] for p in PLATFORMS]
assert len(names) == len(set(names)) assert len(names) == len(set(names))
def test_platforms_use_sleep_infinity(self) -> None:
"""All default platforms must use sleep infinity, not systemd."""
for p in PLATFORMS:
assert p["command"] == "sleep infinity", f"Platform {p['name']} uses {p['command']}"
def test_known_platforms_present(self) -> None: def test_known_platforms_present(self) -> None:
names = {p["name"] for p in PLATFORMS} names = {p["name"] for p in PLATFORMS}
assert "ubuntu-2204" in names assert "ubuntu-2604" in names
assert "ubuntu-2404" in names
assert "debian-12" in names
assert "archlinux" in names class TestLoadPlatforms:
def test_load_platforms_default(self, tmp_path) -> None: # type: ignore[no-untyped-def]
"""load_platforms with no file returns PLATFORMS."""
result = load_platforms(None)
assert result == PLATFORMS
def test_load_platforms_from_file(self, tmp_path) -> None: # type: ignore[no-untyped-def]
"""load_platforms reads custom platforms from JSON file."""
custom = [
{"name": "custom-os", "image": "custom:latest", "command": "sleep infinity"},
]
f = tmp_path / "platforms.json"
f.write_text(json.dumps(custom))
result = load_platforms(f)
assert result == custom
def test_load_platforms_missing_file_falls_back(self, tmp_path) -> None: # type: ignore[no-untyped-def]
"""load_platforms falls back to PLATFORMS when file doesn't exist."""
result = load_platforms(tmp_path / "nonexistent.json")
assert result == PLATFORMS
def test_load_platforms_empty_list_falls_back(self, tmp_path) -> None: # type: ignore[no-untyped-def]
"""load_platforms falls back to PLATFORMS when file has empty list."""
f = tmp_path / "platforms.json"
f.write_text("[]")
result = load_platforms(f)
assert result == PLATFORMS
+38 -8
View File
@@ -1,5 +1,6 @@
"""Unit tests for devx.tools.setup.""" """Unit tests for devx.tools.setup."""
import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@@ -32,20 +33,48 @@ class TestRun:
class TestInstallPythonDeps: class TestInstallPythonDeps:
@patch("devx.tools.setup._run") @patch("devx.tools.setup.subprocess.run")
def test_install_dev(self, mock_run: MagicMock) -> None: def test_install_dev(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0)
_install_python_deps(".venv/bin", "dev") _install_python_deps(".venv/bin", "dev")
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[dev]"]) mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[dev]"], check=False)
@patch("devx.tools.setup._run") @patch("devx.tools.setup.subprocess.run")
def test_install_ci(self, mock_run: MagicMock) -> None: def test_install_ci(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0)
_install_python_deps(".venv/bin", "ci") _install_python_deps(".venv/bin", "ci")
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci]"]) mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci]"], check=False)
@patch("devx.tools.setup.subprocess.run")
def test_install_custom_extras(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0)
_install_python_deps(".venv/bin", "ci,lint")
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci,lint]"], check=False)
@patch("devx.tools.setup.subprocess.run")
def test_install_with_break_system_packages(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0)
with patch.dict(os.environ, {"PIP_BREAK_SYSTEM_PACKAGES": "1"}):
_install_python_deps(".venv/bin", "ci")
mock_run.assert_called_once_with(
[".venv/bin/pip", "install", "-e", ".[ci]", "--break-system-packages"], check=False
)
@patch("devx.tools.setup._run") @patch("devx.tools.setup._run")
def test_install_custom_extras(self, mock_run: MagicMock) -> None: @patch("devx.tools.setup.subprocess.run")
_install_python_deps(".venv/bin", "ci,lint") def test_install_retry_with_ignore_installed(self, mock_subprocess: MagicMock, mock_run: MagicMock) -> None:
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci,lint]"]) mock_subprocess.return_value = MagicMock(returncode=1)
with patch.dict(os.environ, {"PIP_BREAK_SYSTEM_PACKAGES": "1"}):
_install_python_deps(".venv/bin", "ci")
mock_run.assert_called_once_with(
[".venv/bin/pip", "install", "-e", ".[ci]", "--break-system-packages", "--ignore-installed"]
)
@patch("devx.tools.setup.subprocess.run")
def test_install_failure_without_break_system(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=1)
with pytest.raises(subprocess.CalledProcessError):
_install_python_deps(".venv/bin", "ci")
class TestInstallPreCommitHooks: class TestInstallPreCommitHooks:
@@ -60,8 +89,9 @@ class TestInstallPreCommitHooks:
class TestInstallAnsibleCollections: class TestInstallAnsibleCollections:
@patch("devx.tools.setup.shutil.which", return_value="/usr/local/bin/ansible-galaxy")
@patch("devx.tools.setup._run") @patch("devx.tools.setup._run")
def test_installs_from_requirements(self, mock_run: MagicMock, tmp_path: Path) -> None: def test_installs_from_requirements(self, mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path) -> None:
req = tmp_path / "ansible" / "requirements.yml" req = tmp_path / "ansible" / "requirements.yml"
req.parent.mkdir(parents=True) req.parent.mkdir(parents=True)
req.write_text("collections: []") req.write_text("collections: []")
+107
View File
@@ -0,0 +1,107 @@
"""Unit tests for devx.molecule.start_docker."""
from unittest.mock import MagicMock, mock_open, patch
from click.testing import CliRunner
from devx.molecule.start_docker import is_docker_ready, main, start_docker_daemon
class TestIsDockerReady:
@patch("devx.molecule.start_docker.subprocess.run")
def test_ready(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0)
assert is_docker_ready() is True
mock_run.assert_called_once_with(["docker", "info"], capture_output=True, check=False)
@patch("devx.molecule.start_docker.subprocess.run")
def test_not_ready(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=1)
assert is_docker_ready() is False
class TestStartDockerDaemon:
@patch("devx.molecule.start_docker.time.sleep")
@patch("devx.molecule.start_docker.is_docker_ready")
@patch("devx.molecule.start_docker.subprocess.Popen")
@patch("builtins.open", new_callable=mock_open)
def test_starts_successfully(
self,
mock_file: MagicMock,
mock_popen: MagicMock,
mock_ready: MagicMock,
mock_sleep: MagicMock,
) -> None:
# First loop iteration: dockerd not ready yet. Second: ready.
mock_ready.side_effect = [False, True]
assert start_docker_daemon(timeout=5) is True
mock_popen.assert_called_once()
mock_sleep.assert_called_once_with(1)
@patch("devx.molecule.start_docker.time.sleep")
@patch("devx.molecule.start_docker.is_docker_ready", return_value=False)
@patch("devx.molecule.start_docker.subprocess.Popen")
@patch("builtins.open", new_callable=mock_open)
def test_fails_after_timeout(
self,
mock_file: MagicMock,
mock_popen: MagicMock,
mock_ready: MagicMock,
mock_sleep: MagicMock,
) -> None:
assert start_docker_daemon(timeout=3) is False
mock_popen.assert_called_once()
assert mock_sleep.call_count == 3
@patch("devx.molecule.start_docker.time.sleep")
@patch("devx.molecule.start_docker.is_docker_ready")
@patch("devx.molecule.start_docker.subprocess.Popen")
@patch("builtins.open", new_callable=mock_open)
def test_ready_on_first_check(
self,
mock_file: MagicMock,
mock_popen: MagicMock,
mock_ready: MagicMock,
mock_sleep: MagicMock,
) -> None:
mock_ready.return_value = True
assert start_docker_daemon(timeout=5) is True
mock_popen.assert_called_once()
mock_sleep.assert_not_called()
@patch("devx.molecule.start_docker.time.sleep")
@patch("devx.molecule.start_docker.is_docker_ready")
@patch("devx.molecule.start_docker.subprocess.Popen")
@patch("builtins.open", new_callable=mock_open)
def test_custom_timeout(
self,
mock_file: MagicMock,
mock_popen: MagicMock,
mock_ready: MagicMock,
mock_sleep: MagicMock,
) -> None:
# 9 iterations not ready, 10th ready.
mock_ready.side_effect = [False] * 9 + [True]
assert start_docker_daemon(timeout=10) is True
assert mock_sleep.call_count == 9
class TestMain:
@patch("devx.molecule.start_docker.start_docker_daemon", return_value=True)
def test_success(self, mock_start: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(main, [])
assert result.exit_code == 0
@patch("devx.molecule.start_docker.start_docker_daemon", return_value=False)
def test_failure(self, mock_start: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(main, [])
assert result.exit_code == 1
@patch("devx.molecule.start_docker.start_docker_daemon", return_value=True)
def test_custom_timeout_flag(self, mock_start: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(main, ["--timeout", "60"])
assert result.exit_code == 0
mock_start.assert_called_once_with(60)