Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
037d7b0d16 | ||
|
|
9cb706e387 | ||
|
|
5bd6158f2a | ||
|
|
05aa2ffe76 | ||
|
|
b9c3b55680 | ||
|
|
d398c8e971 | ||
|
|
39526d8e6a | ||
|
|
37730e2187 | ||
|
|
e2f66ca70a | ||
|
|
0b88c211f1 | ||
|
|
ea4ee0d303 | ||
|
|
16fba17b03 | ||
|
|
011cf3e093 | ||
|
|
daf99c5fed | ||
|
|
7154e3ad7c | ||
|
|
6053fb9fba | ||
|
|
e76741bfad | ||
|
|
f206a9cd8d | ||
|
|
b4b7428f9c | ||
|
|
0d9e76a838 | ||
|
|
034cbde2f7 | ||
|
|
e0abe6f176 | ||
|
|
15f6837dc2 | ||
|
|
b4dda91e24 | ||
|
|
3e21e774f7 | ||
|
|
7c11215e57 | ||
|
|
5384269c83 | ||
|
|
b3d0dd8ca7 | ||
|
|
a7dcaee5c6 | ||
|
|
02f8d3757b | ||
|
|
4311fb7648 | ||
|
|
2ead959fcf | ||
|
|
9a60009d29 | ||
|
|
c20dfd185a |
@@ -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
|
||||||
|
|||||||
@@ -2,6 +2,99 @@
|
|||||||
|
|
||||||
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.7] - 2026-06-24
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Add rootless socket fallback and GITHUB_ENV export
|
||||||
|
|
||||||
|
## [0.9.6] - 2026-06-24
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Add Docker socket diagnostics to start_docker
|
||||||
|
- Add Docker socket diagnostics to start_docker
|
||||||
|
|
||||||
|
## [0.9.5] - 2026-06-24
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Use host Docker socket with DOCKER_HOST fallback to local dockerd
|
||||||
|
- Use host Docker socket with DOCKER_HOST fallback to local dockerd
|
||||||
|
|
||||||
|
## [0.9.4] - 2026-06-24
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Use separate Docker socket for DinD in CI
|
||||||
|
|
||||||
|
## [0.9.3] - 2026-06-24
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Use tempfile for dockerd log to fix CI permission error
|
||||||
|
|
||||||
|
## [0.9.2] - 2026-06-24
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Use vfs storage driver for Docker-in-Docker in CI
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|||||||
@@ -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
@@ -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,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.7"
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Ensure Docker is available for molecule tests in CI.
|
||||||
|
|
||||||
|
CI runners (e.g. ``gitea/runner-images:ubuntu-latest``) may have the host's
|
||||||
|
Docker socket mounted. This module verifies Docker is accessible and
|
||||||
|
sets ``DOCKER_HOST`` explicitly so molecule's Python docker library
|
||||||
|
connects to the same socket as the Docker CLI.
|
||||||
|
|
||||||
|
If the host socket is not available, it tries the rootless socket, then
|
||||||
|
starts a local ``dockerd`` with the vfs storage driver (requires
|
||||||
|
privileged container).
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
python3 -m devx.molecule.start_docker [--timeout 30]
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess # nosec B404
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from devx.i18n import _
|
||||||
|
|
||||||
|
DEFAULT_TIMEOUT = 30
|
||||||
|
DOCKER_SOCK = "/var/run/docker.sock"
|
||||||
|
# Rootless socket fallback (e.g. /run/user/994/docker.sock)
|
||||||
|
ROOTLESS_SOCK = f"/run/user/{os.getuid()}/docker.sock"
|
||||||
|
|
||||||
|
|
||||||
|
def is_docker_ready() -> bool:
|
||||||
|
"""Check if Docker daemon is responding on the configured socket."""
|
||||||
|
result = subprocess.run( # nosec B603 B607
|
||||||
|
["docker", "info"],
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
env={**os.environ, "DOCKER_HOST": f"unix://{DOCKER_SOCK}"},
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def _diagnose_socket() -> None:
|
||||||
|
"""Print diagnostic info about the Docker socket."""
|
||||||
|
click.echo(f"DOCKER_HOST = {os.environ.get('DOCKER_HOST', '(not set)')}")
|
||||||
|
click.echo(f"Socket path: {DOCKER_SOCK}")
|
||||||
|
click.echo(f"Socket exists: {os.path.exists(DOCKER_SOCK)}")
|
||||||
|
if os.path.exists(DOCKER_SOCK):
|
||||||
|
stat = os.stat(DOCKER_SOCK)
|
||||||
|
click.echo(f"Socket mode: {oct(stat.st_mode)}")
|
||||||
|
click.echo(f"Socket uid: {stat.st_uid}, gid: {stat.st_gid}")
|
||||||
|
# Check if it's a mount point
|
||||||
|
result = subprocess.run( # nosec B603 B607
|
||||||
|
["mount"],
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
docker_mounts = [line for line in result.stdout.splitlines() if "docker" in line.lower()]
|
||||||
|
if docker_mounts:
|
||||||
|
click.echo("Docker-related mounts:")
|
||||||
|
for line in docker_mounts:
|
||||||
|
click.echo(f" {line}")
|
||||||
|
else:
|
||||||
|
click.echo("No Docker-related mounts found")
|
||||||
|
# Check docker context
|
||||||
|
result = subprocess.run( # nosec B603 B607
|
||||||
|
["docker", "context", "ls"],
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
click.echo(f"Docker contexts:\n{result.stdout}")
|
||||||
|
# Try docker info without DOCKER_HOST
|
||||||
|
result = subprocess.run( # nosec B603 B607
|
||||||
|
["docker", "info"],
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
click.echo(f"docker info (no DOCKER_HOST): rc={result.returncode}")
|
||||||
|
if result.returncode != 0:
|
||||||
|
click.echo(f" stderr: {result.stderr[:500]}")
|
||||||
|
else:
|
||||||
|
# Print server version and storage driver
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
if "Server Version" in line or "Storage Driver" in line or "Docker Root Dir" in line:
|
||||||
|
click.echo(f" {line.strip()}")
|
||||||
|
|
||||||
|
|
||||||
|
def start_docker_daemon(timeout: int = DEFAULT_TIMEOUT) -> bool:
|
||||||
|
"""Ensure Docker is ready for molecule tests.
|
||||||
|
|
||||||
|
First tries the host socket. If that works, sets ``DOCKER_HOST`` and
|
||||||
|
returns immediately. If not, tries the rootless socket. If neither
|
||||||
|
works, starts a local ``dockerd`` with vfs storage driver (requires
|
||||||
|
privileged container).
|
||||||
|
|
||||||
|
Returns ``True`` if Docker is ready, ``False`` if it failed to
|
||||||
|
start within the timeout.
|
||||||
|
"""
|
||||||
|
# Point Docker CLI and Python library to the socket explicitly
|
||||||
|
os.environ["DOCKER_HOST"] = f"unix://{DOCKER_SOCK}"
|
||||||
|
|
||||||
|
# Diagnose socket state
|
||||||
|
click.echo("--- Docker socket diagnostics ---")
|
||||||
|
_diagnose_socket()
|
||||||
|
click.echo("--- End diagnostics ---")
|
||||||
|
|
||||||
|
# Check if host Docker is already available
|
||||||
|
if is_docker_ready():
|
||||||
|
click.echo(_("Docker daemon already running"))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Try rootless socket (e.g. /run/user/994/docker.sock)
|
||||||
|
click.echo(f"Trying rootless socket: {ROOTLESS_SOCK}")
|
||||||
|
os.environ["DOCKER_HOST"] = f"unix://{ROOTLESS_SOCK}"
|
||||||
|
if os.path.exists(ROOTLESS_SOCK) and is_docker_ready():
|
||||||
|
click.echo(_("Docker daemon already running"))
|
||||||
|
return True
|
||||||
|
|
||||||
|
click.echo(_("Host Docker not available, starting local dockerd..."))
|
||||||
|
|
||||||
|
# Start local dockerd (requires privileged container)
|
||||||
|
log_file = tempfile.NamedTemporaryFile( # noqa: SIM115
|
||||||
|
mode="w", suffix="dockerd.log", delete=False
|
||||||
|
)
|
||||||
|
click.echo(f"dockerd log: {log_file.name}")
|
||||||
|
subprocess.Popen( # nosec B603 B607
|
||||||
|
[
|
||||||
|
"dockerd",
|
||||||
|
"--storage-driver",
|
||||||
|
"vfs",
|
||||||
|
"-H",
|
||||||
|
f"unix://{DOCKER_SOCK}",
|
||||||
|
],
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Print dockerd log on failure
|
||||||
|
click.echo(_("Docker daemon failed to start"))
|
||||||
|
click.echo("--- dockerd log ---")
|
||||||
|
try:
|
||||||
|
with open(log_file.name) as f:
|
||||||
|
log_content = f.read()
|
||||||
|
click.echo(log_content[-3000:] if len(log_content) > 3000 else log_content)
|
||||||
|
except OSError as e:
|
||||||
|
click.echo(f"Could not read log: {e}")
|
||||||
|
click.echo("--- End dockerd log ---")
|
||||||
|
|
||||||
|
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):
|
||||||
|
# Export DOCKER_HOST to GITHUB_ENV for subsequent CI steps
|
||||||
|
github_env = os.environ.get("GITHUB_ENV")
|
||||||
|
if github_env and os.environ.get("DOCKER_HOST"):
|
||||||
|
with open(github_env, "a") as f:
|
||||||
|
f.write(f"DOCKER_HOST={os.environ['DOCKER_HOST']}\n")
|
||||||
|
click.echo(f"Exported DOCKER_HOST={os.environ['DOCKER_HOST']} to GITHUB_ENV")
|
||||||
|
sys.exit(0)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
main()
|
||||||
@@ -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
@@ -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.")
|
||||||
|
|||||||
+1239
-1197
@@ -1,1199 +1,1241 @@
|
|||||||
{
|
{
|
||||||
"\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 already running": {
|
||||||
"en": "ERROR: REPO_TOKEN is not set.",
|
"bg": "Докер демонът вече работи",
|
||||||
"bg": "ГРЕШКА: REPO_TOKEN не е зададен.",
|
"de": "Docker-Daemon läuft bereits",
|
||||||
"de": "FEHLER: REPO_TOKEN ist nicht gesetzt.",
|
"en": "Docker daemon already running",
|
||||||
"ru": "ОШИБКА: REPO_TOKEN не задан.",
|
"ru": "Демон Docker уже работает",
|
||||||
"zh": "错误:未设置 REPO_TOKEN。"
|
"zh": "Docker 守护进程已在运行"
|
||||||
},
|
},
|
||||||
"ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME.": {
|
"Docker daemon failed to start": {
|
||||||
"en": "ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME.",
|
"bg": "Docker daemon failed to start",
|
||||||
"bg": "ГРЕШКА: Името на хранилището не е указано. Използвайте --repo или задайте DEVX_REPO_NAME.",
|
"de": "Docker-Daemon konnte nicht gestartet werden",
|
||||||
"de": "FEHLER: Repository-Name nicht angegeben. Verwenden Sie --repo oder setzen Sie DEVX_REPO_NAME.",
|
"en": "Docker daemon failed to start",
|
||||||
"ru": "ОШИБКА: Имя репозитория не указано. Используйте --repo или задайте DEVX_REPO_NAME.",
|
"ru": "Не удалось запустить Docker-демон",
|
||||||
"zh": "错误:未指定仓库名称。请使用 --repo 或设置 DEVX_REPO_NAME。"
|
"zh": "Docker 守护进程启动失败"
|
||||||
},
|
},
|
||||||
"ERROR: Tag consistency check failed. Existing tags are misaligned:": {
|
"Docker daemon started": {
|
||||||
"en": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
|
"bg": "Docker daemon started",
|
||||||
"bg": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
|
"de": "Docker-Daemon gestartet",
|
||||||
"de": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
|
"en": "Docker daemon started",
|
||||||
"ru": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
|
"ru": "Docker-демон запущен",
|
||||||
"zh": "ERROR: Tag consistency check failed. Existing tags are misaligned:"
|
"zh": "Docker 守护进程已启动"
|
||||||
},
|
},
|
||||||
"ERROR: VIKUNJA_TOKEN is not set.": {
|
"Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.": {
|
||||||
"en": "ERROR: VIKUNJA_TOKEN is not set.",
|
"bg": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
|
||||||
"bg": "ГРЕШКА: VIKUNJA_TOKEN не е зададен.",
|
"de": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
|
||||||
"de": "FEHLER: VIKUNJA_TOKEN ist nicht gesetzt.",
|
"en": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
|
||||||
"ru": "ОШИБКА: VIKUNJA_TOKEN не задан.",
|
"ru": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
|
||||||
"zh": "错误:未设置 VIKUNJA_TOKEN。"
|
"zh": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently."
|
||||||
},
|
},
|
||||||
"ERROR: mapping.json not found at {path}": {
|
"ERROR: REPO_TOKEN is not set.": {
|
||||||
"en": "ERROR: mapping.json not found at {path}",
|
"bg": "ГРЕШКА: REPO_TOKEN не е зададен.",
|
||||||
"bg": "ERROR: mapping.json not found at {path}",
|
"de": "FEHLER: REPO_TOKEN ist nicht gesetzt.",
|
||||||
"de": "ERROR: mapping.json not found at {path}",
|
"en": "ERROR: REPO_TOKEN is not set.",
|
||||||
"ru": "ERROR: mapping.json not found at {path}",
|
"ru": "ОШИБКА: REPO_TOKEN не задан.",
|
||||||
"zh": "ERROR: mapping.json not found at {path}"
|
"zh": "错误:未设置 REPO_TOKEN。"
|
||||||
},
|
},
|
||||||
"FAILED: {pair} exited with code {code}": {
|
"ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME.": {
|
||||||
"en": "FAILED: {pair} exited with code {code}",
|
"bg": "ГРЕШКА: Името на хранилището не е указано. Използвайте --repo или задайте DEVX_REPO_NAME.",
|
||||||
"bg": "FAILED: {pair} exited with code {code}",
|
"de": "FEHLER: Repository-Name nicht angegeben. Verwenden Sie --repo oder setzen Sie DEVX_REPO_NAME.",
|
||||||
"de": "FAILED: {pair} exited with code {code}",
|
"en": "ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME.",
|
||||||
"ru": "FAILED: {pair} exited with code {code}",
|
"ru": "ОШИБКА: Имя репозитория не указано. Используйте --repo или задайте DEVX_REPO_NAME.",
|
||||||
"zh": "FAILED: {pair} exited with code {code}"
|
"zh": "错误:未指定仓库名称。请使用 --repo 或设置 DEVX_REPO_NAME。"
|
||||||
},
|
},
|
||||||
"Failed to create issue via tea: {error}": {
|
"ERROR: Tag consistency check failed. Existing tags are misaligned:": {
|
||||||
"en": "Failed to create issue via tea: {error}",
|
"bg": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
|
||||||
"bg": "Failed to create issue via tea: {error}",
|
"de": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
|
||||||
"de": "Failed to create issue via tea: {error}",
|
"en": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
|
||||||
"ru": "Failed to create issue via tea: {error}",
|
"ru": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
|
||||||
"zh": "Failed to create issue via tea: {error}"
|
"zh": "ERROR: Tag consistency check failed. Existing tags are misaligned:"
|
||||||
},
|
},
|
||||||
"Found {count} existing wiki pages.": {
|
"ERROR: VIKUNJA_TOKEN is not set.": {
|
||||||
"en": "Found {count} existing wiki pages.",
|
"bg": "ГРЕШКА: VIKUNJA_TOKEN не е зададен.",
|
||||||
"bg": "Found {count} existing wiki pages.",
|
"de": "FEHLER: VIKUNJA_TOKEN ist nicht gesetzt.",
|
||||||
"de": "Found {count} existing wiki pages.",
|
"en": "ERROR: VIKUNJA_TOKEN is not set.",
|
||||||
"ru": "Found {count} existing wiki pages.",
|
"ru": "ОШИБКА: VIKUNJA_TOKEN не задан.",
|
||||||
"zh": "Found {count} existing wiki pages."
|
"zh": "错误:未设置 VIKUNJA_TOKEN。"
|
||||||
},
|
},
|
||||||
"GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.": {
|
"ERROR: mapping.json not found at {path}": {
|
||||||
"en": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
"bg": "ERROR: mapping.json not found at {path}",
|
||||||
"bg": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
"de": "ERROR: mapping.json not found at {path}",
|
||||||
"de": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
"en": "ERROR: mapping.json not found at {path}",
|
||||||
"ru": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
"ru": "ERROR: mapping.json not found at {path}",
|
||||||
"zh": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation."
|
"zh": "ERROR: mapping.json not found at {path}"
|
||||||
},
|
},
|
||||||
"Generated {file} with prefix '{prefix}'.": {
|
"FAILED: {pair} exited with code {code}": {
|
||||||
"en": "Generated {file} with prefix '{prefix}'.",
|
"bg": "FAILED: {pair} exited with code {code}",
|
||||||
"bg": "Generated {file} with prefix '{prefix}'.",
|
"de": "FAILED: {pair} exited with code {code}",
|
||||||
"de": "Generated {file} with prefix '{prefix}'.",
|
"en": "FAILED: {pair} exited with code {code}",
|
||||||
"ru": "Generated {file} with prefix '{prefix}'.",
|
"ru": "FAILED: {pair} exited with code {code}",
|
||||||
"zh": "Generated {file} with prefix '{prefix}'."
|
"zh": "FAILED: {pair} exited with code {code}"
|
||||||
},
|
},
|
||||||
"HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.": {
|
"Failed to create issue via tea: {error}": {
|
||||||
"en": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
|
"bg": "Failed to create issue via tea: {error}",
|
||||||
"bg": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
|
"de": "Failed to create issue via tea: {error}",
|
||||||
"de": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
|
"en": "Failed to create issue via tea: {error}",
|
||||||
"ru": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
|
"ru": "Failed to create issue via tea: {error}",
|
||||||
"zh": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag."
|
"zh": "Failed to create issue via tea: {error}"
|
||||||
},
|
},
|
||||||
"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.": {
|
"Found {count} existing wiki pages.": {
|
||||||
"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": "Found {count} existing wiki pages.",
|
||||||
"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": "Found {count} existing wiki pages.",
|
||||||
"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": "Found {count} existing wiki pages.",
|
||||||
"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": "Found {count} existing wiki pages.",
|
||||||
"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": "Found {count} existing wiki pages."
|
||||||
},
|
},
|
||||||
"HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.": {
|
"GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.": {
|
||||||
"en": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
|
"bg": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
||||||
"bg": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
|
"de": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
||||||
"de": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
|
"en": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
||||||
"ru": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
|
"ru": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
||||||
"zh": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping."
|
"zh": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation."
|
||||||
},
|
},
|
||||||
"HTTP error: {status} — {message}": {
|
"Generated {file} with prefix '{prefix}'.": {
|
||||||
"en": "HTTP error: {status} — {message}",
|
"bg": "Generated {file} with prefix '{prefix}'.",
|
||||||
"bg": "HTTP грешка: {status} — {message}",
|
"de": "Generated {file} with prefix '{prefix}'.",
|
||||||
"de": "HTTP-Fehler: {status} — {message}",
|
"en": "Generated {file} with prefix '{prefix}'.",
|
||||||
"ru": "Ошибка HTTP: {status} — {message}",
|
"ru": "Generated {file} with prefix '{prefix}'.",
|
||||||
"zh": "HTTP 错误: {status} — {message}"
|
"zh": "Generated {file} with prefix '{prefix}'."
|
||||||
},
|
},
|
||||||
"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 ('{msg}') but tag {tag} is missing. Recovering by creating tag.": {
|
||||||
"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 ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
|
||||||
"bg": "HTTP {status} Забранено — вашият токен няма администраторски права.\nУверете се, че токенът принадлежи на собственик на хранилище или администратор на организация.\nАлтернативно, конфигурирайте защитата на клона ръчно в Настройки → Клонове.",
|
"de": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
|
||||||
"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 ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
|
||||||
"ru": "HTTP {status} Запрещено — у вашего токена нет прав администратора.\nУбедитесь, что токен принадлежит владельцу репозитория или администратору организации.\nЛибо настройте защиту ветки вручную в разделе Настройки → Ветки.",
|
"ru": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
|
||||||
"zh": "HTTP {status} 禁止访问 — 您的令牌缺少管理员权限。\n请确保令牌属于仓库所有者或组织管理员。\n或者,您可以在 设置 → 分支 中手动配置分支保护。"
|
"zh": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag."
|
||||||
},
|
},
|
||||||
"Head branch is behind master. Pulling and rebasing...": {
|
"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": "Head branch is behind master. Pulling and rebasing...",
|
"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": "Head branch is behind master. Pulling and rebasing...",
|
"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": "Head branch is behind master. Pulling and rebasing...",
|
"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": "Head branch is behind master. Pulling and rebasing...",
|
"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": "Head branch is behind master. Pulling and rebasing..."
|
"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."
|
||||||
},
|
},
|
||||||
"Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}": {
|
"HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.": {
|
||||||
"en": "Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}",
|
"bg": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
|
||||||
"bg": "Инфраструктурен commit (без идентификатор на задача DEVX-N), пропускаме обновяването на Vikunja: {msg}",
|
"de": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
|
||||||
"de": "Infrastruktur-Commit (keine DEVX-N Task-ID), Vikunja-Update wird übersprungen: {msg}",
|
"en": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
|
||||||
"ru": "Инфраструктурный коммит (без ID задачи DEVX-N), пропуск обновления Vikunja: {msg}",
|
"ru": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
|
||||||
"zh": "基础设施提交(无 DEVX-N 任务 ID),跳过 Vikunja 更新: {msg}"
|
"zh": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping."
|
||||||
},
|
},
|
||||||
"Lint failed — refusing to release. Fix lint errors first.\n{stderr}": {
|
"HTTP error: {status} — {message}": {
|
||||||
"en": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
|
"bg": "HTTP грешка: {status} — {message}",
|
||||||
"bg": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
|
"de": "HTTP-Fehler: {status} — {message}",
|
||||||
"de": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
|
"en": "HTTP error: {status} — {message}",
|
||||||
"ru": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
|
"ru": "Ошибка HTTP: {status} — {message}",
|
||||||
"zh": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}"
|
"zh": "HTTP 错误: {status} — {message}"
|
||||||
},
|
},
|
||||||
"Lint passed.": {
|
"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 passed.",
|
"bg": "HTTP {status} Забранено — вашият токен няма администраторски права.\nУверете се, че токенът принадлежи на собственик на хранилище или администратор на организация.\nАлтернативно, конфигурирайте защитата на клона ръчно в Настройки → Клонове.",
|
||||||
"bg": "Lint passed.",
|
"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 passed.",
|
"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 passed.",
|
"ru": "HTTP {status} Запрещено — у вашего токена нет прав администратора.\nУбедитесь, что токен принадлежит владельцу репозитория или администратору организации.\nЛибо настройте защиту ветки вручную в разделе Настройки → Ветки.",
|
||||||
"zh": "Lint passed."
|
"zh": "HTTP {status} 禁止访问 — 您的令牌缺少管理员权限。\n请确保令牌属于仓库所有者或组织管理员。\n或者,您可以在 设置 → 分支 中手动配置分支保护。"
|
||||||
},
|
},
|
||||||
"Mapped file {file} is empty. Update the content or remove from mapping.json.": {
|
"Head branch is behind master. Pulling and rebasing...": {
|
||||||
"en": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
|
"bg": "Head branch is behind master. Pulling and rebasing...",
|
||||||
"bg": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
|
"de": "Head branch is behind master. Pulling and rebasing...",
|
||||||
"de": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
|
"en": "Head branch is behind master. Pulling and rebasing...",
|
||||||
"ru": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
|
"ru": "Head branch is behind master. Pulling and rebasing...",
|
||||||
"zh": "Mapped file {file} is empty. Update the content or remove from mapping.json."
|
"zh": "Head branch is behind master. Pulling and rebasing..."
|
||||||
},
|
},
|
||||||
"Mapped file {file} not found. Update mapping.json or create the file.": {
|
"Host Docker not available, starting local dockerd...": {
|
||||||
"en": "Mapped file {file} not found. Update mapping.json or create the file.",
|
"bg": "Хост Docker не е наличен, стартиране на локален dockerd...",
|
||||||
"bg": "Mapped file {file} not found. Update mapping.json or create the file.",
|
"de": "Host-Docker nicht verfügbar, lokaler dockerd wird gestartet...",
|
||||||
"de": "Mapped file {file} not found. Update mapping.json or create the file.",
|
"en": "Host Docker not available, starting local dockerd...",
|
||||||
"ru": "Mapped file {file} not found. Update mapping.json or create the file.",
|
"ru": "Хост Docker недоступен, запускается локальный dockerd...",
|
||||||
"zh": "Mapped file {file} not found. Update mapping.json or create the file."
|
"zh": "主机 Docker 不可用,正在启动本地 dockerd..."
|
||||||
},
|
},
|
||||||
"Merge failed after rebase retry: {error}\nPlease rebase the PR manually.": {
|
"Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}": {
|
||||||
"en": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
|
"bg": "Инфраструктурен commit (без идентификатор на задача DEVX-N), пропускаме обновяването на Vikunja: {msg}",
|
||||||
"bg": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
|
"de": "Infrastruktur-Commit (keine DEVX-N Task-ID), Vikunja-Update wird übersprungen: {msg}",
|
||||||
"de": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
|
"en": "Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}",
|
||||||
"ru": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
|
"ru": "Инфраструктурный коммит (без ID задачи DEVX-N), пропуск обновления Vikunja: {msg}",
|
||||||
"zh": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually."
|
"zh": "基础设施提交(无 DEVX-N 任务 ID),跳过 Vikunja 更新: {msg}"
|
||||||
},
|
},
|
||||||
"Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.": {
|
"Integration tests cancelled — another runner failed.": {
|
||||||
"en": "Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.",
|
"bg": "Integration tests cancelled — another runner failed.",
|
||||||
"bg": "Сливането неуспешно с HTTP {status}: {message}\nПроверете дали PR е готов и имате права за сливане.",
|
"de": "Integration tests cancelled — another runner failed.",
|
||||||
"de": "Merge fehlgeschlagen mit HTTP {status}: {message}\nBitte prüfen Sie, ob der PR bereit ist und Sie Merge-Rechte haben.",
|
"en": "Integration tests cancelled — another runner failed.",
|
||||||
"ru": "Слияние не удалось: HTTP {status}: {message}\nПроверьте, что PR готов и у вас есть права на слияние.",
|
"ru": "Integration tests cancelled — another runner failed.",
|
||||||
"zh": "合并失败: HTTP {status}: {message}\n请检查 PR 是否准备就绪且您具有合并权限。"
|
"zh": "Integration tests cancelled — another runner failed."
|
||||||
},
|
},
|
||||||
"Module {mod} has no main() function": {
|
"Integration tests failed with exit code {code}": {
|
||||||
"en": "Module {mod} has no main() function",
|
"bg": "Integration tests failed with exit code {code}",
|
||||||
"bg": "Модул {mod} няма функция main()",
|
"de": "Integration tests failed with exit code {code}",
|
||||||
"de": "Modul {mod} hat keine main()-Funktion",
|
"en": "Integration tests failed with exit code {code}",
|
||||||
"ru": "Модуль {mod} не имеет функции main()",
|
"ru": "Integration tests failed with exit code {code}",
|
||||||
"zh": "模块 {mod} 没有 main() 函数"
|
"zh": "Integration tests failed with exit code {code}"
|
||||||
},
|
},
|
||||||
"Molecule directory not found: {path}": {
|
"Integration tests passed.": {
|
||||||
"en": "Molecule directory not found: {path}",
|
"bg": "Integration tests passed.",
|
||||||
"bg": "Директорията на molecule не е намерена: {path}",
|
"de": "Integration tests passed.",
|
||||||
"de": "Molecule-Verzeichnis nicht gefunden: {path}",
|
"en": "Integration tests passed.",
|
||||||
"ru": "Директория molecule не найдена: {path}",
|
"ru": "Integration tests passed.",
|
||||||
"zh": "未找到 molecule 目录: {path}"
|
"zh": "Integration tests passed."
|
||||||
},
|
},
|
||||||
"Nice! Gitea release {tag} created.": {
|
"Lint failed — refusing to release. Fix lint errors first.\n{stderr}": {
|
||||||
"en": "Nice! Gitea release {tag} created.",
|
"bg": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
|
||||||
"bg": "Отлично! Gitea release {tag} е създаден.",
|
"de": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
|
||||||
"de": "Prima! Gitea-Release {tag} erstellt.",
|
"en": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
|
||||||
"ru": "Отлично! Gitea release {tag} создан.",
|
"ru": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
|
||||||
"zh": "不错!Gitea release {tag} 已创建。"
|
"zh": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}"
|
||||||
},
|
},
|
||||||
"Nice! PR #{pr_number} squash-merged with title: {merge_title}": {
|
"Lint passed.": {
|
||||||
"en": "Nice! PR #{pr_number} squash-merged with title: {merge_title}",
|
"bg": "Lint passed.",
|
||||||
"bg": "Отлично! PR #{pr_number} е squash-merge-нат със заглавие: {merge_title}",
|
"de": "Lint passed.",
|
||||||
"de": "Prima! PR #{pr_number} wurde mit Titel {merge_title} squash-gemergt.",
|
"en": "Lint passed.",
|
||||||
"ru": "Отлично! PR #{pr_number} squash-merge с заголовком: {merge_title}",
|
"ru": "Lint passed.",
|
||||||
"zh": "不错!PR #{pr_number} 已 squash 合并,标题: {merge_title}"
|
"zh": "Lint passed."
|
||||||
},
|
},
|
||||||
"Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.": {
|
"Mapped file {file} is empty. Update the content or remove from mapping.json.": {
|
||||||
"en": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
|
"bg": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
|
||||||
"bg": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
|
"de": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
|
||||||
"de": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
|
"en": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
|
||||||
"ru": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
|
"ru": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
|
||||||
"zh": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered."
|
"zh": "Mapped file {file} is empty. Update the content or remove from mapping.json."
|
||||||
},
|
},
|
||||||
"Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.": {
|
"Mapped file {file} not found. Update mapping.json or create the file.": {
|
||||||
"en": "Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.",
|
"bg": "Mapped file {file} not found. Update mapping.json or create the file.",
|
||||||
"bg": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) е обновена и маркирана като готова.",
|
"de": "Mapped file {file} not found. Update mapping.json or create the file.",
|
||||||
"de": "Prima! Vikunja-Aufgabe {task_id} (ID {vikunja_id}) aktualisiert und als erledigt markiert.",
|
"en": "Mapped file {file} not found. Update mapping.json or create the file.",
|
||||||
"ru": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) обновлена и отмечена как выполненная.",
|
"ru": "Mapped file {file} not found. Update mapping.json or create the file.",
|
||||||
"zh": "不错!Vikunja 任务 {task_id} (ID {vikunja_id}) 已更新并标记为完成。"
|
"zh": "Mapped file {file} not found. Update mapping.json or create the file."
|
||||||
},
|
},
|
||||||
"No changes between {base} and {head}.": {
|
"Merge failed after rebase retry: {error}\nPlease rebase the PR manually.": {
|
||||||
"en": "No changes between {base} and {head}.",
|
"bg": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
|
||||||
"bg": "No changes between {base} and {head}.",
|
"de": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
|
||||||
"de": "No changes between {base} and {head}.",
|
"en": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
|
||||||
"ru": "No changes between {base} and {head}.",
|
"ru": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
|
||||||
"zh": "No changes between {base} and {head}."
|
"zh": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually."
|
||||||
},
|
},
|
||||||
"No staged changes — version and changelog already up to date.": {
|
"Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.": {
|
||||||
"en": "No staged changes — version and changelog already up to date.",
|
"bg": "Сливането неуспешно с HTTP {status}: {message}\nПроверете дали PR е готов и имате права за сливане.",
|
||||||
"bg": "No staged changes — version and changelog already up to date.",
|
"de": "Merge fehlgeschlagen mit HTTP {status}: {message}\nBitte prüfen Sie, ob der PR bereit ist und Sie Merge-Rechte haben.",
|
||||||
"de": "No staged changes — version and changelog already up to date.",
|
"en": "Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.",
|
||||||
"ru": "No staged changes — version and changelog already up to date.",
|
"ru": "Слияние не удалось: HTTP {status}: {message}\nПроверьте, что PR готов и у вас есть права на слияние.",
|
||||||
"zh": "No staged changes — version and changelog already up to date."
|
"zh": "合并失败: HTTP {status}: {message}\n请检查 PR 是否准备就绪且您具有合并权限。"
|
||||||
},
|
},
|
||||||
"No tags found — treating all changes as user-facing.": {
|
"Merged {count} reports: {tests} tests, {failures} failures → {output}": {
|
||||||
"en": "No tags found — treating all changes as user-facing.",
|
"bg": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
|
||||||
"bg": "No tags found — treating all changes as user-facing.",
|
"de": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
|
||||||
"de": "No tags found — treating all changes as user-facing.",
|
"en": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
|
||||||
"ru": "No tags found — treating all changes as user-facing.",
|
"ru": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
|
||||||
"zh": "No tags found — treating all changes as user-facing."
|
"zh": "Merged {count} reports: {tests} tests, {failures} failures → {output}"
|
||||||
},
|
},
|
||||||
"No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.": {
|
"Module {mod} has no main() function": {
|
||||||
"en": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
|
"bg": "Модул {mod} няма функция main()",
|
||||||
"bg": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
|
"de": "Modul {mod} hat keine main()-Funktion",
|
||||||
"de": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
|
"en": "Module {mod} has no main() function",
|
||||||
"ru": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
|
"ru": "Модуль {mod} не имеет функции main()",
|
||||||
"zh": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID."
|
"zh": "模块 {mod} 没有 main() 函数"
|
||||||
},
|
},
|
||||||
"No unreleased changes found. Nothing to release.": {
|
"Molecule directory not found: {path}": {
|
||||||
"en": "No unreleased changes found. Nothing to release.",
|
"bg": "Директорията на molecule не е намерена: {path}",
|
||||||
"bg": "No unreleased changes found. Nothing to release.",
|
"de": "Molecule-Verzeichnis nicht gefunden: {path}",
|
||||||
"de": "No unreleased changes found. Nothing to release.",
|
"en": "Molecule directory not found: {path}",
|
||||||
"ru": "No unreleased changes found. Nothing to release.",
|
"ru": "Директория molecule не найдена: {path}",
|
||||||
"zh": "No unreleased changes found. Nothing to release."
|
"zh": "未找到 molecule 目录: {path}"
|
||||||
},
|
},
|
||||||
"No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.": {
|
"Nice! Gitea release {tag} created.": {
|
||||||
"en": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
|
"bg": "Отлично! Gitea release {tag} е създаден.",
|
||||||
"bg": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
|
"de": "Prima! Gitea-Release {tag} erstellt.",
|
||||||
"de": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
|
"en": "Nice! Gitea release {tag} created.",
|
||||||
"ru": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
|
"ru": "Отлично! Gitea release {tag} создан.",
|
||||||
"zh": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release."
|
"zh": "不错!Gitea release {tag} 已创建。"
|
||||||
},
|
},
|
||||||
"Note: Self-approval not allowed. Posting COMMENT instead.": {
|
"Nice! PR #{pr_number} squash-merged with title: {merge_title}": {
|
||||||
"en": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
"bg": "Отлично! PR #{pr_number} е squash-merge-нат със заглавие: {merge_title}",
|
||||||
"bg": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
"de": "Prima! PR #{pr_number} wurde mit Titel {merge_title} squash-gemergt.",
|
||||||
"de": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
"en": "Nice! PR #{pr_number} squash-merged with title: {merge_title}",
|
||||||
"ru": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
"ru": "Отлично! PR #{pr_number} squash-merge с заголовком: {merge_title}",
|
||||||
"zh": "Note: Self-approval not allowed. Posting COMMENT instead."
|
"zh": "不错!PR #{pr_number} 已 squash 合并,标题: {merge_title}"
|
||||||
},
|
},
|
||||||
"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": {
|
"Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.": {
|
||||||
"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": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
|
||||||
"bg": "Опа! Съобщението за commit трябва да следва конвенционален формат.\n Очаква се: <type>: <description>\n Получено: {subject}\n Разрешени типове: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
"de": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
|
||||||
"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": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
|
||||||
"ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
"ru": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
|
||||||
"zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE"
|
"zh": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered."
|
||||||
},
|
},
|
||||||
"Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.": {
|
"Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.": {
|
||||||
"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": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) е обновена и маркирана като готова.",
|
||||||
"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": "Prima! Vikunja-Aufgabe {task_id} (ID {vikunja_id}) aktualisiert und als erledigt markiert.",
|
||||||
"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": "Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.",
|
||||||
"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": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) обновлена и отмечена как выполненная.",
|
||||||
"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": "不错!Vikunja 任务 {task_id} (ID {vikunja_id}) 已更新并标记为完成。"
|
||||||
},
|
},
|
||||||
"Oops! Gitea PyPI registry publish failed:\n{stderr}": {
|
"No JUnit reports found matching {pattern} — skipping merge.": {
|
||||||
"en": "Oops! Gitea PyPI registry publish failed:\n{stderr}",
|
"bg": "No JUnit reports found matching {pattern} — skipping merge.",
|
||||||
"bg": "Опа! Публикуването в Gitea PyPI registry неуспешно:\n{stderr}",
|
"de": "No JUnit reports found matching {pattern} — skipping merge.",
|
||||||
"de": "Ups! Veröffentlichung in der Gitea PyPI-Registry fehlgeschlagen:\n{stderr}",
|
"en": "No JUnit reports found matching {pattern} — skipping merge.",
|
||||||
"ru": "Ой! Публикация в Gitea PyPI registry не удалась:\n{stderr}",
|
"ru": "No JUnit reports found matching {pattern} — skipping merge.",
|
||||||
"zh": "哎呀!Gitea PyPI registry 发布失败:\n{stderr}"
|
"zh": "No JUnit reports found matching {pattern} — skipping merge."
|
||||||
},
|
},
|
||||||
"Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}": {
|
"No changes between {base} and {head}.": {
|
||||||
"en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
|
"bg": "No changes between {base} and {head}.",
|
||||||
"bg": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
|
"de": "No changes between {base} and {head}.",
|
||||||
"de": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
|
"en": "No changes between {base} and {head}.",
|
||||||
"ru": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
|
"ru": "No changes between {base} and {head}.",
|
||||||
"zh": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}"
|
"zh": "No changes between {base} and {head}."
|
||||||
},
|
},
|
||||||
"Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}": {
|
"No staged changes — version and changelog already up to date.": {
|
||||||
"en": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
|
"bg": "No staged changes — version and changelog already up to date.",
|
||||||
"bg": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
|
"de": "No staged changes — version and changelog already up to date.",
|
||||||
"de": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
|
"en": "No staged changes — version and changelog already up to date.",
|
||||||
"ru": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
|
"ru": "No staged changes — version and changelog already up to date.",
|
||||||
"zh": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}"
|
"zh": "No staged changes — version and changelog already up to date."
|
||||||
},
|
},
|
||||||
"Oops! No task ID found in .taskid file or branch name '{branch}'.": {
|
"No tags found — treating all changes as user-facing.": {
|
||||||
"en": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
|
"bg": "No tags found — treating all changes as user-facing.",
|
||||||
"bg": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
|
"de": "No tags found — treating all changes as user-facing.",
|
||||||
"de": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
|
"en": "No tags found — treating all changes as user-facing.",
|
||||||
"ru": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
|
"ru": "No tags found — treating all changes as user-facing.",
|
||||||
"zh": "Oops! No task ID found in .taskid file or branch name '{branch}'."
|
"zh": "No tags found — treating all changes as user-facing."
|
||||||
},
|
},
|
||||||
"Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}": {
|
"No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.": {
|
||||||
"en": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
"bg": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
|
||||||
"bg": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
"de": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
|
||||||
"de": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
"en": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
|
||||||
"ru": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
"ru": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
|
||||||
"zh": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}"
|
"zh": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID."
|
||||||
},
|
},
|
||||||
"Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}": {
|
"No unreleased changes found. Nothing to release.": {
|
||||||
"en": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
|
"bg": "No unreleased changes found. Nothing to release.",
|
||||||
"bg": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
|
"de": "No unreleased changes found. Nothing to release.",
|
||||||
"de": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
|
"en": "No unreleased changes found. Nothing to release.",
|
||||||
"ru": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
|
"ru": "No unreleased changes found. Nothing to release.",
|
||||||
"zh": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}"
|
"zh": "No unreleased changes found. Nothing to release."
|
||||||
},
|
},
|
||||||
"Oops! Package build failed:\n{stderr}": {
|
"No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.": {
|
||||||
"en": "Oops! Package build failed:\n{stderr}",
|
"bg": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
|
||||||
"bg": "Опа! Сборката на пакета неуспешна:\n{stderr}",
|
"de": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
|
||||||
"de": "Ups! Paket-Build fehlgeschlagen:\n{stderr}",
|
"en": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
|
||||||
"ru": "Ой! Сборка пакета не удалась:\n{stderr}",
|
"ru": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
|
||||||
"zh": "哎呀!包构建失败:\n{stderr}"
|
"zh": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release."
|
||||||
},
|
},
|
||||||
"Oops! PyPI publish failed:\n{stderr}": {
|
"Note: Self-approval not allowed. Posting COMMENT instead.": {
|
||||||
"en": "Oops! PyPI publish failed:\n{stderr}",
|
"bg": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
||||||
"bg": "Опа! Публикуването в PyPI неуспешно:\n{stderr}",
|
"de": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
||||||
"de": "Ups! PyPI-Veröffentlichung fehlgeschlagen:\n{stderr}",
|
"en": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
||||||
"ru": "Ой! Публикация в PyPI не удалась:\n{stderr}",
|
"ru": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
||||||
"zh": "哎呀!PyPI 发布失败:\n{stderr}"
|
"zh": "Note: Self-approval not allowed. Posting COMMENT instead."
|
||||||
},
|
},
|
||||||
"PASSED: {pair}": {
|
"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": "PASSED: {pair}",
|
"bg": "Опа! Съобщението за commit трябва да следва конвенционален формат.\n Очаква се: <type>: <description>\n Получено: {subject}\n Разрешени типове: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
||||||
"bg": "PASSED: {pair}",
|
"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": "PASSED: {pair}",
|
"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": "PASSED: {pair}",
|
"ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
||||||
"zh": "PASSED: {pair}"
|
"zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE"
|
||||||
},
|
},
|
||||||
"PR number must be an integer, got: {pr_number}": {
|
"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": "PR number must be an integer, got: {pr_number}",
|
"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": "PR number must be an integer, got: {pr_number}",
|
"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": "PR number must be an integer, got: {pr_number}",
|
"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": "PR number must be an integer, got: {pr_number}",
|
"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": "PR number must be an integer, got: {pr_number}"
|
"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."
|
||||||
},
|
},
|
||||||
"PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}": {
|
"Oops! Gitea PyPI registry publish failed:\n{stderr}": {
|
||||||
"en": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
"bg": "Опа! Публикуването в Gitea PyPI registry неуспешно:\n{stderr}",
|
||||||
"bg": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
"de": "Ups! Veröffentlichung in der Gitea PyPI-Registry fehlgeschlagen:\n{stderr}",
|
||||||
"de": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
"en": "Oops! Gitea PyPI registry publish failed:\n{stderr}",
|
||||||
"ru": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
"ru": "Ой! Публикация в Gitea PyPI registry не удалась:\n{stderr}",
|
||||||
"zh": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}"
|
"zh": "哎呀!Gitea PyPI registry 发布失败:\n{stderr}"
|
||||||
},
|
},
|
||||||
"PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.": {
|
"Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}": {
|
||||||
"en": "PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.",
|
"bg": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
|
||||||
"bg": "PYPI_TOKEN не е зададен и няма конфигуриран URL на registry — пропускаме публикуването в PyPI. Без притеснения, просто ще създадем Gitea release.",
|
"de": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
|
||||||
"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! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
|
||||||
"ru": "PYPI_TOKEN не задан и URL registry не настроен — пропускаем публикацию в PyPI. Не беспокойтесь, мы просто создадим Gitea release.",
|
"ru": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
|
||||||
"zh": "未设置 PYPI_TOKEN 且未配置 registry URL — 跳过 PyPI 发布。别担心,我们直接创建 Gitea release。"
|
"zh": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}"
|
||||||
},
|
},
|
||||||
"Published to Gitea PyPI registry.": {
|
"Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}": {
|
||||||
"en": "Published to Gitea PyPI registry.",
|
"bg": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
|
||||||
"bg": "Публикувано в Gitea PyPI registry.",
|
"de": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
|
||||||
"de": "In der Gitea PyPI-Registry veröffentlicht.",
|
"en": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
|
||||||
"ru": "Опубликовано в Gitea PyPI registry.",
|
"ru": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
|
||||||
"zh": "已发布到 Gitea PyPI registry。"
|
"zh": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}"
|
||||||
},
|
},
|
||||||
"Published to PyPI.": {
|
"Oops! No task ID found in .taskid file or branch name '{branch}'.": {
|
||||||
"en": "Published to PyPI.",
|
"bg": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
|
||||||
"bg": "Публикувано в PyPI.",
|
"de": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
|
||||||
"de": "In PyPI veröffentlicht.",
|
"en": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
|
||||||
"ru": "Опубликовано в PyPI.",
|
"ru": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
|
||||||
"zh": "已发布到 PyPI。"
|
"zh": "Oops! No task ID found in .taskid file or branch name '{branch}'."
|
||||||
},
|
},
|
||||||
"Pushed release commit to master.": {
|
"Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}": {
|
||||||
"en": "Pushed release commit to master.",
|
"bg": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
||||||
"bg": "Pushed release commit to master.",
|
"de": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
||||||
"de": "Pushed release commit to master.",
|
"en": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
||||||
"ru": "Pushed release commit to master.",
|
"ru": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
||||||
"zh": "Pushed release commit to master."
|
"zh": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}"
|
||||||
},
|
},
|
||||||
"Rebased and pushed. Retrying merge...": {
|
"Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}": {
|
||||||
"en": "Rebased and pushed. Retrying merge...",
|
"bg": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
|
||||||
"bg": "Rebased and pushed. Retrying merge...",
|
"de": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
|
||||||
"de": "Rebased and pushed. Retrying merge...",
|
"en": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
|
||||||
"ru": "Rebased and pushed. Retrying merge...",
|
"ru": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
|
||||||
"zh": "Rebased and pushed. Retrying merge..."
|
"zh": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}"
|
||||||
},
|
},
|
||||||
"Release creation failed: {error}": {
|
"Oops! Package build failed:\n{stderr}": {
|
||||||
"en": "Release creation failed: {error}",
|
"bg": "Опа! Сборката на пакета неуспешна:\n{stderr}",
|
||||||
"bg": "Release creation failed: {error}",
|
"de": "Ups! Paket-Build fehlgeschlagen:\n{stderr}",
|
||||||
"de": "Release creation failed: {error}",
|
"en": "Oops! Package build failed:\n{stderr}",
|
||||||
"ru": "Release creation failed: {error}",
|
"ru": "Ой! Сборка пакета не удалась:\n{stderr}",
|
||||||
"zh": "Release creation failed: {error}"
|
"zh": "哎呀!包构建失败:\n{stderr}"
|
||||||
},
|
},
|
||||||
"Release must be run on master, currently on '{branch}'.": {
|
"Oops! PyPI publish failed:\n{stderr}": {
|
||||||
"en": "Release must be run on master, currently on '{branch}'.",
|
"bg": "Опа! Публикуването в PyPI неуспешно:\n{stderr}",
|
||||||
"bg": "Release must be run on master, currently on '{branch}'.",
|
"de": "Ups! PyPI-Veröffentlichung fehlgeschlagen:\n{stderr}",
|
||||||
"de": "Release must be run on master, currently on '{branch}'.",
|
"en": "Oops! PyPI publish failed:\n{stderr}",
|
||||||
"ru": "Release must be run on master, currently on '{branch}'.",
|
"ru": "Ой! Публикация в PyPI не удалась:\n{stderr}",
|
||||||
"zh": "Release must be run on master, currently on '{branch}'."
|
"zh": "哎呀!PyPI 发布失败:\n{stderr}"
|
||||||
},
|
},
|
||||||
"Repo must be in 'owner/name' format, got: {repo}": {
|
"PASSED: {pair}": {
|
||||||
"en": "Repo must be in 'owner/name' format, got: {repo}",
|
"bg": "PASSED: {pair}",
|
||||||
"bg": "Repo must be in 'owner/name' format, got: {repo}",
|
"de": "PASSED: {pair}",
|
||||||
"de": "Repo must be in 'owner/name' format, got: {repo}",
|
"en": "PASSED: {pair}",
|
||||||
"ru": "Repo must be in 'owner/name' format, got: {repo}",
|
"ru": "PASSED: {pair}",
|
||||||
"zh": "Repo must be in 'owner/name' format, got: {repo}"
|
"zh": "PASSED: {pair}"
|
||||||
},
|
},
|
||||||
"Repository configuration complete.": {
|
"PR number must be an integer, got: {pr_number}": {
|
||||||
"en": "Repository configuration complete.",
|
"bg": "PR number must be an integer, got: {pr_number}",
|
||||||
"bg": "Конфигурирането на хранилището е завършено.",
|
"de": "PR number must be an integer, got: {pr_number}",
|
||||||
"de": "Repository-Konfiguration abgeschlossen.",
|
"en": "PR number must be an integer, got: {pr_number}",
|
||||||
"ru": "Конфигурация репозитория завершена.",
|
"ru": "PR number must be an integer, got: {pr_number}",
|
||||||
"zh": "仓库配置完成。"
|
"zh": "PR number must be an integer, got: {pr_number}"
|
||||||
},
|
},
|
||||||
"Runner index {index} out of range (0..{max})": {
|
"PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}": {
|
||||||
"en": "Runner index {index} out of range (0..{max})",
|
"bg": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
||||||
"bg": "Индексът на runner {index} е извън диапазона (0..{max})",
|
"de": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
||||||
"de": "Runner-Index {index} außerhalb des Bereichs (0..{max})",
|
"en": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
||||||
"ru": "Индекс runner {index} вне диапазона (0..{max})",
|
"ru": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
||||||
"zh": "Runner 索引 {index} 超出范围 (0..{max})"
|
"zh": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}"
|
||||||
},
|
},
|
||||||
"Running lint checks...": {
|
"PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.": {
|
||||||
"en": "Running lint checks...",
|
"bg": "PYPI_TOKEN не е зададен и няма конфигуриран URL на registry — пропускаме публикуването в PyPI. Без притеснения, просто ще създадем Gitea release.",
|
||||||
"bg": "Running lint checks...",
|
"de": "PYPI_TOKEN nicht gesetzt und keine Registry-URL konfiguriert — PyPI-Veröffentlichung wird übersprungen. Keine Sorge, wir erstellen einfach das Gitea-Release.",
|
||||||
"de": "Running lint checks...",
|
"en": "PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.",
|
||||||
"ru": "Running lint checks...",
|
"ru": "PYPI_TOKEN не задан и URL registry не настроен — пропускаем публикацию в PyPI. Не беспокойтесь, мы просто создадим Gitea release.",
|
||||||
"zh": "Running lint checks..."
|
"zh": "未设置 PYPI_TOKEN 且未配置 registry URL — 跳过 PyPI 发布。别担心,我们直接创建 Gitea release。"
|
||||||
},
|
},
|
||||||
"Running tests...": {
|
"Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.": {
|
||||||
"en": "Running tests...",
|
"bg": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
|
||||||
"bg": "Running tests...",
|
"de": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
|
||||||
"de": "Running tests...",
|
"en": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
|
||||||
"ru": "Running tests...",
|
"ru": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
|
||||||
"zh": "Running tests..."
|
"zh": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit."
|
||||||
},
|
},
|
||||||
"Running: {scenario} on {platform}": {
|
"Published to Gitea PyPI registry.": {
|
||||||
"en": "Running: {scenario} on {platform}",
|
"bg": "Публикувано в Gitea PyPI registry.",
|
||||||
"bg": "Running: {scenario} on {platform}",
|
"de": "In der Gitea PyPI-Registry veröffentlicht.",
|
||||||
"de": "Running: {scenario} on {platform}",
|
"en": "Published to Gitea PyPI registry.",
|
||||||
"ru": "Running: {scenario} on {platform}",
|
"ru": "Опубликовано в Gitea PyPI registry.",
|
||||||
"zh": "Running: {scenario} on {platform}"
|
"zh": "已发布到 Gitea PyPI registry。"
|
||||||
},
|
},
|
||||||
"Skipping commit push — no staged changes.": {
|
"Published to PyPI.": {
|
||||||
"en": "Skipping commit push — no staged changes.",
|
"bg": "Публикувано в PyPI.",
|
||||||
"bg": "Skipping commit push — no staged changes.",
|
"de": "In PyPI veröffentlicht.",
|
||||||
"de": "Skipping commit push — no staged changes.",
|
"en": "Published to PyPI.",
|
||||||
"ru": "Skipping commit push — no staged changes.",
|
"ru": "Опубликовано в PyPI.",
|
||||||
"zh": "Skipping commit push — no staged changes."
|
"zh": "已发布到 PyPI。"
|
||||||
},
|
},
|
||||||
"Syncing {count} documentation pages to wiki...": {
|
"Pushed release commit to master.": {
|
||||||
"en": "Syncing {count} documentation pages to wiki...",
|
"bg": "Pushed release commit to master.",
|
||||||
"bg": "Syncing {count} documentation pages to wiki...",
|
"de": "Pushed release commit to master.",
|
||||||
"de": "Syncing {count} documentation pages to wiki...",
|
"en": "Pushed release commit to master.",
|
||||||
"ru": "Syncing {count} documentation pages to wiki...",
|
"ru": "Pushed release commit to master.",
|
||||||
"zh": "Syncing {count} documentation pages to wiki..."
|
"zh": "Pushed release commit to master."
|
||||||
},
|
},
|
||||||
"Tag consistency check failed.": {
|
"Rebased and pushed. Retrying merge...": {
|
||||||
"en": "Tag consistency check failed.",
|
"bg": "Rebased and pushed. Retrying merge...",
|
||||||
"bg": "Tag consistency check failed.",
|
"de": "Rebased and pushed. Retrying merge...",
|
||||||
"de": "Tag consistency check failed.",
|
"en": "Rebased and pushed. Retrying merge...",
|
||||||
"ru": "Tag consistency check failed.",
|
"ru": "Rebased and pushed. Retrying merge...",
|
||||||
"zh": "Tag consistency check failed."
|
"zh": "Rebased and pushed. Retrying merge..."
|
||||||
},
|
},
|
||||||
"Tag v{version} already existed. Publish workflow should already have been triggered.": {
|
"Release creation failed: {error}": {
|
||||||
"en": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
"bg": "Release creation failed: {error}",
|
||||||
"bg": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
"de": "Release creation failed: {error}",
|
||||||
"de": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
"en": "Release creation failed: {error}",
|
||||||
"ru": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
"ru": "Release creation failed: {error}",
|
||||||
"zh": "Tag v{version} already existed. Publish workflow should already have been triggered."
|
"zh": "Release creation failed: {error}"
|
||||||
},
|
},
|
||||||
"Tag {tag} already exists and points to HEAD. Skipping creation.": {
|
"Release must be run on master, currently on '{branch}'.": {
|
||||||
"en": "Tag {tag} already exists and points to HEAD. Skipping creation.",
|
"bg": "Release must be run on master, currently on '{branch}'.",
|
||||||
"bg": "Tag {tag} already exists and points to HEAD. Skipping creation.",
|
"de": "Release must be run on master, currently on '{branch}'.",
|
||||||
"de": "Tag {tag} already exists and points to HEAD. Skipping creation.",
|
"en": "Release must be run on master, currently on '{branch}'.",
|
||||||
"ru": "Tag {tag} already exists and points to HEAD. Skipping creation.",
|
"ru": "Release must be run on master, currently on '{branch}'.",
|
||||||
"zh": "Tag {tag} already exists and points to HEAD. Skipping creation."
|
"zh": "Release must be run on master, currently on '{branch}'."
|
||||||
},
|
},
|
||||||
"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.": {
|
"Repo must be in 'owner/name' format, got: {repo}": {
|
||||||
"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": "Repo must be in 'owner/name' format, got: {repo}",
|
||||||
"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": "Repo must be in 'owner/name' format, got: {repo}",
|
||||||
"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": "Repo must be in 'owner/name' format, got: {repo}",
|
||||||
"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": "Repo must be in 'owner/name' format, got: {repo}",
|
||||||
"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": "Repo must be in 'owner/name' format, got: {repo}"
|
||||||
},
|
},
|
||||||
"Task ID: {task_id}": {
|
"Repository configuration complete.": {
|
||||||
"en": "Task ID: {task_id}",
|
"bg": "Конфигурирането на хранилището е завършено.",
|
||||||
"bg": "Task ID: {task_id}",
|
"de": "Repository-Konfiguration abgeschlossen.",
|
||||||
"de": "Task ID: {task_id}",
|
"en": "Repository configuration complete.",
|
||||||
"ru": "Task ID: {task_id}",
|
"ru": "Конфигурация репозитория завершена.",
|
||||||
"zh": "Task ID: {task_id}"
|
"zh": "仓库配置完成。"
|
||||||
},
|
},
|
||||||
"Tests failed — refusing to release. Fix test failures first.\n{stderr}": {
|
"Roles directory not found: {path}": {
|
||||||
"en": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
|
"bg": "Roles directory not found: {path}",
|
||||||
"bg": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
|
"de": "Roles directory not found: {path}",
|
||||||
"de": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
|
"en": "Roles directory not found: {path}",
|
||||||
"ru": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
|
"ru": "Roles directory not found: {path}",
|
||||||
"zh": "Tests failed — refusing to release. Fix test failures first.\n{stderr}"
|
"zh": "Roles directory not found: {path}"
|
||||||
},
|
},
|
||||||
"Tests passed.": {
|
"Runner index {index} out of range (0..{max})": {
|
||||||
"en": "Tests passed.",
|
"bg": "Индексът на runner {index} е извън диапазона (0..{max})",
|
||||||
"bg": "Tests passed.",
|
"de": "Runner-Index {index} außerhalb des Bereichs (0..{max})",
|
||||||
"de": "Tests passed.",
|
"en": "Runner index {index} out of range (0..{max})",
|
||||||
"ru": "Tests passed.",
|
"ru": "Индекс runner {index} вне диапазона (0..{max})",
|
||||||
"zh": "Tests passed."
|
"zh": "Runner 索引 {index} 超出范围 (0..{max})"
|
||||||
},
|
},
|
||||||
"Unit tests passed in {duration:.2f}s (under {max}s limit).": {
|
"Running lint checks...": {
|
||||||
"en": "Unit tests passed in {duration:.2f}s (under {max}s limit).",
|
"bg": "Running lint checks...",
|
||||||
"bg": "Unit tests passed in {duration:.2f}s (under {max}s limit).",
|
"de": "Running lint checks...",
|
||||||
"de": "Unit tests passed in {duration:.2f}s (under {max}s limit).",
|
"en": "Running lint checks...",
|
||||||
"ru": "Unit tests passed in {duration:.2f}s (under {max}s limit).",
|
"ru": "Running lint checks...",
|
||||||
"zh": "Unit tests passed in {duration:.2f}s (under {max}s limit)."
|
"zh": "Running lint checks..."
|
||||||
},
|
},
|
||||||
"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.": {
|
"Running tests...": {
|
||||||
"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": "Running tests...",
|
||||||
"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": "Running tests...",
|
||||||
"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": "Running tests...",
|
||||||
"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": "Running tests...",
|
||||||
"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": "Running tests..."
|
||||||
},
|
},
|
||||||
"Unknown check category '{check}'. Available: all, user-facing{tags}": {
|
"Running: {scenario} on {platform}": {
|
||||||
"en": "Unknown check category '{check}'. Available: all, user-facing{tags}",
|
"bg": "Running: {scenario} on {platform}",
|
||||||
"bg": "Unknown check category '{check}'. Available: all, user-facing{tags}",
|
"de": "Running: {scenario} on {platform}",
|
||||||
"de": "Unknown check category '{check}'. Available: all, user-facing{tags}",
|
"en": "Running: {scenario} on {platform}",
|
||||||
"ru": "Unknown check category '{check}'. Available: all, user-facing{tags}",
|
"ru": "Running: {scenario} on {platform}",
|
||||||
"zh": "Unknown check category '{check}'. Available: all, user-facing{tags}"
|
"zh": "Running: {scenario} on {platform}"
|
||||||
},
|
},
|
||||||
"Updated version in {init}": {
|
"Skipping commit push — no staged changes.": {
|
||||||
"en": "Updated version in {init}",
|
"bg": "Skipping commit push — no staged changes.",
|
||||||
"bg": "Updated version in {init}",
|
"de": "Skipping commit push — no staged changes.",
|
||||||
"de": "Updated version in {init}",
|
"en": "Skipping commit push — no staged changes.",
|
||||||
"ru": "Updated version in {init}",
|
"ru": "Skipping commit push — no staged changes.",
|
||||||
"zh": "Updated version in {init}"
|
"zh": "Skipping commit push — no staged changes."
|
||||||
},
|
},
|
||||||
"Updated {changelog_file}": {
|
"Syncing {count} documentation pages to wiki...": {
|
||||||
"en": "Updated {changelog_file}",
|
"bg": "Syncing {count} documentation pages to wiki...",
|
||||||
"bg": "Updated {changelog_file}",
|
"de": "Syncing {count} documentation pages to wiki...",
|
||||||
"de": "Updated {changelog_file}",
|
"en": "Syncing {count} documentation pages to wiki...",
|
||||||
"ru": "Updated {changelog_file}",
|
"ru": "Syncing {count} documentation pages to wiki...",
|
||||||
"zh": "Updated {changelog_file}"
|
"zh": "Syncing {count} documentation pages to wiki..."
|
||||||
},
|
},
|
||||||
"VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.": {
|
"Tag consistency check failed.": {
|
||||||
"en": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
"bg": "Tag consistency check failed.",
|
||||||
"bg": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
"de": "Tag consistency check failed.",
|
||||||
"de": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
"en": "Tag consistency check failed.",
|
||||||
"ru": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
"ru": "Tag consistency check failed.",
|
||||||
"zh": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles."
|
"zh": "Tag consistency check failed."
|
||||||
},
|
},
|
||||||
"Version file: {file}": {
|
"Tag v{version} already existed. Publish workflow should already have been triggered.": {
|
||||||
"en": "Version file: {file}",
|
"bg": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
||||||
"bg": "Version file: {file}",
|
"de": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
||||||
"de": "Version file: {file}",
|
"en": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
||||||
"ru": "Version file: {file}",
|
"ru": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
||||||
"zh": "Version file: {file}"
|
"zh": "Tag v{version} already existed. Publish workflow should already have been triggered."
|
||||||
},
|
},
|
||||||
"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 and points to HEAD. Skipping creation.": {
|
||||||
"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 and points to HEAD. Skipping creation.",
|
||||||
"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 and points to HEAD. Skipping creation.",
|
||||||
"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 and points to HEAD. Skipping creation.",
|
||||||
"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 and points to HEAD. Skipping creation.",
|
||||||
"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 and points to HEAD. Skipping creation."
|
||||||
},
|
},
|
||||||
"WARNING: --skip-tests passed — skipping test verification.": {
|
"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": "WARNING: --skip-tests passed — skipping test verification.",
|
"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": "WARNING: --skip-tests passed — skipping test verification.",
|
"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": "WARNING: --skip-tests passed — skipping test verification.",
|
"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": "WARNING: --skip-tests passed — skipping test verification.",
|
"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": "WARNING: --skip-tests passed — skipping test verification."
|
"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: could not fetch tags from origin.": {
|
"Task ID: {task_id}": {
|
||||||
"en": "Warning: could not fetch tags from origin.",
|
"bg": "Task ID: {task_id}",
|
||||||
"bg": "Warning: could not fetch tags from origin.",
|
"de": "Task ID: {task_id}",
|
||||||
"de": "Warning: could not fetch tags from origin.",
|
"en": "Task ID: {task_id}",
|
||||||
"ru": "Warning: could not fetch tags from origin.",
|
"ru": "Task ID: {task_id}",
|
||||||
"zh": "Warning: could not fetch tags from origin."
|
"zh": "Task ID: {task_id}"
|
||||||
},
|
},
|
||||||
"Wiki integrity check failed — {count} issue(s)": {
|
"Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.": {
|
||||||
"en": "Wiki integrity check failed — {count} issue(s)",
|
"bg": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.",
|
||||||
"bg": "Wiki integrity check failed — {count} issue(s)",
|
"de": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.",
|
||||||
"de": "Wiki integrity check failed — {count} issue(s)",
|
"en": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.",
|
||||||
"ru": "Wiki integrity check failed — {count} issue(s)",
|
"ru": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.",
|
||||||
"zh": "Wiki integrity check failed — {count} issue(s)"
|
"zh": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls."
|
||||||
},
|
},
|
||||||
"Wiki verification failed — {failures} page(s) empty or mismatched": {
|
"Tests failed — refusing to release. Fix test failures first.\n{stderr}": {
|
||||||
"en": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
"bg": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
|
||||||
"bg": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
"de": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
|
||||||
"de": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
"en": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
|
||||||
"ru": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
"ru": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
|
||||||
"zh": "Wiki verification failed — {failures} page(s) empty or mismatched"
|
"zh": "Tests failed — refusing to release. Fix test failures first.\n{stderr}"
|
||||||
},
|
},
|
||||||
"[dry-run] Would commit: release: v{version}": {
|
"Tests passed.": {
|
||||||
"en": "[dry-run] Would commit: release: v{version}",
|
"bg": "Tests passed.",
|
||||||
"bg": "[dry-run] Would commit: release: v{version}",
|
"de": "Tests passed.",
|
||||||
"de": "[dry-run] Would commit: release: v{version}",
|
"en": "Tests passed.",
|
||||||
"ru": "[dry-run] Would commit: release: v{version}",
|
"ru": "Tests passed.",
|
||||||
"zh": "[dry-run] Would commit: release: v{version}"
|
"zh": "Tests passed."
|
||||||
},
|
},
|
||||||
"[dry-run] Would create tag: 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 create tag: 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 create tag: 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 create tag: 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 create tag: 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 create tag: 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: {tag}": {
|
"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: {tag}",
|
"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: {tag}",
|
"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: {tag}",
|
"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: {tag}",
|
"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: {tag}"
|
"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 push commit to master": {
|
"Unknown check category '{check}'. Available: all, user-facing{tags}": {
|
||||||
"en": "[dry-run] Would push commit to master",
|
"bg": "Unknown check category '{check}'. Available: all, user-facing{tags}",
|
||||||
"bg": "[dry-run] Would push commit to master",
|
"de": "Unknown check category '{check}'. Available: all, user-facing{tags}",
|
||||||
"de": "[dry-run] Would push commit to master",
|
"en": "Unknown check category '{check}'. Available: all, user-facing{tags}",
|
||||||
"ru": "[dry-run] Would push commit to master",
|
"ru": "Unknown check category '{check}'. Available: all, user-facing{tags}",
|
||||||
"zh": "[dry-run] Would push commit to master"
|
"zh": "Unknown check category '{check}'. Available: all, user-facing{tags}"
|
||||||
},
|
},
|
||||||
"[dry-run] Would sync page: {title} ({chars} chars)": {
|
"Updated version in {init}": {
|
||||||
"en": "[dry-run] Would sync page: {title} ({chars} chars)",
|
"bg": "Updated version in {init}",
|
||||||
"bg": "[dry-run] Would sync page: {title} ({chars} chars)",
|
"de": "Updated version in {init}",
|
||||||
"de": "[dry-run] Would sync page: {title} ({chars} chars)",
|
"en": "Updated version in {init}",
|
||||||
"ru": "[dry-run] Would sync page: {title} ({chars} chars)",
|
"ru": "Updated version in {init}",
|
||||||
"zh": "[dry-run] Would sync page: {title} ({chars} chars)"
|
"zh": "Updated version in {init}"
|
||||||
},
|
},
|
||||||
"[dry-run] Would update {changelog_file}": {
|
"Updated {changelog_file}": {
|
||||||
"en": "[dry-run] Would update {changelog_file}",
|
"bg": "Updated {changelog_file}",
|
||||||
"bg": "[dry-run] Would update {changelog_file}",
|
"de": "Updated {changelog_file}",
|
||||||
"de": "[dry-run] Would update {changelog_file}",
|
"en": "Updated {changelog_file}",
|
||||||
"ru": "[dry-run] Would update {changelog_file}",
|
"ru": "Updated {changelog_file}",
|
||||||
"zh": "[dry-run] Would update {changelog_file}"
|
"zh": "Updated {changelog_file}"
|
||||||
},
|
},
|
||||||
"[dry-run] Would update {init}": {
|
"VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.": {
|
||||||
"en": "[dry-run] Would update {init}",
|
"bg": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
||||||
"bg": "[dry-run] Would update {init}",
|
"de": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
||||||
"de": "[dry-run] Would update {init}",
|
"en": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
||||||
"ru": "[dry-run] Would update {init}",
|
"ru": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
||||||
"zh": "[dry-run] Would update {init}"
|
"zh": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles."
|
||||||
},
|
},
|
||||||
"active": {
|
"Version file: {file}": {
|
||||||
"en": "active",
|
"bg": "Version file: {file}",
|
||||||
"bg": "активен",
|
"de": "Version file: {file}",
|
||||||
"de": "aktiv",
|
"en": "Version file: {file}",
|
||||||
"ru": "активен",
|
"ru": "Version file: {file}",
|
||||||
"zh": "活跃"
|
"zh": "Version file: {file}"
|
||||||
},
|
},
|
||||||
"completed": {
|
"Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.": {
|
||||||
"en": "completed",
|
"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": "abgeschlossen",
|
"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."
|
||||||
},
|
},
|
||||||
"failed": {
|
"WARNING: --skip-tests passed — skipping test verification.": {
|
||||||
"en": "failed",
|
"bg": "WARNING: --skip-tests passed — skipping test verification.",
|
||||||
"bg": "неуспешен",
|
"de": "WARNING: --skip-tests passed — skipping test verification.",
|
||||||
"de": "fehlgeschlagen",
|
"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."
|
||||||
},
|
},
|
||||||
"git command failed ({cmd}): {stderr}": {
|
"Warning: could not fetch tags from origin.": {
|
||||||
"en": "git command failed ({cmd}): {stderr}",
|
"bg": "Warning: could not fetch tags from origin.",
|
||||||
"bg": "git command failed ({cmd}): {stderr}",
|
"de": "Warning: could not fetch tags from origin.",
|
||||||
"de": "git command failed ({cmd}): {stderr}",
|
"en": "Warning: could not fetch tags from origin.",
|
||||||
"ru": "git command failed ({cmd}): {stderr}",
|
"ru": "Warning: could not fetch tags from origin.",
|
||||||
"zh": "git command failed ({cmd}): {stderr}"
|
"zh": "Warning: could not fetch tags from origin."
|
||||||
},
|
},
|
||||||
"git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.": {
|
"Wiki integrity check failed — {count} issue(s)": {
|
||||||
"en": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
|
"bg": "Wiki integrity check failed — {count} issue(s)",
|
||||||
"bg": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
|
"de": "Wiki integrity check failed — {count} issue(s)",
|
||||||
"de": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
|
"en": "Wiki integrity check failed — {count} issue(s)",
|
||||||
"ru": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
|
"ru": "Wiki integrity check failed — {count} issue(s)",
|
||||||
"zh": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history."
|
"zh": "Wiki integrity check failed — {count} issue(s)"
|
||||||
},
|
},
|
||||||
"git-cliff returned empty version.": {
|
"Wiki verification failed — {failures} page(s) empty or mismatched": {
|
||||||
"en": "git-cliff returned empty version.",
|
"bg": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
||||||
"bg": "git-cliff returned empty version.",
|
"de": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
||||||
"de": "git-cliff returned empty version.",
|
"en": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
||||||
"ru": "git-cliff returned empty version.",
|
"ru": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
||||||
"zh": "git-cliff returned empty version."
|
"zh": "Wiki verification failed — {failures} page(s) empty or mismatched"
|
||||||
},
|
},
|
||||||
"git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).": {
|
"[dry-run] Would commit: release: v{version}": {
|
||||||
"en": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
|
"bg": "[dry-run] Would commit: release: v{version}",
|
||||||
"bg": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
|
"de": "[dry-run] Would commit: release: v{version}",
|
||||||
"de": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
|
"en": "[dry-run] Would commit: release: v{version}",
|
||||||
"ru": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
|
"ru": "[dry-run] Would commit: release: v{version}",
|
||||||
"zh": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1)."
|
"zh": "[dry-run] Would commit: release: v{version}"
|
||||||
},
|
},
|
||||||
"in_progress": {
|
"[dry-run] Would create tag: v{version}": {
|
||||||
"en": "in progress",
|
"bg": "[dry-run] Would create tag: v{version}",
|
||||||
"bg": "в процес",
|
"de": "[dry-run] Would create tag: v{version}",
|
||||||
"de": "in Bearbeitung",
|
"en": "[dry-run] Would create tag: v{version}",
|
||||||
"ru": "в процессе",
|
"ru": "[dry-run] Would create tag: v{version}",
|
||||||
"zh": "进行中"
|
"zh": "[dry-run] Would create tag: v{version}"
|
||||||
},
|
},
|
||||||
"inactive": {
|
"[dry-run] Would create tag: {tag}": {
|
||||||
"en": "inactive",
|
"bg": "[dry-run] Would create tag: {tag}",
|
||||||
"bg": "неактивен",
|
"de": "[dry-run] Would create tag: {tag}",
|
||||||
"de": "inaktiv",
|
"en": "[dry-run] Would create tag: {tag}",
|
||||||
"ru": "неактивен",
|
"ru": "[dry-run] Would create tag: {tag}",
|
||||||
"zh": "未激活"
|
"zh": "[dry-run] Would create tag: {tag}"
|
||||||
},
|
},
|
||||||
"mapping.json keys and values must be strings, got {k}={v}": {
|
"[dry-run] Would push commit to master": {
|
||||||
"en": "mapping.json keys and values must be strings, got {k}={v}",
|
"bg": "[dry-run] Would push commit to master",
|
||||||
"bg": "mapping.json keys and values must be strings, got {k}={v}",
|
"de": "[dry-run] Would push commit to master",
|
||||||
"de": "mapping.json keys and values must be strings, got {k}={v}",
|
"en": "[dry-run] Would push commit to master",
|
||||||
"ru": "mapping.json keys and values must be strings, got {k}={v}",
|
"ru": "[dry-run] Would push commit to master",
|
||||||
"zh": "mapping.json keys and values must be strings, got {k}={v}"
|
"zh": "[dry-run] Would push commit to master"
|
||||||
},
|
},
|
||||||
"mapping.json must be a dict of file-path -> page-title, got {type}": {
|
"[dry-run] Would sync page: {title} ({chars} chars)": {
|
||||||
"en": "mapping.json must be a dict of file-path -> page-title, got {type}",
|
"bg": "[dry-run] Would sync page: {title} ({chars} chars)",
|
||||||
"bg": "mapping.json must be a dict of file-path -> page-title, got {type}",
|
"de": "[dry-run] Would sync page: {title} ({chars} chars)",
|
||||||
"de": "mapping.json must be a dict of file-path -> page-title, got {type}",
|
"en": "[dry-run] Would sync page: {title} ({chars} chars)",
|
||||||
"ru": "mapping.json must be a dict of file-path -> page-title, got {type}",
|
"ru": "[dry-run] Would sync page: {title} ({chars} chars)",
|
||||||
"zh": "mapping.json must be a dict of file-path -> page-title, got {type}"
|
"zh": "[dry-run] Would sync page: {title} ({chars} chars)"
|
||||||
},
|
},
|
||||||
"pending": {
|
"[dry-run] Would update {changelog_file}": {
|
||||||
"en": "pending",
|
"bg": "[dry-run] Would update {changelog_file}",
|
||||||
"bg": "в очакване",
|
"de": "[dry-run] Would update {changelog_file}",
|
||||||
"de": "ausstehend",
|
"en": "[dry-run] Would update {changelog_file}",
|
||||||
"ru": "ожидает",
|
"ru": "[dry-run] Would update {changelog_file}",
|
||||||
"zh": "待处理"
|
"zh": "[dry-run] Would update {changelog_file}"
|
||||||
},
|
},
|
||||||
"unknown": {
|
"[dry-run] Would update {init}": {
|
||||||
"en": "unknown",
|
"bg": "[dry-run] Would update {init}",
|
||||||
"bg": "неизвестен",
|
"de": "[dry-run] Would update {init}",
|
||||||
"de": "unbekannt",
|
"en": "[dry-run] Would update {init}",
|
||||||
"ru": "неизвестно",
|
"ru": "[dry-run] Would update {init}",
|
||||||
"zh": "未知"
|
"zh": "[dry-run] Would update {init}"
|
||||||
},
|
},
|
||||||
"{file} already exists. Use --force to overwrite.": {
|
"active": {
|
||||||
"en": "{file} already exists. Use --force to overwrite.",
|
"bg": "активен",
|
||||||
"bg": "{file} already exists. Use --force to overwrite.",
|
"de": "aktiv",
|
||||||
"de": "{file} already exists. Use --force to overwrite.",
|
"en": "active",
|
||||||
"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.": {
|
"completed": {
|
||||||
"en": "--skip-build: skipping package build and PyPI publish.",
|
"bg": "завършен",
|
||||||
"bg": "--skip-build: skipping package build and PyPI publish.",
|
"de": "abgeschlossen",
|
||||||
"de": "--skip-build: skipping package build and PyPI publish.",
|
"en": "completed",
|
||||||
"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.": {
|
"failed": {
|
||||||
"en": "Integration tests cancelled — another runner failed.",
|
"bg": "неуспешен",
|
||||||
"bg": "Integration tests cancelled — another runner failed.",
|
"de": "fehlgeschlagen",
|
||||||
"de": "Integration tests cancelled — another runner failed.",
|
"en": "failed",
|
||||||
"ru": "Integration tests cancelled — another runner failed.",
|
"ru": "неудачный",
|
||||||
"zh": "Integration tests cancelled — another runner failed."
|
"zh": "失败"
|
||||||
},
|
},
|
||||||
"Integration tests failed with exit code {code}": {
|
"git command failed ({cmd}): {stderr}": {
|
||||||
"en": "Integration tests failed with exit code {code}",
|
"bg": "git command failed ({cmd}): {stderr}",
|
||||||
"bg": "Integration tests failed with exit code {code}",
|
"de": "git command failed ({cmd}): {stderr}",
|
||||||
"de": "Integration tests failed with exit code {code}",
|
"en": "git command failed ({cmd}): {stderr}",
|
||||||
"ru": "Integration tests failed with exit code {code}",
|
"ru": "git command failed ({cmd}): {stderr}",
|
||||||
"zh": "Integration tests failed with exit code {code}"
|
"zh": "git command failed ({cmd}): {stderr}"
|
||||||
},
|
},
|
||||||
"Integration tests passed.": {
|
"git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.": {
|
||||||
"en": "Integration tests passed.",
|
"bg": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
|
||||||
"bg": "Integration tests passed.",
|
"de": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
|
||||||
"de": "Integration tests passed.",
|
"en": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
|
||||||
"ru": "Integration tests passed.",
|
"ru": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
|
||||||
"zh": "Integration tests passed."
|
"zh": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history."
|
||||||
},
|
},
|
||||||
"Merged {count} reports: {tests} tests, {failures} failures → {output}": {
|
"git-cliff returned empty version.": {
|
||||||
"en": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
|
"bg": "git-cliff returned empty version.",
|
||||||
"bg": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
|
"de": "git-cliff returned empty version.",
|
||||||
"de": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
|
"en": "git-cliff returned empty version.",
|
||||||
"ru": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
|
"ru": "git-cliff returned empty version.",
|
||||||
"zh": "Merged {count} reports: {tests} tests, {failures} failures → {output}"
|
"zh": "git-cliff returned empty version."
|
||||||
},
|
},
|
||||||
"No JUnit reports found matching {pattern} — skipping merge.": {
|
"git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).": {
|
||||||
"en": "No JUnit reports found matching {pattern} — skipping merge.",
|
"bg": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
|
||||||
"bg": "No JUnit reports found matching {pattern} — skipping merge.",
|
"de": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
|
||||||
"de": "No JUnit reports found matching {pattern} — skipping merge.",
|
"en": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
|
||||||
"ru": "No JUnit reports found matching {pattern} — skipping merge.",
|
"ru": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
|
||||||
"zh": "No JUnit reports found matching {pattern} — skipping merge."
|
"zh": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1)."
|
||||||
},
|
},
|
||||||
"Roles directory not found: {path}": {
|
"in_progress": {
|
||||||
"en": "Roles directory not found: {path}",
|
"bg": "в процес",
|
||||||
"bg": "Roles directory not found: {path}",
|
"de": "in Bearbeitung",
|
||||||
"de": "Roles directory not found: {path}",
|
"en": "in progress",
|
||||||
"ru": "Roles directory not found: {path}",
|
"ru": "в процессе",
|
||||||
"zh": "Roles directory not found: {path}"
|
"zh": "进行中"
|
||||||
}
|
},
|
||||||
|
"inactive": {
|
||||||
|
"bg": "неактивен",
|
||||||
|
"de": "inaktiv",
|
||||||
|
"en": "inactive",
|
||||||
|
"ru": "неактивен",
|
||||||
|
"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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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: []")
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
"""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 (
|
||||||
|
DOCKER_SOCK,
|
||||||
|
_diagnose_socket,
|
||||||
|
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
|
||||||
|
call_kwargs = mock_run.call_args
|
||||||
|
assert call_kwargs.args[0] == ["docker", "info"]
|
||||||
|
assert call_kwargs.kwargs["env"]["DOCKER_HOST"] == f"unix://{DOCKER_SOCK}"
|
||||||
|
|
||||||
|
@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 TestDiagnoseSocket:
|
||||||
|
@patch("devx.molecule.start_docker.os.stat")
|
||||||
|
@patch("devx.molecule.start_docker.os.path.exists", return_value=True)
|
||||||
|
@patch("devx.molecule.start_docker.subprocess.run")
|
||||||
|
def test_socket_exists(self, mock_run: MagicMock, mock_exists: MagicMock, mock_stat: MagicMock) -> None:
|
||||||
|
mock_stat.return_value = MagicMock(st_mode=0o660, st_uid=0, st_gid=0)
|
||||||
|
mock_run.side_effect = [
|
||||||
|
MagicMock(stdout="/dev/sda1 /var/lib/docker ext4\n", returncode=0, text=""),
|
||||||
|
MagicMock(stdout="default\n", returncode=0, text=""),
|
||||||
|
MagicMock(
|
||||||
|
stdout="Server Version: 29.5.2\nStorage Driver: overlay2\nDocker Root Dir: /var/lib/docker\n",
|
||||||
|
returncode=0,
|
||||||
|
text="",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
_diagnose_socket()
|
||||||
|
mock_exists.assert_called_with(DOCKER_SOCK)
|
||||||
|
|
||||||
|
@patch("devx.molecule.start_docker.os.path.exists", return_value=False)
|
||||||
|
@patch("devx.molecule.start_docker.subprocess.run")
|
||||||
|
def test_socket_missing(self, mock_run: MagicMock, mock_exists: MagicMock) -> None:
|
||||||
|
mock_run.side_effect = [
|
||||||
|
MagicMock(stdout="proc on /proc type proc\n", returncode=0, text=""),
|
||||||
|
MagicMock(stdout="default\n", returncode=0, text=""),
|
||||||
|
MagicMock(stdout="", stderr="Cannot connect", returncode=1, text=""),
|
||||||
|
]
|
||||||
|
_diagnose_socket()
|
||||||
|
mock_exists.assert_called_with(DOCKER_SOCK)
|
||||||
|
|
||||||
|
|
||||||
|
class TestStartDockerDaemon:
|
||||||
|
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||||
|
@patch("devx.molecule.start_docker.is_docker_ready", return_value=True)
|
||||||
|
def test_host_socket_available(self, mock_ready: MagicMock, mock_diag: MagicMock) -> None:
|
||||||
|
"""Should return immediately if host Docker is available."""
|
||||||
|
assert start_docker_daemon(timeout=5) is True
|
||||||
|
mock_ready.assert_called_once()
|
||||||
|
mock_diag.assert_called_once()
|
||||||
|
|
||||||
|
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||||
|
@patch("devx.molecule.start_docker.os.path.exists", return_value=True)
|
||||||
|
@patch("devx.molecule.start_docker.is_docker_ready")
|
||||||
|
def test_rootless_socket_available(
|
||||||
|
self, mock_ready: MagicMock, mock_exists: MagicMock, mock_diag: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Should use rootless socket if host socket fails."""
|
||||||
|
# First check (host) fails, second check (rootless) succeeds
|
||||||
|
mock_ready.side_effect = [False, True]
|
||||||
|
assert start_docker_daemon(timeout=5) is True
|
||||||
|
|
||||||
|
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||||
|
@patch("devx.molecule.start_docker.os.path.exists", return_value=False)
|
||||||
|
@patch("devx.molecule.start_docker.is_docker_ready", return_value=False)
|
||||||
|
@patch("devx.molecule.start_docker.time.sleep")
|
||||||
|
@patch("devx.molecule.start_docker.subprocess.Popen")
|
||||||
|
@patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile")
|
||||||
|
def test_starts_local_daemon(
|
||||||
|
self,
|
||||||
|
mock_ntf: MagicMock,
|
||||||
|
mock_popen: MagicMock,
|
||||||
|
mock_sleep: MagicMock,
|
||||||
|
mock_ready: MagicMock,
|
||||||
|
mock_exists: MagicMock,
|
||||||
|
mock_diag: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
mock_ntf.return_value = MagicMock(name="/tmp/dockerd.log")
|
||||||
|
# Host fails, rootless doesn't exist, local daemon starts
|
||||||
|
mock_ready.side_effect = [False, False, False, False, True]
|
||||||
|
assert start_docker_daemon(timeout=5) is True
|
||||||
|
mock_popen.assert_called_once()
|
||||||
|
popen_args = mock_popen.call_args.args[0]
|
||||||
|
assert "dockerd" in popen_args
|
||||||
|
assert "--storage-driver" in popen_args
|
||||||
|
assert "vfs" in popen_args
|
||||||
|
assert "-H" in popen_args
|
||||||
|
|
||||||
|
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||||
|
@patch("devx.molecule.start_docker.os.path.exists", return_value=False)
|
||||||
|
@patch("devx.molecule.start_docker.is_docker_ready", return_value=False)
|
||||||
|
@patch("devx.molecule.start_docker.time.sleep")
|
||||||
|
@patch("devx.molecule.start_docker.subprocess.Popen")
|
||||||
|
@patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile")
|
||||||
|
def test_fails_after_timeout(
|
||||||
|
self,
|
||||||
|
mock_ntf: MagicMock,
|
||||||
|
mock_popen: MagicMock,
|
||||||
|
mock_sleep: MagicMock,
|
||||||
|
mock_ready: MagicMock,
|
||||||
|
mock_exists: MagicMock,
|
||||||
|
mock_diag: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
mock_ntf.return_value = MagicMock(name="/tmp/dockerd.log")
|
||||||
|
with patch("builtins.open", mock_open(read_data="dockerd error log")):
|
||||||
|
assert start_docker_daemon(timeout=3) is False
|
||||||
|
mock_popen.assert_called_once()
|
||||||
|
assert mock_sleep.call_count == 3
|
||||||
|
|
||||||
|
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||||
|
@patch("devx.molecule.start_docker.os.path.exists", return_value=False)
|
||||||
|
@patch("devx.molecule.start_docker.is_docker_ready", return_value=False)
|
||||||
|
@patch("devx.molecule.start_docker.time.sleep")
|
||||||
|
@patch("devx.molecule.start_docker.subprocess.Popen")
|
||||||
|
@patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile")
|
||||||
|
def test_fails_log_read_error(
|
||||||
|
self,
|
||||||
|
mock_ntf: MagicMock,
|
||||||
|
mock_popen: MagicMock,
|
||||||
|
mock_sleep: MagicMock,
|
||||||
|
mock_ready: MagicMock,
|
||||||
|
mock_exists: MagicMock,
|
||||||
|
mock_diag: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Should handle log read errors gracefully."""
|
||||||
|
mock_ntf.return_value = MagicMock(name="/tmp/dockerd.log")
|
||||||
|
with patch("builtins.open", side_effect=OSError("permission denied")):
|
||||||
|
assert start_docker_daemon(timeout=2) is False
|
||||||
|
|
||||||
|
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||||
|
@patch("devx.molecule.start_docker.os.path.exists", return_value=False)
|
||||||
|
@patch("devx.molecule.start_docker.is_docker_ready")
|
||||||
|
@patch("devx.molecule.start_docker.time.sleep")
|
||||||
|
@patch("devx.molecule.start_docker.subprocess.Popen")
|
||||||
|
@patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile")
|
||||||
|
def test_local_daemon_ready_on_first_check(
|
||||||
|
self,
|
||||||
|
mock_ntf: MagicMock,
|
||||||
|
mock_popen: MagicMock,
|
||||||
|
mock_sleep: MagicMock,
|
||||||
|
mock_ready: MagicMock,
|
||||||
|
mock_exists: MagicMock,
|
||||||
|
mock_diag: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
mock_ntf.return_value = MagicMock(name="/tmp/dockerd.log")
|
||||||
|
# Host fails, rootless doesn't exist, local ready on first loop check
|
||||||
|
mock_ready.side_effect = [False, False, True]
|
||||||
|
assert start_docker_daemon(timeout=5) is True
|
||||||
|
assert mock_popen.call_count == 1
|
||||||
|
assert mock_sleep.call_count == 1
|
||||||
|
|
||||||
|
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||||
|
@patch("devx.molecule.start_docker.os.environ")
|
||||||
|
@patch("devx.molecule.start_docker.is_docker_ready", return_value=True)
|
||||||
|
def test_sets_docker_host(
|
||||||
|
self,
|
||||||
|
mock_ready: MagicMock,
|
||||||
|
mock_environ: MagicMock,
|
||||||
|
mock_diag: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""DOCKER_HOST must be set so molecule connects to correct socket."""
|
||||||
|
start_docker_daemon(timeout=5)
|
||||||
|
mock_environ.__setitem__.assert_called_with("DOCKER_HOST", f"unix://{DOCKER_SOCK}")
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@patch("devx.molecule.start_docker.os.environ.get")
|
||||||
|
@patch("devx.molecule.start_docker.start_docker_daemon", return_value=True)
|
||||||
|
def test_exports_github_env(self, mock_start: MagicMock, mock_get: MagicMock) -> None:
|
||||||
|
"""Should write DOCKER_HOST to GITHUB_ENV when available."""
|
||||||
|
mock_get.side_effect = lambda key, default="": (
|
||||||
|
"/tmp/github_env" if key == "GITHUB_ENV" else f"unix://{DOCKER_SOCK}" if key == "DOCKER_HOST" else default
|
||||||
|
)
|
||||||
|
with patch("builtins.open", mock_open()) as mock_file:
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(main, [])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_file.assert_called_with("/tmp/github_env", "a")
|
||||||
|
|
||||||
|
@patch("devx.molecule.start_docker.os.environ.get", return_value="")
|
||||||
|
@patch("devx.molecule.start_docker.start_docker_daemon", return_value=True)
|
||||||
|
def test_no_github_env(self, mock_start: MagicMock, mock_get: MagicMock) -> None:
|
||||||
|
"""Should not crash when GITHUB_ENV is not set."""
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(main, [])
|
||||||
|
assert result.exit_code == 0
|
||||||
Reference in New Issue
Block a user