diff --git a/.taskid b/.taskid index 3f03528..674daa1 100644 --- a/.taskid +++ b/.taskid @@ -1 +1 @@ -DEVX-6 +DEVX-7 diff --git a/src/devx/api_clients.py b/src/devx/api_clients.py index e5cf4b0..b0922a3 100644 --- a/src/devx/api_clients.py +++ b/src/devx/api_clients.py @@ -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 diff --git a/src/devx/ci/auto_merge.py b/src/devx/ci/auto_merge.py index ae91a29..45d051e 100644 --- a/src/devx/ci/auto_merge.py +++ b/src/devx/ci/auto_merge.py @@ -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: - 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, ) ) diff --git a/src/devx/ci/check_translations.py b/src/devx/ci/check_translations.py index b1e078b..1173fc4 100644 --- a/src/devx/ci/check_translations.py +++ b/src/devx/ci/check_translations.py @@ -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 diff --git a/src/devx/ci/discover_runners.py b/src/devx/ci/discover_runners.py index 1d7590c..4a68b1d 100644 --- a/src/devx/ci/discover_runners.py +++ b/src/devx/ci/discover_runners.py @@ -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 diff --git a/src/devx/ci/notify_failure.py b/src/devx/ci/notify_failure.py index 746b0aa..79d6356 100644 --- a/src/devx/ci/notify_failure.py +++ b/src/devx/ci/notify_failure.py @@ -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)) diff --git a/src/devx/ci/post_merge.py b/src/devx/ci/post_merge.py index 58d8b09..8814932 100644 --- a/src/devx/ci/post_merge.py +++ b/src/devx/ci/post_merge.py @@ -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( _( diff --git a/src/devx/ci/release.py b/src/devx/ci/release.py index d6917f9..9a0e799 100644 --- a/src/devx/ci/release.py +++ b/src/devx/ci/release.py @@ -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: @@ -333,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)) diff --git a/src/devx/ci/sync_wiki.py b/src/devx/ci/sync_wiki.py index 276cfe8..7623a5d 100644 --- a/src/devx/ci/sync_wiki.py +++ b/src/devx/ci/sync_wiki.py @@ -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": diff --git a/src/devx/molecule/distribute_molecule.py b/src/devx/molecule/distribute_molecule.py index 039b0f8..f40a8fd 100644 --- a/src/devx/molecule/distribute_molecule.py +++ b/src/devx/molecule/distribute_molecule.py @@ -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) diff --git a/src/devx/molecule/molecule_ci_guard.py b/src/devx/molecule/molecule_ci_guard.py index 7dd6555..b513f5e 100644 --- a/src/devx/molecule/molecule_ci_guard.py +++ b/src/devx/molecule/molecule_ci_guard.py @@ -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) diff --git a/src/devx/translations.json b/src/devx/translations.json index c8a62bd..bb29b30 100644 --- a/src/devx/translations.json +++ b/src/devx/translations.json @@ -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: : \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: : \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: : \n Получено: {subject}", "zh": "哎呀!master 分支提交在任务 ID 后必须遵循 conventional commit 格式。\n 预期格式: DEVX-N: : \n 实际: {subject}" }, - "Oops! Master branch commit must follow conventional format after task ID.\n Expected: GRM-N: : \n Got: {subject}": { - "en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: GRM-N: : \n Got: {subject}", - "bg": "Опа! Commit-ът в клона master трябва да следва конвенционален формат след идентификатора.\n Очаква се: GRM-N: : \n Получено: {subject}", - "de": "Ups! Master-Branch-Commit muss nach der Task-ID dem konventionellen Format folgen.\n Erwartet: GRM-N: : \n Erhalten: {subject}", - "ru": "Ой! Коммит в ветку master после ID задачи должен соответствовать conventional формату.\n Ожидается: GRM-N: : \n Получено: {subject}", - "zh": "哎呀!master 分支提交在任务 ID 后必须遵循 conventional commit 格式。\n 预期格式: GRM-N: : \n 实际: {subject}" - }, "Oops! Master branch commits must start with a task ID.\n Expected: DEVX-N: \n Got: {subject}": { "en": "Oops! Master branch commits must start with a task ID.\n Expected: DEVX-N: \n Got: {subject}", "bg": "Опа! Commit-ите в клона master трябва да започват с идентификатор на задача.\n Очаква се: DEVX-N: \n Получено: {subject}", @@ -391,28 +587,19 @@ "ru": "Ой! Коммиты в ветку master должны начинаться с ID задачи.\n Ожидается: DEVX-N: \n Получено: {subject}", "zh": "哎呀!master 分支的提交必须以任务 ID 开头。\n 预期格式: DEVX-N: \n 实际: {subject}" }, - "Oops! Master branch commits must start with a task ID.\n Expected: GRM-N: \n Got: {subject}": { - "en": "Oops! Master branch commits must start with a task ID.\n Expected: GRM-N: \n Got: {subject}", - "bg": "Опа! Commit-ите в клона master трябва да започват с идентификатор на задача.\n Очаква се: GRM-N: \n Получено: {subject}", - "de": "Ups! Master-Branch-Commits müssen mit einer Task-ID beginnen.\n Erwartet: GRM-N: \n Erhalten: {subject}", - "ru": "Ой! Коммиты в ветку master должны начинаться с ID задачи.\n Ожидается: GRM-N: \n Получено: {subject}", - "zh": "哎呀!master 分支的提交必须以任务 ID 开头。\n 预期格式: GRM-N: \n 实际: {subject}" - }, "Oops! No task ID found in .taskid file or branch name '{branch}'.": { - "en": "Oops! No task ID found in .taskid file or branch name '{branch}'." - }, - "Oops! PR title must follow format 'DEVX-N: '.\n Expected: {task_id}: \n Got: {pr_title}": { - "en": "Oops! PR title must follow format 'DEVX-N: '.\n Expected: {task_id}: \n Got: {pr_title}", - "bg": "Опа! Заглавието на PR трябва да следва формата 'DEVX-N: <заглавие на задача>'.\n Очаква се: {task_id}: <заглавие на задача>\n Получено: {pr_title}", - "de": "Ups! PR-Titel muss dem Format 'DEVX-N: ' folgen.\n Erwartet: {task_id}: \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: '.\n Expected: {task_id}: \n Got: {pr_title}": { - "en": "Oops! PR title must follow format 'GRM-N: '.\n Expected: {task_id}: \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,21 +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: '.\n Expected: {task_id}: \n Got: {pr_title}": { - "en": "Oops! PR title must follow format '{prefix}-N: '.\n Expected: {task_id}: \n Got: {pr_title}" + "en": "Oops! PR title must follow format '{prefix}-N: '.\n Expected: {task_id}: \n Got: {pr_title}", + "bg": "Oops! PR title must follow format '{prefix}-N: '.\n Expected: {task_id}: \n Got: {pr_title}", + "de": "Oops! PR title must follow format '{prefix}-N: '.\n Expected: {task_id}: \n Got: {pr_title}", + "ru": "Oops! PR title must follow format '{prefix}-N: '.\n Expected: {task_id}: \n Got: {pr_title}", + "zh": "Oops! PR title must follow format '{prefix}-N: '.\n Expected: {task_id}: \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." + "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." + "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)." } } diff --git a/tests/unit/test_auto_merge.py b/tests/unit/test_auto_merge.py index f0d7248..1a8fdd9 100644 --- a/tests/unit/test_auto_merge.py +++ b/tests/unit/test_auto_merge.py @@ -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") diff --git a/tests/unit/test_check_translations.py b/tests/unit/test_check_translations.py index 5ea406e..181f016 100644 --- a/tests/unit/test_check_translations.py +++ b/tests/unit/test_check_translations.py @@ -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: diff --git a/tests/unit/test_discover_runners.py b/tests/unit/test_discover_runners.py index 3450c45..5f22324 100644 --- a/tests/unit/test_discover_runners.py +++ b/tests/unit/test_discover_runners.py @@ -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) diff --git a/tests/unit/test_distribute_molecule.py b/tests/unit/test_distribute_molecule.py index 578c6d6..b63614a 100644 --- a/tests/unit/test_distribute_molecule.py +++ b/tests/unit/test_distribute_molecule.py @@ -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 diff --git a/tests/unit/test_molecule_ci_guard.py b/tests/unit/test_molecule_ci_guard.py index 514350c..c879a77 100644 --- a/tests/unit/test_molecule_ci_guard.py +++ b/tests/unit/test_molecule_ci_guard.py @@ -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 diff --git a/tests/unit/test_post_merge.py b/tests/unit/test_post_merge.py index 3cc3a8e..260b230 100644 --- a/tests/unit/test_post_merge.py +++ b/tests/unit/test_post_merge.py @@ -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: diff --git a/tests/unit/test_release.py b/tests/unit/test_release.py index c15a9d5..41eff91 100644 --- a/tests/unit/test_release.py +++ b/tests/unit/test_release.py @@ -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") @@ -384,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, @@ -397,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) diff --git a/tests/unit/test_sync_wiki.py b/tests/unit/test_sync_wiki.py index 7b35dd9..3e21328 100644 --- a/tests/unit/test_sync_wiki.py +++ b/tests/unit/test_sync_wiki.py @@ -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")