Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b3b604c2c | ||
|
|
53990dc10c | ||
|
|
8ea044a942 | ||
|
|
8411c92c95 | ||
|
|
621b9936c8 | ||
|
|
33063038a1 |
+43
-18
@@ -2,14 +2,49 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.1.0] - 2026-06-22
|
||||
## [0.4.3] - 2026-06-22
|
||||
|
||||
## [0.1.0] - 2026-06-22
|
||||
### Bug Fixes
|
||||
|
||||
## [0.1.0] - 2026-06-22
|
||||
- Expand DEFAULT_INFRASTRUCTURE to cover all common project files
|
||||
|
||||
## [0.1.0] - 2026-06-22
|
||||
## [0.4.2] - 2026-06-22
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Make all warnings into errors across devx tools
|
||||
|
||||
## [0.4.1] - 2026-06-22
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Correct version tags, changelog, and release script recovery
|
||||
|
||||
## [0.4.0] - 2026-06-22
|
||||
|
||||
### Features
|
||||
|
||||
- Add DEFAULT_INFRASTRUCTURE and configurable task prefix
|
||||
## [0.3.0] - 2026-06-22
|
||||
|
||||
### Features
|
||||
|
||||
- Add --no-ansible-collections option to setup tool
|
||||
## [0.2.0] - 2026-06-22
|
||||
|
||||
### Features
|
||||
|
||||
- Pluggable change classification framework
|
||||
## [0.1.2] - 2026-06-22
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Make sync-wiki and vikunja depend on release
|
||||
## [0.1.1] - 2026-06-22
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Disable push whitelist, allow direct pushes to master
|
||||
## [0.1.0] - 2026-06-22
|
||||
|
||||
## [0.1.0] - 2026-06-22
|
||||
@@ -18,18 +53,8 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
- Extract reusable dev/CI tools from GRM into devx package
|
||||
|
||||
## [unreleased]
|
||||
|
||||
### Features
|
||||
|
||||
- Extract reusable development and CI/CD tools from GRM into a standalone Python package
|
||||
- Port core modules: config, exceptions, i18n, api_clients, gitea_cli
|
||||
- Port 14 CI scripts: auto_merge, check_translations, classify_changes, detect_release_commit, discover_runners, doc_coverage, notify_failure, post_merge, pr_review, publish, push_badges, release, sync_wiki, validate_commit_msg
|
||||
- Port 6 dev tools: check_test_speed, generate_badges, install_checkmake, install_tools, setup, configure_repo
|
||||
- Port 5 molecule tools as optional extra: platforms, distribute_molecule, discover_runners, molecule_ci_guard, molecule_all
|
||||
- Add CLI entry point with subcommands: devx ci, devx tools, devx molecule
|
||||
- Add Gitea PyPI registry publishing support in publish.py
|
||||
- Add configurable workflow-only patterns in classify_changes.py
|
||||
- Add configurable version file path in release.py
|
||||
- Replicate GRM's automated workflow: CI, auto-merge, post-merge, release, badges, wiki sync, Vikunja
|
||||
### Bug Fixes
|
||||
|
||||
- Use python3 and venv python in workflows and Makefile
|
||||
- Fix post-merge job failures (configure-repo, badges, notify-failure)
|
||||
- Allow release bot to push to protected master
|
||||
|
||||
+5
-2
@@ -39,8 +39,8 @@ sort_commits = "oldest"
|
||||
recurse_submodules = false
|
||||
|
||||
commit_preprocessors = [
|
||||
# Strip DEVX-N task ID prefix from merge commits so git-cliff sees conventional commits
|
||||
{ pattern = "^DEVX-\\d+\\s+", replace = "" },
|
||||
# Strip DEVX-N: task ID prefix from squash-merge commits so git-cliff sees conventional commits
|
||||
{ pattern = "^DEVX-\\d+:\\s+", replace = "" },
|
||||
]
|
||||
|
||||
commit_parsers = [
|
||||
@@ -66,3 +66,6 @@ commit_parsers = [
|
||||
features_always_bump_minor = true
|
||||
breaking_always_bump_major = false
|
||||
initial_tag = "0.1.0"
|
||||
# Refactor commits bump patch — structural changes to src/ or pyproject.toml
|
||||
# affect users even though no new feature was added.
|
||||
refactor_always_bump_patch = true
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "0.4.3"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
@@ -21,7 +22,7 @@ def _parse_error(e: requests.HTTPError) -> tuple[int, str]:
|
||||
try:
|
||||
body: dict[str, Any] = response.json() if response is not None else {}
|
||||
message: str = body.get("message", str(e))
|
||||
except Exception:
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
message = str(e)
|
||||
return status, message
|
||||
|
||||
|
||||
+18
-14
@@ -114,12 +114,11 @@ def validate_pr_title(pr_title: str, task_id: str) -> None:
|
||||
def get_vikunja_task_title(task_id: str) -> str:
|
||||
"""Fetch the Vikunja task title for the given DEVX-N identifier.
|
||||
|
||||
Returns empty string if VIKUNJA_TOKEN is not set (local dev without token).
|
||||
Raises ClickException if the token is set but the task is not found.
|
||||
Raises ClickException if VIKUNJA_TOKEN is not set or the task is not found.
|
||||
"""
|
||||
token = os.environ.get("VIKUNJA_TOKEN", "")
|
||||
if not token:
|
||||
return ""
|
||||
raise click.ClickException(_("VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles."))
|
||||
client = VikunjaClient(VIKUNJA_API_URL, token)
|
||||
page = 1
|
||||
while True:
|
||||
@@ -145,14 +144,10 @@ def get_vikunja_task_title(task_id: str) -> str:
|
||||
def validate_pr_title_matches_vikunja(pr_title: str, task_id: str) -> None:
|
||||
"""Validate that PR title matches the Vikunja task title.
|
||||
|
||||
Skips validation if VIKUNJA_TOKEN is not set (local dev).
|
||||
Raises ClickException if the task is not found or the title doesn't match.
|
||||
Raises ClickException if VIKUNJA_TOKEN is not set, the task is not found,
|
||||
or the title doesn't match.
|
||||
"""
|
||||
vikunja_title = get_vikunja_task_title(task_id)
|
||||
if not vikunja_title:
|
||||
# VIKUNJA_TOKEN not set — skip validation (local dev)
|
||||
click.echo(_("Warning: VIKUNJA_TOKEN not set, skipping title match validation."))
|
||||
return
|
||||
expected = f"{task_id}: {vikunja_title}"
|
||||
if pr_title != expected:
|
||||
raise click.ClickException(
|
||||
@@ -193,7 +188,16 @@ def main(branch: str, pr_title: str, repo: str, pr_number: str) -> None:
|
||||
if not token:
|
||||
raise click.ClickException(_("ERROR: REPO_TOKEN is not set."))
|
||||
|
||||
owner, repo_name = repo.split("/")
|
||||
# Validate PR number is an integer
|
||||
try:
|
||||
pr_num = int(pr_number)
|
||||
except ValueError:
|
||||
raise click.ClickException(_("PR number must be an integer, got: {pr_number}", pr_number=pr_number)) from None
|
||||
|
||||
# Validate repo format
|
||||
if "/" not in repo:
|
||||
raise click.ClickException(_("Repo must be in 'owner/name' format, got: {repo}", repo=repo))
|
||||
owner, repo_name = repo.split("/", 1)
|
||||
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
|
||||
|
||||
task_id = read_taskid(branch)
|
||||
@@ -210,14 +214,14 @@ def main(branch: str, pr_title: str, repo: str, pr_number: str) -> None:
|
||||
validate_pr_title_matches_vikunja(pr_title, task_id)
|
||||
|
||||
# Build merge title: DEVX-N: <conventional commit message>
|
||||
commits = client.get_pr_commits(pr_number)
|
||||
commits = client.get_pr_commits(pr_num)
|
||||
conv_msg = extract_conventional_msg(commits)
|
||||
if not conv_msg:
|
||||
raise click.ClickException(_("Could not extract conventional commit message from PR commits."))
|
||||
merge_title = f"{task_id}: {conv_msg}"
|
||||
|
||||
try:
|
||||
client.merge_pr(pr_number, merge_title)
|
||||
client.merge_pr(pr_num, merge_title)
|
||||
except APIError as e:
|
||||
if e.status == 405 and "behind" in e.message.lower():
|
||||
# Head branch is behind master — pull master and rebase, then retry
|
||||
@@ -229,7 +233,7 @@ def main(branch: str, pr_title: str, repo: str, pr_number: str) -> None:
|
||||
run_cmd(["git", "rebase", "origin/master"])
|
||||
run_cmd(["git", "push", "--force-with-lease", "origin", f"HEAD:{branch}"])
|
||||
click.echo(_("Rebased and pushed. Retrying merge..."))
|
||||
client.merge_pr(pr_number, merge_title)
|
||||
client.merge_pr(pr_num, merge_title)
|
||||
except (APIError, Exception) as retry_err:
|
||||
raise click.ClickException(
|
||||
_(
|
||||
@@ -250,7 +254,7 @@ def main(branch: str, pr_title: str, repo: str, pr_number: str) -> None:
|
||||
click.echo(
|
||||
_(
|
||||
"Nice! PR #{pr_number} squash-merged with title: {merge_title}",
|
||||
pr_number=pr_number,
|
||||
pr_number=pr_num,
|
||||
merge_title=merge_title,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -12,13 +12,13 @@ Checks performed (all fail with exit code 1 on error):
|
||||
translations file.
|
||||
- **Dead keys**: a key in a translations file is not used in any code.
|
||||
- **Missing languages**: a key exists but is missing one of the 5 supported
|
||||
languages (en, bg, de, ru, zh). Reported as a warning, not an error.
|
||||
languages (en, bg, de, ru, zh). This is an error — all supported languages
|
||||
must have translations for every key.
|
||||
|
||||
Usage::
|
||||
|
||||
python3 -m devx.ci.check_translations
|
||||
python3 -m devx.ci.check_translations --translations path/to/translations.json
|
||||
python3 -m devx.ci.check_translations --strict # warnings are errors
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -130,14 +130,15 @@ def check_translation_set(name: str, src_dir: Path, trans_file: Path) -> Transla
|
||||
# Check for dead keys (in translations but not used in code)
|
||||
result.dead_keys = result.defined_keys - result.used_keys
|
||||
for key in sorted(result.dead_keys):
|
||||
result.warnings.append(f"Dead key in {name}: {key!r}")
|
||||
result.errors.append(f"Dead key in {name}: {key!r}")
|
||||
|
||||
# Check for missing languages
|
||||
# Check for missing languages — this is an error, not a warning.
|
||||
# All supported languages must have translations for every key.
|
||||
for key, langs in translations.items():
|
||||
missing = [lang for lang in SUPPORTED_LANGS if lang not in langs]
|
||||
if missing:
|
||||
result.missing_langs[key] = missing
|
||||
result.warnings.append(f"Missing languages {missing} for key {key!r} in {name}")
|
||||
result.errors.append(f"Missing languages {missing} for key {key!r} in {name}")
|
||||
|
||||
return result
|
||||
|
||||
@@ -170,8 +171,7 @@ def print_result(result: TranslationCheckResult) -> None:
|
||||
type=click.Path(exists=False, path_type=Path),
|
||||
help="Path to a translations JSON file to check (can be repeated). Defaults to src/devx/translations.json.",
|
||||
)
|
||||
@click.option("--strict", is_flag=True, default=False, help="Treat warnings as errors.")
|
||||
def main(translations: tuple[Path, ...], strict: bool) -> None:
|
||||
def main(translations: tuple[Path, ...]) -> None:
|
||||
"""Check translation files for gaps, dead keys, and missing languages."""
|
||||
if not translations:
|
||||
# Default: check the devx package's own translations
|
||||
@@ -187,25 +187,16 @@ def main(translations: tuple[Path, ...], strict: bool) -> None:
|
||||
results.append(check_translation_set(name, src_dir, trans_file))
|
||||
|
||||
has_errors = False
|
||||
has_warnings = False
|
||||
for result in results:
|
||||
print_result(result)
|
||||
if result.errors:
|
||||
has_errors = True
|
||||
if result.warnings:
|
||||
has_warnings = True
|
||||
|
||||
click.echo()
|
||||
if has_errors:
|
||||
click.echo("FAIL: Translation check found errors.", err=True)
|
||||
sys.exit(1)
|
||||
if strict and has_warnings:
|
||||
click.echo("FAIL: Translation check found warnings (--strict mode).", err=True)
|
||||
sys.exit(1)
|
||||
if has_warnings:
|
||||
click.echo("PASS with warnings: Translation check passed (warnings present).")
|
||||
else:
|
||||
click.echo("PASS: All translations are complete and up to date.")
|
||||
click.echo("PASS: All translations are complete and up to date.")
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
|
||||
@@ -271,19 +271,28 @@ DEFAULT_INFRASTRUCTURE: list[str] = [
|
||||
# Build tooling
|
||||
"Makefile",
|
||||
"cliff.toml",
|
||||
"uv.lock",
|
||||
# Linting / formatting config
|
||||
".pre-commit-config.yaml",
|
||||
".ruff.toml",
|
||||
".ansible-lint",
|
||||
".checkmake.ini",
|
||||
".editorconfig",
|
||||
# Environment templates (not the actual .env which is gitignored)
|
||||
".env.example",
|
||||
# Git config
|
||||
".gitignore",
|
||||
".gitattributes",
|
||||
# Project-level documentation (not part of the installed package)
|
||||
"AGENTS.md",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"TROUBLESHOOTING.md",
|
||||
"CONTRIBUTING.md",
|
||||
"CODE_OF_CONDUCT.md",
|
||||
"REVIEW_CHECKLIST.md",
|
||||
# Agent/CI tooling config (not part of the installed package)
|
||||
".devin/**",
|
||||
# Generated venv activation scripts (created by `make setup`)
|
||||
"activate.sh",
|
||||
"activate.fish",
|
||||
|
||||
@@ -39,7 +39,7 @@ def query_runners(api_url: str, token: str, owner: str, repo: str) -> int:
|
||||
|
||||
Returns the total count of active runners. If the API call fails
|
||||
(e.g., no admin access for instance-level runners), falls back to
|
||||
what we can see.
|
||||
what we can see. Fallbacks are logged to stderr for debugging.
|
||||
"""
|
||||
headers = {"Authorization": f"token {token}"}
|
||||
total = 0
|
||||
@@ -54,8 +54,10 @@ def query_runners(api_url: str, token: str, owner: str, repo: str) -> int:
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
total += data.get("total_count", 0)
|
||||
except (requests.RequestException, ValueError):
|
||||
pass
|
||||
else:
|
||||
click.echo(f"Warning: repo-level runners query returned HTTP {r.status_code}", err=True)
|
||||
except (requests.RequestException, ValueError) as e:
|
||||
click.echo(f"Warning: repo-level runners query failed: {e}", err=True)
|
||||
|
||||
# 2. Organization-level runners
|
||||
try:
|
||||
@@ -67,8 +69,10 @@ def query_runners(api_url: str, token: str, owner: str, repo: str) -> int:
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
total += data.get("total_count", 0)
|
||||
except (requests.RequestException, ValueError):
|
||||
pass
|
||||
else:
|
||||
click.echo(f"Warning: org-level runners query returned HTTP {r.status_code}", err=True)
|
||||
except (requests.RequestException, ValueError) as e:
|
||||
click.echo(f"Warning: org-level runners query failed: {e}", err=True)
|
||||
|
||||
# 3. Instance-level runners (requires admin scope)
|
||||
try:
|
||||
@@ -80,8 +84,10 @@ def query_runners(api_url: str, token: str, owner: str, repo: str) -> int:
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
total += data.get("total_count", 0)
|
||||
except (requests.RequestException, ValueError):
|
||||
pass
|
||||
elif r.status_code != 403: # 403 is expected without admin scope
|
||||
click.echo(f"Warning: instance-level runners query returned HTTP {r.status_code}", err=True)
|
||||
except (requests.RequestException, ValueError) as e:
|
||||
click.echo(f"Warning: instance-level runners query failed: {e}", err=True)
|
||||
|
||||
return total
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Usage:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
import click
|
||||
@@ -27,25 +27,33 @@ from devx.i18n import _
|
||||
|
||||
load_dotenv()
|
||||
|
||||
logger = logging.getLogger("devx")
|
||||
|
||||
|
||||
def _create_issue_via_tea(repo: str, title: str, body: str) -> int:
|
||||
"""Create issue via tea CLI. Returns issue index.
|
||||
|
||||
Raises TeaCLIError if tea is not installed or the command fails.
|
||||
Label operations are best-effort — failures are logged but don't
|
||||
prevent issue creation.
|
||||
"""
|
||||
tea = TeaCLI(repo=repo)
|
||||
|
||||
# Check if "bug" label exists
|
||||
# Check if "bug" label exists (best-effort)
|
||||
labels: list[str] = []
|
||||
with contextlib.suppress(TeaCLIError):
|
||||
try:
|
||||
existing_labels = tea.list_labels(repo)
|
||||
if any(label.get("name") == "bug" for label in existing_labels):
|
||||
labels = ["bug"]
|
||||
except TeaCLIError as e:
|
||||
logger.warning("Could not fetch labels (best-effort): %s", e)
|
||||
|
||||
issue = tea.create_issue(repo, title=title, body=body, labels=labels if labels else None)
|
||||
if labels:
|
||||
with contextlib.suppress(TeaCLIError):
|
||||
try:
|
||||
tea.add_label(repo, issue["index"], labels)
|
||||
except TeaCLIError as e:
|
||||
logger.warning("Could not add label to issue #%s (best-effort): %s", issue.get("index"), e)
|
||||
return int(issue.get("index", 0))
|
||||
|
||||
|
||||
|
||||
+14
-13
@@ -13,7 +13,7 @@ import click
|
||||
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
|
||||
|
||||
from devx.api_clients import VikunjaClient
|
||||
from devx.config import DEFAULT_PER_PAGE, TASK_ID_RE, VIKUNJA_API_URL, VIKUNJA_PROJECT_ID
|
||||
from devx.config import DEFAULT_PER_PAGE, TASK_ID_RE, TASK_PREFIX, VIKUNJA_API_URL, VIKUNJA_PROJECT_ID
|
||||
from devx.exceptions import APIError
|
||||
from devx.i18n import _
|
||||
|
||||
@@ -151,14 +151,16 @@ def main(commit_msg: str | None, commit_sha: str, from_git: bool, git_sha: str)
|
||||
)
|
||||
)
|
||||
return
|
||||
# Non-infrastructure commits without DEVX-N prefix — warn but don't fail
|
||||
click.echo(
|
||||
# Non-infrastructure commits without DEVX-N prefix — this is a
|
||||
# convention violation. Fail the post-merge job so the issue is visible.
|
||||
raise click.ClickException(
|
||||
_(
|
||||
"Warning: No task ID (DEVX-N) found in commit message: {msg}. Skipping Vikunja update.",
|
||||
"No task ID ({prefix}-N) found in commit message: {msg}. "
|
||||
"Every non-infrastructure commit must have a task ID.",
|
||||
prefix=TASK_PREFIX,
|
||||
msg=first_line,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
client = VikunjaClient(VIKUNJA_API_URL, token)
|
||||
vikunja_task_id = resolve_task_id(client, task_id)
|
||||
@@ -170,19 +172,18 @@ def main(commit_msg: str | None, commit_sha: str, from_git: bool, git_sha: str)
|
||||
client.post_comment(vikunja_task_id, html)
|
||||
client.update_task(vikunja_task_id, done=True)
|
||||
except APIError as e:
|
||||
# Vikunja is a project management tool — if it's down, the merge
|
||||
# still succeeded. Warn but don't fail the post-merge workflow.
|
||||
click.echo(
|
||||
# Vikunja API failures must be visible — the task was not updated
|
||||
# and needs manual intervention. Failing the CI job makes this visible.
|
||||
raise click.ClickException(
|
||||
_(
|
||||
"Warning: Vikunja API error (HTTP {status}): {message}. "
|
||||
"Task {task_id} was NOT updated. The merge succeeded — "
|
||||
"please update the Vikunja task manually.",
|
||||
"Vikunja API error (HTTP {status}): {message}. "
|
||||
"Task {task_id} was NOT updated. "
|
||||
"The merge succeeded but the Vikunja task needs manual update.",
|
||||
status=e.status,
|
||||
message=e.message,
|
||||
task_id=task_id,
|
||||
)
|
||||
)
|
||||
return
|
||||
) from e
|
||||
|
||||
click.echo(
|
||||
_(
|
||||
|
||||
+35
-6
@@ -86,7 +86,13 @@ def get_bumped_version() -> str:
|
||||
if not version:
|
||||
raise click.ClickException(_("git-cliff returned empty version."))
|
||||
# git-cliff may return with or without 'v' prefix
|
||||
return version.lstrip("v")
|
||||
version = version.lstrip("v")
|
||||
# Validate semver format
|
||||
if not re.match(r"^\d+\.\d+\.\d+$", version):
|
||||
raise click.ClickException(
|
||||
_("git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).", version=version)
|
||||
)
|
||||
return version
|
||||
|
||||
|
||||
def get_changelog(new_version: str) -> str:
|
||||
@@ -271,16 +277,34 @@ def main(dry_run: bool, skip_tests: bool) -> None:
|
||||
)
|
||||
)
|
||||
|
||||
# Release lock: if HEAD is already a release commit, another release
|
||||
# run is in progress (or already completed). Skip to prevent duplicate tags.
|
||||
# Release lock: if HEAD is already a release commit, check if the tag
|
||||
# exists. If the tag is missing (e.g., tag push failed in a previous run),
|
||||
# create and push it instead of skipping — this recovers from the
|
||||
# common failure mode where the commit was pushed but the tag was not.
|
||||
head_msg = run_cmd(["git", "log", "-1", "--pretty=%s"]).stdout.strip()
|
||||
if re.match(r"^release: v\d+\.\d+\.\d+", head_msg):
|
||||
release_match = re.match(r"^release: v(\d+\.\d+\.\d+)", head_msg)
|
||||
if release_match:
|
||||
release_version = release_match.group(1)
|
||||
release_tag = f"v{release_version}"
|
||||
if tag_exists(release_tag):
|
||||
click.echo(
|
||||
_(
|
||||
"HEAD is already a release commit ('{msg}') and tag {tag} exists. Skipping.",
|
||||
msg=head_msg,
|
||||
tag=release_tag,
|
||||
)
|
||||
)
|
||||
return
|
||||
# Tag is missing — recover by creating and pushing it
|
||||
click.echo(
|
||||
_(
|
||||
"HEAD is already a release commit ('{msg}'). Another release may have just completed. Skipping.",
|
||||
"HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
|
||||
msg=head_msg,
|
||||
tag=release_tag,
|
||||
)
|
||||
)
|
||||
changelog = get_changelog(release_version)
|
||||
create_and_push_tag(release_version, changelog, dry_run)
|
||||
return
|
||||
|
||||
# Check if any user-facing files changed since the last tag.
|
||||
@@ -315,7 +339,12 @@ def main(dry_run: bool, skip_tests: bool) -> None:
|
||||
# Generate changelog
|
||||
changelog = get_changelog(new_version)
|
||||
if not changelog:
|
||||
click.echo(_("Warning: git-cliff generated empty changelog."))
|
||||
raise click.ClickException(
|
||||
_(
|
||||
"git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
|
||||
version=new_version,
|
||||
)
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
click.echo(_("\n[dry-run] Changelog:\n{changelog}", changelog=changelog))
|
||||
|
||||
@@ -39,9 +39,20 @@ MAPPING_FILE = DOCS_DIR / "mapping.json"
|
||||
|
||||
|
||||
def load_mapping() -> dict[str, str]:
|
||||
"""Load the file-to-wiki-page mapping from mapping.json."""
|
||||
"""Load the file-to-wiki-page mapping from mapping.json.
|
||||
|
||||
Validates that the mapping is a dict of string-to-string pairs.
|
||||
"""
|
||||
with open(MAPPING_FILE) as f:
|
||||
return json.load(f)
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
raise click.ClickException(
|
||||
_("mapping.json must be a dict of file-path -> page-title, got {type}", type=type(data).__name__)
|
||||
)
|
||||
for k, v in data.items():
|
||||
if not isinstance(k, str) or not isinstance(v, str):
|
||||
raise click.ClickException(_("mapping.json keys and values must be strings, got {k}={v}", k=k, v=v))
|
||||
return data
|
||||
|
||||
|
||||
def read_doc_content(file_path: str) -> str:
|
||||
@@ -239,14 +250,14 @@ def main(dry_run: bool, repo: str | None, verify: bool, strict: bool) -> None:
|
||||
try:
|
||||
content = read_doc_content(file_path)
|
||||
except FileNotFoundError:
|
||||
click.echo(_("WARNING: File {file} not found — skipping.", file=file_path))
|
||||
skipped += 1
|
||||
continue
|
||||
raise click.ClickException(
|
||||
_("Mapped file {file} not found. Update mapping.json or create the file.", file=file_path)
|
||||
) from None
|
||||
|
||||
if not content.strip():
|
||||
click.echo(_("WARNING: File {file} is empty — skipping.", file=file_path))
|
||||
skipped += 1
|
||||
continue
|
||||
raise click.ClickException(
|
||||
_("Mapped file {file} is empty. Update the content or remove from mapping.json.", file=file_path)
|
||||
) from None
|
||||
|
||||
result = sync_page(client, page_title, content, existing_pages, dry_run)
|
||||
if result == "created":
|
||||
|
||||
@@ -174,6 +174,10 @@ def cli(
|
||||
_write_github_env("SKIP", "true")
|
||||
return
|
||||
|
||||
# Validate runner index is in range
|
||||
if runner_index < 1:
|
||||
raise click.ClickException(f"Runner index {runner_index} is out of range (must be >= 1)")
|
||||
|
||||
# Convert 1-based CLI index to 0-based internal index
|
||||
zero_based = runner_index - 1
|
||||
assigned = pairs_for_runner(pairs, zero_based, max_runners)
|
||||
|
||||
@@ -159,8 +159,11 @@ def cli(pairs: tuple[str, ...]) -> None:
|
||||
if failed_event.is_set():
|
||||
sys.exit(1)
|
||||
|
||||
scenario = pair.split("|")[0]
|
||||
platform_name = pair.split("|")[1]
|
||||
parts = pair.split("|")
|
||||
if len(parts) < 2:
|
||||
raise click.ClickException(f"Invalid pair format: {pair!r} (expected at least 2 pipe-delimited parts)")
|
||||
scenario = parts[0]
|
||||
platform_name = parts[1]
|
||||
click.echo(_("Running: {scenario} on {platform}", scenario=scenario, platform=platform_name))
|
||||
|
||||
cmd = build_molecule_cmd(scenario)
|
||||
|
||||
+566
-176
@@ -1,63 +1,129 @@
|
||||
{
|
||||
"\nAll documentation coverage checks passed!": {
|
||||
"en": "\nAll documentation coverage checks passed!"
|
||||
},
|
||||
"\nAnsible files changed ({count}):": {
|
||||
"en": "\nAnsible files changed ({count}):"
|
||||
"en": "\nAll documentation coverage checks passed!",
|
||||
"bg": "\nAll documentation coverage checks passed!",
|
||||
"de": "\nAll documentation coverage checks passed!",
|
||||
"ru": "\nAll documentation coverage checks passed!",
|
||||
"zh": "\nAll documentation coverage checks passed!"
|
||||
},
|
||||
"\nChecking CI script documentation in ci-cd-workflow.md...": {
|
||||
"en": "\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...",
|
||||
"de": "\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..."
|
||||
},
|
||||
"\nChecking module documentation in architecture.md...": {
|
||||
"en": "\nChecking module documentation in architecture.md..."
|
||||
"en": "\nChecking module documentation in architecture.md...",
|
||||
"bg": "\nChecking module documentation in architecture.md...",
|
||||
"de": "\nChecking module documentation in architecture.md...",
|
||||
"ru": "\nChecking module documentation in architecture.md...",
|
||||
"zh": "\nChecking module documentation in architecture.md..."
|
||||
},
|
||||
"\nDoc coverage: {covered}/{total} ({pct}%)": {
|
||||
"en": "\nDoc coverage: {covered}/{total} ({pct}%)"
|
||||
"en": "\nDoc coverage: {covered}/{total} ({pct}%)",
|
||||
"bg": "\nDoc coverage: {covered}/{total} ({pct}%)",
|
||||
"de": "\nDoc coverage: {covered}/{total} ({pct}%)",
|
||||
"ru": "\nDoc coverage: {covered}/{total} ({pct}%)",
|
||||
"zh": "\nDoc coverage: {covered}/{total} ({pct}%)"
|
||||
},
|
||||
"\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}": {
|
||||
"en": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}"
|
||||
"en": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
|
||||
"bg": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
|
||||
"de": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
|
||||
"ru": "\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.": {
|
||||
"en": "\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.",
|
||||
"de": "\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."
|
||||
},
|
||||
"\nIntegrity check FAILED ({count} issues):": {
|
||||
"en": "\nIntegrity check FAILED ({count} issues):"
|
||||
"en": "\nIntegrity check FAILED ({count} issues):",
|
||||
"bg": "\nIntegrity check FAILED ({count} issues):",
|
||||
"de": "\nIntegrity check FAILED ({count} issues):",
|
||||
"ru": "\nIntegrity check FAILED ({count} issues):",
|
||||
"zh": "\nIntegrity check FAILED ({count} issues):"
|
||||
},
|
||||
"\nIntegrity check passed — all {count} pages verified.": {
|
||||
"en": "\nIntegrity check passed — all {count} pages verified."
|
||||
"en": "\nIntegrity check passed — all {count} pages verified.",
|
||||
"bg": "\nIntegrity check passed — all {count} pages verified.",
|
||||
"de": "\nIntegrity check passed — all {count} pages verified.",
|
||||
"ru": "\nIntegrity check passed — all {count} pages verified.",
|
||||
"zh": "\nIntegrity check passed — all {count} pages verified."
|
||||
},
|
||||
"\nMissing documentation:": {
|
||||
"en": "\nMissing documentation:"
|
||||
"en": "\nMissing documentation:",
|
||||
"bg": "\nMissing documentation:",
|
||||
"de": "\nMissing documentation:",
|
||||
"ru": "\nMissing documentation:",
|
||||
"zh": "\nMissing documentation:"
|
||||
},
|
||||
"\nResult: {status}": {
|
||||
"en": "\nResult: {status}"
|
||||
"en": "\nResult: {status}",
|
||||
"bg": "\nResult: {status}",
|
||||
"de": "\nResult: {status}",
|
||||
"ru": "\nResult: {status}",
|
||||
"zh": "\nResult: {status}"
|
||||
},
|
||||
"\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)."
|
||||
"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).",
|
||||
"de": "\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)."
|
||||
},
|
||||
"\nRunning full wiki integrity check...": {
|
||||
"en": "\nRunning full wiki integrity check..."
|
||||
"en": "\nRunning full wiki integrity check...",
|
||||
"bg": "\nRunning full wiki integrity check...",
|
||||
"de": "\nRunning full wiki integrity check...",
|
||||
"ru": "\nRunning full wiki integrity check...",
|
||||
"zh": "\nRunning full wiki integrity check..."
|
||||
},
|
||||
"\nUser-facing changes ({count}):": {
|
||||
"en": "\nUser-facing changes ({count}):"
|
||||
},
|
||||
"\nUser-facing files changed ({count}):": {
|
||||
"en": "\nUser-facing files changed ({count}):"
|
||||
"en": "\nUser-facing changes ({count}):",
|
||||
"bg": "\nUser-facing changes ({count}):",
|
||||
"de": "\nUser-facing changes ({count}):",
|
||||
"ru": "\nUser-facing changes ({count}):",
|
||||
"zh": "\nUser-facing changes ({count}):"
|
||||
},
|
||||
"\nVerification FAILED: {failures} page(s) have empty or mismatched content!": {
|
||||
"en": "\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!",
|
||||
"de": "\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!"
|
||||
},
|
||||
"\nVerification passed — all wiki pages have correct content.": {
|
||||
"en": "\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.",
|
||||
"de": "\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."
|
||||
},
|
||||
"\nVerifying wiki pages have content...": {
|
||||
"en": "\nVerifying wiki pages have content..."
|
||||
"en": "\nVerifying wiki pages have content...",
|
||||
"bg": "\nVerifying wiki pages have content...",
|
||||
"de": "\nVerifying wiki pages have content...",
|
||||
"ru": "\nVerifying wiki pages have content...",
|
||||
"zh": "\nVerifying wiki pages have content..."
|
||||
},
|
||||
"\nWorkflow-only changes ({count}):": {
|
||||
"en": "\nWorkflow-only changes ({count}):"
|
||||
"en": "\nWorkflow-only changes ({count}):",
|
||||
"bg": "\nWorkflow-only changes ({count}):",
|
||||
"de": "\nWorkflow-only changes ({count}):",
|
||||
"ru": "\nWorkflow-only changes ({count}):",
|
||||
"zh": "\nWorkflow-only changes ({count}):"
|
||||
},
|
||||
"\n[dry-run] Changelog:\n{changelog}": {
|
||||
"en": "\n[dry-run] Changelog:\n{changelog}"
|
||||
"en": "\n[dry-run] Changelog:\n{changelog}",
|
||||
"bg": "\n[dry-run] Changelog:\n{changelog}",
|
||||
"de": "\n[dry-run] Changelog:\n{changelog}",
|
||||
"ru": "\n[dry-run] Changelog:\n{changelog}",
|
||||
"zh": "\n[dry-run] Changelog:\n{changelog}"
|
||||
},
|
||||
" - Auto-delete branch after merge: yes": {
|
||||
"en": " - Auto-delete branch after merge: yes",
|
||||
@@ -81,7 +147,11 @@
|
||||
"zh": " - 阻止被拒绝的审查: 是"
|
||||
},
|
||||
" - Direct pushes: BLOCKED (require PR, whitelisted users can push)": {
|
||||
"en": " - 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)",
|
||||
"de": " - 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)"
|
||||
},
|
||||
" - Dismiss stale approvals: yes": {
|
||||
"en": " - Dismiss stale approvals: yes",
|
||||
@@ -105,10 +175,18 @@
|
||||
"zh": " - 必需状态检查: {checks}"
|
||||
},
|
||||
" Created: {title}": {
|
||||
"en": " Created: {title}"
|
||||
"en": " Created: {title}",
|
||||
"bg": " Created: {title}",
|
||||
"de": " Created: {title}",
|
||||
"ru": " Created: {title}",
|
||||
"zh": " Created: {title}"
|
||||
},
|
||||
" FAIL: {title} — content mismatch or empty!": {
|
||||
"en": " FAIL: {title} — content mismatch or empty!"
|
||||
"en": " FAIL: {title} — content mismatch or empty!",
|
||||
"bg": " FAIL: {title} — content mismatch or empty!",
|
||||
"de": " FAIL: {title} — content mismatch or empty!",
|
||||
"ru": " FAIL: {title} — content mismatch or empty!",
|
||||
"zh": " FAIL: {title} — content mismatch or empty!"
|
||||
},
|
||||
" MISSING: devx {cmd}": {
|
||||
"en": " MISSING: devx {cmd}",
|
||||
@@ -117,14 +195,19 @@
|
||||
"ru": " ОТСУТСТВУЕТ: devx {cmd}",
|
||||
"zh": " 缺失: devx {cmd}"
|
||||
},
|
||||
" MISSING: grm {cmd}": {
|
||||
"en": " MISSING: grm {cmd}"
|
||||
},
|
||||
" MISSING: {module}": {
|
||||
"en": " MISSING: {module}"
|
||||
"en": " MISSING: {module}",
|
||||
"bg": " MISSING: {module}",
|
||||
"de": " MISSING: {module}",
|
||||
"ru": " MISSING: {module}",
|
||||
"zh": " MISSING: {module}"
|
||||
},
|
||||
" MISSING: {script}": {
|
||||
"en": " MISSING: {script}"
|
||||
"en": " MISSING: {script}",
|
||||
"bg": " MISSING: {script}",
|
||||
"de": " MISSING: {script}",
|
||||
"ru": " MISSING: {script}",
|
||||
"zh": " MISSING: {script}"
|
||||
},
|
||||
" OK: devx {cmd}": {
|
||||
"en": " OK: devx {cmd}",
|
||||
@@ -133,41 +216,82 @@
|
||||
"ru": " ОК: devx {cmd}",
|
||||
"zh": " 正常: devx {cmd}"
|
||||
},
|
||||
" OK: grm {cmd}": {
|
||||
"en": " OK: grm {cmd}"
|
||||
},
|
||||
" OK: {module}": {
|
||||
"en": " OK: {module}"
|
||||
"en": " OK: {module}",
|
||||
"bg": " OK: {module}",
|
||||
"de": " OK: {module}",
|
||||
"ru": " OK: {module}",
|
||||
"zh": " OK: {module}"
|
||||
},
|
||||
" OK: {script}": {
|
||||
"en": " OK: {script}"
|
||||
"en": " OK: {script}",
|
||||
"bg": " OK: {script}",
|
||||
"de": " OK: {script}",
|
||||
"ru": " OK: {script}",
|
||||
"zh": " OK: {script}"
|
||||
},
|
||||
" OK: {title} ({chars} chars)": {
|
||||
"en": " OK: {title} ({chars} chars)"
|
||||
"en": " OK: {title} ({chars} chars)",
|
||||
"bg": " OK: {title} ({chars} chars)",
|
||||
"de": " OK: {title} ({chars} chars)",
|
||||
"ru": " OK: {title} ({chars} chars)",
|
||||
"zh": " OK: {title} ({chars} chars)"
|
||||
},
|
||||
" Updated: {title}": {
|
||||
"en": " Updated: {title}"
|
||||
"en": " Updated: {title}",
|
||||
"bg": " Updated: {title}",
|
||||
"de": " Updated: {title}",
|
||||
"ru": " Updated: {title}",
|
||||
"zh": " Updated: {title}"
|
||||
},
|
||||
"API poll warning: {exc}": {
|
||||
"en": "API poll warning: {exc}"
|
||||
"en": "API poll warning: {exc}",
|
||||
"bg": "API poll warning: {exc}",
|
||||
"de": "API poll warning: {exc}",
|
||||
"ru": "API poll warning: {exc}",
|
||||
"zh": "API poll warning: {exc}"
|
||||
},
|
||||
"All molecule tests passed.": {
|
||||
"en": "All molecule tests passed."
|
||||
"en": "All molecule tests passed.",
|
||||
"bg": "All molecule tests passed.",
|
||||
"de": "All molecule tests passed.",
|
||||
"ru": "All molecule tests passed.",
|
||||
"zh": "All molecule tests passed."
|
||||
},
|
||||
"Another molecule runner failed. Stopping this runner early.": {
|
||||
"en": "Another molecule runner failed. Stopping this runner early."
|
||||
"en": "Another molecule runner failed. Stopping this runner early.",
|
||||
"bg": "Another molecule runner failed. Stopping this runner early.",
|
||||
"de": "Another molecule runner failed. Stopping this runner early.",
|
||||
"ru": "Another molecule runner failed. Stopping this runner early.",
|
||||
"zh": "Another molecule runner failed. Stopping this runner early."
|
||||
},
|
||||
"Bumping version: {current} -> v{new_version}": {
|
||||
"en": "Bumping version: {current} -> v{new_version}"
|
||||
"en": "Bumping version: {current} -> v{new_version}",
|
||||
"bg": "Bumping version: {current} -> v{new_version}",
|
||||
"de": "Bumping version: {current} -> v{new_version}",
|
||||
"ru": "Bumping version: {current} -> v{new_version}",
|
||||
"zh": "Bumping version: {current} -> v{new_version}"
|
||||
},
|
||||
"Checking CLI command documentation...": {
|
||||
"en": "Checking CLI command documentation..."
|
||||
"en": "Checking CLI command documentation...",
|
||||
"bg": "Checking CLI command documentation...",
|
||||
"de": "Checking CLI command documentation...",
|
||||
"ru": "Checking CLI command documentation...",
|
||||
"zh": "Checking CLI command documentation..."
|
||||
},
|
||||
"Command failed ({cmd}): {stderr}": {
|
||||
"en": "Command failed ({cmd}): {stderr}"
|
||||
"en": "Command failed ({cmd}): {stderr}",
|
||||
"bg": "Command failed ({cmd}): {stderr}",
|
||||
"de": "Command failed ({cmd}): {stderr}",
|
||||
"ru": "Command failed ({cmd}): {stderr}",
|
||||
"zh": "Command failed ({cmd}): {stderr}"
|
||||
},
|
||||
"Comparing {base}..{head} ({count} files changed)": {
|
||||
"en": "Comparing {base}..{head} ({count} files changed)"
|
||||
"en": "Comparing {base}..{head} ({count} files changed)",
|
||||
"bg": "Comparing {base}..{head} ({count} files changed)",
|
||||
"de": "Comparing {base}..{head} ({count} files changed)",
|
||||
"ru": "Comparing {base}..{head} ({count} files changed)",
|
||||
"zh": "Comparing {base}..{head} ({count} files changed)"
|
||||
},
|
||||
"Configuring branch protection for {branch}...": {
|
||||
"en": "Configuring branch protection for {branch}...",
|
||||
@@ -184,25 +308,53 @@
|
||||
"zh": "正在配置仓库设置..."
|
||||
},
|
||||
"Could not extract conventional commit message from PR commits.": {
|
||||
"en": "Could not extract conventional commit message from PR commits."
|
||||
"en": "Could not extract conventional commit message from PR commits.",
|
||||
"bg": "Could not extract conventional commit message from PR commits.",
|
||||
"de": "Could not extract conventional commit message from PR commits.",
|
||||
"ru": "Could not extract conventional commit message from PR commits.",
|
||||
"zh": "Could not extract conventional commit message from PR commits."
|
||||
},
|
||||
"Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.": {
|
||||
"en": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task."
|
||||
"en": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
|
||||
"bg": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
|
||||
"de": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
|
||||
"ru": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
|
||||
"zh": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task."
|
||||
},
|
||||
"Could not find __version__ in {file}": {
|
||||
"en": "Could not find __version__ in {file}"
|
||||
"en": "Could not find __version__ in {file}",
|
||||
"bg": "Could not find __version__ in {file}",
|
||||
"de": "Could not find __version__ in {file}",
|
||||
"ru": "Could not find __version__ in {file}",
|
||||
"zh": "Could not find __version__ in {file}"
|
||||
},
|
||||
"Could not parse test execution time from output.": {
|
||||
"en": "Could not parse test execution time from output."
|
||||
"en": "Could not parse test execution time from output.",
|
||||
"bg": "Could not parse test execution time from output.",
|
||||
"de": "Could not parse test execution time from output.",
|
||||
"ru": "Could not parse test execution time from output.",
|
||||
"zh": "Could not parse test execution time from output."
|
||||
},
|
||||
"Created issue #{issue_id}: {title}": {
|
||||
"en": "Created issue #{issue_id}: {title}"
|
||||
"en": "Created issue #{issue_id}: {title}",
|
||||
"bg": "Created issue #{issue_id}: {title}",
|
||||
"de": "Created issue #{issue_id}: {title}",
|
||||
"ru": "Created issue #{issue_id}: {title}",
|
||||
"zh": "Created issue #{issue_id}: {title}"
|
||||
},
|
||||
"Created release commit.": {
|
||||
"en": "Created release commit."
|
||||
"en": "Created release commit.",
|
||||
"bg": "Created release commit.",
|
||||
"de": "Created release commit.",
|
||||
"ru": "Created release commit.",
|
||||
"zh": "Created release commit."
|
||||
},
|
||||
"Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.": {
|
||||
"en": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently."
|
||||
"en": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
|
||||
"bg": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
|
||||
"de": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
|
||||
"ru": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
|
||||
"zh": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently."
|
||||
},
|
||||
"ERROR: REPO_TOKEN is not set.": {
|
||||
"en": "ERROR: REPO_TOKEN is not set.",
|
||||
@@ -226,22 +378,39 @@
|
||||
"zh": "错误:未指定仓库名称。请使用 --repo 或设置 DEVX_REPO_NAME。"
|
||||
},
|
||||
"ERROR: mapping.json not found at {path}": {
|
||||
"en": "ERROR: mapping.json not found at {path}"
|
||||
"en": "ERROR: mapping.json not found at {path}",
|
||||
"bg": "ERROR: mapping.json not found at {path}",
|
||||
"de": "ERROR: mapping.json not found at {path}",
|
||||
"ru": "ERROR: mapping.json not found at {path}",
|
||||
"zh": "ERROR: mapping.json not found at {path}"
|
||||
},
|
||||
"FAILED: {pair} exited with code {code}": {
|
||||
"en": "FAILED: {pair} exited with code {code}"
|
||||
"en": "FAILED: {pair} exited with code {code}",
|
||||
"bg": "FAILED: {pair} exited with code {code}",
|
||||
"de": "FAILED: {pair} exited with code {code}",
|
||||
"ru": "FAILED: {pair} exited with code {code}",
|
||||
"zh": "FAILED: {pair} exited with code {code}"
|
||||
},
|
||||
"Failed to create issue via tea: {error}": {
|
||||
"en": "Failed to create issue via tea: {error}"
|
||||
"en": "Failed to create issue via tea: {error}",
|
||||
"bg": "Failed to create issue via tea: {error}",
|
||||
"de": "Failed to create issue via tea: {error}",
|
||||
"ru": "Failed to create issue via tea: {error}",
|
||||
"zh": "Failed to create issue via tea: {error}"
|
||||
},
|
||||
"Found {count} existing wiki pages.": {
|
||||
"en": "Found {count} existing wiki pages."
|
||||
"en": "Found {count} existing wiki pages.",
|
||||
"bg": "Found {count} existing wiki pages.",
|
||||
"de": "Found {count} existing wiki pages.",
|
||||
"ru": "Found {count} existing wiki pages.",
|
||||
"zh": "Found {count} existing wiki pages."
|
||||
},
|
||||
"GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.": {
|
||||
"en": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation."
|
||||
},
|
||||
"HEAD is already a release commit ('{msg}'). Another release may have just completed. Skipping.": {
|
||||
"en": "HEAD is already a release commit ('{msg}'). Another release may have just completed. Skipping."
|
||||
"en": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
||||
"bg": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
||||
"de": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
||||
"ru": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
||||
"zh": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation."
|
||||
},
|
||||
"HTTP error: {status} — {message}": {
|
||||
"en": "HTTP error: {status} — {message}",
|
||||
@@ -258,7 +427,11 @@
|
||||
"zh": "HTTP {status} 禁止访问 — 您的令牌缺少管理员权限。\n请确保令牌属于仓库所有者或组织管理员。\n或者,您可以在 设置 → 分支 中手动配置分支保护。"
|
||||
},
|
||||
"Head branch is behind master. Pulling and rebasing...": {
|
||||
"en": "Head branch is behind master. Pulling and rebasing..."
|
||||
"en": "Head branch is behind master. Pulling and rebasing...",
|
||||
"bg": "Head branch is behind master. Pulling and rebasing...",
|
||||
"de": "Head branch is behind master. Pulling and rebasing...",
|
||||
"ru": "Head branch is behind master. Pulling and rebasing...",
|
||||
"zh": "Head branch is behind master. Pulling and rebasing..."
|
||||
},
|
||||
"Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}": {
|
||||
"en": "Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}",
|
||||
@@ -267,17 +440,26 @@
|
||||
"ru": "Инфраструктурный коммит (без ID задачи DEVX-N), пропуск обновления Vikunja: {msg}",
|
||||
"zh": "基础设施提交(无 DEVX-N 任务 ID),跳过 Vikunja 更新: {msg}"
|
||||
},
|
||||
"Infrastructure commit (no GRM-N task ID), skipping Vikunja update: {msg}": {
|
||||
"en": "Infrastructure commit (no GRM-N task ID), skipping Vikunja update: {msg}"
|
||||
},
|
||||
"Lint failed — refusing to release. Fix lint errors first.\n{stderr}": {
|
||||
"en": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}"
|
||||
"en": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
|
||||
"bg": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
|
||||
"de": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
|
||||
"ru": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
|
||||
"zh": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}"
|
||||
},
|
||||
"Lint passed.": {
|
||||
"en": "Lint passed."
|
||||
"en": "Lint passed.",
|
||||
"bg": "Lint passed.",
|
||||
"de": "Lint passed.",
|
||||
"ru": "Lint passed.",
|
||||
"zh": "Lint passed."
|
||||
},
|
||||
"Merge failed after rebase retry: {error}\nPlease rebase the PR manually.": {
|
||||
"en": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually."
|
||||
"en": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
|
||||
"bg": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
|
||||
"de": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
|
||||
"ru": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
|
||||
"zh": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually."
|
||||
},
|
||||
"Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.": {
|
||||
"en": "Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.",
|
||||
@@ -315,7 +497,11 @@
|
||||
"zh": "不错!PR #{pr_number} 已 squash 合并,标题: {merge_title}"
|
||||
},
|
||||
"Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.": {
|
||||
"en": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered."
|
||||
"en": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
|
||||
"bg": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
|
||||
"de": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
|
||||
"ru": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
|
||||
"zh": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered."
|
||||
},
|
||||
"Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.": {
|
||||
"en": "Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.",
|
||||
@@ -325,22 +511,46 @@
|
||||
"zh": "不错!Vikunja 任务 {task_id} (ID {vikunja_id}) 已更新并标记为完成。"
|
||||
},
|
||||
"No changes between {base} and {head}.": {
|
||||
"en": "No changes between {base} and {head}."
|
||||
"en": "No changes between {base} and {head}.",
|
||||
"bg": "No changes between {base} and {head}.",
|
||||
"de": "No changes between {base} and {head}.",
|
||||
"ru": "No changes between {base} and {head}.",
|
||||
"zh": "No changes between {base} and {head}."
|
||||
},
|
||||
"No staged changes — version and changelog already up to date.": {
|
||||
"en": "No staged changes — version and changelog already up to date."
|
||||
"en": "No staged changes — version and changelog already up to date.",
|
||||
"bg": "No staged changes — version and changelog already up to date.",
|
||||
"de": "No staged changes — version and changelog already up to date.",
|
||||
"ru": "No staged changes — version and changelog already up to date.",
|
||||
"zh": "No staged changes — version and changelog already up to date."
|
||||
},
|
||||
"No tags found — treating all changes as user-facing.": {
|
||||
"en": "No tags found — treating all changes as user-facing."
|
||||
"en": "No tags found — treating all changes as user-facing.",
|
||||
"bg": "No tags found — treating all changes as user-facing.",
|
||||
"de": "No tags found — treating all changes as user-facing.",
|
||||
"ru": "No tags found — treating all changes as user-facing.",
|
||||
"zh": "No tags found — treating all changes as user-facing."
|
||||
},
|
||||
"No unreleased changes found. Nothing to release.": {
|
||||
"en": "No unreleased changes found. Nothing to release."
|
||||
"en": "No unreleased changes found. Nothing to release.",
|
||||
"bg": "No unreleased changes found. Nothing to release.",
|
||||
"de": "No unreleased changes found. Nothing to release.",
|
||||
"ru": "No unreleased changes found. Nothing to release.",
|
||||
"zh": "No unreleased changes found. Nothing to release."
|
||||
},
|
||||
"No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.": {
|
||||
"en": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release."
|
||||
"en": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
|
||||
"bg": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
|
||||
"de": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
|
||||
"ru": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
|
||||
"zh": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release."
|
||||
},
|
||||
"Note: Self-approval not allowed. Posting COMMENT instead.": {
|
||||
"en": "Note: Self-approval not allowed. Posting COMMENT instead."
|
||||
"en": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
||||
"bg": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
||||
"de": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
||||
"ru": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
||||
"zh": "Note: Self-approval not allowed. Posting COMMENT instead."
|
||||
},
|
||||
"Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE": {
|
||||
"en": "Oops! 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",
|
||||
@@ -356,13 +566,6 @@
|
||||
"ru": "Ой! Не включайте ID задачи (DEVX-N) в коммиты feature-веток.\n ID задачи будет добавлен автоматически при слиянии через CI.",
|
||||
"zh": "哎呀!不要在 feature 分支的提交中包含任务 ID (DEVX-N)。\n 任务 ID 将在通过 CI 合并时自动添加。"
|
||||
},
|
||||
"Oops! Do not include task ID (GRM-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.": {
|
||||
"en": "Oops! Do not include task ID (GRM-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.",
|
||||
"bg": "Опа! Не включвайте идентификатор на задача (GRM-N) в commit-и от feature клонове.\n Идентификаторът ще бъде добавен автоматично при сливане чрез CI.",
|
||||
"de": "Ups! Keine Task-ID (GRM-N) in Feature-Branch-Commits einfügen.\n Die Task-ID wird beim Merge automatisch über CI hinzugefügt.",
|
||||
"ru": "Ой! Не включайте ID задачи (GRM-N) в коммиты feature-веток.\n ID задачи будет добавлен автоматически при слиянии через CI.",
|
||||
"zh": "哎呀!不要在 feature 分支的提交中包含任务 ID (GRM-N)。\n 任务 ID 将在通过 CI 合并时自动添加。"
|
||||
},
|
||||
"Oops! Gitea PyPI registry publish failed:\n{stderr}": {
|
||||
"en": "Oops! Gitea PyPI registry publish failed:\n{stderr}",
|
||||
"bg": "Опа! Публикуването в Gitea PyPI registry неуспешно:\n{stderr}",
|
||||
@@ -377,13 +580,6 @@
|
||||
"ru": "Ой! Коммит в ветку master после ID задачи должен соответствовать conventional формату.\n Ожидается: DEVX-N: <type>: <description>\n Получено: {subject}",
|
||||
"zh": "哎呀!master 分支提交在任务 ID 后必须遵循 conventional commit 格式。\n 预期格式: DEVX-N: <type>: <description>\n 实际: {subject}"
|
||||
},
|
||||
"Oops! Master branch commit must follow conventional format after task ID.\n Expected: GRM-N: <type>: <description>\n Got: {subject}": {
|
||||
"en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: GRM-N: <type>: <description>\n Got: {subject}",
|
||||
"bg": "Опа! Commit-ът в клона master трябва да следва конвенционален формат след идентификатора.\n Очаква се: GRM-N: <type>: <description>\n Получено: {subject}",
|
||||
"de": "Ups! Master-Branch-Commit muss nach der Task-ID dem konventionellen Format folgen.\n Erwartet: GRM-N: <type>: <description>\n Erhalten: {subject}",
|
||||
"ru": "Ой! Коммит в ветку master после ID задачи должен соответствовать conventional формату.\n Ожидается: GRM-N: <type>: <description>\n Получено: {subject}",
|
||||
"zh": "哎呀!master 分支提交在任务 ID 后必须遵循 conventional commit 格式。\n 预期格式: GRM-N: <type>: <description>\n 实际: {subject}"
|
||||
},
|
||||
"Oops! Master branch commits must start with a task ID.\n Expected: DEVX-N: <conventional commit message>\n Got: {subject}": {
|
||||
"en": "Oops! Master branch commits must start with a task ID.\n Expected: DEVX-N: <conventional commit message>\n Got: {subject}",
|
||||
"bg": "Опа! Commit-ите в клона master трябва да започват с идентификатор на задача.\n Очаква се: DEVX-N: <conventional commit message>\n Получено: {subject}",
|
||||
@@ -391,28 +587,19 @@
|
||||
"ru": "Ой! Коммиты в ветку master должны начинаться с ID задачи.\n Ожидается: DEVX-N: <conventional commit message>\n Получено: {subject}",
|
||||
"zh": "哎呀!master 分支的提交必须以任务 ID 开头。\n 预期格式: DEVX-N: <conventional commit message>\n 实际: {subject}"
|
||||
},
|
||||
"Oops! Master branch commits must start with a task ID.\n Expected: GRM-N: <conventional commit message>\n Got: {subject}": {
|
||||
"en": "Oops! Master branch commits must start with a task ID.\n Expected: GRM-N: <conventional commit message>\n Got: {subject}",
|
||||
"bg": "Опа! Commit-ите в клона master трябва да започват с идентификатор на задача.\n Очаква се: GRM-N: <conventional commit message>\n Получено: {subject}",
|
||||
"de": "Ups! Master-Branch-Commits müssen mit einer Task-ID beginnen.\n Erwartet: GRM-N: <conventional commit message>\n Erhalten: {subject}",
|
||||
"ru": "Ой! Коммиты в ветку master должны начинаться с ID задачи.\n Ожидается: GRM-N: <conventional commit message>\n Получено: {subject}",
|
||||
"zh": "哎呀!master 分支的提交必须以任务 ID 开头。\n 预期格式: GRM-N: <conventional commit message>\n 实际: {subject}"
|
||||
},
|
||||
"Oops! No task ID found in .taskid file or branch name '{branch}'.": {
|
||||
"en": "Oops! No task ID found in .taskid file or branch name '{branch}'."
|
||||
},
|
||||
"Oops! PR title must follow format 'DEVX-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}": {
|
||||
"en": "Oops! PR title must follow format 'DEVX-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
||||
"bg": "Опа! Заглавието на PR трябва да следва формата 'DEVX-N: <заглавие на задача>'.\n Очаква се: {task_id}: <заглавие на задача>\n Получено: {pr_title}",
|
||||
"de": "Ups! PR-Titel muss dem Format 'DEVX-N: <Task-Titel>' folgen.\n Erwartet: {task_id}: <Task-Titel>\n Erhalten: {pr_title}",
|
||||
"ru": "Ой! Заголовок PR должен соответствовать формату 'DEVX-N: <название задачи>'.\n Ожидается: {task_id}: <название задачи>\n Получено: {pr_title}",
|
||||
"zh": "哎呀!PR 标题必须遵循格式 'DEVX-N: <任务标题>'。\n 预期格式: {task_id}: <任务标题>\n 实际: {pr_title}"
|
||||
},
|
||||
"Oops! PR title must follow format 'GRM-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}": {
|
||||
"en": "Oops! PR title must follow format 'GRM-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}"
|
||||
"en": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
|
||||
"bg": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
|
||||
"de": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
|
||||
"ru": "Oops! No task ID found in .taskid file or branch name '{branch}'.",
|
||||
"zh": "Oops! No task ID found in .taskid file or branch name '{branch}'."
|
||||
},
|
||||
"Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}": {
|
||||
"en": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}"
|
||||
"en": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
|
||||
"bg": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
|
||||
"de": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
|
||||
"ru": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
|
||||
"zh": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}"
|
||||
},
|
||||
"Oops! Package build failed:\n{stderr}": {
|
||||
"en": "Oops! Package build failed:\n{stderr}",
|
||||
@@ -429,10 +616,18 @@
|
||||
"zh": "哎呀!PyPI 发布失败:\n{stderr}"
|
||||
},
|
||||
"PASSED: {pair}": {
|
||||
"en": "PASSED: {pair}"
|
||||
"en": "PASSED: {pair}",
|
||||
"bg": "PASSED: {pair}",
|
||||
"de": "PASSED: {pair}",
|
||||
"ru": "PASSED: {pair}",
|
||||
"zh": "PASSED: {pair}"
|
||||
},
|
||||
"PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}": {
|
||||
"en": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}"
|
||||
"en": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
||||
"bg": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
||||
"de": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
||||
"ru": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
||||
"zh": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}"
|
||||
},
|
||||
"PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.": {
|
||||
"en": "PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.",
|
||||
@@ -441,13 +636,6 @@
|
||||
"ru": "PYPI_TOKEN не задан и URL registry не настроен — пропускаем публикацию в PyPI. Не беспокойтесь, мы просто создадим Gitea release.",
|
||||
"zh": "未设置 PYPI_TOKEN 且未配置 registry URL — 跳过 PyPI 发布。别担心,我们直接创建 Gitea release。"
|
||||
},
|
||||
"PYPI_TOKEN not set — skipping PyPI publish. No worries, we'll just create the Gitea release.": {
|
||||
"en": "PYPI_TOKEN not set — skipping PyPI publish. No worries, we'll just create the Gitea release.",
|
||||
"bg": "PYPI_TOKEN не е зададен — пропускаме публикуването в PyPI. Без притеснения, просто ще създадем Gitea release.",
|
||||
"de": "PYPI_TOKEN nicht gesetzt — PyPI-Veröffentlichung wird übersprungen. Keine Sorge, wir erstellen einfach das Gitea-Release.",
|
||||
"ru": "PYPI_TOKEN не задан — пропускаем публикацию в PyPI. Не беспокойтесь, мы просто создадим Gitea release.",
|
||||
"zh": "未设置 PYPI_TOKEN — 跳过 PyPI 发布。别担心,我们直接创建 Gitea release。"
|
||||
},
|
||||
"Published to Gitea PyPI registry.": {
|
||||
"en": "Published to Gitea PyPI registry.",
|
||||
"bg": "Публикувано в Gitea PyPI registry.",
|
||||
@@ -463,16 +651,32 @@
|
||||
"zh": "已发布到 PyPI。"
|
||||
},
|
||||
"Pushed release commit to master.": {
|
||||
"en": "Pushed release commit to master."
|
||||
"en": "Pushed release commit to master.",
|
||||
"bg": "Pushed release commit to master.",
|
||||
"de": "Pushed release commit to master.",
|
||||
"ru": "Pushed release commit to master.",
|
||||
"zh": "Pushed release commit to master."
|
||||
},
|
||||
"Rebased and pushed. Retrying merge...": {
|
||||
"en": "Rebased and pushed. Retrying merge..."
|
||||
"en": "Rebased and pushed. Retrying merge...",
|
||||
"bg": "Rebased and pushed. Retrying merge...",
|
||||
"de": "Rebased and pushed. Retrying merge...",
|
||||
"ru": "Rebased and pushed. Retrying merge...",
|
||||
"zh": "Rebased and pushed. Retrying merge..."
|
||||
},
|
||||
"Release creation failed: {error}": {
|
||||
"en": "Release creation failed: {error}"
|
||||
"en": "Release creation failed: {error}",
|
||||
"bg": "Release creation failed: {error}",
|
||||
"de": "Release creation failed: {error}",
|
||||
"ru": "Release creation failed: {error}",
|
||||
"zh": "Release creation failed: {error}"
|
||||
},
|
||||
"Release must be run on master, currently on '{branch}'.": {
|
||||
"en": "Release must be run on master, currently on '{branch}'."
|
||||
"en": "Release must be run on master, currently on '{branch}'.",
|
||||
"bg": "Release must be run on master, currently on '{branch}'.",
|
||||
"de": "Release must be run on master, currently on '{branch}'.",
|
||||
"ru": "Release must be run on master, currently on '{branch}'.",
|
||||
"zh": "Release must be run on master, currently on '{branch}'."
|
||||
},
|
||||
"Repository configuration complete.": {
|
||||
"en": "Repository configuration complete.",
|
||||
@@ -489,101 +693,172 @@
|
||||
"zh": "Runner 索引 {index} 超出范围 (0..{max})"
|
||||
},
|
||||
"Running lint checks...": {
|
||||
"en": "Running lint checks..."
|
||||
"en": "Running lint checks...",
|
||||
"bg": "Running lint checks...",
|
||||
"de": "Running lint checks...",
|
||||
"ru": "Running lint checks...",
|
||||
"zh": "Running lint checks..."
|
||||
},
|
||||
"Running tests...": {
|
||||
"en": "Running tests..."
|
||||
"en": "Running tests...",
|
||||
"bg": "Running tests...",
|
||||
"de": "Running tests...",
|
||||
"ru": "Running tests...",
|
||||
"zh": "Running tests..."
|
||||
},
|
||||
"Running: {scenario} on {platform}": {
|
||||
"en": "Running: {scenario} on {platform}"
|
||||
"en": "Running: {scenario} on {platform}",
|
||||
"bg": "Running: {scenario} on {platform}",
|
||||
"de": "Running: {scenario} on {platform}",
|
||||
"ru": "Running: {scenario} on {platform}",
|
||||
"zh": "Running: {scenario} on {platform}"
|
||||
},
|
||||
"Skipping commit push — no staged changes.": {
|
||||
"en": "Skipping commit push — no staged changes."
|
||||
"en": "Skipping commit push — no staged changes.",
|
||||
"bg": "Skipping commit push — no staged changes.",
|
||||
"de": "Skipping commit push — no staged changes.",
|
||||
"ru": "Skipping commit push — no staged changes.",
|
||||
"zh": "Skipping commit push — no staged changes."
|
||||
},
|
||||
"Syncing {count} documentation pages to wiki...": {
|
||||
"en": "Syncing {count} documentation pages to wiki..."
|
||||
"en": "Syncing {count} documentation pages to wiki...",
|
||||
"bg": "Syncing {count} documentation pages to wiki...",
|
||||
"de": "Syncing {count} documentation pages to wiki...",
|
||||
"ru": "Syncing {count} documentation pages to wiki...",
|
||||
"zh": "Syncing {count} documentation pages to wiki..."
|
||||
},
|
||||
"Tag v{version} already existed. Publish workflow should already have been triggered.": {
|
||||
"en": "Tag v{version} already existed. Publish workflow should already have been triggered."
|
||||
"en": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
||||
"bg": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
||||
"de": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
||||
"ru": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
||||
"zh": "Tag v{version} already existed. Publish workflow should already have been triggered."
|
||||
},
|
||||
"Tag {tag} already exists, skipping creation.": {
|
||||
"en": "Tag {tag} already exists, skipping creation."
|
||||
"en": "Tag {tag} already exists, skipping creation.",
|
||||
"bg": "Tag {tag} already exists, skipping creation.",
|
||||
"de": "Tag {tag} already exists, skipping creation.",
|
||||
"ru": "Tag {tag} already exists, skipping creation.",
|
||||
"zh": "Tag {tag} already exists, skipping creation."
|
||||
},
|
||||
"Task ID: {task_id}": {
|
||||
"en": "Task ID: {task_id}"
|
||||
"en": "Task ID: {task_id}",
|
||||
"bg": "Task ID: {task_id}",
|
||||
"de": "Task ID: {task_id}",
|
||||
"ru": "Task ID: {task_id}",
|
||||
"zh": "Task ID: {task_id}"
|
||||
},
|
||||
"Tests failed — refusing to release. Fix test failures first.\n{stderr}": {
|
||||
"en": "Tests failed — refusing to release. Fix test failures first.\n{stderr}"
|
||||
"en": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
|
||||
"bg": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
|
||||
"de": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
|
||||
"ru": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
|
||||
"zh": "Tests failed — refusing to release. Fix test failures first.\n{stderr}"
|
||||
},
|
||||
"Tests passed.": {
|
||||
"en": "Tests passed."
|
||||
"en": "Tests passed.",
|
||||
"bg": "Tests passed.",
|
||||
"de": "Tests passed.",
|
||||
"ru": "Tests passed.",
|
||||
"zh": "Tests passed."
|
||||
},
|
||||
"Unit tests passed in {duration:.2f}s (under {max}s limit).": {
|
||||
"en": "Unit tests passed in {duration:.2f}s (under {max}s limit)."
|
||||
"en": "Unit tests passed in {duration:.2f}s (under {max}s limit).",
|
||||
"bg": "Unit tests passed in {duration:.2f}s (under {max}s limit).",
|
||||
"de": "Unit tests passed in {duration:.2f}s (under {max}s limit).",
|
||||
"ru": "Unit tests passed in {duration:.2f}s (under {max}s limit).",
|
||||
"zh": "Unit tests passed in {duration:.2f}s (under {max}s limit)."
|
||||
},
|
||||
"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": "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": "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": "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": "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": "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": "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."
|
||||
},
|
||||
"Updated version in {init}": {
|
||||
"en": "Updated version in {init}"
|
||||
"en": "Updated version in {init}",
|
||||
"bg": "Updated version in {init}",
|
||||
"de": "Updated version in {init}",
|
||||
"ru": "Updated version in {init}",
|
||||
"zh": "Updated version in {init}"
|
||||
},
|
||||
"Updated {changelog_file}": {
|
||||
"en": "Updated {changelog_file}"
|
||||
"en": "Updated {changelog_file}",
|
||||
"bg": "Updated {changelog_file}",
|
||||
"de": "Updated {changelog_file}",
|
||||
"ru": "Updated {changelog_file}",
|
||||
"zh": "Updated {changelog_file}"
|
||||
},
|
||||
"WARNING: --skip-tests passed — skipping test verification.": {
|
||||
"en": "WARNING: --skip-tests passed — skipping test verification."
|
||||
},
|
||||
"WARNING: File {file} is empty — skipping.": {
|
||||
"en": "WARNING: File {file} is empty — skipping."
|
||||
},
|
||||
"WARNING: File {file} not found — skipping.": {
|
||||
"en": "WARNING: File {file} not found — skipping."
|
||||
},
|
||||
"Warning: No task ID (DEVX-N) found in commit message: {msg}. Skipping Vikunja update.": {
|
||||
"en": "Warning: No task ID (DEVX-N) found in commit message: {msg}. Skipping Vikunja update.",
|
||||
"bg": "Предупреждение: Не е намерен идентификатор на задача (DEVX-N) в съобщението за commit: {msg}. Пропускаме обновяването на Vikunja.",
|
||||
"de": "Warnung: Keine Task-ID (DEVX-N) in Commit-Nachricht gefunden: {msg}. Vikunja-Update wird übersprungen.",
|
||||
"ru": "Предупреждение: ID задачи (DEVX-N) не найден в сообщении коммита: {msg}. Пропуск обновления Vikunja.",
|
||||
"zh": "警告:提交消息中未找到任务 ID (DEVX-N): {msg}。跳过 Vikunja 更新。"
|
||||
},
|
||||
"Warning: No task ID (GRM-N) found in commit message: {msg}. Skipping Vikunja update.": {
|
||||
"en": "Warning: No task ID (GRM-N) found in commit message: {msg}. Skipping Vikunja update."
|
||||
},
|
||||
"Warning: VIKUNJA_TOKEN not set, skipping title match validation.": {
|
||||
"en": "Warning: VIKUNJA_TOKEN not set, skipping title match validation."
|
||||
},
|
||||
"Warning: Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded — please update the Vikunja task manually.": {
|
||||
"en": "Warning: Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded — please update the Vikunja task manually."
|
||||
},
|
||||
"Warning: git-cliff generated empty changelog.": {
|
||||
"en": "Warning: git-cliff generated empty changelog."
|
||||
"en": "WARNING: --skip-tests passed — skipping test verification.",
|
||||
"bg": "WARNING: --skip-tests passed — skipping test verification.",
|
||||
"de": "WARNING: --skip-tests passed — skipping test verification.",
|
||||
"ru": "WARNING: --skip-tests passed — skipping test verification.",
|
||||
"zh": "WARNING: --skip-tests passed — skipping test verification."
|
||||
},
|
||||
"Wiki integrity check failed — {count} issue(s)": {
|
||||
"en": "Wiki integrity check failed — {count} issue(s)"
|
||||
"en": "Wiki integrity check failed — {count} issue(s)",
|
||||
"bg": "Wiki integrity check failed — {count} issue(s)",
|
||||
"de": "Wiki integrity check failed — {count} issue(s)",
|
||||
"ru": "Wiki integrity check failed — {count} issue(s)",
|
||||
"zh": "Wiki integrity check failed — {count} issue(s)"
|
||||
},
|
||||
"Wiki verification failed — {failures} page(s) empty or mismatched": {
|
||||
"en": "Wiki verification failed — {failures} page(s) empty or mismatched"
|
||||
"en": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
||||
"bg": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
||||
"de": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
||||
"ru": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
||||
"zh": "Wiki verification failed — {failures} page(s) empty or mismatched"
|
||||
},
|
||||
"[dry-run] Would commit: release: v{version}": {
|
||||
"en": "[dry-run] Would commit: release: v{version}"
|
||||
"en": "[dry-run] Would commit: release: v{version}",
|
||||
"bg": "[dry-run] Would commit: release: v{version}",
|
||||
"de": "[dry-run] Would commit: release: v{version}",
|
||||
"ru": "[dry-run] Would commit: release: v{version}",
|
||||
"zh": "[dry-run] Would commit: release: v{version}"
|
||||
},
|
||||
"[dry-run] Would create tag: v{version}": {
|
||||
"en": "[dry-run] Would create tag: v{version}"
|
||||
"en": "[dry-run] Would create tag: v{version}",
|
||||
"bg": "[dry-run] Would create tag: v{version}",
|
||||
"de": "[dry-run] Would create tag: v{version}",
|
||||
"ru": "[dry-run] Would create tag: v{version}",
|
||||
"zh": "[dry-run] Would create tag: v{version}"
|
||||
},
|
||||
"[dry-run] Would create tag: {tag}": {
|
||||
"en": "[dry-run] Would create tag: {tag}"
|
||||
"en": "[dry-run] Would create tag: {tag}",
|
||||
"bg": "[dry-run] Would create tag: {tag}",
|
||||
"de": "[dry-run] Would create tag: {tag}",
|
||||
"ru": "[dry-run] Would create tag: {tag}",
|
||||
"zh": "[dry-run] Would create tag: {tag}"
|
||||
},
|
||||
"[dry-run] Would push commit to master": {
|
||||
"en": "[dry-run] Would push commit to master"
|
||||
"en": "[dry-run] Would push commit to master",
|
||||
"bg": "[dry-run] Would push commit to master",
|
||||
"de": "[dry-run] Would push commit to master",
|
||||
"ru": "[dry-run] Would push commit to master",
|
||||
"zh": "[dry-run] Would push commit to master"
|
||||
},
|
||||
"[dry-run] Would sync page: {title} ({chars} chars)": {
|
||||
"en": "[dry-run] Would sync page: {title} ({chars} chars)"
|
||||
"en": "[dry-run] Would sync page: {title} ({chars} chars)",
|
||||
"bg": "[dry-run] Would sync page: {title} ({chars} chars)",
|
||||
"de": "[dry-run] Would sync page: {title} ({chars} chars)",
|
||||
"ru": "[dry-run] Would sync page: {title} ({chars} chars)",
|
||||
"zh": "[dry-run] Would sync page: {title} ({chars} chars)"
|
||||
},
|
||||
"[dry-run] Would update {changelog_file}": {
|
||||
"en": "[dry-run] Would update {changelog_file}"
|
||||
"en": "[dry-run] Would update {changelog_file}",
|
||||
"bg": "[dry-run] Would update {changelog_file}",
|
||||
"de": "[dry-run] Would update {changelog_file}",
|
||||
"ru": "[dry-run] Would update {changelog_file}",
|
||||
"zh": "[dry-run] Would update {changelog_file}"
|
||||
},
|
||||
"[dry-run] Would update {init}": {
|
||||
"en": "[dry-run] Would update {init}"
|
||||
"en": "[dry-run] Would update {init}",
|
||||
"bg": "[dry-run] Would update {init}",
|
||||
"de": "[dry-run] Would update {init}",
|
||||
"ru": "[dry-run] Would update {init}",
|
||||
"zh": "[dry-run] Would update {init}"
|
||||
},
|
||||
"active": {
|
||||
"en": "active",
|
||||
@@ -607,10 +882,18 @@
|
||||
"zh": "失败"
|
||||
},
|
||||
"git command failed ({cmd}): {stderr}": {
|
||||
"en": "git command failed ({cmd}): {stderr}"
|
||||
"en": "git command failed ({cmd}): {stderr}",
|
||||
"bg": "git command failed ({cmd}): {stderr}",
|
||||
"de": "git command failed ({cmd}): {stderr}",
|
||||
"ru": "git command failed ({cmd}): {stderr}",
|
||||
"zh": "git command failed ({cmd}): {stderr}"
|
||||
},
|
||||
"git-cliff returned empty version.": {
|
||||
"en": "git-cliff returned empty version."
|
||||
"en": "git-cliff returned empty version.",
|
||||
"bg": "git-cliff returned empty version.",
|
||||
"de": "git-cliff returned empty version.",
|
||||
"ru": "git-cliff returned empty version.",
|
||||
"zh": "git-cliff returned empty version."
|
||||
},
|
||||
"inactive": {
|
||||
"en": "inactive",
|
||||
@@ -641,15 +924,122 @@
|
||||
"zh": "未知"
|
||||
},
|
||||
"\n{label} files changed ({count}):": {
|
||||
"en": "\n{label} files changed ({count}):"
|
||||
"en": "\n{label} files changed ({count}):",
|
||||
"bg": "\n{label} files changed ({count}):",
|
||||
"de": "\n{label} files changed ({count}):",
|
||||
"ru": "\n{label} files changed ({count}):",
|
||||
"zh": "\n{label} files changed ({count}):"
|
||||
},
|
||||
"\n{tag} files ({count}):": {
|
||||
"en": "\n{tag} files ({count}):"
|
||||
"en": "\n{tag} files ({count}):",
|
||||
"bg": "\n{tag} files ({count}):",
|
||||
"de": "\n{tag} files ({count}):",
|
||||
"ru": "\n{tag} files ({count}):",
|
||||
"zh": "\n{tag} files ({count}):"
|
||||
},
|
||||
"Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}": {
|
||||
"en": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}"
|
||||
"en": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
||||
"bg": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
||||
"de": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
||||
"ru": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
||||
"zh": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}"
|
||||
},
|
||||
"Unknown check category '{check}'. Available: all, user-facing{tags}": {
|
||||
"en": "Unknown check category '{check}'. Available: all, user-facing{tags}"
|
||||
"en": "Unknown check category '{check}'. Available: all, user-facing{tags}",
|
||||
"bg": "Unknown check category '{check}'. Available: all, user-facing{tags}",
|
||||
"de": "Unknown check category '{check}'. Available: all, user-facing{tags}",
|
||||
"ru": "Unknown check category '{check}'. Available: all, user-facing{tags}",
|
||||
"zh": "Unknown check category '{check}'. Available: all, user-facing{tags}"
|
||||
},
|
||||
"HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.": {
|
||||
"en": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
|
||||
"bg": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
|
||||
"de": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
|
||||
"ru": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
|
||||
"zh": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag."
|
||||
},
|
||||
"HEAD is already a release commit ('{msg}') and tag {tag} exists. Skipping.": {
|
||||
"en": "HEAD is already a release commit ('{msg}') and tag {tag} exists. Skipping.",
|
||||
"bg": "HEAD is already a release commit ('{msg}') and tag {tag} exists. Skipping.",
|
||||
"de": "HEAD is already a release commit ('{msg}') and tag {tag} exists. Skipping.",
|
||||
"ru": "HEAD is already a release commit ('{msg}') and tag {tag} exists. Skipping.",
|
||||
"zh": "HEAD is already a release commit ('{msg}') and tag {tag} exists. Skipping."
|
||||
},
|
||||
"Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.": {
|
||||
"en": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.",
|
||||
"bg": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.",
|
||||
"de": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.",
|
||||
"ru": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.",
|
||||
"zh": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update."
|
||||
},
|
||||
"PR number must be an integer, got: {pr_number}": {
|
||||
"en": "PR number must be an integer, got: {pr_number}",
|
||||
"bg": "PR number must be an integer, got: {pr_number}",
|
||||
"de": "PR number must be an integer, got: {pr_number}",
|
||||
"ru": "PR number must be an integer, got: {pr_number}",
|
||||
"zh": "PR number must be an integer, got: {pr_number}"
|
||||
},
|
||||
"git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.": {
|
||||
"en": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
|
||||
"bg": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
|
||||
"de": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
|
||||
"ru": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
|
||||
"zh": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history."
|
||||
},
|
||||
"Repo must be in 'owner/name' format, got: {repo}": {
|
||||
"en": "Repo must be in 'owner/name' format, got: {repo}",
|
||||
"bg": "Repo must be in 'owner/name' format, got: {repo}",
|
||||
"de": "Repo must be in 'owner/name' format, got: {repo}",
|
||||
"ru": "Repo must be in 'owner/name' format, got: {repo}",
|
||||
"zh": "Repo must be in 'owner/name' format, got: {repo}"
|
||||
},
|
||||
"Mapped file {file} is empty. Update the content or remove from mapping.json.": {
|
||||
"en": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
|
||||
"bg": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
|
||||
"de": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
|
||||
"ru": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
|
||||
"zh": "Mapped file {file} is empty. Update the content or remove from mapping.json."
|
||||
},
|
||||
"VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.": {
|
||||
"en": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
||||
"bg": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
||||
"de": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
||||
"ru": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
||||
"zh": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles."
|
||||
},
|
||||
"mapping.json keys and values must be strings, got {k}={v}": {
|
||||
"en": "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}",
|
||||
"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}": {
|
||||
"en": "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}",
|
||||
"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}"
|
||||
},
|
||||
"Mapped file {file} not found. Update mapping.json or create the file.": {
|
||||
"en": "Mapped file {file} not found. Update mapping.json or create the file.",
|
||||
"bg": "Mapped file {file} not found. Update mapping.json or create the file.",
|
||||
"de": "Mapped file {file} not found. Update mapping.json or create the file.",
|
||||
"ru": "Mapped file {file} not found. Update mapping.json or create the file.",
|
||||
"zh": "Mapped file {file} not found. Update mapping.json or create the file."
|
||||
},
|
||||
"No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.": {
|
||||
"en": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
|
||||
"bg": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
|
||||
"de": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
|
||||
"ru": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
|
||||
"zh": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID."
|
||||
},
|
||||
"git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).": {
|
||||
"en": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
|
||||
"bg": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
|
||||
"de": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
|
||||
"ru": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
|
||||
"zh": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1)."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,9 +77,10 @@ class TestValidatePrTitle:
|
||||
|
||||
class TestValidatePrTitleMatchesVikunja:
|
||||
@patch.dict("os.environ", {}, clear=True)
|
||||
def test_skips_when_no_token(self) -> None:
|
||||
# Should not raise — just warn
|
||||
validate_pr_title_matches_vikunja("DEVX-19: test", "DEVX-19")
|
||||
def test_raises_when_no_token(self) -> None:
|
||||
"""Should raise ClickException when VIKUNJA_TOKEN is not set."""
|
||||
with pytest.raises(click.ClickException, match="VIKUNJA_TOKEN is not set"):
|
||||
validate_pr_title_matches_vikunja("DEVX-19: test", "DEVX-19")
|
||||
|
||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.auto_merge.VikunjaClient")
|
||||
@@ -192,9 +193,12 @@ class TestRunCmd:
|
||||
|
||||
|
||||
class TestMain:
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "VIKUNJA_TOKEN": ""}, clear=True)
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.auto_merge.validate_pr_title_matches_vikunja")
|
||||
@patch("devx.ci.auto_merge.GiteaClient")
|
||||
def test_full_merge_flow(self, mock_client_cls: MagicMock, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||
def test_full_merge_flow(
|
||||
self, mock_client_cls: MagicMock, _mock_validate: MagicMock, tmp_path, monkeypatch
|
||||
) -> None: # type: ignore[no-untyped-def]
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / ".taskid").write_text("DEVX-19\n")
|
||||
|
||||
@@ -210,7 +214,7 @@ class TestMain:
|
||||
["DEVX-19-fix-bug", "DEVX-19: Fix timeout", "owner/repo", "7"],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_client.merge_pr.assert_called_once_with("7", "DEVX-19: fix: resolve timeout")
|
||||
mock_client.merge_pr.assert_called_once_with(7, "DEVX-19: fix: resolve timeout")
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
|
||||
def test_no_token_raises(self) -> None:
|
||||
@@ -240,9 +244,12 @@ class TestMain:
|
||||
assert result.exit_code != 0
|
||||
assert "format" in result.output.lower()
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.auto_merge.validate_pr_title_matches_vikunja")
|
||||
@patch("devx.ci.auto_merge.GiteaClient")
|
||||
def test_merge_behind_master_rebases(self, mock_client_cls: MagicMock, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||
def test_merge_behind_master_rebases(
|
||||
self, mock_client_cls: MagicMock, _mock_validate: MagicMock, tmp_path, monkeypatch
|
||||
) -> None: # type: ignore[no-untyped-def]
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / ".taskid").write_text("DEVX-19\n")
|
||||
|
||||
@@ -267,9 +274,12 @@ class TestMain:
|
||||
# Should have fetched, rebased, and pushed
|
||||
assert mock_run.call_count == 5 # config name, config email, fetch, rebase, push
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.auto_merge.validate_pr_title_matches_vikunja")
|
||||
@patch("devx.ci.auto_merge.GiteaClient")
|
||||
def test_merge_failure_raises(self, mock_client_cls: MagicMock, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||
def test_merge_failure_raises(
|
||||
self, mock_client_cls: MagicMock, _mock_validate: MagicMock, tmp_path, monkeypatch
|
||||
) -> None: # type: ignore[no-untyped-def]
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / ".taskid").write_text("DEVX-19\n")
|
||||
|
||||
@@ -288,9 +298,12 @@ class TestMain:
|
||||
assert result.exit_code != 0
|
||||
assert "Merge failed" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.auto_merge.validate_pr_title_matches_vikunja")
|
||||
@patch("devx.ci.auto_merge.GiteaClient")
|
||||
def test_no_conventional_msg_raises(self, mock_client_cls: MagicMock, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||
def test_no_conventional_msg_raises(
|
||||
self, mock_client_cls: MagicMock, _mock_validate: MagicMock, tmp_path, monkeypatch
|
||||
) -> None: # type: ignore[no-untyped-def]
|
||||
"""When no conventional commit message is found in PR commits, raises."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / ".taskid").write_text("DEVX-19\n")
|
||||
@@ -308,8 +321,31 @@ class TestMain:
|
||||
assert "conventional commit" in result.output.lower()
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
def test_invalid_pr_number_raises(self, tmp_path, monkeypatch) -> None:
|
||||
"""Non-integer PR number should raise."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / ".taskid").write_text("DEVX-19\n")
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["DEVX-19-fix", "DEVX-19: Test", "owner/repo", "not-a-number"])
|
||||
assert result.exit_code != 0
|
||||
assert "PR number must be an integer" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
def test_invalid_repo_format_raises(self, tmp_path, monkeypatch) -> None:
|
||||
"""Repo without owner/name should raise."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / ".taskid").write_text("DEVX-19\n")
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["DEVX-19-fix", "DEVX-19: Test", "invalidrepo", "7"])
|
||||
assert result.exit_code != 0
|
||||
assert "owner/name" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok", "VIKUNJA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.auto_merge.validate_pr_title_matches_vikunja")
|
||||
@patch("devx.ci.auto_merge.GiteaClient")
|
||||
def test_rebase_retry_failure_raises(self, mock_client_cls: MagicMock, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||
def test_rebase_retry_failure_raises(
|
||||
self, mock_client_cls: MagicMock, _mock_validate: MagicMock, tmp_path, monkeypatch
|
||||
) -> None: # type: ignore[no-untyped-def]
|
||||
"""When rebase retry also fails, raises with helpful message."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / ".taskid").write_text("DEVX-19\n")
|
||||
|
||||
@@ -67,7 +67,7 @@ class TestCheckTranslationSet:
|
||||
trans_file.write_text(json.dumps({"Used": {"en": "Used"}, "Dead": {"en": "Dead"}}))
|
||||
|
||||
result = check_translations.check_translation_set("test", src_dir, trans_file)
|
||||
assert any("Dead key" in w for w in result.warnings)
|
||||
assert any("Dead key" in e for e in result.errors)
|
||||
|
||||
def test_missing_language(self, tmp_path: Path) -> None:
|
||||
src_dir = tmp_path / "src"
|
||||
@@ -77,7 +77,7 @@ class TestCheckTranslationSet:
|
||||
trans_file.write_text(json.dumps({"Hello": {"en": "Hello"}}))
|
||||
|
||||
result = check_translations.check_translation_set("test", src_dir, trans_file)
|
||||
assert any("Missing languages" in w for w in result.warnings)
|
||||
assert any("Missing languages" in e for e in result.errors)
|
||||
|
||||
def test_missing_translations_file(self, tmp_path: Path) -> None:
|
||||
src_dir = tmp_path / "src"
|
||||
@@ -108,23 +108,19 @@ class TestMain:
|
||||
result = runner.invoke(check_translations.main, [])
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_strict_fails_on_warnings(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""--strict should fail if there are missing language warnings."""
|
||||
warn_result = check_translations.TranslationCheckResult(
|
||||
def test_errors_fail(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Errors should cause exit code 1."""
|
||||
error_result = check_translations.TranslationCheckResult(
|
||||
name="devx",
|
||||
src_dir=Path("/tmp"),
|
||||
trans_file=Path("/tmp/t.json"),
|
||||
used_keys={"a"},
|
||||
defined_keys={"a"},
|
||||
warnings=["Dead key: 'bar'"],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
check_translations,
|
||||
"check_translation_set",
|
||||
lambda name, src, trans: warn_result,
|
||||
errors=["Dead key: 'bar'"],
|
||||
)
|
||||
monkeypatch.setattr(check_translations, "check_translation_set", lambda name, src, trans: error_result)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(check_translations.main, ["--strict"])
|
||||
result = runner.invoke(check_translations.main, [])
|
||||
assert result.exit_code == 1
|
||||
|
||||
def test_fails_on_errors(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -311,26 +307,25 @@ class TestCollectKeys:
|
||||
assert "in_progress" in keys
|
||||
|
||||
|
||||
class TestMainWarnings:
|
||||
def test_passes_with_warnings(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Should pass with exit code 0 and 'PASS with warnings' message."""
|
||||
warn_result = check_translations.TranslationCheckResult(
|
||||
class TestMainCleanPass:
|
||||
def test_passes_clean(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Should pass with exit code 0 and 'PASS:' message when no errors."""
|
||||
ok_result = check_translations.TranslationCheckResult(
|
||||
name="devx",
|
||||
src_dir=Path("/tmp"),
|
||||
trans_file=Path("/tmp/t.json"),
|
||||
used_keys={"a"},
|
||||
defined_keys={"a"},
|
||||
warnings=["Dead key: 'bar'"],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
check_translations,
|
||||
"check_translation_set",
|
||||
lambda name, src, trans: warn_result,
|
||||
lambda name, src, trans: ok_result,
|
||||
)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(check_translations.main, [])
|
||||
assert result.exit_code == 0
|
||||
assert "PASS with warnings" in result.output
|
||||
assert "PASS:" in result.output
|
||||
|
||||
|
||||
class TestI18nProjectTranslations:
|
||||
|
||||
@@ -178,6 +178,39 @@ class TestClassifierConfig:
|
||||
assert "tests/**" in DEFAULT_INFRASTRUCTURE
|
||||
assert "docs/**" in DEFAULT_INFRASTRUCTURE
|
||||
|
||||
def test_default_infrastructure_covers_common_project_files(self) -> None:
|
||||
"""DEFAULT_INFRASTRUCTURE must cover common project-level files
|
||||
that are not part of the installed package.
|
||||
|
||||
This test prevents regression of the root cause of GRM-64
|
||||
misclassification: 28 files (scripts/**, REVIEW_CHECKLIST.md)
|
||||
were classified as user-facing because these patterns were
|
||||
missing from the defaults.
|
||||
"""
|
||||
# Project documentation
|
||||
assert "AGENTS.md" in DEFAULT_INFRASTRUCTURE
|
||||
assert "README.md" in DEFAULT_INFRASTRUCTURE
|
||||
assert "CHANGELOG.md" in DEFAULT_INFRASTRUCTURE
|
||||
assert "TROUBLESHOOTING.md" in DEFAULT_INFRASTRUCTURE
|
||||
assert "CONTRIBUTING.md" in DEFAULT_INFRASTRUCTURE
|
||||
assert "CODE_OF_CONDUCT.md" in DEFAULT_INFRASTRUCTURE
|
||||
assert "REVIEW_CHECKLIST.md" in DEFAULT_INFRASTRUCTURE
|
||||
# Build tooling
|
||||
assert "Makefile" in DEFAULT_INFRASTRUCTURE
|
||||
assert "cliff.toml" in DEFAULT_INFRASTRUCTURE
|
||||
assert "uv.lock" in DEFAULT_INFRASTRUCTURE
|
||||
# Lint config
|
||||
assert ".pre-commit-config.yaml" in DEFAULT_INFRASTRUCTURE
|
||||
assert ".ruff.toml" in DEFAULT_INFRASTRUCTURE
|
||||
assert ".ansible-lint" in DEFAULT_INFRASTRUCTURE
|
||||
assert ".checkmake.ini" in DEFAULT_INFRASTRUCTURE
|
||||
assert ".editorconfig" in DEFAULT_INFRASTRUCTURE
|
||||
# Git config
|
||||
assert ".gitignore" in DEFAULT_INFRASTRUCTURE
|
||||
assert ".gitattributes" in DEFAULT_INFRASTRUCTURE
|
||||
# Agent config
|
||||
assert ".devin/**" in DEFAULT_INFRASTRUCTURE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ChangeClassifier tests
|
||||
|
||||
@@ -120,6 +120,35 @@ class TestQueryRunners:
|
||||
result = query_runners("https://api.example.com", "token", "owner", "repo")
|
||||
assert result == 0
|
||||
|
||||
@patch("devx.ci.discover_runners.requests.get")
|
||||
def test_query_runners_403_no_warning(self, mock_get: MagicMock, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""403 on instance-level runners should not produce a warning (expected without admin scope)."""
|
||||
responses = [
|
||||
MagicMock(status_code=200, json=lambda: {"total_count": 2}),
|
||||
MagicMock(status_code=200, json=lambda: {"total_count": 1}),
|
||||
MagicMock(status_code=403, json=lambda: {"message": "forbidden"}),
|
||||
]
|
||||
mock_get.side_effect = responses
|
||||
result = query_runners("https://api.example.com", "token", "owner", "repo")
|
||||
assert result == 3
|
||||
captured = capsys.readouterr()
|
||||
assert "instance-level" not in captured.err
|
||||
|
||||
@patch("devx.ci.discover_runners.requests.get")
|
||||
def test_instance_level_non_403_warns(self, mock_get: MagicMock, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""Non-200, non-403 status on instance-level runners should produce a warning."""
|
||||
responses = [
|
||||
MagicMock(status_code=200, json=lambda: {"total_count": 1}),
|
||||
MagicMock(status_code=200, json=lambda: {"total_count": 1}),
|
||||
MagicMock(status_code=500, json=lambda: {"message": "server error"}),
|
||||
]
|
||||
mock_get.side_effect = responses
|
||||
result = query_runners("https://api.example.com", "token", "owner", "repo")
|
||||
assert result == 2
|
||||
captured = capsys.readouterr()
|
||||
assert "instance-level" in captured.err
|
||||
assert "500" in captured.err
|
||||
|
||||
|
||||
class TestGetRunnerCount:
|
||||
@patch("devx.ci.discover_runners.query_runners", return_value=5)
|
||||
|
||||
@@ -240,6 +240,16 @@ class TestGithubEnv:
|
||||
assert result.exit_code != 0
|
||||
|
||||
|
||||
class TestRunnerIndexValidation:
|
||||
def test_runner_index_zero_raises(self) -> None:
|
||||
"""Runner index < 1 should raise."""
|
||||
with patch("devx.molecule.distribute_molecule.discover_scenarios", return_value=["dummy"]):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["--runner-index", "0", "--max-runners", "3"])
|
||||
assert result.exit_code != 0
|
||||
assert "out of range" in result.output
|
||||
|
||||
|
||||
def test_main_module_block() -> None:
|
||||
import devx.molecule.distribute_molecule as dm
|
||||
|
||||
|
||||
@@ -168,6 +168,15 @@ class TestCli:
|
||||
assert result.exit_code == 0
|
||||
assert "All molecule tests passed" in result.output
|
||||
|
||||
def test_invalid_pair_format_raises(self) -> None:
|
||||
"""Pair with fewer than 2 parts should raise."""
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["invalid_no_pipe"])
|
||||
assert result.exit_code != 0
|
||||
assert "Invalid pair format" in result.output
|
||||
|
||||
def test_failure_exits_nonzero(self) -> None:
|
||||
from click.testing import CliRunner
|
||||
|
||||
|
||||
@@ -132,13 +132,12 @@ class TestMain:
|
||||
assert "VIKUNJA_TOKEN" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||
def test_no_task_id_non_release_warns(self) -> None:
|
||||
"""Non-release commits without DEVX-N prefix should warn, not fail."""
|
||||
def test_no_task_id_non_release_fails(self) -> None:
|
||||
"""Non-release commits without DEVX-N prefix should fail."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["fix: resolve bug"])
|
||||
assert result.exit_code == 0
|
||||
assert result.exit_code != 0
|
||||
assert "No task ID" in result.output
|
||||
assert "Skipping" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||
def test_release_commit_without_task_id_skips(self) -> None:
|
||||
@@ -179,8 +178,8 @@ class TestMain:
|
||||
|
||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||
@patch("devx.ci.post_merge.VikunjaClient")
|
||||
def test_post_comment_failure_warns(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Vikunja API errors should warn, not fail — the merge already succeeded."""
|
||||
def test_post_comment_failure_fails(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Vikunja API errors should fail — the task was not updated."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.list_project_tasks.return_value = [
|
||||
{"id": 267, "identifier": "DEVX-20"},
|
||||
@@ -189,14 +188,13 @@ class TestMain:
|
||||
mock_client_cls.return_value = mock_client
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["DEVX-20: fix: bug"])
|
||||
assert result.exit_code == 0
|
||||
assert "Warning" in result.output
|
||||
assert "not updated" in result.output.lower()
|
||||
assert result.exit_code != 0
|
||||
assert "Vikunja API error" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
|
||||
@patch("devx.ci.post_merge.VikunjaClient")
|
||||
def test_mark_done_failure_warns(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Vikunja API errors should warn, not fail — the merge already succeeded."""
|
||||
def test_mark_done_failure_fails(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Vikunja API errors should fail — the task was not updated."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.list_project_tasks.return_value = [
|
||||
{"id": 267, "identifier": "DEVX-20"},
|
||||
@@ -206,9 +204,8 @@ class TestMain:
|
||||
mock_client_cls.return_value = mock_client
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["DEVX-20: fix: bug"])
|
||||
assert result.exit_code == 0
|
||||
assert "Warning" in result.output
|
||||
assert "not updated" in result.output.lower()
|
||||
assert result.exit_code != 0
|
||||
assert "Vikunja API error" in result.output
|
||||
|
||||
|
||||
class TestGetGitCommitMessage:
|
||||
|
||||
@@ -84,6 +84,13 @@ class TestGetBumpedVersion:
|
||||
with pytest.raises(click.ClickException):
|
||||
get_bumped_version()
|
||||
|
||||
@patch("devx.ci.release.run_cmd")
|
||||
def test_invalid_version_format_raises(self, mock_run_cmd: MagicMock) -> None:
|
||||
"""Non-semver version from git-cliff should raise."""
|
||||
mock_run_cmd.return_value = MagicMock(returncode=0, stdout="not-a-version\n", stderr="")
|
||||
with pytest.raises(click.ClickException, match="invalid version format"):
|
||||
get_bumped_version()
|
||||
|
||||
|
||||
class TestGetChangelog:
|
||||
@patch("devx.ci.release.run_cmd")
|
||||
@@ -318,12 +325,15 @@ class TestMain:
|
||||
@patch.dict("os.environ", {})
|
||||
@patch("devx.ci.release.has_user_facing_changes", return_value=True)
|
||||
@patch("devx.ci.release.run_cmd")
|
||||
def test_release_lock_skips_when_head_is_release_commit(self, mock_run_cmd: MagicMock, mock_uf: MagicMock) -> None:
|
||||
"""If HEAD is already a release commit, should skip to prevent duplicate releases."""
|
||||
# First call: git rev-parse (master), second: git log -1 (release commit)
|
||||
def test_release_lock_skips_when_head_is_release_commit_and_tag_exists(
|
||||
self, mock_run_cmd: MagicMock, mock_uf: MagicMock
|
||||
) -> None:
|
||||
"""If HEAD is a release commit and the tag exists, skip."""
|
||||
# git rev-parse, git log -1, git tag -l (tag exists)
|
||||
mock_run_cmd.side_effect = [
|
||||
MagicMock(returncode=0, stdout="master\n", stderr=""),
|
||||
MagicMock(returncode=0, stdout="release: v0.5.0\n", stderr=""),
|
||||
MagicMock(returncode=0, stdout="v0.5.0\n", stderr=""), # tag -l finds tag
|
||||
]
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, [])
|
||||
@@ -331,6 +341,27 @@ class TestMain:
|
||||
assert "already a release commit" in result.output
|
||||
assert "Skipping" in result.output
|
||||
|
||||
@patch.dict("os.environ", {})
|
||||
@patch("devx.ci.release.get_changelog", return_value="## changelog")
|
||||
@patch("devx.ci.release.create_and_push_tag", return_value=True)
|
||||
@patch("devx.ci.release.run_cmd")
|
||||
def test_release_lock_recovers_when_tag_missing(
|
||||
self, mock_run_cmd: MagicMock, mock_create_tag: MagicMock, mock_changelog: MagicMock
|
||||
) -> None:
|
||||
"""If HEAD is a release commit but the tag is missing, create the tag."""
|
||||
# git rev-parse, git log -1, git tag -l (tag NOT found)
|
||||
mock_run_cmd.side_effect = [
|
||||
MagicMock(returncode=0, stdout="master\n", stderr=""),
|
||||
MagicMock(returncode=0, stdout="release: v0.5.0\n", stderr=""),
|
||||
MagicMock(returncode=0, stdout="", stderr=""), # tag -l finds nothing
|
||||
]
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, [])
|
||||
assert result.exit_code == 0
|
||||
assert "tag v0.5.0 is missing" in result.output
|
||||
assert "Recovering" in result.output
|
||||
mock_create_tag.assert_called_once_with("0.5.0", "## changelog", False)
|
||||
|
||||
@patch.dict("os.environ", {})
|
||||
@patch("devx.ci.release.has_user_facing_changes", return_value=True)
|
||||
@patch("devx.ci.release.has_unreleased_changes", return_value=False)
|
||||
@@ -360,7 +391,7 @@ class TestMain:
|
||||
@patch("devx.ci.release.get_bumped_version", return_value="0.2.0")
|
||||
@patch("devx.ci.release.has_unreleased_changes", return_value=True)
|
||||
@patch("devx.ci.release.run_cmd")
|
||||
def test_dry_run_empty_changelog(
|
||||
def test_dry_run_empty_changelog_fails(
|
||||
self,
|
||||
mock_run_cmd: MagicMock,
|
||||
mock_has: MagicMock,
|
||||
@@ -373,11 +404,12 @@ class TestMain:
|
||||
mock_tag: MagicMock,
|
||||
mock_user: MagicMock,
|
||||
) -> None:
|
||||
"""Empty changelog should fail, not warn."""
|
||||
mock_run_cmd.return_value = MagicMock(returncode=0, stdout="master\n", stderr="")
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--dry-run"])
|
||||
assert result.exit_code == 0
|
||||
assert "empty changelog" in result.output
|
||||
assert result.exit_code != 0
|
||||
assert "empty changelog" in result.output.lower()
|
||||
|
||||
@patch.dict("os.environ", {})
|
||||
@patch("devx.ci.release.has_user_facing_changes", return_value=True)
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import click
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
@@ -63,6 +64,22 @@ class TestLoadMapping:
|
||||
with pytest.raises(FileNotFoundError):
|
||||
load_mapping()
|
||||
|
||||
def test_non_dict_mapping_raises(self, tmp_path: Path) -> None:
|
||||
"""Non-dict mapping.json should raise."""
|
||||
mapping_file = tmp_path / "mapping.json"
|
||||
mapping_file.write_text('["not", "a", "dict"]')
|
||||
with patch("devx.ci.sync_wiki.MAPPING_FILE", mapping_file):
|
||||
with pytest.raises(click.ClickException, match="must be a dict"):
|
||||
load_mapping()
|
||||
|
||||
def test_non_string_values_raise(self, tmp_path: Path) -> None:
|
||||
"""Non-string values in mapping.json should raise."""
|
||||
mapping_file = tmp_path / "mapping.json"
|
||||
mapping_file.write_text('{"file.md": 123}')
|
||||
with patch("devx.ci.sync_wiki.MAPPING_FILE", mapping_file):
|
||||
with pytest.raises(click.ClickException, match="must be strings"):
|
||||
load_mapping()
|
||||
|
||||
|
||||
class TestReadDocContent:
|
||||
def test_reads_file(self, tmp_path: Path) -> None:
|
||||
@@ -332,8 +349,8 @@ class TestMain:
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_file_not_found_warning(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that missing doc files are skipped with a warning."""
|
||||
def test_file_not_found_fails(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that missing doc files cause an error, not a warning."""
|
||||
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
|
||||
mock_mapping.exists.return_value = True
|
||||
with patch("devx.ci.sync_wiki.load_mapping", return_value={"missing.md": "Missing"}):
|
||||
@@ -341,14 +358,13 @@ class TestMain:
|
||||
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={}):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--dry-run", "--repo", "owner/repo"])
|
||||
assert result.exit_code == 0
|
||||
assert result.exit_code != 0
|
||||
assert "not found" in result.output
|
||||
assert "Skipped: 1" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_empty_doc_file_skipped(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that empty doc files are skipped with a warning."""
|
||||
def test_empty_doc_file_fails(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that empty doc files cause an error, not a warning."""
|
||||
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
|
||||
mock_mapping.exists.return_value = True
|
||||
with patch("devx.ci.sync_wiki.load_mapping", return_value={"empty.md": "Empty-Page"}):
|
||||
@@ -356,9 +372,8 @@ class TestMain:
|
||||
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={}):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--dry-run", "--repo", "owner/repo"])
|
||||
assert result.exit_code == 0
|
||||
assert result.exit_code != 0
|
||||
assert "empty" in result.output.lower()
|
||||
assert "Skipped: 1" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
|
||||
Reference in New Issue
Block a user