Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d7bf7118a | ||
|
|
f98534ebe2 | ||
|
|
c62b168b25 | ||
|
|
40a94df029 | ||
|
|
f50c4c1e00 | ||
|
|
bbb264efc9 | ||
|
|
c97b249935 | ||
|
|
32b9a53151 | ||
|
|
ae68df63f1 | ||
|
|
6402f31345 | ||
|
|
f017fec8f5 | ||
|
|
add02273b6 | ||
|
|
e489fdb206 | ||
|
|
45a9c7d431 | ||
|
|
8e1c7d03a4 | ||
|
|
2de3ab4d84 | ||
|
|
fa501adfbc | ||
|
|
0a5625b70b |
@@ -55,8 +55,7 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
vale --minAlertLevel=error docs/ AGENTS.md README.md
|
||||
make devx-vale
|
||||
- name: Translation completeness check
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
|
||||
@@ -190,7 +190,7 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --strict
|
||||
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --verify
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
|
||||
@@ -18,19 +18,21 @@ venv activation automatically — always prefer `make <target>` over raw command
|
||||
|
||||
```bash
|
||||
make setup # Create venv, install deps, set up hooks, install CI tools
|
||||
make install-tools # Install actionlint, git-cliff, act_runner, tea, hadolint to ~/.local/bin
|
||||
make install-tools # Install actionlint, git-cliff, act_runner, tea, hadolint, vale to ~/.local/bin
|
||||
make lint-all # ruff + pyright + bandit + actionlint + lint-dockerfiles
|
||||
make pytest-cov # Unit tests with 100% coverage enforcement
|
||||
make test-unit # Unit tests without coverage
|
||||
make workflow-lint # Static lint of .gitea/workflows/*.yml (actionlint)
|
||||
make workflow-dryrun # Dry-run all workflows in Docker (act_runner exec --dryrun)
|
||||
make workflow-check # workflow-lint + workflow-dryrun
|
||||
make devx-check-doc-versions # Verify docs version refs match __version__
|
||||
make devx-vale # Run Vale prose linter on docs and README
|
||||
make clean # Remove caches, build artifacts, coverage data
|
||||
```
|
||||
|
||||
`make setup` automatically installs all development tools:
|
||||
- **Python deps** via `python -m devx.tools.setup` (pip install -e .[dev], pre-commit hooks)
|
||||
- **actionlint, git-cliff, act_runner, tea, hadolint** via `python -m devx.tools.install_tools` (CI/CD tools to ~/.local/bin)
|
||||
- **actionlint, git-cliff, act_runner, tea, hadolint, vale** via `python -m devx.tools.install_tools` (CI/CD tools to ~/.local/bin)
|
||||
- **tea CLI login** via `python -m devx.tools.setup` (configures `tea login` from `.env` `CI_GITEA_TOKEN`)
|
||||
|
||||
## Workflow Verification (Before Push)
|
||||
@@ -86,11 +88,12 @@ src/devx/
|
||||
│ ├── integration_guard.py # Run pytest with cross-runner fail-fast
|
||||
│ ├── check_translations.py # Translation completeness check
|
||||
│ ├── doc_coverage.py # Documentation coverage check
|
||||
│ └── lint_docs.py # Documentation linter (structure, links, headings)
|
||||
│ └── lint_docs.py # Documentation linter (structure, links, headings, code blocks, orphans)
|
||||
├── tools/ # Developer tooling modules (run locally or by CI)
|
||||
│ ├── setup.py # Environment setup (venv, deps, hooks)
|
||||
│ ├── install_tools.py # Install actionlint, git-cliff, act_runner, tea, hadolint
|
||||
│ ├── install_tools.py # Install actionlint, git-cliff, act_runner, tea, hadolint, vale
|
||||
│ ├── install_checkmake.py # Install checkmake (Makefile linter)
|
||||
│ ├── check_doc_versions.py # Verify docs version refs match __version__
|
||||
│ ├── build_image.py # Build and push Docker images to Gitea registry
|
||||
│ ├── clean_images.py # Clean up old Docker image versions from Gitea registry
|
||||
│ ├── check_test_speed.py # Measure unit test execution time
|
||||
|
||||
@@ -2,6 +2,54 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.36.0] - 2026-07-07
|
||||
|
||||
### Features
|
||||
|
||||
- Add GiteaClient repo variable methods and parallelize pytest-cov
|
||||
|
||||
## [0.35.7] - 2026-07-06
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Use Gitea wiki dash-marker filename convention
|
||||
|
||||
## [0.35.6] - 2026-07-06
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add delay before wiki verification to avoid race condition
|
||||
|
||||
## [0.35.5] - 2026-07-06
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Embed token in wiki clone URL for push auth
|
||||
|
||||
## [0.35.4] - 2026-07-06
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Configure git identity before commit in sync_wiki
|
||||
|
||||
## [0.35.3] - 2026-07-06
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Replace --strict with --verify for sync_wiki
|
||||
|
||||
## [0.35.2] - 2026-07-06
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Exclude .vale directory from lint_docs scanning
|
||||
|
||||
## [0.35.1] - 2026-07-06
|
||||
|
||||
### Refactor
|
||||
|
||||
- Rewrite sync_wiki.py to use git-based approach
|
||||
|
||||
## [0.35.0] - 2026-07-06
|
||||
|
||||
### Features
|
||||
|
||||
@@ -87,7 +87,7 @@ extra index and list devx in your dependencies:
|
||||
```toml
|
||||
[project]
|
||||
dependencies = [
|
||||
"devx>=0.35.0",
|
||||
"devx>=0.36.0",
|
||||
]
|
||||
|
||||
[tool.pip]
|
||||
@@ -101,8 +101,8 @@ pip install -e .
|
||||
```
|
||||
|
||||
> **Note:** If your project requires a specific devx version, pin it in
|
||||
> `dependencies` (for example, `"devx==0.35.0"`) or use a version constraint
|
||||
> (for example, `"devx>=0.35.0,<0.36"`).
|
||||
> `dependencies` (for example, `"devx==0.36.0"`) or use a version constraint
|
||||
> (for example, `"devx>=0.36.0,<0.37"`).
|
||||
|
||||
### Optional extras
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ RUN pip install --no-cache-dir /tmp/devx[lint] \
|
||||
&& rm -rf /tmp/devx
|
||||
|
||||
# Install CI/CD binary tools
|
||||
RUN python3 -m devx.tools.install_tools --tool actionlint \
|
||||
RUN python3 -m devx.tools.install_tools --tool actionlint --tool vale \
|
||||
&& python3 -m devx.tools.install_checkmake
|
||||
|
||||
# Install hadolint (Dockerfile linter)
|
||||
|
||||
+2
-2
@@ -74,14 +74,14 @@ Add devx to your `pyproject.toml` dependencies and configure the registry:
|
||||
```toml
|
||||
[project]
|
||||
dependencies = [
|
||||
"devx>=0.35.0",
|
||||
"devx>=0.36.0",
|
||||
]
|
||||
|
||||
[tool.pip]
|
||||
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
|
||||
```
|
||||
|
||||
Pin a specific version if needed: `"devx==0.35.0"` or `"devx>=0.35.0,<0.36"`.
|
||||
Pin a specific version if needed: `"devx==0.36.0"` or `"devx>=0.36.0,<0.37"`.
|
||||
|
||||
### Optional extras
|
||||
|
||||
|
||||
@@ -122,7 +122,9 @@ exponential backoff (2s, 4s, 8s).
|
||||
- Labels (list, create, add to issues)
|
||||
- Issues (create, list)
|
||||
- Pull requests (get commits, merge, create review)
|
||||
- Releases (list)
|
||||
- Releases (list, create idempotent)
|
||||
- Actions (list runs, list jobs, get job logs)
|
||||
- Actions variables (get, set idempotent)
|
||||
- Wiki pages (list, fetch, create, update, delete)
|
||||
|
||||
**`VikunjaClient`** — Vikunja REST API wrapper:
|
||||
|
||||
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
|
||||
```toml
|
||||
[project]
|
||||
dependencies = [
|
||||
"devx>=0.35.0",
|
||||
"devx>=0.36.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"devx>=0.35.0",
|
||||
"devx>=0.36.0",
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
||||
|
||||
__version__ = "0.35.0"
|
||||
__version__ = "0.36.0"
|
||||
|
||||
@@ -372,6 +372,35 @@ class GiteaClient:
|
||||
r = self._request("GET", f"/actions/jobs/{job_id}/logs")
|
||||
return r.text
|
||||
|
||||
# -- actions variables (repo-level) --
|
||||
|
||||
def get_repo_variable(self, name: str) -> str | None:
|
||||
"""Read a Gitea Actions repository variable.
|
||||
|
||||
Returns the variable value, or ``None`` if the variable is not set.
|
||||
Raises :class:`APIError` on other HTTP errors.
|
||||
"""
|
||||
try:
|
||||
r = self._request("GET", f"/actions/variables/{name}")
|
||||
return r.json().get("value")
|
||||
except APIError as e:
|
||||
if e.status == 404:
|
||||
return None
|
||||
raise
|
||||
|
||||
def set_repo_variable(self, name: str, value: str) -> None:
|
||||
"""Create or update a Gitea Actions repository variable (idempotent).
|
||||
|
||||
Tries PATCH first; if the variable doesn't exist (404), creates it
|
||||
via POST.
|
||||
"""
|
||||
try:
|
||||
self._request("PATCH", f"/actions/variables/{name}", json={"value": value})
|
||||
except APIError as e:
|
||||
if e.status != 404:
|
||||
raise
|
||||
self._request("POST", "/actions/variables", json={"name": name, "value": value})
|
||||
|
||||
|
||||
class VikunjaClient:
|
||||
"""Low-level Vikunja REST API client with connection pooling."""
|
||||
|
||||
@@ -185,6 +185,7 @@ def main(translations: tuple[Path, ...], source_dir: str | None) -> None:
|
||||
# Try common locations
|
||||
candidates = [
|
||||
root / "src" / "devx" / "translations.json",
|
||||
root / "src" / "grm" / "translations.json",
|
||||
]
|
||||
# Also search for any translations.json in src/
|
||||
for match in root.glob("src/*/translations.json"):
|
||||
|
||||
@@ -85,6 +85,7 @@ _EXCLUDE_DIRS = {
|
||||
".pytest_cache",
|
||||
".devin",
|
||||
".terraform",
|
||||
".vale",
|
||||
"site-packages",
|
||||
"dist-info",
|
||||
}
|
||||
|
||||
+253
-315
@@ -1,17 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sync documentation from /docs/ to the Gitea wiki via API.
|
||||
"""Sync documentation from /docs/ to the Gitea wiki via Git.
|
||||
|
||||
Reads markdown files from the ``docs/`` directory, uses ``mapping.json`` to
|
||||
map file paths to wiki page titles, and creates/updates wiki pages via the
|
||||
Gitea API. Pages that exist in the wiki but not in the mapping are left
|
||||
untouched (not deleted).
|
||||
Instead of using the Gitea wiki API (which is slow, unreliable, and
|
||||
prone to timeouts), this module clones the wiki Git repository,
|
||||
copies the documentation files into it, transforms internal links
|
||||
to wiki-friendly format, commits, and pushes.
|
||||
|
||||
Gitea 1.26 wiki API endpoints (all use content_base64, NOT content):
|
||||
- Create: POST /repos/{owner}/{repo}/wiki/new {title, content_base64, message}
|
||||
- Update: PATCH /repos/{owner}/{repo}/wiki/page/{sub_url} {title, content_base64, message}
|
||||
- List: GET /repos/{owner}/{repo}/wiki/pages → [{title, sub_url, ...}]
|
||||
- Fetch: GET /repos/{owner}/{repo}/wiki/page/{sub_url} → {title, content_base64, ...}
|
||||
- Delete: DELETE /repos/{owner}/{repo}/wiki/page/{sub_url}
|
||||
This approach is:
|
||||
- **Faster** — a single git push vs N API calls
|
||||
- **More reliable** — no API timeouts or rate limits
|
||||
- **Atomic** — all pages sync in one commit
|
||||
- **Auto-pruning** — stale wiki pages are removed automatically
|
||||
|
||||
The wiki Git URL is ``{clone_url}.wiki.git`` (Gitea convention).
|
||||
|
||||
Link transformations:
|
||||
- ``[text](file.md)`` → ``[text](file)`` (wiki pages don't use .md)
|
||||
- ``[text](docs/file.md)`` → ``[text](file)``
|
||||
- External links (http/https/mailto) are preserved
|
||||
- Anchor-only links (``#section``) are preserved
|
||||
|
||||
Usage:
|
||||
CI_GITEA_TOKEN=<token> python3 -m devx.ci.sync_wiki [--dry-run] [--repo owner/repo]
|
||||
@@ -19,44 +26,49 @@ Usage:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess # nosec B404
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
import click
|
||||
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
|
||||
from tenacity import (
|
||||
before_sleep_log,
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
)
|
||||
|
||||
from devx.api_clients import GiteaClient
|
||||
from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER
|
||||
from devx.exceptions import APIError
|
||||
from devx.i18n import _
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# DOCS_DIR is the repo's docs/ directory. When devx is installed as a
|
||||
# package (e.g., in .venv/lib/python3.12/site-packages/devx/), the
|
||||
# __file__-relative path would point inside the venv, not the repo.
|
||||
# Use DEVX_DOCS_DIR env var if set, otherwise fall back to ./docs
|
||||
# (relative to the current working directory, which is the repo root
|
||||
# in CI and local development).
|
||||
DOCS_DIR = Path(os.environ.get("DEVX_DOCS_DIR", "docs"))
|
||||
MAPPING_FILE = DOCS_DIR / "mapping.json"
|
||||
|
||||
# Markdown link pattern: [text](url)
|
||||
_LINK_RE = re.compile(r"\[([^\]]*)\]\(([^)]+)\)")
|
||||
|
||||
|
||||
def wiki_filename(page_title: str) -> str:
|
||||
"""Convert a wiki page title to its Gitea wiki filename.
|
||||
|
||||
Gitea uses a "dash marker" (``.-``) suffix to distinguish literal dashes
|
||||
from space-to-dash conversions. See Gitea's ``services/wiki/wiki_path.go``.
|
||||
|
||||
- "Architecture" (no dashes) → ``Architecture.md``
|
||||
- "Getting-Started" (has dashes) → ``Getting-Started.-.md``
|
||||
- "Home" (no dashes) → ``Home.md``
|
||||
"""
|
||||
name = page_title.replace(" ", "-")
|
||||
if "-" in name:
|
||||
name += ".-"
|
||||
name += ".md"
|
||||
return quote(name, safe="")
|
||||
|
||||
|
||||
def load_mapping() -> dict[str, str]:
|
||||
"""Load the file-to-wiki-page mapping from mapping.json.
|
||||
|
||||
Validates that the mapping is a dict of string-to-string pairs.
|
||||
"""
|
||||
"""Load the file-to-wiki-page mapping from mapping.json."""
|
||||
with open(MAPPING_FILE, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
@@ -69,214 +81,186 @@ def load_mapping() -> dict[str, str]:
|
||||
return data
|
||||
|
||||
|
||||
def read_doc_content(file_path: str) -> str:
|
||||
"""Read markdown content from a docs file."""
|
||||
full_path = DOCS_DIR / file_path
|
||||
with open(full_path, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
def transform_links(content: str) -> str:
|
||||
"""Transform markdown links from file-based to wiki-friendly format.
|
||||
|
||||
|
||||
def encode_content(content: str) -> str:
|
||||
"""Encode content as base64 for the Gitea wiki API.
|
||||
|
||||
The Gitea wiki API requires content_base64, not plain content.
|
||||
Sending plain content silently fails (pages are created/updated
|
||||
but with empty content).
|
||||
- ``[text](file.md)`` → ``[text](file)``
|
||||
- ``[text](docs/file.md)`` → ``[text](file)``
|
||||
- ``[text](../file.md)`` → ``[text](file)``
|
||||
- External links (http/https/mailto) preserved
|
||||
- Anchor-only links (``#section``) preserved
|
||||
"""
|
||||
return base64.b64encode(content.encode("utf-8")).decode("ascii")
|
||||
|
||||
def replace_link(match: re.Match[str]) -> str:
|
||||
text = match.group(1)
|
||||
url = match.group(2).strip()
|
||||
# Skip external links and mailto
|
||||
if url.startswith(("http://", "https://", "mailto:")):
|
||||
return match.group(0)
|
||||
# Skip anchor-only links
|
||||
if url.startswith("#"):
|
||||
return match.group(0)
|
||||
# Split path and anchor
|
||||
if "#" in url:
|
||||
path_part, anchor = url.split("#", 1)
|
||||
anchor = f"#{anchor}"
|
||||
else:
|
||||
path_part, anchor = url, ""
|
||||
# Remove .md extension and directory prefixes
|
||||
if path_part.endswith(".md"):
|
||||
path_part = path_part[:-3]
|
||||
# Remove directory prefix (docs/, ../, etc.)
|
||||
path_part = path_part.split("/")[-1]
|
||||
return f"[{text}]({path_part}{anchor})"
|
||||
|
||||
return _LINK_RE.sub(replace_link, content)
|
||||
|
||||
|
||||
def decode_content(content_b64: str) -> str:
|
||||
"""Decode base64 content from the Gitea wiki API."""
|
||||
if not content_b64:
|
||||
return ""
|
||||
return base64.b64decode(content_b64).decode("utf-8")
|
||||
def get_wiki_clone_url(owner: str, repo: str, token: str) -> str:
|
||||
"""Build the wiki Git clone URL with token auth."""
|
||||
# Gitea wiki repos are at {clone_url}.wiki.git
|
||||
# Extract base URL from API URL
|
||||
base = GITEA_API_URL.rsplit("/api/v1", 1)[0]
|
||||
# Embed token in URL for both clone and push auth
|
||||
# Format: https://token@host/owner/repo.wiki.git
|
||||
parsed = urlparse(base)
|
||||
return f"{parsed.scheme}://{token}@{parsed.hostname}/{owner}/{repo}.wiki.git"
|
||||
|
||||
|
||||
def list_wiki_pages(client: GiteaClient) -> dict[str, str]:
|
||||
"""List existing wiki pages, returning {title: sub_url}.
|
||||
def clone_wiki(wiki_url: str, dest: Path) -> bool:
|
||||
"""Clone the wiki repo into dest. Returns True if clone succeeded.
|
||||
|
||||
Raises :class:`APIError` if the wiki API is unavailable — the caller
|
||||
is responsible for retrying or handling the failure.
|
||||
If the wiki repo doesn't exist yet (no pages created), returns False.
|
||||
"""
|
||||
pages = client._request("GET", "/wiki/pages").json()
|
||||
return {page.get("title", ""): page.get("sub_url", page.get("title", "")) for page in pages}
|
||||
|
||||
|
||||
def fetch_page_content(client: GiteaClient, sub_url: str) -> str:
|
||||
"""Fetch a wiki page's content by sub_url, decoded from base64."""
|
||||
try:
|
||||
page = client._request("GET", f"/wiki/page/{sub_url}").json()
|
||||
return decode_content(page.get("content_base64", ""))
|
||||
except APIError:
|
||||
return ""
|
||||
|
||||
|
||||
def sync_page(
|
||||
client: GiteaClient,
|
||||
page_title: str,
|
||||
content: str,
|
||||
existing_pages: dict[str, str],
|
||||
dry_run: bool,
|
||||
) -> str:
|
||||
"""Create or update a single wiki page.
|
||||
|
||||
Returns "created", "updated", or "skipped" (if dry-run).
|
||||
|
||||
If a create fails with HTTP 400 "already exists" (the page list was
|
||||
stale), re-lists the wiki and falls back to an update.
|
||||
"""
|
||||
if dry_run:
|
||||
click.echo(_("[dry-run] Would sync page: {title} ({chars} chars)", title=page_title, chars=len(content)))
|
||||
return "skipped"
|
||||
|
||||
content_b64 = encode_content(content)
|
||||
|
||||
if page_title in existing_pages:
|
||||
# Update existing page via PATCH
|
||||
sub_url = existing_pages[page_title]
|
||||
client._request(
|
||||
"PATCH",
|
||||
f"/wiki/page/{sub_url}",
|
||||
json={
|
||||
"title": page_title,
|
||||
"content_base64": content_b64,
|
||||
"message": f"Sync from docs/ — update {page_title}",
|
||||
},
|
||||
)
|
||||
return "updated"
|
||||
|
||||
# Create new page via POST /wiki/new
|
||||
try:
|
||||
client._request(
|
||||
"POST",
|
||||
"/wiki/new",
|
||||
json={
|
||||
"title": page_title,
|
||||
"content_base64": content_b64,
|
||||
"message": f"Sync from docs/ — create {page_title}",
|
||||
},
|
||||
)
|
||||
return "created"
|
||||
except APIError as e:
|
||||
if e.status == 400 and "already exists" in e.message.lower():
|
||||
# The page list was stale (e.g. after a timeout-retry returned
|
||||
# incomplete data). Re-list and fall back to update.
|
||||
click.echo(_(" Page '{title}' already exists (stale list). Re-listing and updating...", title=page_title))
|
||||
fresh_pages = _list_wiki_pages_with_retry(client)
|
||||
if page_title in fresh_pages:
|
||||
sub_url = fresh_pages[page_title]
|
||||
client._request(
|
||||
"PATCH",
|
||||
f"/wiki/page/{sub_url}",
|
||||
json={
|
||||
"title": page_title,
|
||||
"content_base64": content_b64,
|
||||
"message": f"Sync from docs/ — update {page_title} (create→update fallback)",
|
||||
},
|
||||
)
|
||||
return "updated"
|
||||
raise
|
||||
|
||||
|
||||
def verify_wiki_page(
|
||||
client: GiteaClient, page_title: str, expected_content: str, existing_pages: dict[str, str]
|
||||
) -> bool:
|
||||
"""Verify that a wiki page has non-empty content matching the docs.
|
||||
|
||||
Returns True if the page content matches, False otherwise.
|
||||
"""
|
||||
if page_title not in existing_pages:
|
||||
return False
|
||||
sub_url = existing_pages[page_title]
|
||||
actual = fetch_page_content(client, sub_url)
|
||||
return actual.strip() == expected_content.strip()
|
||||
|
||||
|
||||
def _list_wiki_pages_with_retry(client: GiteaClient) -> dict[str, str]:
|
||||
"""List wiki pages with tenacity retry on APIError.
|
||||
|
||||
The Gitea wiki API can be slow (it renders pages on each request)
|
||||
and may time out. Uses 5 attempts with exponential backoff to handle
|
||||
transient slowness.
|
||||
"""
|
||||
_logger = logging.getLogger("sync_wiki")
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(5),
|
||||
wait=wait_exponential(multiplier=2, min=2, max=16),
|
||||
retry=retry_if_exception_type(APIError),
|
||||
before_sleep=before_sleep_log(_logger, logging.WARNING),
|
||||
reraise=True,
|
||||
result = subprocess.run( # nosec
|
||||
["git", "clone", "--depth", "1", wiki_url, str(dest)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
def _do_list() -> dict[str, str]:
|
||||
return list_wiki_pages(client)
|
||||
|
||||
return _do_list()
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def verify_wiki_integrity(
|
||||
client: GiteaClient,
|
||||
def init_wiki(dest: Path) -> None:
|
||||
"""Initialize a fresh wiki repo (when clone fails)."""
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(["git", "init"], cwd=dest, capture_output=True, check=True) # nosec
|
||||
subprocess.run( # nosec
|
||||
["git", "config", "user.email", "ci@oblachno.fyi"],
|
||||
cwd=dest,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
subprocess.run( # nosec
|
||||
["git", "config", "user.name", "CI Wiki Sync"],
|
||||
cwd=dest,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def sync_files(
|
||||
docs_dir: Path,
|
||||
wiki_dir: Path,
|
||||
mapping: dict[str, str],
|
||||
synced: dict[str, str],
|
||||
) -> list[str]:
|
||||
"""Comprehensive wiki verification.
|
||||
dry_run: bool,
|
||||
) -> tuple[int, int]:
|
||||
"""Copy docs files to wiki dir with link transformation.
|
||||
|
||||
Checks:
|
||||
1. Every mapped page exists in the wiki
|
||||
2. Every mapped page has non-empty content
|
||||
3. Every mapped page's content matches the docs
|
||||
4. No stale pages exist in the wiki (pages not in mapping)
|
||||
5. Page count matches
|
||||
|
||||
Returns a list of failure messages (empty if all checks pass).
|
||||
If the wiki API is temporarily unavailable (all retry attempts
|
||||
fail), returns an empty list with a warning — the sync itself
|
||||
already succeeded, so a transient API outage should not fail the job.
|
||||
Returns (synced, pruned) counts.
|
||||
"""
|
||||
failures: list[str] = []
|
||||
synced = 0
|
||||
|
||||
try:
|
||||
existing_pages = _list_wiki_pages_with_retry(client)
|
||||
except APIError:
|
||||
click.echo(
|
||||
_(
|
||||
"WARNING: Could not fetch wiki page list after retries. "
|
||||
"The sync itself succeeded ({count} pages updated), but the "
|
||||
"integrity check could not verify them due to a transient API issue.",
|
||||
count=len(synced),
|
||||
)
|
||||
)
|
||||
return []
|
||||
# Build set of expected wiki filenames
|
||||
expected_files: set[str] = set()
|
||||
|
||||
expected_titles = set(mapping.values())
|
||||
for file_path, page_title in sorted(mapping.items()):
|
||||
src = docs_dir / file_path
|
||||
if not src.exists():
|
||||
click.echo(_(" WARN: Mapped file {file} not found, skipping", file=file_path))
|
||||
continue
|
||||
|
||||
# Check 1: Page count
|
||||
if len(existing_pages) != len(expected_titles):
|
||||
failures.append(f"Page count mismatch: wiki has {len(existing_pages)}, mapping has {len(expected_titles)}")
|
||||
content = src.read_text(encoding="utf-8")
|
||||
if not content.strip():
|
||||
click.echo(_(" WARN: Mapped file {file} is empty, skipping", file=file_path))
|
||||
continue
|
||||
|
||||
# Check 2: Missing pages (in mapping but not in wiki)
|
||||
missing = expected_titles - set(existing_pages.keys())
|
||||
for title in sorted(missing):
|
||||
failures.append(f"Missing page: {title}")
|
||||
# Transform links
|
||||
transformed = transform_links(content)
|
||||
|
||||
# Check 3: Stale pages (in wiki but not in mapping)
|
||||
stale = set(existing_pages.keys()) - expected_titles
|
||||
for title in sorted(stale):
|
||||
failures.append(f"Stale page (not in mapping): {title}")
|
||||
# Wiki filename: Gitea uses a dash-marker convention for titles with dashes
|
||||
fname = wiki_filename(page_title)
|
||||
expected_files.add(fname)
|
||||
|
||||
# Check 4: Content verification
|
||||
for page_title, expected_content in sorted(synced.items()):
|
||||
ok = verify_wiki_page(client, page_title, expected_content, existing_pages)
|
||||
if not ok:
|
||||
sub_url = existing_pages.get(page_title, "?")
|
||||
actual = fetch_page_content(client, sub_url)
|
||||
if not actual.strip():
|
||||
failures.append(f"Empty content: {page_title}")
|
||||
else:
|
||||
failures.append(f"Content mismatch: {page_title}")
|
||||
if not dry_run:
|
||||
dest = wiki_dir / fname
|
||||
dest.write_text(transformed, encoding="utf-8")
|
||||
synced += 1
|
||||
click.echo(_(" Synced: {title} → {file}", title=page_title, file=fname))
|
||||
|
||||
return failures
|
||||
# Prune stale pages (in wiki but not in mapping)
|
||||
pruned = 0
|
||||
if not dry_run:
|
||||
for existing in wiki_dir.glob("*.md"):
|
||||
if existing.name not in expected_files:
|
||||
existing.unlink()
|
||||
pruned += 1
|
||||
click.echo(_(" Pruned: {file} (not in mapping)", file=existing.name))
|
||||
|
||||
return synced, pruned
|
||||
|
||||
|
||||
def commit_and_push(wiki_dir: Path, wiki_url: str, dry_run: bool) -> bool:
|
||||
"""Commit changes and push to the wiki repo. Returns True if pushed."""
|
||||
if dry_run:
|
||||
click.echo(_("[dry-run] Would commit and push wiki changes"))
|
||||
return False
|
||||
|
||||
# Stage all changes
|
||||
subprocess.run(["git", "add", "-A"], cwd=wiki_dir, capture_output=True, check=True) # nosec
|
||||
|
||||
# Check if there are changes to commit
|
||||
result = subprocess.run( # nosec
|
||||
["git", "diff", "--cached", "--quiet"],
|
||||
cwd=wiki_dir,
|
||||
capture_output=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
click.echo(_("No changes to sync — wiki is up to date."))
|
||||
return False
|
||||
|
||||
# Commit — ensure git identity is configured (CI environments may lack it)
|
||||
subprocess.run( # nosec
|
||||
["git", "config", "user.email", "devin-ai-integration[bot]@users.noreply.github.com"],
|
||||
cwd=wiki_dir,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
subprocess.run( # nosec
|
||||
["git", "config", "user.name", "Devin CI"],
|
||||
cwd=wiki_dir,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
subprocess.run( # nosec
|
||||
["git", "commit", "-m", "Sync wiki from docs/ [skip ci]"],
|
||||
cwd=wiki_dir,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Push
|
||||
result = subprocess.run( # nosec
|
||||
["git", "push", "--force", wiki_url, "HEAD:master"],
|
||||
cwd=wiki_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
click.echo(_("Push failed: {error}", error=result.stderr))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -286,15 +270,10 @@ def verify_wiki_integrity(
|
||||
"--verify",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="After syncing, verify each page has non-empty content. Exit 1 if any page is empty or mismatched.",
|
||||
help="After syncing, verify each page exists in the wiki. Exit 1 if any page is missing.",
|
||||
)
|
||||
@click.option(
|
||||
"--strict",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Full integrity check: verify page count, missing pages, stale pages, and content. Implies --verify.",
|
||||
)
|
||||
def main(dry_run: bool, repo: str | None, verify: bool, strict: bool) -> None:
|
||||
def main(dry_run: bool, repo: str | None, verify: bool) -> None:
|
||||
"""Sync documentation to the Gitea wiki via Git."""
|
||||
token = os.environ.get("CI_GITEA_TOKEN", "")
|
||||
if not token:
|
||||
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set."))
|
||||
@@ -309,107 +288,66 @@ def main(dry_run: bool, repo: str | None, verify: bool, strict: bool) -> None:
|
||||
raise click.ClickException(_("ERROR: mapping.json not found at {path}", path=MAPPING_FILE))
|
||||
|
||||
mapping = load_mapping()
|
||||
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
|
||||
wiki_url = get_wiki_clone_url(owner, repo_name, token)
|
||||
|
||||
click.echo(_("Syncing {count} documentation pages to wiki...", count=len(mapping)))
|
||||
click.echo(_("Syncing {count} documentation pages to wiki via Git...", count=len(mapping)))
|
||||
|
||||
try:
|
||||
existing_pages = _list_wiki_pages_with_retry(client)
|
||||
except APIError as e:
|
||||
raise click.ClickException(
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
wiki_dir = Path(tmpdir) / "wiki"
|
||||
|
||||
click.echo(_("Cloning wiki repo..."))
|
||||
if clone_wiki(wiki_url, wiki_dir):
|
||||
click.echo(_("Cloned existing wiki."))
|
||||
else:
|
||||
click.echo(_("Wiki repo not found or empty — initializing fresh."))
|
||||
init_wiki(wiki_dir)
|
||||
|
||||
click.echo(_("Syncing files..."))
|
||||
synced, pruned = sync_files(DOCS_DIR, wiki_dir, mapping, dry_run)
|
||||
|
||||
click.echo(
|
||||
_(
|
||||
"Failed to list existing wiki pages after retries: {error}. "
|
||||
"Aborting to avoid creating duplicate pages.",
|
||||
error=e,
|
||||
"\nDone! Synced: {synced}, Pruned: {pruned}",
|
||||
synced=synced,
|
||||
pruned=pruned,
|
||||
)
|
||||
) from e
|
||||
if existing_pages:
|
||||
click.echo(_("Found {count} existing wiki pages.", count=len(existing_pages)))
|
||||
|
||||
created = 0
|
||||
updated = 0
|
||||
skipped = 0
|
||||
synced: dict[str, str] = {} # title -> content, for verification
|
||||
|
||||
for file_path, page_title in sorted(mapping.items()):
|
||||
try:
|
||||
content = read_doc_content(file_path)
|
||||
except FileNotFoundError:
|
||||
raise click.ClickException(
|
||||
_("Mapped file {file} not found. Update mapping.json or create the file.", file=file_path)
|
||||
) from None
|
||||
|
||||
if not content.strip():
|
||||
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":
|
||||
created += 1
|
||||
click.echo(_(" Created: {title}", title=page_title))
|
||||
elif result == "updated":
|
||||
updated += 1
|
||||
click.echo(_(" Updated: {title}", title=page_title))
|
||||
else:
|
||||
skipped += 1
|
||||
|
||||
synced[page_title] = content
|
||||
|
||||
click.echo(
|
||||
_(
|
||||
"\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
|
||||
created=created,
|
||||
updated=updated,
|
||||
skipped=skipped,
|
||||
)
|
||||
)
|
||||
|
||||
# --strict implies --verify
|
||||
do_verify = verify or strict
|
||||
if dry_run:
|
||||
click.echo(_("[dry-run] No changes pushed."))
|
||||
return
|
||||
|
||||
if do_verify and not dry_run:
|
||||
if strict:
|
||||
click.echo(_("\nRunning full wiki integrity check..."))
|
||||
failures = verify_wiki_integrity(client, mapping, synced)
|
||||
if failures:
|
||||
click.echo(_("\nIntegrity check FAILED ({count} issues):", count=len(failures)))
|
||||
for f in failures:
|
||||
click.echo(f" - {f}")
|
||||
raise click.ClickException(_("Wiki integrity check failed — {count} issue(s)", count=len(failures)))
|
||||
click.echo(_("\nIntegrity check passed — all {count} pages verified.", count=len(synced)))
|
||||
else:
|
||||
click.echo(_("\nVerifying wiki pages have content..."))
|
||||
# Re-fetch the page list to get updated sub_urls
|
||||
try:
|
||||
existing_pages = _list_wiki_pages_with_retry(client)
|
||||
except APIError:
|
||||
click.echo(
|
||||
_(
|
||||
"WARNING: Could not re-fetch wiki page list for verification. "
|
||||
"Skipping content verification due to transient API issue."
|
||||
)
|
||||
)
|
||||
return
|
||||
click.echo(_("Committing and pushing..."))
|
||||
pushed = commit_and_push(wiki_dir, wiki_url, dry_run)
|
||||
if pushed:
|
||||
click.echo(_("Wiki synced successfully."))
|
||||
elif not dry_run:
|
||||
click.echo(_("No push needed (no changes or push failed)."))
|
||||
|
||||
# Verification
|
||||
if verify and not dry_run:
|
||||
if pushed:
|
||||
click.echo(_("Waiting 5s for Gitea to process pushed commits..."))
|
||||
time.sleep(5)
|
||||
click.echo(_("\nVerifying wiki pages..."))
|
||||
# Re-clone to verify
|
||||
verify_dir = Path(tmpdir) / "verify"
|
||||
if not clone_wiki(wiki_url, verify_dir):
|
||||
click.echo(_("FAIL: Could not clone wiki for verification."))
|
||||
raise click.ClickException(_("Wiki verification failed — could not clone wiki"))
|
||||
failures = 0
|
||||
for page_title, expected_content in sorted(synced.items()):
|
||||
ok = verify_wiki_page(client, page_title, expected_content, existing_pages)
|
||||
if ok:
|
||||
click.echo(_(" OK: {title} ({chars} chars)", title=page_title, chars=len(expected_content)))
|
||||
for _file_path, page_title in sorted(mapping.items()):
|
||||
fname = wiki_filename(page_title)
|
||||
if (verify_dir / fname).exists():
|
||||
click.echo(_(" OK: {title}", title=page_title))
|
||||
else:
|
||||
click.echo(_(" FAIL: {title} — content mismatch or empty!", title=page_title))
|
||||
click.echo(_(" FAIL: {title} — page not found in wiki!", title=page_title))
|
||||
failures += 1
|
||||
if failures > 0:
|
||||
click.echo(
|
||||
_(
|
||||
"\nVerification FAILED: {failures} page(s) have empty or mismatched content!",
|
||||
failures=failures,
|
||||
)
|
||||
)
|
||||
raise click.ClickException(
|
||||
_("Wiki verification failed — {failures} page(s) empty or mismatched", failures=failures)
|
||||
_("Wiki verification failed — {failures} page(s) missing", failures=failures)
|
||||
)
|
||||
click.echo(_("\nVerification passed — all wiki pages have correct content."))
|
||||
click.echo(_("\nVerification passed — all wiki pages exist."))
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
|
||||
@@ -304,7 +304,7 @@ devx-test-unit:
|
||||
@$(DEVX_BIN)/pytest $(DEVX_TEST_PATHS) -q --no-cov
|
||||
|
||||
devx-pytest-cov:
|
||||
@$(DEVX_BIN)/pytest $(DEVX_TEST_PATHS) -v --cov=$(DEVX_COV_PKG) --cov-report=term-missing --cov-fail-under=100
|
||||
@$(DEVX_BIN)/pytest $(DEVX_TEST_PATHS) -n auto --cov=$(DEVX_COV_PKG) --cov-report=term-missing --cov-fail-under=100
|
||||
|
||||
# ── Quality checks ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -328,10 +328,14 @@ devx-check-docs:
|
||||
devx-check-doc-versions:
|
||||
@$(DEVX_PYTHON) -m devx.tools.check_doc_versions --root .
|
||||
|
||||
# Run Vale prose linter on docs and README
|
||||
# Run Vale prose linter on docs and README (skips if vale not installed)
|
||||
devx-vale:
|
||||
@export PATH="$$HOME/.local/bin:$$PATH" && \
|
||||
vale --minAlertLevel=error docs/ AGENTS.md README.md
|
||||
if ! command -v vale >/dev/null 2>&1; then \
|
||||
echo "[devx-vale] vale not installed — skipping (install with 'make install-tools')"; \
|
||||
else \
|
||||
vale --minAlertLevel=error docs/ AGENTS.md README.md; \
|
||||
fi
|
||||
|
||||
# Verify test suite timing
|
||||
devx-check-test-speed:
|
||||
|
||||
@@ -68,8 +68,8 @@ def detect_package_name(repo_root: Path) -> str | None:
|
||||
Looks for the first subdirectory under ``src/`` that contains
|
||||
an ``__init__.py`` file with ``__version__``.
|
||||
|
||||
Returns the package directory name (e.g., ``devx``) or ``None`` if
|
||||
no package is found.
|
||||
Returns the package directory name (e.g., ``devx``,
|
||||
``grm``) or ``None`` if no package is found.
|
||||
"""
|
||||
src_dir = repo_root / "src"
|
||||
if not src_dir.is_dir():
|
||||
|
||||
+816
-616
@@ -47,13 +47,13 @@
|
||||
"ru": "\nDoc coverage: {covered}/{total} ({pct}%)",
|
||||
"zh": "\nDoc coverage: {covered}/{total} ({pct}%)"
|
||||
},
|
||||
"\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}": {
|
||||
"bg": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
|
||||
"de": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
|
||||
"en": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
|
||||
"pl": "\nGotowe! Utworzono: {created}, Zaktualizowano: {updated}, Pominięto: {skipped}",
|
||||
"ru": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
|
||||
"zh": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}"
|
||||
"\nDone! Synced: {synced}, Pruned: {pruned}": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "\nDone! Synced: {synced}, Pruned: {pruned}",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"\nDone. Deleted {deleted}, kept {kept}, failed {failed}.": {
|
||||
"bg": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}.",
|
||||
@@ -71,6 +71,14 @@
|
||||
"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."
|
||||
},
|
||||
"\nFAIL: {n} stale version reference(s) found:": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "\nFAIL: {n} stale version reference(s) found:",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.": {
|
||||
"bg": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
|
||||
"de": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
|
||||
@@ -79,6 +87,14 @@
|
||||
"ru": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
|
||||
"zh": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report."
|
||||
},
|
||||
"\nFixed {n} stale version reference(s).": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "\nFixed {n} stale version reference(s).",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"\nGenerated {count} badges:": {
|
||||
"bg": "\nGenerated {count} badges:",
|
||||
"de": "\nGenerated {count} badges:",
|
||||
@@ -87,22 +103,6 @@
|
||||
"ru": "\nGenerated {count} badges:",
|
||||
"zh": "\nGenerated {count} badges:"
|
||||
},
|
||||
"\nIntegrity check FAILED ({count} issues):": {
|
||||
"bg": "\nIntegrity check FAILED ({count} issues):",
|
||||
"de": "\nIntegrity check FAILED ({count} issues):",
|
||||
"en": "\nIntegrity check FAILED ({count} issues):",
|
||||
"pl": "\nKontrola integralności NIEUDANA ({count} problemów):",
|
||||
"ru": "\nIntegrity check FAILED ({count} issues):",
|
||||
"zh": "\nIntegrity check FAILED ({count} issues):"
|
||||
},
|
||||
"\nIntegrity check passed — all {count} pages verified.": {
|
||||
"bg": "\nIntegrity check passed — all {count} pages verified.",
|
||||
"de": "\nIntegrity check passed — all {count} pages verified.",
|
||||
"en": "\nIntegrity check passed — all {count} pages verified.",
|
||||
"pl": "\nKontrola integralności zakończona pomyślnie — wszystkie {count} stron zweryfikowane.",
|
||||
"ru": "\nIntegrity check passed — all {count} pages verified.",
|
||||
"zh": "\nIntegrity check passed — all {count} pages verified."
|
||||
},
|
||||
"\nKeeping {kept}, would delete {count}": {
|
||||
"bg": "\nKeeping {kept}, would delete {count}",
|
||||
"de": "\nKeeping {kept}, would delete {count}",
|
||||
@@ -127,6 +127,22 @@
|
||||
"ru": "\nMissing documentation:",
|
||||
"zh": "\nMissing documentation:"
|
||||
},
|
||||
"\nNo stale version references found.": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "\nNo stale version references found.",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"\nPASS: All version references are current.": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "\nPASS: All version references are current.",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"\nResult: {status}": {
|
||||
"bg": "\nResult: {status}",
|
||||
"de": "\nResult: {status}",
|
||||
@@ -151,13 +167,13 @@
|
||||
"ru": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}'.",
|
||||
"zh": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}'."
|
||||
},
|
||||
"\nRunning full wiki integrity check...": {
|
||||
"bg": "\nRunning full wiki integrity check...",
|
||||
"de": "\nRunning full wiki integrity check...",
|
||||
"en": "\nRunning full wiki integrity check...",
|
||||
"pl": "\nUruchamianie pełnej kontroli integralności wiki...",
|
||||
"ru": "\nRunning full wiki integrity check...",
|
||||
"zh": "\nRunning full wiki integrity check..."
|
||||
"\nRun with --fix to auto-update version references.": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "\nRun with --fix to auto-update version references.",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"\nTag → Commit alignment:": {
|
||||
"bg": "\nTag → Commit alignment:",
|
||||
@@ -183,29 +199,21 @@
|
||||
"ru": "\nUser-facing changes ({count}):",
|
||||
"zh": "\nUser-facing changes ({count}):"
|
||||
},
|
||||
"\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!",
|
||||
"en": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!",
|
||||
"pl": "\nWeryfikacja NIEUDANA: {failures} strona(y) ma pustą lub niezgodną treść!",
|
||||
"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 exist.": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "\nVerification passed — all wiki pages exist.",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"\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.",
|
||||
"en": "\nVerification passed — all wiki pages have correct content.",
|
||||
"pl": "\nWeryfikacja zakończona pomyślnie — wszystkie strony wiki mają poprawną treść.",
|
||||
"ru": "\nVerification passed — all wiki pages have correct content.",
|
||||
"zh": "\nVerification passed — all wiki pages have correct content."
|
||||
},
|
||||
"\nVerifying wiki pages have content...": {
|
||||
"bg": "\nVerifying wiki pages have content...",
|
||||
"de": "\nVerifying wiki pages have content...",
|
||||
"en": "\nVerifying wiki pages have content...",
|
||||
"pl": "\nWeryfikowanie, czy strony wiki mają treść...",
|
||||
"ru": "\nVerifying wiki pages have content...",
|
||||
"zh": "\nVerifying wiki pages have content..."
|
||||
"\nVerifying wiki pages...": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "\nVerifying wiki pages...",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"\nWorkflow-only changes ({count}):": {
|
||||
"bg": "\nWorkflow-only changes ({count}):",
|
||||
@@ -351,6 +359,30 @@
|
||||
"ru": " - Требуемые проверки статуса: {checks}",
|
||||
"zh": " - 必需状态检查: {checks}"
|
||||
},
|
||||
" - {count} standard labels verified": {
|
||||
"bg": " - {count} standard labels verified",
|
||||
"de": " - {count} standard labels verified",
|
||||
"en": " - {count} standard labels verified",
|
||||
"pl": " - {count} standard labels verified",
|
||||
"ru": " - {count} standard labels verified",
|
||||
"zh": " - {count} standard labels verified"
|
||||
},
|
||||
" -> {dir}": {
|
||||
"en": " -> {dir}",
|
||||
"bg": " -> {dir}",
|
||||
"de": " -> {dir}",
|
||||
"pl": " -> {dir}",
|
||||
"ru": " -> {dir}",
|
||||
"zh": " -> {dir}"
|
||||
},
|
||||
" ... and {n} more": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": " ... and {n} more",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
" Auto-fixed trailing whitespace in {n} files": {
|
||||
"bg": " Auto-fixed trailing whitespace in {n} files",
|
||||
"de": " Auto-fixed trailing whitespace in {n} files",
|
||||
@@ -391,14 +423,6 @@
|
||||
"ru": " Collecting version...",
|
||||
"zh": " Collecting version..."
|
||||
},
|
||||
" Created: {title}": {
|
||||
"bg": " Created: {title}",
|
||||
"de": " Created: {title}",
|
||||
"en": " Created: {title}",
|
||||
"pl": " Utworzono: {title}",
|
||||
"ru": " Created: {title}",
|
||||
"zh": " Created: {title}"
|
||||
},
|
||||
" Deleted: {version}": {
|
||||
"bg": " Deleted: {version}",
|
||||
"de": " Deleted: {version}",
|
||||
@@ -407,13 +431,13 @@
|
||||
"ru": " Deleted: {version}",
|
||||
"zh": " Deleted: {version}"
|
||||
},
|
||||
" FAIL: {title} — content mismatch or empty!": {
|
||||
"bg": " FAIL: {title} — content mismatch or empty!",
|
||||
"de": " FAIL: {title} — content mismatch or empty!",
|
||||
"en": " FAIL: {title} — content mismatch or empty!",
|
||||
"pl": " BŁĄD: {title} — treść niezgodna lub pusta!",
|
||||
"ru": " FAIL: {title} — content mismatch or empty!",
|
||||
"zh": " FAIL: {title} — content mismatch or empty!"
|
||||
" FAIL: {title} — page not found in wiki!": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": " FAIL: {title} — page not found in wiki!",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
" FAILED to delete: {version}": {
|
||||
"bg": " FAILED to delete: {version}",
|
||||
@@ -423,6 +447,14 @@
|
||||
"ru": " FAILED to delete: {version}",
|
||||
"zh": " FAILED to delete: {version}"
|
||||
},
|
||||
" Fixed {fixes} version ref(s) in {file}": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": " Fixed {fixes} version ref(s) in {file}",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
" Generated: {path}": {
|
||||
"bg": " Generated: {path}",
|
||||
"de": " Generated: {path}",
|
||||
@@ -479,13 +511,13 @@
|
||||
"ru": " OK: {script}",
|
||||
"zh": " OK: {script}"
|
||||
},
|
||||
" OK: {title} ({chars} chars)": {
|
||||
"bg": " OK: {title} ({chars} chars)",
|
||||
"de": " OK: {title} ({chars} chars)",
|
||||
"en": " OK: {title} ({chars} chars)",
|
||||
"pl": " OK: {title} ({chars} znaków)",
|
||||
"ru": " OK: {title} ({chars} chars)",
|
||||
"zh": " OK: {title} ({chars} chars)"
|
||||
" OK: {title}": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": " OK: {title}",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
" Package: {pkg}": {
|
||||
"bg": " Package: {pkg}",
|
||||
@@ -495,6 +527,14 @@
|
||||
"ru": " Package: {pkg}",
|
||||
"zh": " Package: {pkg}"
|
||||
},
|
||||
" Pruned: {file} (not in mapping)": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": " Pruned: {file} (not in mapping)",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
" Quality checks: {checks}": {
|
||||
"bg": " Quality checks: {checks}",
|
||||
"de": " Quality checks: {checks}",
|
||||
@@ -511,6 +551,22 @@
|
||||
"ru": " Repo root: {root}",
|
||||
"zh": " Repo root: {root}"
|
||||
},
|
||||
" Run 'make install-checkmake' to install the Makefile linter.": {
|
||||
"en": " Run 'make install-checkmake' to install the Makefile linter.",
|
||||
"bg": " Изпълнете 'make install-checkmake' за инсталиране на Makefile линтера.",
|
||||
"de": " Führen Sie 'make install-checkmake' aus, um den Makefile-Linter zu installieren.",
|
||||
"pl": " Uruchom 'make install-checkmake', aby zainstalować linter Makefile.",
|
||||
"ru": " Выполните 'make install-checkmake' для установки линтера Makefile.",
|
||||
"zh": " 运行 'make install-checkmake' 来安装 Makefile 检查器。"
|
||||
},
|
||||
" Synced: {title} → {file}": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": " Synced: {title} → {file}",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
" Test paths: {testpaths}": {
|
||||
"bg": " Test paths: {testpaths}",
|
||||
"de": " Test paths: {testpaths}",
|
||||
@@ -519,13 +575,21 @@
|
||||
"ru": " Test paths: {testpaths}",
|
||||
"zh": " Test paths: {testpaths}"
|
||||
},
|
||||
" Updated: {title}": {
|
||||
"bg": " Updated: {title}",
|
||||
"de": " Updated: {title}",
|
||||
"en": " Updated: {title}",
|
||||
"pl": " Zaktualizowano: {title}",
|
||||
"ru": " Updated: {title}",
|
||||
"zh": " Updated: {title}"
|
||||
" WARN: Mapped file {file} is empty, skipping": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": " WARN: Mapped file {file} is empty, skipping",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
" WARN: Mapped file {file} not found, skipping": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": " WARN: Mapped file {file} not found, skipping",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
" WARNING: Could not extract coverage from pytest output (rc={rc})": {
|
||||
"bg": " WARNING: Could not extract coverage from pytest output (rc={rc})",
|
||||
@@ -615,6 +679,22 @@
|
||||
"ru": " {name}: {label}={message} ({color})",
|
||||
"zh": " {name}: {label}={message} ({color})"
|
||||
},
|
||||
" {n} long lines found (warnings only)": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": " {n} long lines found (warnings only)",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
" {n} orphan docs found (warnings only)": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": " {n} orphan docs found (warnings only)",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
" {n} stale docs found (warnings only)": {
|
||||
"bg": " {n} stale docs found (warnings only)",
|
||||
"de": " {n} stale docs found (warnings only)",
|
||||
@@ -623,6 +703,14 @@
|
||||
"ru": " {n} stale docs found (warnings only)",
|
||||
"zh": " {n} stale docs found (warnings only)"
|
||||
},
|
||||
" {tool}: found at {path}": {
|
||||
"en": " {tool}: found at {path}",
|
||||
"bg": " {tool}: намерен на {path}",
|
||||
"de": " {tool}: gefunden unter {path}",
|
||||
"pl": " {tool}: znaleziono w {path}",
|
||||
"ru": " {tool}: найден в {path}",
|
||||
"zh": " {tool}: 在 {path} 找到"
|
||||
},
|
||||
" {version} (created: {created})": {
|
||||
"bg": " {version} (created: {created})",
|
||||
"de": " {version} (created: {created})",
|
||||
@@ -727,6 +815,22 @@
|
||||
"ru": "Assigned {count} items to runner {runner_index}: {encoded}",
|
||||
"zh": "Assigned {count} items to runner {runner_index}: {encoded}"
|
||||
},
|
||||
"Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.": {
|
||||
"bg": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
|
||||
"de": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
|
||||
"en": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
|
||||
"pl": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
|
||||
"ru": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
|
||||
"zh": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label."
|
||||
},
|
||||
"Automated CI commit (badge) — skipping post-merge jobs.": {
|
||||
"bg": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"de": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"en": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"pl": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"ru": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"zh": "Automated CI commit (badge) — skipping post-merge jobs."
|
||||
},
|
||||
"Badge push attempt {attempt}/{retries} failed — retrying: {error}": {
|
||||
"bg": "Badge push attempt {attempt}/{retries} failed — retrying: {error}",
|
||||
"de": "Badge push attempt {attempt}/{retries} failed — retrying: {error}",
|
||||
@@ -775,6 +879,22 @@
|
||||
"ru": "Ветка '{branch}' не содержит ID задачи.\n Ожидаемый формат: {prefix}-N-краткое-описание\n Пример: {prefix}-42-add-feature\n Исправление: переименуйте ветку или создайте задачу Vikunja:\n python -m devx.tools.create_task --title \"Заголовок задачи\"",
|
||||
"zh": "分支 '{branch}' 不包含任务 ID。\n 预期格式: {prefix}-N-简短描述\n 示例: {prefix}-42-add-feature\n 修复: 重命名分支或先创建 Vikunja 任务:\n python -m devx.tools.create_task --title \"任务标题\""
|
||||
},
|
||||
"Branch is already up-to-date with origin/master.": {
|
||||
"bg": "Branch is already up-to-date with origin/master.",
|
||||
"de": "Branch is already up-to-date with origin/master.",
|
||||
"en": "Branch is already up-to-date with origin/master.",
|
||||
"pl": "Branch is already up-to-date with origin/master.",
|
||||
"ru": "Branch is already up-to-date with origin/master.",
|
||||
"zh": "Branch is already up-to-date with origin/master."
|
||||
},
|
||||
"Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.": {
|
||||
"bg": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
|
||||
"de": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
|
||||
"en": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
|
||||
"pl": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
|
||||
"ru": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
|
||||
"zh": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR."
|
||||
},
|
||||
"Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master": {
|
||||
"bg": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
|
||||
"de": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
|
||||
@@ -783,6 +903,14 @@
|
||||
"ru": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
|
||||
"zh": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master"
|
||||
},
|
||||
"Branch is {count} commit(s) behind master. Rebasing...": {
|
||||
"bg": "Branch is {count} commit(s) behind master. Rebasing...",
|
||||
"de": "Branch is {count} commit(s) behind master. Rebasing...",
|
||||
"en": "Branch is {count} commit(s) behind master. Rebasing...",
|
||||
"pl": "Branch is {count} commit(s) behind master. Rebasing...",
|
||||
"ru": "Branch is {count} commit(s) behind master. Rebasing...",
|
||||
"zh": "Branch is {count} commit(s) behind master. Rebasing..."
|
||||
},
|
||||
"Branch name (e.g., DEVX-256-fix-foo)": {
|
||||
"bg": "Branch name (e.g., DEVX-256-fix-foo)",
|
||||
"de": "Branch name (e.g., DEVX-256-fix-foo)",
|
||||
@@ -847,6 +975,14 @@
|
||||
"ru": "CI_GITEA_TOKEN is not set.",
|
||||
"zh": "CI_GITEA_TOKEN is not set."
|
||||
},
|
||||
"CI_GITEA_TOKEN is not set. Add it to .env or export it.": {
|
||||
"bg": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
|
||||
"de": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
|
||||
"en": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
|
||||
"pl": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
|
||||
"ru": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
|
||||
"zh": "CI_GITEA_TOKEN is not set. Add it to .env or export it."
|
||||
},
|
||||
"CI_GITEA_TOKEN is not set. Required to create a PR.": {
|
||||
"bg": "CI_GITEA_TOKEN не е зададен. Необходим за създаване на PR.",
|
||||
"de": "CI_GITEA_TOKEN nicht gesetzt. Erforderlich zum Erstellen eines PR.",
|
||||
@@ -863,6 +999,22 @@
|
||||
"ru": "CI_GITEA_TOKEN not set — skipping login configuration.",
|
||||
"zh": "CI_GITEA_TOKEN not set — skipping login configuration."
|
||||
},
|
||||
"Cannot read __version__ from src/{pkg}/__init__.py — skipping.": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Cannot read __version__ from src/{pkg}/__init__.py — skipping.",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Cannot rebase: not on a branch (detached HEAD).": {
|
||||
"bg": "Cannot rebase: not on a branch (detached HEAD).",
|
||||
"de": "Cannot rebase: not on a branch (detached HEAD).",
|
||||
"en": "Cannot rebase: not on a branch (detached HEAD).",
|
||||
"pl": "Cannot rebase: not on a branch (detached HEAD).",
|
||||
"ru": "Cannot rebase: not on a branch (detached HEAD).",
|
||||
"zh": "Cannot rebase: not on a branch (detached HEAD)."
|
||||
},
|
||||
"Checking CLI command documentation...": {
|
||||
"bg": "Checking CLI command documentation...",
|
||||
"de": "Checking CLI command documentation...",
|
||||
@@ -871,6 +1023,14 @@
|
||||
"ru": "Checking CLI command documentation...",
|
||||
"zh": "Checking CLI command documentation..."
|
||||
},
|
||||
"Checking code block languages...": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Checking code block languages...",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Checking docs structure...": {
|
||||
"bg": "Checking docs structure...",
|
||||
"de": "Checking docs structure...",
|
||||
@@ -895,6 +1055,14 @@
|
||||
"ru": "Checking for TODO/FIXME markers...",
|
||||
"zh": "Checking for TODO/FIXME markers..."
|
||||
},
|
||||
"Checking for orphan docs...": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Checking for orphan docs...",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Checking for stale docs...": {
|
||||
"bg": "Checking for stale docs...",
|
||||
"de": "Checking for stale docs...",
|
||||
@@ -919,6 +1087,22 @@
|
||||
"ru": "Checking internal links...",
|
||||
"zh": "Checking internal links..."
|
||||
},
|
||||
"Checking line length...": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Checking line length...",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Checking max heading depth...": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Checking max heading depth...",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Checking required files...": {
|
||||
"bg": "Checking required files...",
|
||||
"de": "Checking required files...",
|
||||
@@ -927,6 +1111,14 @@
|
||||
"ru": "Checking required files...",
|
||||
"zh": "Checking required files..."
|
||||
},
|
||||
"Checking single H1 per file...": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Checking single H1 per file...",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Checking status for PR #{pr_number}...": {
|
||||
"bg": "Checking status for PR #{pr_number}...",
|
||||
"de": "Checking status for PR #{pr_number}...",
|
||||
@@ -943,6 +1135,30 @@
|
||||
"ru": "Checking trailing whitespace...",
|
||||
"zh": "Checking trailing whitespace..."
|
||||
},
|
||||
"Checking version references for {pkg} (current: v{version})": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Checking version references for {pkg} (current: v{version})",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Cloned existing wiki.": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Cloned existing wiki.",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Cloning wiki repo...": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Cloning wiki repo...",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Command failed ({cmd}): {stderr}": {
|
||||
"bg": "Command failed ({cmd}): {stderr}",
|
||||
"de": "Command failed ({cmd}): {stderr}",
|
||||
@@ -967,6 +1183,14 @@
|
||||
"ru": "Commit: {sha}",
|
||||
"zh": "Commit: {sha}"
|
||||
},
|
||||
"Committing and pushing...": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Committing and pushing...",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Comparing {base}..{head} ({count} files changed)": {
|
||||
"bg": "Comparing {base}..{head} ({count} files changed)",
|
||||
"de": "Comparing {base}..{head} ({count} files changed)",
|
||||
@@ -1015,6 +1239,14 @@
|
||||
"ru": "Configuring tea login '{name}' for {url}...",
|
||||
"zh": "Configuring tea login '{name}' for {url}..."
|
||||
},
|
||||
"Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.": {
|
||||
"bg": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
|
||||
"de": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
|
||||
"en": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
|
||||
"pl": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
|
||||
"ru": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
|
||||
"zh": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR."
|
||||
},
|
||||
"Could not detect current branch: {error}": {
|
||||
"bg": "Не може да се определи текущия клон: {error}",
|
||||
"de": "Aktueller Branch konnte nicht erkannt werden: {error}",
|
||||
@@ -1031,6 +1263,14 @@
|
||||
"ru": "Could not determine head SHA for PR #{pr_number}.",
|
||||
"zh": "Could not determine head SHA for PR #{pr_number}."
|
||||
},
|
||||
"Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.": {
|
||||
"bg": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
|
||||
"de": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
|
||||
"en": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
|
||||
"pl": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
|
||||
"ru": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
|
||||
"zh": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables."
|
||||
},
|
||||
"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.",
|
||||
@@ -1119,6 +1359,14 @@
|
||||
"ru": "Dependencies must have documentation comments.",
|
||||
"zh": "Dependencies must have documentation comments."
|
||||
},
|
||||
"Directory to scan (default: tests/integration). Can be repeated.": {
|
||||
"en": "Directory to scan (default: tests/integration). Can be repeated.",
|
||||
"bg": "Директория за сканиране (по подразбиране: tests/integration). Може да се повтаря.",
|
||||
"de": "Zu scannendes Verzeichnis (Standard: tests/integration). Kann wiederholt werden.",
|
||||
"pl": "Katalog do skanowania (domyślnie: tests/integration). Można powtarzać.",
|
||||
"ru": "Директория для сканирования (по умолчанию: tests/integration). Можно повторять.",
|
||||
"zh": "要扫描的目录(默认:tests/integration)。可重复。"
|
||||
},
|
||||
"Docker daemon already running": {
|
||||
"bg": "Докер демонът вече работи",
|
||||
"de": "Docker-Daemon läuft bereits",
|
||||
@@ -1207,6 +1455,22 @@
|
||||
"ru": "Каждый элемент должен быть строкой или объектом с 'id', получено {type}",
|
||||
"zh": "每个元素必须是字符串或带有 'id' 的对象,得到 {type}"
|
||||
},
|
||||
"Ensuring standard labels...": {
|
||||
"bg": "Ensuring standard labels...",
|
||||
"de": "Ensuring standard labels...",
|
||||
"en": "Ensuring standard labels...",
|
||||
"pl": "Ensuring standard labels...",
|
||||
"ru": "Ensuring standard labels...",
|
||||
"zh": "Ensuring standard labels..."
|
||||
},
|
||||
"FAIL: Could not clone wiki for verification.": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "FAIL: Could not clone wiki for verification.",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"FAIL: {n} documentation issues found:": {
|
||||
"bg": "FAIL: {n} documentation issues found:",
|
||||
"de": "FAIL: {n} documentation issues found:",
|
||||
@@ -1263,6 +1527,30 @@
|
||||
"ru": "Failed to list versions for {name}: {error}",
|
||||
"zh": "Failed to list versions for {name}: {error}"
|
||||
},
|
||||
"Failed to push release commit after 3 attempts. Manual intervention required.": {
|
||||
"bg": "Failed to push release commit after 3 attempts. Manual intervention required.",
|
||||
"de": "Failed to push release commit after 3 attempts. Manual intervention required.",
|
||||
"en": "Failed to push release commit after 3 attempts. Manual intervention required.",
|
||||
"pl": "Failed to push release commit after 3 attempts. Manual intervention required.",
|
||||
"ru": "Failed to push release commit after 3 attempts. Manual intervention required.",
|
||||
"zh": "Failed to push release commit after 3 attempts. Manual intervention required."
|
||||
},
|
||||
"Failed to start ssh-agent: {error}": {
|
||||
"en": "Failed to start ssh-agent: {error}",
|
||||
"bg": "Неуспешно стартиране на ssh-agent: {error}",
|
||||
"de": "Starten von ssh-agent fehlgeschlagen: {error}",
|
||||
"pl": "Nie udało się uruchomić ssh-agent: {error}",
|
||||
"ru": "Не удалось запустить ssh-agent: {error}",
|
||||
"zh": "启动 ssh-agent 失败: {error}"
|
||||
},
|
||||
"Fetch failed: {error}": {
|
||||
"bg": "Fetch failed: {error}",
|
||||
"de": "Fetch failed: {error}",
|
||||
"en": "Fetch failed: {error}",
|
||||
"pl": "Fetch failed: {error}",
|
||||
"ru": "Fetch failed: {error}",
|
||||
"zh": "Fetch failed: {error}"
|
||||
},
|
||||
"Fetching logs for PR #{pr_number}...": {
|
||||
"bg": "Fetching logs for PR #{pr_number}...",
|
||||
"de": "Fetching logs for PR #{pr_number}...",
|
||||
@@ -1271,13 +1559,29 @@
|
||||
"ru": "Fetching logs for PR #{pr_number}...",
|
||||
"zh": "Fetching logs for PR #{pr_number}..."
|
||||
},
|
||||
"Found {count} existing wiki pages.": {
|
||||
"bg": "Found {count} existing wiki pages.",
|
||||
"de": "Found {count} existing wiki pages.",
|
||||
"en": "Found {count} existing wiki pages.",
|
||||
"pl": "Znaleziono {count} istniejących stron wiki.",
|
||||
"ru": "Found {count} existing wiki pages.",
|
||||
"zh": "Found {count} existing wiki pages."
|
||||
"Fetching origin/master...": {
|
||||
"bg": "Fetching origin/master...",
|
||||
"de": "Fetching origin/master...",
|
||||
"en": "Fetching origin/master...",
|
||||
"pl": "Fetching origin/master...",
|
||||
"ru": "Fetching origin/master...",
|
||||
"zh": "Fetching origin/master..."
|
||||
},
|
||||
"Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.": {
|
||||
"bg": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
|
||||
"de": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
|
||||
"en": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
|
||||
"pl": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
|
||||
"ru": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
|
||||
"zh": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again."
|
||||
},
|
||||
"Force-pushing...": {
|
||||
"bg": "Force-pushing...",
|
||||
"de": "Force-pushing...",
|
||||
"en": "Force-pushing...",
|
||||
"pl": "Force-pushing...",
|
||||
"ru": "Force-pushing...",
|
||||
"zh": "Force-pushing..."
|
||||
},
|
||||
"Found {count} mutable global(s) — use factory functions or pytest fixtures.": {
|
||||
"bg": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
|
||||
@@ -1295,6 +1599,14 @@
|
||||
"ru": "Found {count} stale documentation reference(s)",
|
||||
"zh": "Found {count} stale documentation reference(s)"
|
||||
},
|
||||
"Found {count} unsafe identity check(s) in integration tests.": {
|
||||
"en": "Found {count} unsafe identity check(s) in integration tests.",
|
||||
"bg": "Намерени са {count} небрежни проверки за идентичност в интеграционните тестове.",
|
||||
"de": "{count} unsichere Identitätsprüfung(en) in Integrationstests gefunden.",
|
||||
"pl": "Znaleziono {count} niebezpiecznych sprawdzeń tożsamości w testach integracyjnych.",
|
||||
"ru": "Найдено {count} небезопасных проверок идентичности в интеграционных тестах.",
|
||||
"zh": "在集成测试中发现 {count} 个不安全的身份检查。"
|
||||
},
|
||||
"Found {count} version(s):": {
|
||||
"bg": "Found {count} version(s):",
|
||||
"de": "Found {count} version(s):",
|
||||
@@ -1519,6 +1831,14 @@
|
||||
"ru": "Linting documentation in {root}...",
|
||||
"zh": "Linting documentation in {root}..."
|
||||
},
|
||||
"Login to {registry} failed: {error}": {
|
||||
"en": "Login to {registry} failed: {error}",
|
||||
"bg": "Влизането в {registry} не успя: {error}",
|
||||
"de": "Anmeldung bei {registry} fehlgeschlagen: {error}",
|
||||
"pl": "Logowanie do {registry} nie powiodło się: {error}",
|
||||
"ru": "Ошибка входа в {registry}: {error}",
|
||||
"zh": "登录 {registry} 失败: {error}"
|
||||
},
|
||||
"Manifest file not found: {path}": {
|
||||
"bg": "Manifest file not found: {path}",
|
||||
"de": "Manifest file not found: {path}",
|
||||
@@ -1535,22 +1855,6 @@
|
||||
"ru": "Manifest must be a JSON list",
|
||||
"zh": "Manifest must be a JSON list"
|
||||
},
|
||||
"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.",
|
||||
"en": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
|
||||
"pl": "Mapowany plik {file} jest pusty. Zaktualizuj treść lub usuń z 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."
|
||||
},
|
||||
"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.",
|
||||
"en": "Mapped file {file} not found. Update mapping.json or create the file.",
|
||||
"pl": "Mapowany plik {file} nie znaleziony. Zaktualizuj mapping.json lub utwórz plik.",
|
||||
"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."
|
||||
},
|
||||
"Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.": {
|
||||
"bg": "Сливането неуспешно с HTTP {status}: {message}\nПроверете дали PR е готов и имате права за сливане.",
|
||||
"de": "Merge fehlgeschlagen mit HTTP {status}: {message}\nBitte prüfen Sie, ob der PR bereit ist und Sie Merge-Rechte haben.",
|
||||
@@ -1631,6 +1935,14 @@
|
||||
"ru": "No CI checks found for commit {sha}.",
|
||||
"zh": "No CI checks found for commit {sha}."
|
||||
},
|
||||
"No Python package found under src/ — skipping version check.": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "No Python package found under src/ — skipping version check.",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"No badge SVG files generated": {
|
||||
"bg": "No badge SVG files generated",
|
||||
"de": "No badge SVG files generated",
|
||||
@@ -1647,6 +1959,14 @@
|
||||
"ru": "No badge URLs found to update — README already up to date",
|
||||
"zh": "No badge URLs found to update — README already up to date"
|
||||
},
|
||||
"No badge changes — skipping commit": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "No badge changes — skipping commit",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"No changes between {base} and {head}.": {
|
||||
"bg": "No changes between {base} and {head}.",
|
||||
"de": "No changes between {base} and {head}.",
|
||||
@@ -1655,6 +1975,14 @@
|
||||
"ru": "No changes between {base} and {head}.",
|
||||
"zh": "No changes between {base} and {head}."
|
||||
},
|
||||
"No changes to sync — wiki is up to date.": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "No changes to sync — wiki is up to date.",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"No failed jobs.": {
|
||||
"bg": "No failed jobs.",
|
||||
"de": "No failed jobs.",
|
||||
@@ -1687,6 +2015,14 @@
|
||||
"ru": "No open PR found for branch '{branch}'.",
|
||||
"zh": "No open PR found for branch '{branch}'."
|
||||
},
|
||||
"No push needed (no changes or push failed).": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "No push needed (no changes or push failed).",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"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.",
|
||||
@@ -1767,6 +2103,14 @@
|
||||
"ru": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
||||
"zh": "Note: Self-approval not allowed. Posting COMMENT instead."
|
||||
},
|
||||
"Nothing to push.": {
|
||||
"bg": "Nothing to push.",
|
||||
"de": "Nothing to push.",
|
||||
"en": "Nothing to push.",
|
||||
"pl": "Nothing to push.",
|
||||
"ru": "Nothing to push.",
|
||||
"zh": "Nothing to push."
|
||||
},
|
||||
"Only check staged files (for pre-commit)": {
|
||||
"bg": "Only check staged files (for pre-commit)",
|
||||
"de": "Only check staged files (for pre-commit)",
|
||||
@@ -1871,6 +2215,14 @@
|
||||
"ru": "PASSED: {pair}",
|
||||
"zh": "PASSED: {pair}"
|
||||
},
|
||||
"PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.": {
|
||||
"bg": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
|
||||
"de": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
|
||||
"en": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
|
||||
"pl": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
|
||||
"ru": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
|
||||
"zh": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR."
|
||||
},
|
||||
"PR already exists: #{index} — {url}": {
|
||||
"bg": "PR вече съществува: #{index} — {url}",
|
||||
"de": "PR existiert bereits: #{index} — {url}",
|
||||
@@ -2047,6 +2399,14 @@
|
||||
"ru": "Publishing release {tag}...",
|
||||
"zh": "Publishing release {tag}..."
|
||||
},
|
||||
"Push attempt {n}/3 failed: {err}": {
|
||||
"bg": "Push attempt {n}/3 failed: {err}",
|
||||
"de": "Push attempt {n}/3 failed: {err}",
|
||||
"en": "Push attempt {n}/3 failed: {err}",
|
||||
"pl": "Push attempt {n}/3 failed: {err}",
|
||||
"ru": "Push attempt {n}/3 failed: {err}",
|
||||
"zh": "Push attempt {n}/3 failed: {err}"
|
||||
},
|
||||
"Push failed for {tag}: {error}": {
|
||||
"bg": "Push failed for {tag}: {error}",
|
||||
"de": "Push failed for {tag}: {error}",
|
||||
@@ -2055,6 +2415,14 @@
|
||||
"ru": "Push failed for {tag}: {error}",
|
||||
"zh": "Push failed for {tag}: {error}"
|
||||
},
|
||||
"Push failed: {error}": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Push failed: {error}",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Pushed README update with badge SHA {sha}": {
|
||||
"bg": "Pushed README update with badge SHA {sha}",
|
||||
"de": "Pushed README update with badge SHA {sha}",
|
||||
@@ -2071,6 +2439,14 @@
|
||||
"ru": "Pushed release commit to master.",
|
||||
"zh": "Pushed release commit to master."
|
||||
},
|
||||
"Pushed {branch} to origin.": {
|
||||
"bg": "Pushed {branch} to origin.",
|
||||
"de": "Pushed {branch} to origin.",
|
||||
"en": "Pushed {branch} to origin.",
|
||||
"pl": "Pushed {branch} to origin.",
|
||||
"ru": "Pushed {branch} to origin.",
|
||||
"zh": "Pushed {branch} to origin."
|
||||
},
|
||||
"PyPI publish failed (non-fatal — continuing to Gitea release):\n{error}": {
|
||||
"bg": "Публикуването в PyPI неуспешно (некритично — продължава към Gitea release):\n{error}",
|
||||
"de": "PyPI-Veröffentlichung fehlgeschlagen (nicht fatal — Gitea-Release wird fortgesetzt):\n{error}",
|
||||
@@ -2087,6 +2463,46 @@
|
||||
"ru": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
|
||||
"zh": "REPO argument is required (or set GITHUB_REPOSITORY env var)."
|
||||
},
|
||||
"Rebase attempt {n}/3 failed: {err}": {
|
||||
"bg": "Rebase attempt {n}/3 failed: {err}",
|
||||
"de": "Rebase attempt {n}/3 failed: {err}",
|
||||
"en": "Rebase attempt {n}/3 failed: {err}",
|
||||
"pl": "Rebase attempt {n}/3 failed: {err}",
|
||||
"ru": "Rebase attempt {n}/3 failed: {err}",
|
||||
"zh": "Rebase attempt {n}/3 failed: {err}"
|
||||
},
|
||||
"Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue": {
|
||||
"bg": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
|
||||
"de": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
|
||||
"en": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
|
||||
"pl": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
|
||||
"ru": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
|
||||
"zh": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue"
|
||||
},
|
||||
"Rebase failed with HTTP {status}: {message}": {
|
||||
"bg": "Rebase failed with HTTP {status}: {message}",
|
||||
"de": "Rebase failed with HTTP {status}: {message}",
|
||||
"en": "Rebase failed with HTTP {status}: {message}",
|
||||
"pl": "Rebase failed with HTTP {status}: {message}",
|
||||
"ru": "Rebase failed with HTTP {status}: {message}",
|
||||
"zh": "Rebase failed with HTTP {status}: {message}"
|
||||
},
|
||||
"Rebase successful.": {
|
||||
"bg": "Rebase successful.",
|
||||
"de": "Rebase successful.",
|
||||
"en": "Rebase successful.",
|
||||
"pl": "Rebase successful.",
|
||||
"ru": "Rebase successful.",
|
||||
"zh": "Rebase successful."
|
||||
},
|
||||
"Rebasing PR #{pr} via Gitea API...": {
|
||||
"bg": "Rebasing PR #{pr} via Gitea API...",
|
||||
"de": "Rebasing PR #{pr} via Gitea API...",
|
||||
"en": "Rebasing PR #{pr} via Gitea API...",
|
||||
"pl": "Rebasing PR #{pr} via Gitea API...",
|
||||
"ru": "Rebasing PR #{pr} via Gitea API...",
|
||||
"zh": "Rebasing PR #{pr} via Gitea API..."
|
||||
},
|
||||
"Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars": {
|
||||
"bg": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars",
|
||||
"de": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars",
|
||||
@@ -2127,14 +2543,6 @@
|
||||
"ru": "Release commit — skipping all post-merge jobs.",
|
||||
"zh": "Release commit — skipping all post-merge jobs."
|
||||
},
|
||||
"Automated CI commit (badge) — skipping post-merge jobs.": {
|
||||
"bg": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"de": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"en": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"pl": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"ru": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"zh": "Automated CI commit (badge) — skipping post-merge jobs."
|
||||
},
|
||||
"Release creation failed: {error}": {
|
||||
"bg": "Release creation failed: {error}",
|
||||
"de": "Release creation failed: {error}",
|
||||
@@ -2191,6 +2599,14 @@
|
||||
"ru": "Владелец репозитория не установлен. Используйте --owner или DEVX_REPO_OWNER env var.",
|
||||
"zh": "仓库所有者未设置。使用 --owner 或 DEVX_REPO_OWNER 环境变量。"
|
||||
},
|
||||
"Required tools missing.": {
|
||||
"en": "Required tools missing.",
|
||||
"bg": "Липсват задължителни инструменти.",
|
||||
"de": "Erforderliche Werkzeuge fehlen.",
|
||||
"pl": "Brak wymaganych narzędzi.",
|
||||
"ru": "Отсутствуют обязательные инструменты.",
|
||||
"zh": "缺少必需的工具。"
|
||||
},
|
||||
"Review body must be at least 50 characters.": {
|
||||
"bg": "Review body must be at least 50 characters.",
|
||||
"de": "Review body must be at least 50 characters.",
|
||||
@@ -2279,6 +2695,30 @@
|
||||
"ru": "Running: {scenario} on {platform}",
|
||||
"zh": "Running: {scenario} on {platform}"
|
||||
},
|
||||
"SSH key set up successfully": {
|
||||
"en": "SSH key set up successfully",
|
||||
"bg": "SSH ключът е настроен успешно",
|
||||
"de": "SSH-Schlüssel erfolgreich eingerichtet",
|
||||
"pl": "Klucz SSH skonfigurowany pomyślnie",
|
||||
"ru": "SSH-ключ успешно настроен",
|
||||
"zh": "SSH 密钥设置成功"
|
||||
},
|
||||
"SSH key setup skipped (no key provided)": {
|
||||
"en": "SSH key setup skipped (no key provided)",
|
||||
"bg": "Настройката на SSH ключ е пропусната (не е предоставен ключ)",
|
||||
"de": "SSH-Schlüssel-Setup übersprungen (kein Schlüssel bereitgestellt)",
|
||||
"pl": "Pominięto konfigurację klucza SSH (brak klucza)",
|
||||
"ru": "Настройка SSH-ключа пропущена (ключ не предоставлен)",
|
||||
"zh": "SSH 密钥设置已跳过(未提供密钥)"
|
||||
},
|
||||
"SSH_PRIVATE_KEY not set — skipping SSH key setup": {
|
||||
"en": "SSH_PRIVATE_KEY not set — skipping SSH key setup",
|
||||
"bg": "SSH_PRIVATE_KEY не е зададен — пропускане на SSH ключ настройката",
|
||||
"de": "SSH_PRIVATE_KEY nicht gesetzt — SSH-Schlüssel-Setup übersprungen",
|
||||
"pl": "SSH_PRIVATE_KEY nie ustawione — pomijanie konfiguracji klucza SSH",
|
||||
"ru": "SSH_PRIVATE_KEY не задан — пропуск настройки SSH-ключа",
|
||||
"zh": "SSH_PRIVATE_KEY 未设置 — 跳过 SSH 密钥设置"
|
||||
},
|
||||
"Skip Vikunja title match check": {
|
||||
"bg": "Skip Vikunja title match check",
|
||||
"de": "Skip Vikunja title match check",
|
||||
@@ -2319,13 +2759,21 @@
|
||||
"ru": "Synced to latest origin/{branch}",
|
||||
"zh": "Synced to latest origin/{branch}"
|
||||
},
|
||||
"Syncing {count} documentation pages to wiki...": {
|
||||
"bg": "Syncing {count} documentation pages to wiki...",
|
||||
"de": "Syncing {count} documentation pages to wiki...",
|
||||
"en": "Syncing {count} documentation pages to wiki...",
|
||||
"pl": "Synchronizowanie {count} stron dokumentacji do wiki...",
|
||||
"ru": "Syncing {count} documentation pages to wiki...",
|
||||
"zh": "Syncing {count} documentation pages to wiki..."
|
||||
"Syncing files...": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Syncing files...",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Syncing {count} documentation pages to wiki via Git...": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Syncing {count} documentation pages to wiki via Git...",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Tag consistency check failed.": {
|
||||
"bg": "Tag consistency check failed.",
|
||||
@@ -2439,6 +2887,14 @@
|
||||
"ru": "Updated badge URLs in {filename}",
|
||||
"zh": "Updated badge URLs in {filename}"
|
||||
},
|
||||
"Updated documentation version references to v{version}": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Updated documentation version references to v{version}",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Updated version in {init}": {
|
||||
"bg": "Updated version in {init}",
|
||||
"de": "Updated version in {init}",
|
||||
@@ -2455,6 +2911,14 @@
|
||||
"ru": "Updated {changelog_file}",
|
||||
"zh": "Updated {changelog_file}"
|
||||
},
|
||||
"Use string comparison or _is_truthy()/_is_falsy() helpers instead. Add '{marker}' to suppress individual lines.": {
|
||||
"en": "Use string comparison or _is_truthy()/_is_falsy() helpers instead. Add '{marker}' to suppress individual lines.",
|
||||
"bg": "Използвайте сравнение на низове или _is_truthy()/_is_falsy() помощници. Добавете '{marker}' за потискане на отделни редове.",
|
||||
"de": "Verwenden Sie String-Vergleich oder _is_truthy()/_is_falsy() Hilfsfunktionen. Fügen Sie '{marker}' hinzu, um einzelne Zeilen zu unterdrücken.",
|
||||
"pl": "Użyj porównania ciągów lub pomocników _is_truthy()/_is_falsy(). Dodaj '{marker}', aby pominąć pojedyncze linie.",
|
||||
"ru": "Используйте строковое сравнение или помощники _is_truthy()/_is_falsy(). Добавьте '{marker}' для подавления отдельных строк.",
|
||||
"zh": "使用字符串比较或 _is_truthy()/_is_falsy() 辅助函数。添加 '{marker}' 以抑制个别行。"
|
||||
},
|
||||
"VIKUNJA_TOKEN is not set. Required to derive PR title.": {
|
||||
"bg": "VIKUNJA_TOKEN не е зададен. Необходим за извличане на PR заглавие.",
|
||||
"de": "VIKUNJA_TOKEN nicht gesetzt. Erforderlich zum Ableiten des PR-Titels.",
|
||||
@@ -2511,6 +2975,38 @@
|
||||
"ru": "Задача Vikunja {task_id} не найдена в проекте {project_id}.\n Сначала создайте её:\n python -m devx.tools.create_task --title \"Заголовок задачи\"\n Или проверьте, что ID задачи в имени ветки корректен.",
|
||||
"zh": "在项目 {project_id} 中找不到 Vikunja 任务 {task_id}。\n 请先创建:\n python -m devx.tools.create_task --title \"任务标题\"\n 或检查分支名称中的任务 ID 是否正确。"
|
||||
},
|
||||
"WARN: .venv has Python {version}, but >={req} is required.": {
|
||||
"en": "WARN: .venv has Python {version}, but >={req} is required.",
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: .venv има Python {version}, но се изисква >={req}.",
|
||||
"de": "WARNUNG: .venv hat Python {version}, aber >={req} ist erforderlich.",
|
||||
"pl": "OSTRZEŻENIE: .venv ma Python {version}, ale wymagane jest >={req}.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: в .venv установлен Python {version}, но требуется >={req}.",
|
||||
"zh": "警告: .venv 的 Python 版本为 {version},但要求 >={req}。"
|
||||
},
|
||||
"WARN: .venv not found. Run 'make setup-venv' to create it.": {
|
||||
"en": "WARN: .venv not found. Run 'make setup-venv' to create it.",
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: .venv не е намерен. Изпълнете 'make setup-venv' за създаване.",
|
||||
"de": "WARNUNG: .venv nicht gefunden. Führen Sie 'make setup-venv' aus, um es zu erstellen.",
|
||||
"pl": "OSTRZEŻENIE: Nie znaleziono .venv. Uruchom 'make setup-venv', aby utworzyć.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: .venv не найден. Выполните 'make setup-venv' для создания.",
|
||||
"zh": "警告: 未找到 .venv。运行 'make setup-venv' 来创建。"
|
||||
},
|
||||
"WARN: Could not determine Python version in .venv.": {
|
||||
"en": "WARN: Could not determine Python version in .venv.",
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: Не може да се определи версията на Python в .venv.",
|
||||
"de": "WARNUNG: Python-Version in .venv konnte nicht bestimmt werden.",
|
||||
"pl": "OSTRZEŻENIE: Nie można określić wersji Python w .venv.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: Не удалось определить версию Python в .venv.",
|
||||
"zh": "警告: 无法确定 .venv 中的 Python 版本。"
|
||||
},
|
||||
"WARN: Could not parse Python version '{version}'.": {
|
||||
"en": "WARN: Could not parse Python version '{version}'.",
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: Не може да се анализира версията на Python '{version}'.",
|
||||
"de": "WARNUNG: Python-Version '{version}' konnte nicht analysiert werden.",
|
||||
"pl": "OSTRZEŻENIE: Nie można przeanalizować wersji Python '{version}'.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: Не удалось разобрать версию Python '{version}'.",
|
||||
"zh": "警告: 无法解析 Python 版本 '{version}'。"
|
||||
},
|
||||
"WARNING: --skip-tests passed — skipping test verification.": {
|
||||
"bg": "WARNING: --skip-tests passed — skipping test verification.",
|
||||
"de": "WARNING: --skip-tests passed — skipping test verification.",
|
||||
@@ -2527,22 +3023,6 @@
|
||||
"ru": "ВНИМАНИЕ: Файл .taskid ({file_id}) устарел и не совпадает с именем ветки ({branch_id}). Удалите .taskid из репозитория — имя ветки — единственный источник истины.",
|
||||
"zh": "警告:.taskid 文件 ({file_id}) 已弃用,与分支名称 ({branch_id}) 不一致。请从仓库中删除 .taskid — 分支名称是唯一的真实来源。"
|
||||
},
|
||||
"WARNING: Could not fetch wiki page list after retries. The sync itself succeeded ({count} pages updated), but the integrity check could not verify them due to a transient API issue.": {
|
||||
"bg": "WARNING: Could not fetch wiki page list after retries. The sync itself succeeded ({count} pages updated), but the integrity check could not verify them due to a transient API issue.",
|
||||
"de": "WARNING: Could not fetch wiki page list after retries. The sync itself succeeded ({count} pages updated), but the integrity check could not verify them due to a transient API issue.",
|
||||
"en": "WARNING: Could not fetch wiki page list after retries. The sync itself succeeded ({count} pages updated), but the integrity check could not verify them due to a transient API issue.",
|
||||
"pl": "OSTRZEŻENIE: Nie można pobrać listy stron wiki po ponownych próbach. Sama synchronizacja zakończyła się sukcesem (zaktualizowano {count} stron), ale kontrola integralności nie mogła ich zweryfikować z powodu przejściowego problemu z API.",
|
||||
"ru": "WARNING: Could not fetch wiki page list after retries. The sync itself succeeded ({count} pages updated), but the integrity check could not verify them due to a transient API issue.",
|
||||
"zh": "WARNING: Could not fetch wiki page list after retries. The sync itself succeeded ({count} pages updated), but the integrity check could not verify them due to a transient API issue."
|
||||
},
|
||||
"WARNING: Could not re-fetch wiki page list for verification. Skipping content verification due to transient API issue.": {
|
||||
"bg": "WARNING: Could not re-fetch wiki page list for verification. Skipping content verification due to transient API issue.",
|
||||
"de": "WARNING: Could not re-fetch wiki page list for verification. Skipping content verification due to transient API issue.",
|
||||
"en": "WARNING: Could not re-fetch wiki page list for verification. Skipping content verification due to transient API issue.",
|
||||
"pl": "OSTRZEŻENIE: Nie można ponownie pobrać listy stron wiki do weryfikacji. Pomijanie weryfikacji treści z powodu przejściowego problemu z API.",
|
||||
"ru": "WARNING: Could not re-fetch wiki page list for verification. Skipping content verification due to transient API issue.",
|
||||
"zh": "WARNING: Could not re-fetch wiki page list for verification. Skipping content verification due to transient API issue."
|
||||
},
|
||||
"WARNING: VIKUNJA_TOKEN not set — skipping task existence check. Set it in .env to enable full validation.": {
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: VIKUNJA_TOKEN не е зададен — пропускане на проверката за съществуване на задача. Задайте го в .env за пълна валидация.",
|
||||
"de": "WARNUNG: VIKUNJA_TOKEN nicht gesetzt — Task-Existenzprüfung übersprungen. In .env setzen für volle Validierung.",
|
||||
@@ -2551,6 +3031,30 @@
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: VIKUNJA_TOKEN не установлен — пропуск проверки существования задачи. Установите в .env для полной проверки.",
|
||||
"zh": "警告: VIKUNJA_TOKEN 未设置 — 跳过任务存在性检查。在 .env 中设置以启用完整验证。"
|
||||
},
|
||||
"WARNING: Version badge shows stale version (expected v{version}) — regenerating": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "WARNING: Version badge shows stale version (expected v{version}) — regenerating",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"WARNING: check_doc_versions --fix failed (rc={rc}): {err}": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "WARNING: check_doc_versions --fix failed (rc={rc}): {err}",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Waiting 5s for Gitea to process pushed commits...": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Waiting 5s for Gitea to process pushed commits...",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Waiting for CI checks to complete (timeout: {timeout}s)...": {
|
||||
"bg": "Waiting for CI checks to complete (timeout: {timeout}s)...",
|
||||
"de": "Waiting for CI checks to complete (timeout: {timeout}s)...",
|
||||
@@ -2615,21 +3119,37 @@
|
||||
"ru": "Warning: repo-level runners query returned HTTP {status}",
|
||||
"zh": "Warning: repo-level runners query returned HTTP {status}"
|
||||
},
|
||||
"Wiki integrity check failed — {count} issue(s)": {
|
||||
"bg": "Wiki integrity check failed — {count} issue(s)",
|
||||
"de": "Wiki integrity check failed — {count} issue(s)",
|
||||
"en": "Wiki integrity check failed — {count} issue(s)",
|
||||
"pl": "Kontrola integralności wiki nie powiodła się — {count} problem(ów)",
|
||||
"ru": "Wiki integrity check failed — {count} issue(s)",
|
||||
"zh": "Wiki integrity check failed — {count} issue(s)"
|
||||
"Wiki repo not found or empty — initializing fresh.": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Wiki repo not found or empty — initializing fresh.",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"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",
|
||||
"en": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
||||
"pl": "Weryfikacja wiki nie powiodła się — {failures} strona(y) pusta lub niezgodna",
|
||||
"ru": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
||||
"zh": "Wiki verification failed — {failures} page(s) empty or mismatched"
|
||||
"Wiki synced successfully.": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Wiki synced successfully.",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Wiki verification failed — could not clone wiki": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Wiki verification failed — could not clone wiki",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Wiki verification failed — {failures} page(s) missing": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "Wiki verification failed — {failures} page(s) missing",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"Wrote tag {tag} to GITHUB_OUTPUT.": {
|
||||
"bg": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
@@ -2639,6 +3159,14 @@
|
||||
"ru": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"zh": "Wrote tag {tag} to GITHUB_OUTPUT."
|
||||
},
|
||||
"[check-api-identity-checks] Passed: no unsafe identity checks found": {
|
||||
"en": "[check-api-identity-checks] Passed: no unsafe identity checks found",
|
||||
"bg": "[check-api-identity-checks] Мина: не са намерени небрежни проверки за идентичност",
|
||||
"de": "[check-api-identity-checks] Bestanden: keine unsicheren Identitätsprüfungen gefunden",
|
||||
"pl": "[check-api-identity-checks] Passed: nie znaleziono niebezpiecznych sprawdzeń tożsamości",
|
||||
"ru": "[check-api-identity-checks] Пройдено: небезопасных проверок идентичности не найдено",
|
||||
"zh": "[check-api-identity-checks] 通过:未发现不安全的身份检查"
|
||||
},
|
||||
"[check-dep-docs] Passed: all dependencies are documented": {
|
||||
"bg": "[check-dep-docs] Passed: all dependencies are documented",
|
||||
"de": "[check-dep-docs] Passed: all dependencies are documented",
|
||||
@@ -2647,6 +3175,30 @@
|
||||
"ru": "[check-dep-docs] Passed: all dependencies are documented",
|
||||
"zh": "[check-dep-docs] Passed: all dependencies are documented"
|
||||
},
|
||||
"[check-deps] All core tools present.": {
|
||||
"en": "[check-deps] All core tools present.",
|
||||
"bg": "[check-deps] Всички основни инструменти са налични.",
|
||||
"de": "[check-deps] Alle Kernwerkzeuge vorhanden.",
|
||||
"pl": "[check-deps] Wszystkie podstawowe narzędzia są dostępne.",
|
||||
"ru": "[check-deps] Все основные инструменты доступны.",
|
||||
"zh": "[check-deps] 所有核心工具均已就绪。"
|
||||
},
|
||||
"[check-deps] Verifying tools...": {
|
||||
"en": "[check-deps] Verifying tools...",
|
||||
"bg": "[check-deps] Проверка на инструментите...",
|
||||
"de": "[check-deps] Werkzeuge werden überprüft...",
|
||||
"pl": "[check-deps] Sprawdzanie narzędzi...",
|
||||
"ru": "[check-deps] Проверка инструментов...",
|
||||
"zh": "[check-deps] 正在验证工具..."
|
||||
},
|
||||
"[check-deps] Virtualenv .venv ready (Python {version}).": {
|
||||
"en": "[check-deps] Virtualenv .venv ready (Python {version}).",
|
||||
"bg": "[check-deps] Виртуална среда .venv готова (Python {version}).",
|
||||
"de": "[check-deps] Virtuelle Umgebung .venv bereit (Python {version}).",
|
||||
"pl": "[check-deps] Środowisko wirtualne .venv gotowe (Python {version}).",
|
||||
"ru": "[check-deps] Виртуальное окружение .venv готово (Python {version}).",
|
||||
"zh": "[check-deps] 虚拟环境 .venv 已就绪 (Python {version})。"
|
||||
},
|
||||
"[check-mutable-globals] Passed: no mutable path globals found": {
|
||||
"bg": "[check-mutable-globals] Passed: no mutable path globals found",
|
||||
"de": "[check-mutable-globals] Passed: no mutable path globals found",
|
||||
@@ -2671,6 +3223,46 @@
|
||||
"ru": "[check_test_coverage] No changed files to check.",
|
||||
"zh": "[check_test_coverage] No changed files to check."
|
||||
},
|
||||
"[docker-login] Logged in to {registry}.": {
|
||||
"en": "[docker-login] Logged in to {registry}.",
|
||||
"bg": "[docker-login] Влязъл в {registry}.",
|
||||
"de": "[docker-login] Angemeldet bei {registry}.",
|
||||
"pl": "[docker-login] Zalogowano do {registry}.",
|
||||
"ru": "[docker-login] Выполнен вход в {registry}.",
|
||||
"zh": "[docker-login] 已登录到 {registry}。"
|
||||
},
|
||||
"[docker-login] Login to {registry} failed (continuing).": {
|
||||
"en": "[docker-login] Login to {registry} failed (continuing).",
|
||||
"bg": "[docker-login] Влизането в {registry} не успя (продължава).",
|
||||
"de": "[docker-login] Anmeldung bei {registry} fehlgeschlagen (wird fortgesetzt).",
|
||||
"pl": "[docker-login] Logowanie do {registry} nie powiodło się (kontynuowanie).",
|
||||
"ru": "[docker-login] Ошибка входа в {registry} (продолжаем).",
|
||||
"zh": "[docker-login] 登录 {registry} 失败(继续)。"
|
||||
},
|
||||
"[docker-login] Skipping {registry} (token {env} not set).": {
|
||||
"en": "[docker-login] Skipping {registry} (token {env} not set).",
|
||||
"bg": "[docker-login] Пропускане на {registry} (токен {env} не е зададен).",
|
||||
"de": "[docker-login] {registry} übersprungen (Token {env} nicht gesetzt).",
|
||||
"pl": "[docker-login] Pomijanie {registry} (token {env} nie ustawiony).",
|
||||
"ru": "[docker-login] Пропуск {registry} (токен {env} не задан).",
|
||||
"zh": "[docker-login] 跳过 {registry}(未设置令牌 {env})。"
|
||||
},
|
||||
"[dry-run] No changes pushed.": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "[dry-run] No changes pushed.",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"[dry-run] Would commit and push wiki changes": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "[dry-run] Would commit and push wiki changes",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"[dry-run] Would commit: release: v{version} [skip ci]": {
|
||||
"bg": "[dry-run] Would commit: release: v{version} [skip ci]",
|
||||
"de": "[dry-run] Would commit: release: v{version} [skip ci]",
|
||||
@@ -2703,13 +3295,13 @@
|
||||
"ru": "[dry-run] Would push commit to master",
|
||||
"zh": "[dry-run] Would push commit to master"
|
||||
},
|
||||
"[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)",
|
||||
"en": "[dry-run] Would sync page: {title} ({chars} chars)",
|
||||
"pl": "[dry-run] Zsynchronizowano by stronę: {title} ({chars} znaków)",
|
||||
"ru": "[dry-run] Would sync page: {title} ({chars} chars)",
|
||||
"zh": "[dry-run] Would sync page: {title} ({chars} chars)"
|
||||
"[dry-run] Would update doc version references via check_doc_versions --fix": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"en": "[dry-run] Would update doc version references via check_doc_versions --fix",
|
||||
"pl": "",
|
||||
"ru": "",
|
||||
"zh": ""
|
||||
},
|
||||
"[dry-run] Would update {changelog_file}": {
|
||||
"bg": "[dry-run] Would update {changelog_file}",
|
||||
@@ -2727,6 +3319,38 @@
|
||||
"ru": "[dry-run] Would update {init}",
|
||||
"zh": "[dry-run] Would update {init}"
|
||||
},
|
||||
"[tofu-init] Done.": {
|
||||
"en": "[tofu-init] Done.",
|
||||
"bg": "[tofu-init] Готово.",
|
||||
"de": "[tofu-init] Fertig.",
|
||||
"pl": "[tofu-init] Gotowe.",
|
||||
"ru": "[tofu-init] Готово.",
|
||||
"zh": "[tofu-init] 完成。"
|
||||
},
|
||||
"[tofu-init] Initializing {dir}...": {
|
||||
"en": "[tofu-init] Initializing {dir}...",
|
||||
"bg": "[tofu-init] Инициализиране на {dir}...",
|
||||
"de": "[tofu-init] Initialisiere {dir}...",
|
||||
"pl": "[tofu-init] Inicjalizacja {dir}...",
|
||||
"ru": "[tofu-init] Инициализация {dir}...",
|
||||
"zh": "[tofu-init] 正在初始化 {dir}..."
|
||||
},
|
||||
"[tofu-{mode}] All configurations valid.": {
|
||||
"en": "[tofu-{mode}] All configurations valid.",
|
||||
"bg": "[tofu-{mode}] Всички конфигурации са валидни.",
|
||||
"de": "[tofu-{mode}] Alle Konfigurationen gültig.",
|
||||
"pl": "[tofu-{mode}] Wszystkie konfiguracje są poprawne.",
|
||||
"ru": "[tofu-{mode}] Все конфигурации валидны.",
|
||||
"zh": "[tofu-{mode}] 所有配置有效。"
|
||||
},
|
||||
"[tofu-{mode}] Validating OpenTofu configurations...": {
|
||||
"en": "[tofu-{mode}] Validating OpenTofu configurations...",
|
||||
"bg": "[tofu-{mode}] Проверка на OpenTofu конфигурациите...",
|
||||
"de": "[tofu-{mode}] Validiere OpenTofu-Konfigurationen...",
|
||||
"pl": "[tofu-{mode}] Sprawdzanie konfiguracji OpenTofu...",
|
||||
"ru": "[tofu-{mode}] Проверка конфигураций OpenTofu...",
|
||||
"zh": "[tofu-{mode}] 正在验证 OpenTofu 配置..."
|
||||
},
|
||||
"[tool.devx] missing required keys: {keys}": {
|
||||
"bg": "[tool.devx] липсват задължителни ключове: {keys}",
|
||||
"de": "[tool.devx] fehlt erforderliche Schlüssel: {keys}",
|
||||
@@ -2879,6 +3503,14 @@
|
||||
"ru": "tea not installed — skipping login configuration.",
|
||||
"zh": "tea not installed — skipping login configuration."
|
||||
},
|
||||
"tofu command failed in {dir}: {error}": {
|
||||
"en": "tofu command failed in {dir}: {error}",
|
||||
"bg": "командата tofu не успя в {dir}: {error}",
|
||||
"de": "tofu-Befehl fehlgeschlagen in {dir}: {error}",
|
||||
"pl": "polecenie tofu nie powiodło się w {dir}: {error}",
|
||||
"ru": "команда tofu не удалась в {dir}: {error}",
|
||||
"zh": "tofu 命令在 {dir} 中失败: {error}"
|
||||
},
|
||||
"unknown": {
|
||||
"bg": "неизвестен",
|
||||
"de": "unbekannt",
|
||||
@@ -2887,286 +3519,6 @@
|
||||
"ru": "неизвестно",
|
||||
"zh": "未知"
|
||||
},
|
||||
"{file} already exists. Use --force to overwrite.": {
|
||||
"bg": "{file} already exists. Use --force to overwrite.",
|
||||
"de": "{file} already exists. Use --force to overwrite.",
|
||||
"en": "{file} already exists. Use --force to overwrite.",
|
||||
"pl": "{file} już istnieje. Użyj --force, aby nadpisać.",
|
||||
"ru": "{file} already exists. Use --force to overwrite.",
|
||||
"zh": "{file} already exists. Use --force to overwrite."
|
||||
},
|
||||
"{separator}": {
|
||||
"bg": "{separator}",
|
||||
"de": "{separator}",
|
||||
"en": "{separator}",
|
||||
"pl": "{separator}",
|
||||
"ru": "{separator}",
|
||||
"zh": "{separator}"
|
||||
},
|
||||
"Failed to push release commit after 3 attempts. Manual intervention required.": {
|
||||
"bg": "Failed to push release commit after 3 attempts. Manual intervention required.",
|
||||
"de": "Failed to push release commit after 3 attempts. Manual intervention required.",
|
||||
"en": "Failed to push release commit after 3 attempts. Manual intervention required.",
|
||||
"pl": "Failed to push release commit after 3 attempts. Manual intervention required.",
|
||||
"ru": "Failed to push release commit after 3 attempts. Manual intervention required.",
|
||||
"zh": "Failed to push release commit after 3 attempts. Manual intervention required."
|
||||
},
|
||||
"Push attempt {n}/3 failed: {err}": {
|
||||
"bg": "Push attempt {n}/3 failed: {err}",
|
||||
"de": "Push attempt {n}/3 failed: {err}",
|
||||
"en": "Push attempt {n}/3 failed: {err}",
|
||||
"pl": "Push attempt {n}/3 failed: {err}",
|
||||
"ru": "Push attempt {n}/3 failed: {err}",
|
||||
"zh": "Push attempt {n}/3 failed: {err}"
|
||||
},
|
||||
"Rebase attempt {n}/3 failed: {err}": {
|
||||
"bg": "Rebase attempt {n}/3 failed: {err}",
|
||||
"de": "Rebase attempt {n}/3 failed: {err}",
|
||||
"en": "Rebase attempt {n}/3 failed: {err}",
|
||||
"pl": "Rebase attempt {n}/3 failed: {err}",
|
||||
"ru": "Rebase attempt {n}/3 failed: {err}",
|
||||
"zh": "Rebase attempt {n}/3 failed: {err}"
|
||||
},
|
||||
"Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.": {
|
||||
"bg": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
|
||||
"de": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
|
||||
"en": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
|
||||
"pl": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
|
||||
"ru": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
|
||||
"zh": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label."
|
||||
},
|
||||
"Branch is already up-to-date with origin/master.": {
|
||||
"bg": "Branch is already up-to-date with origin/master.",
|
||||
"de": "Branch is already up-to-date with origin/master.",
|
||||
"en": "Branch is already up-to-date with origin/master.",
|
||||
"pl": "Branch is already up-to-date with origin/master.",
|
||||
"ru": "Branch is already up-to-date with origin/master.",
|
||||
"zh": "Branch is already up-to-date with origin/master."
|
||||
},
|
||||
"Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.": {
|
||||
"bg": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
|
||||
"de": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
|
||||
"en": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
|
||||
"pl": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
|
||||
"ru": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
|
||||
"zh": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR."
|
||||
},
|
||||
"Branch is {count} commit(s) behind master. Rebasing...": {
|
||||
"bg": "Branch is {count} commit(s) behind master. Rebasing...",
|
||||
"de": "Branch is {count} commit(s) behind master. Rebasing...",
|
||||
"en": "Branch is {count} commit(s) behind master. Rebasing...",
|
||||
"pl": "Branch is {count} commit(s) behind master. Rebasing...",
|
||||
"ru": "Branch is {count} commit(s) behind master. Rebasing...",
|
||||
"zh": "Branch is {count} commit(s) behind master. Rebasing..."
|
||||
},
|
||||
"CI_GITEA_TOKEN is not set. Add it to .env or export it.": {
|
||||
"bg": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
|
||||
"de": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
|
||||
"en": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
|
||||
"pl": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
|
||||
"ru": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
|
||||
"zh": "CI_GITEA_TOKEN is not set. Add it to .env or export it."
|
||||
},
|
||||
"Cannot rebase: not on a branch (detached HEAD).": {
|
||||
"bg": "Cannot rebase: not on a branch (detached HEAD).",
|
||||
"de": "Cannot rebase: not on a branch (detached HEAD).",
|
||||
"en": "Cannot rebase: not on a branch (detached HEAD).",
|
||||
"pl": "Cannot rebase: not on a branch (detached HEAD).",
|
||||
"ru": "Cannot rebase: not on a branch (detached HEAD).",
|
||||
"zh": "Cannot rebase: not on a branch (detached HEAD)."
|
||||
},
|
||||
"Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.": {
|
||||
"bg": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
|
||||
"de": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
|
||||
"en": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
|
||||
"pl": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
|
||||
"ru": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
|
||||
"zh": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR."
|
||||
},
|
||||
"Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.": {
|
||||
"bg": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
|
||||
"de": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
|
||||
"en": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
|
||||
"pl": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
|
||||
"ru": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
|
||||
"zh": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables."
|
||||
},
|
||||
"Fetch failed: {error}": {
|
||||
"bg": "Fetch failed: {error}",
|
||||
"de": "Fetch failed: {error}",
|
||||
"en": "Fetch failed: {error}",
|
||||
"pl": "Fetch failed: {error}",
|
||||
"ru": "Fetch failed: {error}",
|
||||
"zh": "Fetch failed: {error}"
|
||||
},
|
||||
"Fetching origin/master...": {
|
||||
"bg": "Fetching origin/master...",
|
||||
"de": "Fetching origin/master...",
|
||||
"en": "Fetching origin/master...",
|
||||
"pl": "Fetching origin/master...",
|
||||
"ru": "Fetching origin/master...",
|
||||
"zh": "Fetching origin/master..."
|
||||
},
|
||||
"Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.": {
|
||||
"bg": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
|
||||
"de": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
|
||||
"en": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
|
||||
"pl": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
|
||||
"ru": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
|
||||
"zh": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again."
|
||||
},
|
||||
"Force-pushing...": {
|
||||
"bg": "Force-pushing...",
|
||||
"de": "Force-pushing...",
|
||||
"en": "Force-pushing...",
|
||||
"pl": "Force-pushing...",
|
||||
"ru": "Force-pushing...",
|
||||
"zh": "Force-pushing..."
|
||||
},
|
||||
"Nothing to push.": {
|
||||
"bg": "Nothing to push.",
|
||||
"de": "Nothing to push.",
|
||||
"en": "Nothing to push.",
|
||||
"pl": "Nothing to push.",
|
||||
"ru": "Nothing to push.",
|
||||
"zh": "Nothing to push."
|
||||
},
|
||||
"PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.": {
|
||||
"bg": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
|
||||
"de": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
|
||||
"en": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
|
||||
"pl": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
|
||||
"ru": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
|
||||
"zh": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR."
|
||||
},
|
||||
"Pushed {branch} to origin.": {
|
||||
"bg": "Pushed {branch} to origin.",
|
||||
"de": "Pushed {branch} to origin.",
|
||||
"en": "Pushed {branch} to origin.",
|
||||
"pl": "Pushed {branch} to origin.",
|
||||
"ru": "Pushed {branch} to origin.",
|
||||
"zh": "Pushed {branch} to origin."
|
||||
},
|
||||
"Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue": {
|
||||
"bg": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
|
||||
"de": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
|
||||
"en": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
|
||||
"pl": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
|
||||
"ru": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
|
||||
"zh": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue"
|
||||
},
|
||||
"Rebase failed with HTTP {status}: {message}": {
|
||||
"bg": "Rebase failed with HTTP {status}: {message}",
|
||||
"de": "Rebase failed with HTTP {status}: {message}",
|
||||
"en": "Rebase failed with HTTP {status}: {message}",
|
||||
"pl": "Rebase failed with HTTP {status}: {message}",
|
||||
"ru": "Rebase failed with HTTP {status}: {message}",
|
||||
"zh": "Rebase failed with HTTP {status}: {message}"
|
||||
},
|
||||
"Rebase successful.": {
|
||||
"bg": "Rebase successful.",
|
||||
"de": "Rebase successful.",
|
||||
"en": "Rebase successful.",
|
||||
"pl": "Rebase successful.",
|
||||
"ru": "Rebase successful.",
|
||||
"zh": "Rebase successful."
|
||||
},
|
||||
"Rebasing PR #{pr} via Gitea API...": {
|
||||
"bg": "Rebasing PR #{pr} via Gitea API...",
|
||||
"de": "Rebasing PR #{pr} via Gitea API...",
|
||||
"en": "Rebasing PR #{pr} via Gitea API...",
|
||||
"pl": "Rebasing PR #{pr} via Gitea API...",
|
||||
"ru": "Rebasing PR #{pr} via Gitea API...",
|
||||
"zh": "Rebasing PR #{pr} via Gitea API..."
|
||||
},
|
||||
"Ensuring standard labels...": {
|
||||
"bg": "Ensuring standard labels...",
|
||||
"de": "Ensuring standard labels...",
|
||||
"en": "Ensuring standard labels...",
|
||||
"pl": "Ensuring standard labels...",
|
||||
"ru": "Ensuring standard labels...",
|
||||
"zh": "Ensuring standard labels..."
|
||||
},
|
||||
" - {count} standard labels verified": {
|
||||
"bg": " - {count} standard labels verified",
|
||||
"de": " - {count} standard labels verified",
|
||||
"en": " - {count} standard labels verified",
|
||||
"pl": " - {count} standard labels verified",
|
||||
"ru": " - {count} standard labels verified",
|
||||
"zh": " - {count} standard labels verified"
|
||||
},
|
||||
"[check-deps] Virtualenv .venv ready (Python {version}).": {
|
||||
"en": "[check-deps] Virtualenv .venv ready (Python {version}).",
|
||||
"bg": "[check-deps] Виртуална среда .venv готова (Python {version}).",
|
||||
"de": "[check-deps] Virtuelle Umgebung .venv bereit (Python {version}).",
|
||||
"pl": "[check-deps] Środowisko wirtualne .venv gotowe (Python {version}).",
|
||||
"ru": "[check-deps] Виртуальное окружение .venv готово (Python {version}).",
|
||||
"zh": "[check-deps] 虚拟环境 .venv 已就绪 (Python {version})。"
|
||||
},
|
||||
"{level}: {tool} not found.{hint}": {
|
||||
"en": "{level}: {tool} not found.{hint}",
|
||||
"bg": "{level}: {tool} не е намерен.{hint}",
|
||||
"de": "{level}: {tool} nicht gefunden.{hint}",
|
||||
"pl": "{level}: {tool} nie znaleziono.{hint}",
|
||||
"ru": "{level}: {tool} не найден.{hint}",
|
||||
"zh": "{level}: 未找到 {tool}。{hint}"
|
||||
},
|
||||
"WARN: Could not determine Python version in .venv.": {
|
||||
"en": "WARN: Could not determine Python version in .venv.",
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: Не може да се определи версията на Python в .venv.",
|
||||
"de": "WARNUNG: Python-Version in .venv konnte nicht bestimmt werden.",
|
||||
"pl": "OSTRZEŻENIE: Nie można określić wersji Python w .venv.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: Не удалось определить версию Python в .venv.",
|
||||
"zh": "警告: 无法确定 .venv 中的 Python 版本。"
|
||||
},
|
||||
"WARN: Could not parse Python version '{version}'.": {
|
||||
"en": "WARN: Could not parse Python version '{version}'.",
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: Не може да се анализира версията на Python '{version}'.",
|
||||
"de": "WARNUNG: Python-Version '{version}' konnte nicht analysiert werden.",
|
||||
"pl": "OSTRZEŻENIE: Nie można przeanalizować wersji Python '{version}'.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: Не удалось разобрать версию Python '{version}'.",
|
||||
"zh": "警告: 无法解析 Python 版本 '{version}'。"
|
||||
},
|
||||
"WARN: .venv not found. Run 'make setup-venv' to create it.": {
|
||||
"en": "WARN: .venv not found. Run 'make setup-venv' to create it.",
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: .venv не е намерен. Изпълнете 'make setup-venv' за създаване.",
|
||||
"de": "WARNUNG: .venv nicht gefunden. Führen Sie 'make setup-venv' aus, um es zu erstellen.",
|
||||
"pl": "OSTRZEŻENIE: Nie znaleziono .venv. Uruchom 'make setup-venv', aby utworzyć.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: .venv не найден. Выполните 'make setup-venv' для создания.",
|
||||
"zh": "警告: 未找到 .venv。运行 'make setup-venv' 来创建。"
|
||||
},
|
||||
"[docker-login] Logged in to {registry}.": {
|
||||
"en": "[docker-login] Logged in to {registry}.",
|
||||
"bg": "[docker-login] Влязъл в {registry}.",
|
||||
"de": "[docker-login] Angemeldet bei {registry}.",
|
||||
"pl": "[docker-login] Zalogowano do {registry}.",
|
||||
"ru": "[docker-login] Выполнен вход в {registry}.",
|
||||
"zh": "[docker-login] 已登录到 {registry}。"
|
||||
},
|
||||
"[docker-login] Login to {registry} failed (continuing).": {
|
||||
"en": "[docker-login] Login to {registry} failed (continuing).",
|
||||
"bg": "[docker-login] Влизането в {registry} не успя (продължава).",
|
||||
"de": "[docker-login] Anmeldung bei {registry} fehlgeschlagen (wird fortgesetzt).",
|
||||
"pl": "[docker-login] Logowanie do {registry} nie powiodło się (kontynuowanie).",
|
||||
"ru": "[docker-login] Ошибка входа в {registry} (продолжаем).",
|
||||
"zh": "[docker-login] 登录 {registry} 失败(继续)。"
|
||||
},
|
||||
"[docker-login] Skipping {registry} (token {env} not set).": {
|
||||
"en": "[docker-login] Skipping {registry} (token {env} not set).",
|
||||
"bg": "[docker-login] Пропускане на {registry} (токен {env} не е зададен).",
|
||||
"de": "[docker-login] {registry} übersprungen (Token {env} nicht gesetzt).",
|
||||
"pl": "[docker-login] Pomijanie {registry} (token {env} nie ustawiony).",
|
||||
"ru": "[docker-login] Пропуск {registry} (токен {env} не задан).",
|
||||
"zh": "[docker-login] 跳过 {registry}(未设置令牌 {env})。"
|
||||
},
|
||||
"{env} is not set. Set it in your .env file.": {
|
||||
"en": "{env} is not set. Set it in your .env file.",
|
||||
"bg": "{env} не е зададен. Задайте го във вашия .env файл.",
|
||||
"de": "{env} ist nicht gesetzt. Setzen Sie es in Ihrer .env-Datei.",
|
||||
"pl": "{env} nie jest ustawiony. Ustaw go w pliku .env.",
|
||||
"ru": "{env} не задан. Установите его в файле .env.",
|
||||
"zh": "{env} 未设置。请在 .env 文件中设置。"
|
||||
},
|
||||
"{env} is not set. Set it in your .env file or pass it as an environment variable.": {
|
||||
"en": "{env} is not set. Set it in your .env file or pass it as an environment variable.",
|
||||
"bg": "{env} не е зададен. Задайте го във вашия .env файл или го подайте като променлива на средата.",
|
||||
@@ -3175,188 +3527,36 @@
|
||||
"ru": "{env} не задан. Установите его в файле .env или передайте как переменную окружения.",
|
||||
"zh": "{env} 未设置。请在 .env 文件中设置或作为环境变量传递。"
|
||||
},
|
||||
"Login to {registry} failed: {error}": {
|
||||
"en": "Login to {registry} failed: {error}",
|
||||
"bg": "Влизането в {registry} не успя: {error}",
|
||||
"de": "Anmeldung bei {registry} fehlgeschlagen: {error}",
|
||||
"pl": "Logowanie do {registry} nie powiodło się: {error}",
|
||||
"ru": "Ошибка входа в {registry}: {error}",
|
||||
"zh": "登录 {registry} 失败: {error}"
|
||||
"{env} is not set. Set it in your .env file.": {
|
||||
"en": "{env} is not set. Set it in your .env file.",
|
||||
"bg": "{env} не е зададен. Задайте го във вашия .env файл.",
|
||||
"de": "{env} ist nicht gesetzt. Setzen Sie es in Ihrer .env-Datei.",
|
||||
"pl": "{env} nie jest ustawiony. Ustaw go w pliku .env.",
|
||||
"ru": "{env} не задан. Установите его в файле .env.",
|
||||
"zh": "{env} 未设置。请在 .env 文件中设置。"
|
||||
},
|
||||
"tofu command failed in {dir}: {error}": {
|
||||
"en": "tofu command failed in {dir}: {error}",
|
||||
"bg": "командата tofu не успя в {dir}: {error}",
|
||||
"de": "tofu-Befehl fehlgeschlagen in {dir}: {error}",
|
||||
"pl": "polecenie tofu nie powiodło się w {dir}: {error}",
|
||||
"ru": "команда tofu не удалась в {dir}: {error}",
|
||||
"zh": "tofu 命令在 {dir} 中失败: {error}"
|
||||
"{file} already exists. Use --force to overwrite.": {
|
||||
"bg": "{file} already exists. Use --force to overwrite.",
|
||||
"de": "{file} already exists. Use --force to overwrite.",
|
||||
"en": "{file} already exists. Use --force to overwrite.",
|
||||
"pl": "{file} już istnieje. Użyj --force, aby nadpisać.",
|
||||
"ru": "{file} already exists. Use --force to overwrite.",
|
||||
"zh": "{file} already exists. Use --force to overwrite."
|
||||
},
|
||||
"WARN: .venv has Python {version}, but >={req} is required.": {
|
||||
"en": "WARN: .venv has Python {version}, but >={req} is required.",
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: .venv има Python {version}, но се изисква >={req}.",
|
||||
"de": "WARNUNG: .venv hat Python {version}, aber >={req} ist erforderlich.",
|
||||
"pl": "OSTRZEŻENIE: .venv ma Python {version}, ale wymagane jest >={req}.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: в .venv установлен Python {version}, но требуется >={req}.",
|
||||
"zh": "警告: .venv 的 Python 版本为 {version},但要求 >={req}。"
|
||||
"{level}: {tool} not found.{hint}": {
|
||||
"en": "{level}: {tool} not found.{hint}",
|
||||
"bg": "{level}: {tool} не е намерен.{hint}",
|
||||
"de": "{level}: {tool} nicht gefunden.{hint}",
|
||||
"pl": "{level}: {tool} nie znaleziono.{hint}",
|
||||
"ru": "{level}: {tool} не найден.{hint}",
|
||||
"zh": "{level}: 未找到 {tool}。{hint}"
|
||||
},
|
||||
" -> {dir}": {
|
||||
"en": " -> {dir}",
|
||||
"bg": " -> {dir}",
|
||||
"de": " -> {dir}",
|
||||
"pl": " -> {dir}",
|
||||
"ru": " -> {dir}",
|
||||
"zh": " -> {dir}"
|
||||
},
|
||||
"[tofu-init] Initializing {dir}...": {
|
||||
"en": "[tofu-init] Initializing {dir}...",
|
||||
"bg": "[tofu-init] Инициализиране на {dir}...",
|
||||
"de": "[tofu-init] Initialisiere {dir}...",
|
||||
"pl": "[tofu-init] Inicjalizacja {dir}...",
|
||||
"ru": "[tofu-init] Инициализация {dir}...",
|
||||
"zh": "[tofu-init] 正在初始化 {dir}..."
|
||||
},
|
||||
"[tofu-init] Done.": {
|
||||
"en": "[tofu-init] Done.",
|
||||
"bg": "[tofu-init] Готово.",
|
||||
"de": "[tofu-init] Fertig.",
|
||||
"pl": "[tofu-init] Gotowe.",
|
||||
"ru": "[tofu-init] Готово.",
|
||||
"zh": "[tofu-init] 完成。"
|
||||
},
|
||||
"[tofu-{mode}] Validating OpenTofu configurations...": {
|
||||
"en": "[tofu-{mode}] Validating OpenTofu configurations...",
|
||||
"bg": "[tofu-{mode}] Проверка на OpenTofu конфигурациите...",
|
||||
"de": "[tofu-{mode}] Validiere OpenTofu-Konfigurationen...",
|
||||
"pl": "[tofu-{mode}] Sprawdzanie konfiguracji OpenTofu...",
|
||||
"ru": "[tofu-{mode}] Проверка конфигураций OpenTofu...",
|
||||
"zh": "[tofu-{mode}] 正在验证 OpenTofu 配置..."
|
||||
},
|
||||
"[tofu-{mode}] All configurations valid.": {
|
||||
"en": "[tofu-{mode}] All configurations valid.",
|
||||
"bg": "[tofu-{mode}] Всички конфигурации са валидни.",
|
||||
"de": "[tofu-{mode}] Alle Konfigurationen gültig.",
|
||||
"pl": "[tofu-{mode}] Wszystkie konfiguracje są poprawne.",
|
||||
"ru": "[tofu-{mode}] Все конфигурации валидны.",
|
||||
"zh": "[tofu-{mode}] 所有配置有效。"
|
||||
},
|
||||
"[check-deps] Verifying tools...": {
|
||||
"en": "[check-deps] Verifying tools...",
|
||||
"bg": "[check-deps] Проверка на инструментите...",
|
||||
"de": "[check-deps] Werkzeuge werden überprüft...",
|
||||
"pl": "[check-deps] Sprawdzanie narzędzi...",
|
||||
"ru": "[check-deps] Проверка инструментов...",
|
||||
"zh": "[check-deps] 正在验证工具..."
|
||||
},
|
||||
" {tool}: found at {path}": {
|
||||
"en": " {tool}: found at {path}",
|
||||
"bg": " {tool}: намерен на {path}",
|
||||
"de": " {tool}: gefunden unter {path}",
|
||||
"pl": " {tool}: znaleziono w {path}",
|
||||
"ru": " {tool}: найден в {path}",
|
||||
"zh": " {tool}: 在 {path} 找到"
|
||||
},
|
||||
" Run 'make install-checkmake' to install the Makefile linter.": {
|
||||
"en": " Run 'make install-checkmake' to install the Makefile linter.",
|
||||
"bg": " Изпълнете 'make install-checkmake' за инсталиране на Makefile линтера.",
|
||||
"de": " Führen Sie 'make install-checkmake' aus, um den Makefile-Linter zu installieren.",
|
||||
"pl": " Uruchom 'make install-checkmake', aby zainstalować linter Makefile.",
|
||||
"ru": " Выполните 'make install-checkmake' для установки линтера Makefile.",
|
||||
"zh": " 运行 'make install-checkmake' 来安装 Makefile 检查器。"
|
||||
},
|
||||
"Required tools missing.": {
|
||||
"en": "Required tools missing.",
|
||||
"bg": "Липсват задължителни инструменти.",
|
||||
"de": "Erforderliche Werkzeuge fehlen.",
|
||||
"pl": "Brak wymaganych narzędzi.",
|
||||
"ru": "Отсутствуют обязательные инструменты.",
|
||||
"zh": "缺少必需的工具。"
|
||||
},
|
||||
"[check-deps] All core tools present.": {
|
||||
"en": "[check-deps] All core tools present.",
|
||||
"bg": "[check-deps] Всички основни инструменти са налични.",
|
||||
"de": "[check-deps] Alle Kernwerkzeuge vorhanden.",
|
||||
"pl": "[check-deps] Wszystkie podstawowe narzędzia są dostępne.",
|
||||
"ru": "[check-deps] Все основные инструменты доступны.",
|
||||
"zh": "[check-deps] 所有核心工具均已就绪。"
|
||||
},
|
||||
"SSH_PRIVATE_KEY not set — skipping SSH key setup": {
|
||||
"en": "SSH_PRIVATE_KEY not set — skipping SSH key setup",
|
||||
"bg": "SSH_PRIVATE_KEY не е зададен — пропускане на SSH ключ настройката",
|
||||
"de": "SSH_PRIVATE_KEY nicht gesetzt — SSH-Schlüssel-Setup übersprungen",
|
||||
"pl": "SSH_PRIVATE_KEY nie ustawione — pomijanie konfiguracji klucza SSH",
|
||||
"ru": "SSH_PRIVATE_KEY не задан — пропуск настройки SSH-ключа",
|
||||
"zh": "SSH_PRIVATE_KEY 未设置 — 跳过 SSH 密钥设置"
|
||||
},
|
||||
"Failed to start ssh-agent: {error}": {
|
||||
"en": "Failed to start ssh-agent: {error}",
|
||||
"bg": "Неуспешно стартиране на ssh-agent: {error}",
|
||||
"de": "Starten von ssh-agent fehlgeschlagen: {error}",
|
||||
"pl": "Nie udało się uruchomić ssh-agent: {error}",
|
||||
"ru": "Не удалось запустить ssh-agent: {error}",
|
||||
"zh": "启动 ssh-agent 失败: {error}"
|
||||
},
|
||||
"SSH key set up successfully": {
|
||||
"en": "SSH key set up successfully",
|
||||
"bg": "SSH ключът е настроен успешно",
|
||||
"de": "SSH-Schlüssel erfolgreich eingerichtet",
|
||||
"pl": "Klucz SSH skonfigurowany pomyślnie",
|
||||
"ru": "SSH-ключ успешно настроен",
|
||||
"zh": "SSH 密钥设置成功"
|
||||
},
|
||||
"SSH key setup skipped (no key provided)": {
|
||||
"en": "SSH key setup skipped (no key provided)",
|
||||
"bg": "Настройката на SSH ключ е пропусната (не е предоставен ключ)",
|
||||
"de": "SSH-Schlüssel-Setup übersprungen (kein Schlüssel bereitgestellt)",
|
||||
"pl": "Pominięto konfigurację klucza SSH (brak klucza)",
|
||||
"ru": "Настройка SSH-ключа пропущена (ключ не предоставлен)",
|
||||
"zh": "SSH 密钥设置已跳过(未提供密钥)"
|
||||
},
|
||||
"Found {count} unsafe identity check(s) in integration tests.": {
|
||||
"en": "Found {count} unsafe identity check(s) in integration tests.",
|
||||
"bg": "Намерени са {count} небрежни проверки за идентичност в интеграционните тестове.",
|
||||
"de": "{count} unsichere Identitätsprüfung(en) in Integrationstests gefunden.",
|
||||
"pl": "Znaleziono {count} niebezpiecznych sprawdzeń tożsamości w testach integracyjnych.",
|
||||
"ru": "Найдено {count} небезопасных проверок идентичности в интеграционных тестах.",
|
||||
"zh": "在集成测试中发现 {count} 个不安全的身份检查。"
|
||||
},
|
||||
"Use string comparison or _is_truthy()/_is_falsy() helpers instead. Add '{marker}' to suppress individual lines.": {
|
||||
"en": "Use string comparison or _is_truthy()/_is_falsy() helpers instead. Add '{marker}' to suppress individual lines.",
|
||||
"bg": "Използвайте сравнение на низове или _is_truthy()/_is_falsy() помощници. Добавете '{marker}' за потискане на отделни редове.",
|
||||
"de": "Verwenden Sie String-Vergleich oder _is_truthy()/_is_falsy() Hilfsfunktionen. Fügen Sie '{marker}' hinzu, um einzelne Zeilen zu unterdrücken.",
|
||||
"pl": "Użyj porównania ciągów lub pomocników _is_truthy()/_is_falsy(). Dodaj '{marker}', aby pominąć pojedyncze linie.",
|
||||
"ru": "Используйте строковое сравнение или помощники _is_truthy()/_is_falsy(). Добавьте '{marker}' для подавления отдельных строк.",
|
||||
"zh": "使用字符串比较或 _is_truthy()/_is_falsy() 辅助函数。添加 '{marker}' 以抑制个别行。"
|
||||
},
|
||||
"[check-api-identity-checks] Passed: no unsafe identity checks found": {
|
||||
"en": "[check-api-identity-checks] Passed: no unsafe identity checks found",
|
||||
"bg": "[check-api-identity-checks] Мина: не са намерени небрежни проверки за идентичност",
|
||||
"de": "[check-api-identity-checks] Bestanden: keine unsicheren Identitätsprüfungen gefunden",
|
||||
"pl": "[check-api-identity-checks] Passed: nie znaleziono niebezpiecznych sprawdzeń tożsamości",
|
||||
"ru": "[check-api-identity-checks] Пройдено: небезопасных проверок идентичности не найдено",
|
||||
"zh": "[check-api-identity-checks] 通过:未发现不安全的身份检查"
|
||||
},
|
||||
"Directory to scan (default: tests/integration). Can be repeated.": {
|
||||
"en": "Directory to scan (default: tests/integration). Can be repeated.",
|
||||
"bg": "Директория за сканиране (по подразбиране: tests/integration). Може да се повтаря.",
|
||||
"de": "Zu scannendes Verzeichnis (Standard: tests/integration). Kann wiederholt werden.",
|
||||
"pl": "Katalog do skanowania (domyślnie: tests/integration). Można powtarzać.",
|
||||
"ru": "Директория для сканирования (по умолчанию: tests/integration). Можно повторять.",
|
||||
"zh": "要扫描的目录(默认:tests/integration)。可重复。"
|
||||
},
|
||||
"Failed to list existing wiki pages after retries: {error}. Aborting to avoid creating duplicate pages.": {
|
||||
"bg": "Неуспешно извличане на съществуващи wiki страници след повторни опити: {error}. Прекратяване, за да се избегне създаване на дублирани страници.",
|
||||
"de": "Abrufen bestehender Wiki-Seiten nach Wiederholungen fehlgeschlagen: {error}. Abbruch, um doppelte Seiten zu vermeiden.",
|
||||
"en": "Failed to list existing wiki pages after retries: {error}. Aborting to avoid creating duplicate pages.",
|
||||
"pl": "Nie udało się wylistować istniejących stron wiki po ponownych próbach: {error}. Przerywanie, aby uniknąć tworzenia zduplikowanych stron.",
|
||||
"ru": "Не удалось получить список существующих wiki-страниц после повторных попыток: {error}. Прерывание, чтобы избежать создания дубликатов страниц.",
|
||||
"zh": "重试后列出现有 wiki 页面失败:{error}。正在中止以避免创建重复页面。"
|
||||
},
|
||||
" Page '{title}' already exists (stale list). Re-listing and updating...": {
|
||||
"bg": " Страницата '{title}' вече съществува (остарял списък). Пресписване и обновяване...",
|
||||
"de": " Seite '{title}' existiert bereits (veraltete Liste). Neu auflisten und aktualisieren...",
|
||||
"en": " Page '{title}' already exists (stale list). Re-listing and updating...",
|
||||
"pl": " Strona '{title}' już istnieje (nieaktualna lista). Ponowne listowanie i aktualizacja...",
|
||||
"ru": " Страница '{title}' уже существует (устаревший список). Повторное получение списка и обновление...",
|
||||
"zh": " 页面 '{title}' 已存在(列表过期)。重新列出并更新..."
|
||||
"{separator}": {
|
||||
"bg": "{separator}",
|
||||
"de": "{separator}",
|
||||
"en": "{separator}",
|
||||
"pl": "{separator}",
|
||||
"ru": "{separator}",
|
||||
"zh": "{separator}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -909,3 +909,65 @@ class TestGiteaClientActions:
|
||||
"https://git.example.com/repos/owner/repo/actions/jobs/10026/logs",
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
def test_get_repo_variable_returns_value(self) -> None:
|
||||
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
|
||||
client._session.request = MagicMock(return_value=_mock_response({"value": "v0.28.1"}))
|
||||
result = client.get_repo_variable("PRODUCTION_DEPLOY_TAG")
|
||||
assert result == "v0.28.1"
|
||||
client._session.request.assert_called_once_with(
|
||||
"GET",
|
||||
"https://git.example.com/repos/owner/repo/actions/variables/PRODUCTION_DEPLOY_TAG",
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
def test_get_repo_variable_returns_none_on_404(self) -> None:
|
||||
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
|
||||
not_found = MagicMock()
|
||||
not_found.raise_for_status.side_effect = _mock_http_error(404, "not found")
|
||||
client._session.request = MagicMock(return_value=not_found)
|
||||
result = client.get_repo_variable("MISSING_VAR")
|
||||
assert result is None
|
||||
|
||||
@patch("time.sleep")
|
||||
def test_get_repo_variable_reraises_non_404(self, mock_sleep: MagicMock) -> None:
|
||||
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
|
||||
server_error = MagicMock()
|
||||
server_error.raise_for_status.side_effect = _mock_http_error(500, "server error")
|
||||
client._session.request = MagicMock(return_value=server_error)
|
||||
with pytest.raises(APIError) as exc_info:
|
||||
client.get_repo_variable("SOME_VAR")
|
||||
assert exc_info.value.status == 500
|
||||
|
||||
def test_set_repo_variable_updates_existing(self) -> None:
|
||||
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
|
||||
client._session.request = MagicMock(return_value=_mock_response({}))
|
||||
client.set_repo_variable("PRODUCTION_DEPLOY_TAG", "v0.28.2")
|
||||
client._session.request.assert_called_once_with(
|
||||
"PATCH",
|
||||
"https://git.example.com/repos/owner/repo/actions/variables/PRODUCTION_DEPLOY_TAG",
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
json={"value": "v0.28.2"},
|
||||
)
|
||||
|
||||
def test_set_repo_variable_creates_on_404(self) -> None:
|
||||
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
|
||||
not_found = MagicMock()
|
||||
not_found.raise_for_status.side_effect = _mock_http_error(404, "not found")
|
||||
created = _mock_response({})
|
||||
client._session.request = MagicMock(side_effect=[not_found, created])
|
||||
client.set_repo_variable("NEW_VAR", "v0.29.0")
|
||||
assert client._session.request.call_count == 2
|
||||
second_call = client._session.request.call_args_list[1]
|
||||
assert second_call.args[0] == "POST"
|
||||
assert second_call.args[1] == "https://git.example.com/repos/owner/repo/actions/variables"
|
||||
assert second_call.kwargs["json"] == {"name": "NEW_VAR", "value": "v0.29.0"}
|
||||
|
||||
def test_set_repo_variable_reraises_non_404(self) -> None:
|
||||
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
|
||||
forbidden = MagicMock()
|
||||
forbidden.raise_for_status.side_effect = _mock_http_error(403, "forbidden")
|
||||
client._session.request = MagicMock(return_value=forbidden)
|
||||
with pytest.raises(APIError) as exc_info:
|
||||
client.set_repo_variable("SOME_VAR", "val")
|
||||
assert exc_info.value.status == 403
|
||||
|
||||
+440
-552
@@ -1,6 +1,7 @@
|
||||
"""Unit tests for scripts/ci/sync_wiki.py."""
|
||||
"""Unit tests for devx.ci.sync_wiki (git-based approach)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -10,593 +11,480 @@ import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from devx.ci.sync_wiki import (
|
||||
decode_content,
|
||||
encode_content,
|
||||
fetch_page_content,
|
||||
list_wiki_pages,
|
||||
clone_wiki,
|
||||
commit_and_push,
|
||||
get_wiki_clone_url,
|
||||
init_wiki,
|
||||
load_mapping,
|
||||
main,
|
||||
read_doc_content,
|
||||
sync_page,
|
||||
verify_wiki_integrity,
|
||||
verify_wiki_page,
|
||||
sync_files,
|
||||
transform_links,
|
||||
wiki_filename,
|
||||
)
|
||||
from devx.exceptions import APIError
|
||||
|
||||
|
||||
class TestEncodeContent:
|
||||
def test_encodes_utf8_to_base64(self) -> None:
|
||||
result = encode_content("# Hello World")
|
||||
assert result == base64.b64encode(b"# Hello World").decode("ascii")
|
||||
class TestTransformLinks:
|
||||
def test_removes_md_extension(self) -> None:
|
||||
result = transform_links("[link](page.md)")
|
||||
assert result == "[link](page)"
|
||||
|
||||
def test_encodes_empty_string(self) -> None:
|
||||
assert encode_content("") == ""
|
||||
def test_removes_directory_prefix(self) -> None:
|
||||
result = transform_links("[link](docs/page.md)")
|
||||
assert result == "[link](page)"
|
||||
|
||||
def test_encodes_unicode(self) -> None:
|
||||
result = encode_content("# Café — résumé")
|
||||
decoded = base64.b64decode(result).decode("utf-8")
|
||||
assert decoded == "# Café — résumé"
|
||||
def test_removes_parent_dir_prefix(self) -> None:
|
||||
result = transform_links("[link](../page.md)")
|
||||
assert result == "[link](page)"
|
||||
|
||||
def test_preserves_external_links(self) -> None:
|
||||
result = transform_links("[link](https://example.com)")
|
||||
assert result == "[link](https://example.com)"
|
||||
|
||||
class TestDecodeContent:
|
||||
def test_decodes_base64_to_utf8(self) -> None:
|
||||
encoded = base64.b64encode(b"# Hello").decode("ascii")
|
||||
assert decode_content(encoded) == "# Hello"
|
||||
def test_preserves_http_links(self) -> None:
|
||||
result = transform_links("[link](http://example.com)")
|
||||
assert result == "[link](http://example.com)"
|
||||
|
||||
def test_empty_string_returns_empty(self) -> None:
|
||||
assert decode_content("") == ""
|
||||
def test_preserves_mailto(self) -> None:
|
||||
result = transform_links("[email](mailto:test@example.com)")
|
||||
assert result == "[email](mailto:test@example.com)"
|
||||
|
||||
def test_roundtrip(self) -> None:
|
||||
original = "# Wiki Page\n\nContent with **markdown**."
|
||||
encoded = encode_content(original)
|
||||
assert decode_content(encoded) == original
|
||||
def test_preserves_anchor_only(self) -> None:
|
||||
result = transform_links("[section](#section)")
|
||||
assert result == "[section](#section)"
|
||||
|
||||
def test_preserves_anchor_with_path(self) -> None:
|
||||
result = transform_links("[section](page.md#section)")
|
||||
assert result == "[section](page#section)"
|
||||
|
||||
def test_no_links_unchanged(self) -> None:
|
||||
text = "# Title\n\nSome text without links.\n"
|
||||
assert transform_links(text) == text
|
||||
|
||||
def test_multiple_links(self) -> None:
|
||||
result = transform_links("[a](one.md) and [b](two.md)")
|
||||
assert result == "[a](one) and [b](two)"
|
||||
|
||||
|
||||
class TestLoadMapping:
|
||||
def test_loads_mapping(self, tmp_path: Path) -> None:
|
||||
mapping_file = tmp_path / "mapping.json"
|
||||
mapping_file.write_text(json.dumps({"user/getting-started.md": "Getting-Started"}))
|
||||
with patch("devx.ci.sync_wiki.MAPPING_FILE", mapping_file):
|
||||
result = load_mapping()
|
||||
assert result == {"user/getting-started.md": "Getting-Started"}
|
||||
def test_loads_mapping(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
mapping_file = tmp_path / "docs" / "mapping.json"
|
||||
mapping_file.parent.mkdir()
|
||||
mapping_file.write_text(json.dumps({"index.md": "Home", "guide.md": "Guide"}))
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.MAPPING_FILE", mapping_file)
|
||||
mapping = load_mapping()
|
||||
assert mapping == {"index.md": "Home", "guide.md": "Guide"}
|
||||
|
||||
def test_missing_mapping_raises(self, tmp_path: Path) -> None:
|
||||
with patch("devx.ci.sync_wiki.MAPPING_FILE", tmp_path / "nonexistent.json"):
|
||||
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"
|
||||
def test_non_dict_raises(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
mapping_file = tmp_path / "docs" / "mapping.json"
|
||||
mapping_file.parent.mkdir()
|
||||
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()
|
||||
monkeypatch.setattr("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()
|
||||
def test_non_string_values_raises(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
mapping_file = tmp_path / "docs" / "mapping.json"
|
||||
mapping_file.parent.mkdir()
|
||||
mapping_file.write_text(json.dumps({"key": 123}))
|
||||
monkeypatch.setattr("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:
|
||||
docs_dir = tmp_path / "docs"
|
||||
docs_dir.mkdir()
|
||||
(docs_dir / "test.md").write_text("# Test\n\nContent")
|
||||
with patch("devx.ci.sync_wiki.DOCS_DIR", docs_dir):
|
||||
content = read_doc_content("test.md")
|
||||
assert content == "# Test\n\nContent"
|
||||
|
||||
def test_missing_file_raises(self, tmp_path: Path) -> None:
|
||||
with patch("devx.ci.sync_wiki.DOCS_DIR", tmp_path):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
read_doc_content("nonexistent.md")
|
||||
class TestGetWikiCloneUrl:
|
||||
def test_builds_url(self) -> None:
|
||||
url = get_wiki_clone_url("owner", "repo", "token")
|
||||
assert "owner/repo.wiki.git" in url
|
||||
|
||||
|
||||
class TestListWikiPages:
|
||||
def test_raises_on_api_error(self) -> None:
|
||||
client = MagicMock()
|
||||
client._request.side_effect = APIError(404, "not found")
|
||||
with pytest.raises(APIError):
|
||||
list_wiki_pages(client)
|
||||
class TestWikiFilename:
|
||||
def test_no_dashes(self) -> None:
|
||||
assert wiki_filename("Home") == "Home.md"
|
||||
|
||||
def test_returns_page_dict(self) -> None:
|
||||
client = MagicMock()
|
||||
client._request.return_value.json.return_value = [
|
||||
{"title": "Home", "sub_url": "Home"},
|
||||
{"title": "Getting-Started", "sub_url": "Getting-Started.-"},
|
||||
def test_single_word(self) -> None:
|
||||
assert wiki_filename("Architecture") == "Architecture.md"
|
||||
|
||||
def test_with_dashes_adds_marker(self) -> None:
|
||||
assert wiki_filename("Getting-Started") == "Getting-Started.-.md"
|
||||
|
||||
def test_spaces_become_dashes_with_marker(self) -> None:
|
||||
# "Getting Started" → "Getting-Started" (has dash) → marker added
|
||||
assert wiki_filename("Getting Started") == "Getting-Started.-.md"
|
||||
|
||||
def test_spaces_no_dashes_no_marker(self) -> None:
|
||||
# "Foo Bar" → "Foo-Bar" (has dash) → marker added
|
||||
assert wiki_filename("Foo Bar") == "Foo-Bar.-.md"
|
||||
|
||||
def test_single_word_with_spaces_no_dash(self) -> None:
|
||||
# No dash at all after conversion → no marker
|
||||
assert wiki_filename("HelloWorld") == "HelloWorld.md"
|
||||
|
||||
|
||||
class TestCloneWiki:
|
||||
@patch("devx.ci.sync_wiki.subprocess.run")
|
||||
def test_clone_success(self, mock_run: MagicMock, tmp_path: Path) -> None:
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||
result = clone_wiki("https://example.com/repo.wiki.git", tmp_path / "wiki")
|
||||
assert result is True
|
||||
|
||||
@patch("devx.ci.sync_wiki.subprocess.run")
|
||||
def test_clone_failure_returns_false(self, mock_run: MagicMock, tmp_path: Path) -> None:
|
||||
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="not found")
|
||||
result = clone_wiki("https://example.com/repo.wiki.git", tmp_path / "wiki")
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestInitWiki:
|
||||
@patch("devx.ci.sync_wiki.subprocess.run")
|
||||
def test_init_calls_git(self, mock_run: MagicMock, tmp_path: Path) -> None:
|
||||
wiki_dir = tmp_path / "wiki"
|
||||
init_wiki(wiki_dir)
|
||||
assert wiki_dir.exists()
|
||||
calls = [c.args[0] for c in mock_run.call_args_list]
|
||||
assert ["git", "init"] in calls
|
||||
assert ["git", "config", "user.email", "ci@oblachno.fyi"] in calls
|
||||
|
||||
|
||||
class TestSyncFiles:
|
||||
def test_syncs_files(self, tmp_path: Path) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n[link](page.md)\n")
|
||||
(docs / "page.md").write_text("# Page\n")
|
||||
wiki = tmp_path / "wiki"
|
||||
wiki.mkdir()
|
||||
mapping = {"index.md": "Home", "page.md": "Page"}
|
||||
synced, pruned = sync_files(docs, wiki, mapping, dry_run=False)
|
||||
assert synced == 2
|
||||
assert pruned == 0
|
||||
assert (wiki / "Home.md").exists()
|
||||
assert (wiki / "Page.md").exists()
|
||||
# Check link transformation
|
||||
content = (wiki / "Home.md").read_text()
|
||||
assert "[link](page)" in content
|
||||
|
||||
def test_prunes_stale(self, tmp_path: Path) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
wiki = tmp_path / "wiki"
|
||||
wiki.mkdir()
|
||||
(wiki / "OldPage.md").write_text("# Old\n")
|
||||
(wiki / "Home.md").write_text("# Old Home\n")
|
||||
mapping = {"index.md": "Home"}
|
||||
synced, pruned = sync_files(docs, wiki, mapping, dry_run=False)
|
||||
assert synced == 1
|
||||
assert pruned == 1 # OldPage.md pruned, Home.md overwritten
|
||||
assert not (wiki / "OldPage.md").exists()
|
||||
assert (wiki / "Home.md").exists()
|
||||
|
||||
def test_dry_run_no_writes(self, tmp_path: Path) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
wiki = tmp_path / "wiki"
|
||||
wiki.mkdir()
|
||||
mapping = {"index.md": "Home"}
|
||||
synced, pruned = sync_files(docs, wiki, mapping, dry_run=True)
|
||||
assert synced == 1
|
||||
assert pruned == 0
|
||||
assert not (wiki / "Home.md").exists()
|
||||
|
||||
def test_missing_file_warns(self, tmp_path: Path) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
wiki = tmp_path / "wiki"
|
||||
wiki.mkdir()
|
||||
mapping = {"missing.md": "Missing"}
|
||||
synced, pruned = sync_files(docs, wiki, mapping, dry_run=False)
|
||||
assert synced == 0
|
||||
|
||||
def test_empty_file_warns(self, tmp_path: Path) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "empty.md").write_text("")
|
||||
wiki = tmp_path / "wiki"
|
||||
wiki.mkdir()
|
||||
mapping = {"empty.md": "Empty"}
|
||||
synced, pruned = sync_files(docs, wiki, mapping, dry_run=False)
|
||||
assert synced == 0
|
||||
|
||||
|
||||
class TestCommitAndPush:
|
||||
@patch("devx.ci.sync_wiki.subprocess.run")
|
||||
def test_dry_run_returns_false(self, mock_run: MagicMock, tmp_path: Path) -> None:
|
||||
result = commit_and_push(tmp_path, "url", dry_run=True)
|
||||
assert result is False
|
||||
mock_run.assert_not_called()
|
||||
|
||||
@patch("devx.ci.sync_wiki.subprocess.run")
|
||||
def test_no_changes_returns_false(self, mock_run: MagicMock, tmp_path: Path) -> None:
|
||||
# git add succeeds, git diff --cached --quiet returns 0 (no changes)
|
||||
mock_run.side_effect = [
|
||||
MagicMock(returncode=0), # git add
|
||||
MagicMock(returncode=0), # git diff --cached --quiet (no changes)
|
||||
]
|
||||
result = list_wiki_pages(client)
|
||||
assert result == {"Home": "Home", "Getting-Started": "Getting-Started.-"}
|
||||
result = commit_and_push(tmp_path, "url", dry_run=False)
|
||||
assert result is False
|
||||
|
||||
@patch("devx.ci.sync_wiki.subprocess.run")
|
||||
def test_pushes_changes(self, mock_run: MagicMock, tmp_path: Path) -> None:
|
||||
mock_run.side_effect = [
|
||||
MagicMock(returncode=0), # git add
|
||||
MagicMock(returncode=1), # git diff --cached --quiet (has changes)
|
||||
MagicMock(returncode=0), # git config user.email
|
||||
MagicMock(returncode=0), # git config user.name
|
||||
MagicMock(returncode=0), # git commit
|
||||
MagicMock(returncode=0, stdout="", stderr=""), # git push
|
||||
]
|
||||
result = commit_and_push(tmp_path, "url", dry_run=False)
|
||||
assert result is True
|
||||
|
||||
class TestFetchPageContent:
|
||||
def test_fetches_and_decodes_content(self) -> None:
|
||||
client = MagicMock()
|
||||
encoded = base64.b64encode(b"# Hello Wiki").decode("ascii")
|
||||
client._request.return_value.json.return_value = {"content_base64": encoded}
|
||||
result = fetch_page_content(client, "Home")
|
||||
assert result == "# Hello Wiki"
|
||||
|
||||
def test_returns_empty_on_api_error(self) -> None:
|
||||
from devx.exceptions import APIError
|
||||
|
||||
client = MagicMock()
|
||||
client._request.side_effect = APIError(404, "not found")
|
||||
assert fetch_page_content(client, "Missing") == ""
|
||||
|
||||
def test_returns_empty_for_empty_content(self) -> None:
|
||||
client = MagicMock()
|
||||
client._request.return_value.json.return_value = {"content_base64": ""}
|
||||
assert fetch_page_content(client, "Home") == ""
|
||||
|
||||
|
||||
class TestSyncPage:
|
||||
def test_dry_run_skips(self) -> None:
|
||||
client = MagicMock()
|
||||
result = sync_page(client, "Test-Page", "# Content", {}, dry_run=True)
|
||||
assert result == "skipped"
|
||||
client._request.assert_not_called()
|
||||
|
||||
def test_creates_new_page_with_base64(self) -> None:
|
||||
client = MagicMock()
|
||||
result = sync_page(client, "New-Page", "# Content", {}, dry_run=False)
|
||||
assert result == "created"
|
||||
client._request.assert_called_once()
|
||||
call_args = client._request.call_args
|
||||
assert call_args.args[0] == "POST"
|
||||
assert call_args.args[1] == "/wiki/new"
|
||||
# Verify content_base64 is used, not content
|
||||
payload = call_args.kwargs["json"]
|
||||
assert "content_base64" in payload
|
||||
assert "content" not in payload
|
||||
assert base64.b64decode(payload["content_base64"]).decode("utf-8") == "# Content"
|
||||
|
||||
def test_updates_existing_page_with_base64(self) -> None:
|
||||
client = MagicMock()
|
||||
existing = {"Existing-Page": "Existing-Page.-"}
|
||||
result = sync_page(client, "Existing-Page", "# Updated", existing, dry_run=False)
|
||||
assert result == "updated"
|
||||
client._request.assert_called_once()
|
||||
call_args = client._request.call_args
|
||||
assert call_args.args[0] == "PATCH"
|
||||
assert "/wiki/page/Existing-Page.-" in call_args.args[1]
|
||||
# Verify content_base64 is used
|
||||
payload = call_args.kwargs["json"]
|
||||
assert "content_base64" in payload
|
||||
assert "content" not in payload
|
||||
assert base64.b64decode(payload["content_base64"]).decode("utf-8") == "# Updated"
|
||||
|
||||
def test_create_falls_back_to_update_on_already_exists(self) -> None:
|
||||
"""When create fails with 400 'already exists', re-list and update."""
|
||||
client = MagicMock()
|
||||
# First call: POST /wiki/new → 400 already exists
|
||||
# Second call: PATCH /wiki/page/{sub_url} → success
|
||||
create_error = APIError(400, "wiki page already exists [title: Test-Page]")
|
||||
client._request.side_effect = [create_error, MagicMock()]
|
||||
with patch("devx.ci.sync_wiki._list_wiki_pages_with_retry", return_value={"Test-Page": "Test-Page.-"}):
|
||||
result = sync_page(client, "Test-Page", "# Content", {}, dry_run=False)
|
||||
assert result == "updated"
|
||||
# Verify PATCH was called (second call)
|
||||
patch_call = client._request.call_args_list[1]
|
||||
assert patch_call.args[0] == "PATCH"
|
||||
assert "/wiki/page/Test-Page.-" in patch_call.args[1]
|
||||
|
||||
def test_create_raises_non_400_error(self) -> None:
|
||||
"""Non-400 errors from create should propagate, not trigger fallback."""
|
||||
client = MagicMock()
|
||||
client._request.side_effect = APIError(500, "server error")
|
||||
with pytest.raises(APIError):
|
||||
sync_page(client, "Test-Page", "# Content", {}, dry_run=False)
|
||||
|
||||
def test_create_raises_400_not_already_exists(self) -> None:
|
||||
"""400 errors that don't mention 'already exists' should propagate."""
|
||||
client = MagicMock()
|
||||
client._request.side_effect = APIError(400, "invalid title")
|
||||
with pytest.raises(APIError):
|
||||
sync_page(client, "Test-Page", "# Content", {}, dry_run=False)
|
||||
|
||||
|
||||
class TestVerifyWikiPage:
|
||||
def test_verifies_matching_content(self) -> None:
|
||||
client = MagicMock()
|
||||
encoded = base64.b64encode(b"# Hello Wiki").decode("ascii")
|
||||
client._request.return_value.json.return_value = {"content_base64": encoded}
|
||||
existing = {"Home": "Home"}
|
||||
assert verify_wiki_page(client, "Home", "# Hello Wiki", existing) is True
|
||||
|
||||
def test_fails_on_mismatch(self) -> None:
|
||||
client = MagicMock()
|
||||
encoded = base64.b64encode(b"# Old Content").decode("ascii")
|
||||
client._request.return_value.json.return_value = {"content_base64": encoded}
|
||||
existing = {"Home": "Home"}
|
||||
assert verify_wiki_page(client, "Home", "# New Content", existing) is False
|
||||
|
||||
def test_fails_on_empty_wiki_content(self) -> None:
|
||||
client = MagicMock()
|
||||
client._request.return_value.json.return_value = {"content_base64": ""}
|
||||
existing = {"Home": "Home"}
|
||||
assert verify_wiki_page(client, "Home", "# Expected", existing) is False
|
||||
|
||||
def test_fails_when_page_not_in_existing(self) -> None:
|
||||
client = MagicMock()
|
||||
assert verify_wiki_page(client, "Missing", "# Content", {}) is False
|
||||
|
||||
|
||||
class TestVerifyWikiIntegrity:
|
||||
def _make_client(self, pages: dict[str, str], contents: dict[str, str]) -> MagicMock:
|
||||
"""Create a mock client that returns the given pages and contents."""
|
||||
client = MagicMock()
|
||||
# list_wiki_pages calls GET /wiki/pages
|
||||
page_list = [{"title": t, "sub_url": s} for t, s in pages.items()]
|
||||
|
||||
# fetch_page_content calls GET /wiki/page/{sub_url}
|
||||
def mock_request(method, path, **kwargs):
|
||||
resp = MagicMock()
|
||||
if path == "/wiki/pages":
|
||||
resp.json.return_value = page_list
|
||||
elif path.startswith("/wiki/page/"):
|
||||
sub_url = path.replace("/wiki/page/", "")
|
||||
content = contents.get(sub_url, "")
|
||||
encoded = base64.b64encode(content.encode()).decode("ascii") if content else ""
|
||||
resp.json.return_value = {"content_base64": encoded}
|
||||
return resp
|
||||
|
||||
client._request.side_effect = mock_request
|
||||
return client
|
||||
|
||||
def test_all_good_no_failures(self) -> None:
|
||||
pages = {"Home": "Home", "FAQ": "FAQ"}
|
||||
contents = {"Home": "# Home", "FAQ": "# FAQ"}
|
||||
client = self._make_client(pages, contents)
|
||||
mapping = {"index.md": "Home", "faq.md": "FAQ"}
|
||||
synced = {"Home": "# Home", "FAQ": "# FAQ"}
|
||||
failures = verify_wiki_integrity(client, mapping, synced)
|
||||
assert failures == []
|
||||
|
||||
def test_missing_page_detected(self) -> None:
|
||||
pages = {"Home": "Home"} # FAQ missing from wiki
|
||||
contents = {"Home": "# Home"}
|
||||
client = self._make_client(pages, contents)
|
||||
mapping = {"index.md": "Home", "faq.md": "FAQ"}
|
||||
synced = {"Home": "# Home"}
|
||||
failures = verify_wiki_integrity(client, mapping, synced)
|
||||
assert any("Missing page: FAQ" in f for f in failures)
|
||||
|
||||
def test_stale_page_detected(self) -> None:
|
||||
pages = {"Home": "Home", "Old-Page": "Old-Page"} # Old-Page not in mapping
|
||||
contents = {"Home": "# Home", "Old-Page": "# Old"}
|
||||
client = self._make_client(pages, contents)
|
||||
mapping = {"index.md": "Home"}
|
||||
synced = {"Home": "# Home"}
|
||||
failures = verify_wiki_integrity(client, mapping, synced)
|
||||
assert any("Stale page" in f and "Old-Page" in f for f in failures)
|
||||
|
||||
def test_page_count_mismatch_detected(self) -> None:
|
||||
pages = {"Home": "Home", "Extra": "Extra"}
|
||||
contents = {"Home": "# Home", "Extra": "# Extra"}
|
||||
client = self._make_client(pages, contents)
|
||||
mapping = {"index.md": "Home"}
|
||||
synced = {"Home": "# Home"}
|
||||
failures = verify_wiki_integrity(client, mapping, synced)
|
||||
assert any("Page count mismatch" in f for f in failures)
|
||||
|
||||
def test_empty_content_detected(self) -> None:
|
||||
pages = {"Home": "Home"}
|
||||
contents = {"Home": ""} # Empty content
|
||||
client = self._make_client(pages, contents)
|
||||
mapping = {"index.md": "Home"}
|
||||
synced = {"Home": "# Expected Content"}
|
||||
failures = verify_wiki_integrity(client, mapping, synced)
|
||||
assert any("Empty content: Home" in f for f in failures)
|
||||
|
||||
def test_content_mismatch_detected(self) -> None:
|
||||
pages = {"Home": "Home"}
|
||||
contents = {"Home": "# Wrong Content"}
|
||||
client = self._make_client(pages, contents)
|
||||
mapping = {"index.md": "Home"}
|
||||
synced = {"Home": "# Correct Content"}
|
||||
failures = verify_wiki_integrity(client, mapping, synced)
|
||||
assert any("Content mismatch: Home" in f for f in failures)
|
||||
|
||||
def test_multiple_failures_all_reported(self) -> None:
|
||||
pages = {"Home": "Home", "Stale": "Stale"}
|
||||
contents = {"Home": "", "Stale": "# Stale"}
|
||||
client = self._make_client(pages, contents)
|
||||
mapping = {"index.md": "Home", "faq.md": "FAQ"} # FAQ missing
|
||||
synced = {"Home": "# Home Content"}
|
||||
failures = verify_wiki_integrity(client, mapping, synced)
|
||||
assert len(failures) >= 3 # count mismatch, missing FAQ, stale Stale, empty Home
|
||||
|
||||
def test_transient_api_failure_returns_empty(self) -> None:
|
||||
"""When the wiki API is unavailable after retries, integrity check
|
||||
should return no failures (sync already succeeded)."""
|
||||
client = MagicMock()
|
||||
|
||||
# _list_wiki_pages_with_retry raises APIError (retries exhausted)
|
||||
with patch("devx.ci.sync_wiki._list_wiki_pages_with_retry", side_effect=APIError(0, "timeout")):
|
||||
mapping = {"index.md": "Home", "faq.md": "FAQ"}
|
||||
synced = {"Home": "# Home", "FAQ": "# FAQ"}
|
||||
failures = verify_wiki_integrity(client, mapping, synced)
|
||||
assert failures == []
|
||||
|
||||
def test_transient_api_failure_recovers_on_retry(self) -> None:
|
||||
"""When the wiki API recovers after a retry, integrity check proceeds normally."""
|
||||
client = MagicMock()
|
||||
pages = {"Home": "Home", "FAQ": "FAQ"}
|
||||
contents = {"Home": "# Home", "FAQ": "# FAQ"}
|
||||
|
||||
def mock_request(method, path, **kwargs):
|
||||
resp = MagicMock()
|
||||
if path == "/wiki/pages":
|
||||
page_list = [{"title": t, "sub_url": s} for t, s in pages.items()]
|
||||
resp.json.return_value = page_list
|
||||
elif path.startswith("/wiki/page/"):
|
||||
sub_url = path.replace("/wiki/page/", "")
|
||||
content = contents.get(sub_url, "")
|
||||
encoded = base64.b64encode(content.encode()).decode("ascii") if content else ""
|
||||
resp.json.return_value = {"content_base64": encoded}
|
||||
return resp
|
||||
|
||||
client._request.side_effect = mock_request
|
||||
|
||||
mapping = {"index.md": "Home", "faq.md": "FAQ"}
|
||||
synced = {"Home": "# Home", "FAQ": "# FAQ"}
|
||||
failures = verify_wiki_integrity(client, mapping, synced)
|
||||
assert failures == []
|
||||
@patch("devx.ci.sync_wiki.subprocess.run")
|
||||
def test_push_failure_returns_false(self, mock_run: MagicMock, tmp_path: Path) -> None:
|
||||
mock_run.side_effect = [
|
||||
MagicMock(returncode=0), # git add
|
||||
MagicMock(returncode=1), # git diff --cached --quiet (has changes)
|
||||
MagicMock(returncode=0), # git config user.email
|
||||
MagicMock(returncode=0), # git config user.name
|
||||
MagicMock(returncode=0), # git commit
|
||||
MagicMock(returncode=1, stdout="", stderr="push failed"), # git push
|
||||
]
|
||||
result = commit_and_push(tmp_path, "url", dry_run=False)
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestMain:
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
|
||||
@patch("devx.ci.sync_wiki.MAPPING_FILE")
|
||||
@patch("devx.ci.sync_wiki.DOCS_DIR")
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_dry_run(self, mock_client_cls: MagicMock, mock_docs_dir: Path, mock_mapping_file: Path) -> None:
|
||||
mock_mapping_file.exists.return_value = True
|
||||
mock_mapping_file.__str__ = lambda _: "/docs/mapping.json"
|
||||
with patch("devx.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
|
||||
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 "dry-run" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": ""}, clear=True)
|
||||
def test_missing_token_exits(self) -> None:
|
||||
def test_no_token_raises(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("CI_GITEA_TOKEN", raising=False)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--repo", "owner/repo"])
|
||||
assert result.exit_code == 1
|
||||
result = runner.invoke(main, [])
|
||||
assert result.exit_code != 0
|
||||
assert "CI_GITEA_TOKEN" in result.output
|
||||
|
||||
@patch.dict(
|
||||
"os.environ", {"CI_GITEA_TOKEN": "tok", "DEVX_REPO_OWNER": "me", "DEVX_REPO_NAME": "myrepo"}, clear=True
|
||||
)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_auto_detect_repo(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that repo is auto-detected from env vars when --repo is not passed."""
|
||||
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={"index.md": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
|
||||
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={}):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--dry-run"])
|
||||
assert result.exit_code == 0
|
||||
mock_client_cls.assert_called_once()
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_missing_mapping_file(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that missing mapping.json exits with error."""
|
||||
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
|
||||
mock_mapping.exists.return_value = False
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--repo", "owner/repo"])
|
||||
assert result.exit_code == 1
|
||||
def test_no_mapping_raises(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("CI_GITEA_TOKEN", "fake")
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.MAPPING_FILE", tmp_path / "nonexistent.json")
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--repo", "owner/repo"])
|
||||
assert result.exit_code != 0
|
||||
assert "mapping.json" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_existing_pages_message(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that existing wiki pages are reported."""
|
||||
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={"index.md": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
|
||||
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={"Home": "Home"}):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--dry-run", "--repo", "owner/repo"])
|
||||
@patch("devx.ci.sync_wiki.clone_wiki", return_value=True)
|
||||
@patch("devx.ci.sync_wiki.commit_and_push", return_value=True)
|
||||
@patch("devx.ci.sync_wiki.sync_files", return_value=(1, 0))
|
||||
def test_dry_run(
|
||||
self,
|
||||
mock_sync: MagicMock,
|
||||
mock_push: MagicMock,
|
||||
mock_clone: MagicMock,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
mapping_file = docs / "mapping.json"
|
||||
mapping_file.write_text(json.dumps({"index.md": "Home"}))
|
||||
monkeypatch.setenv("CI_GITEA_TOKEN", "fake")
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.MAPPING_FILE", mapping_file)
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.DOCS_DIR", docs)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--dry-run", "--repo", "owner/repo"])
|
||||
assert result.exit_code == 0
|
||||
assert "existing wiki pages" in result.output
|
||||
assert "dry-run" in result.output
|
||||
mock_push.assert_not_called()
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
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"}):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", side_effect=FileNotFoundError):
|
||||
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={}):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--dry-run", "--repo", "owner/repo"])
|
||||
@patch("devx.ci.sync_wiki.clone_wiki", return_value=True)
|
||||
@patch("devx.ci.sync_wiki.commit_and_push", return_value=True)
|
||||
@patch("devx.ci.sync_wiki.sync_files", return_value=(2, 0))
|
||||
def test_full_sync(
|
||||
self,
|
||||
mock_sync: MagicMock,
|
||||
mock_push: MagicMock,
|
||||
mock_clone: MagicMock,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n[link](page.md)\n")
|
||||
(docs / "page.md").write_text("# Page\n")
|
||||
mapping_file = docs / "mapping.json"
|
||||
mapping_file.write_text(json.dumps({"index.md": "Home", "page.md": "Page"}))
|
||||
monkeypatch.setenv("CI_GITEA_TOKEN", "fake")
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.MAPPING_FILE", mapping_file)
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.DOCS_DIR", docs)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--repo", "owner/repo"])
|
||||
assert result.exit_code == 0
|
||||
assert "Synced" in result.output
|
||||
mock_push.assert_called_once()
|
||||
|
||||
@patch("devx.ci.sync_wiki.clone_wiki", return_value=False)
|
||||
@patch("devx.ci.sync_wiki.init_wiki")
|
||||
@patch("devx.ci.sync_wiki.commit_and_push", return_value=True)
|
||||
@patch("devx.ci.sync_wiki.sync_files", return_value=(1, 0))
|
||||
def test_init_fresh_wiki(
|
||||
self,
|
||||
mock_sync: MagicMock,
|
||||
mock_push: MagicMock,
|
||||
mock_init: MagicMock,
|
||||
mock_clone: MagicMock,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
mapping_file = docs / "mapping.json"
|
||||
mapping_file.write_text(json.dumps({"index.md": "Home"}))
|
||||
monkeypatch.setenv("CI_GITEA_TOKEN", "fake")
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.MAPPING_FILE", mapping_file)
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.DOCS_DIR", docs)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--repo", "owner/repo"])
|
||||
assert result.exit_code == 0
|
||||
mock_init.assert_called_once()
|
||||
|
||||
@patch("devx.ci.sync_wiki.time.sleep")
|
||||
@patch("devx.ci.sync_wiki.clone_wiki")
|
||||
@patch("devx.ci.sync_wiki.commit_and_push", return_value=True)
|
||||
@patch("devx.ci.sync_wiki.sync_files", return_value=(1, 0))
|
||||
def test_verify(
|
||||
self,
|
||||
mock_sync: MagicMock,
|
||||
mock_push: MagicMock,
|
||||
mock_clone: MagicMock,
|
||||
mock_sleep: MagicMock,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
mapping_file = docs / "mapping.json"
|
||||
mapping_file.write_text(json.dumps({"index.md": "Home"}))
|
||||
monkeypatch.setenv("CI_GITEA_TOKEN", "fake")
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.MAPPING_FILE", mapping_file)
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.DOCS_DIR", docs)
|
||||
|
||||
# Mock clone_wiki to create the wiki dir with the expected file
|
||||
def fake_clone(url: str, dest: Path) -> bool:
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
(dest / "Home.md").write_text("# Home\n")
|
||||
return True
|
||||
|
||||
mock_clone.side_effect = fake_clone
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--verify", "--repo", "owner/repo"])
|
||||
assert result.exit_code == 0
|
||||
assert "Verification" in result.output
|
||||
|
||||
@patch("devx.ci.sync_wiki.clone_wiki", return_value=True)
|
||||
@patch("devx.ci.sync_wiki.commit_and_push", return_value=False)
|
||||
@patch("devx.ci.sync_wiki.sync_files", return_value=(1, 0))
|
||||
def test_push_failed_message(
|
||||
self,
|
||||
mock_sync: MagicMock,
|
||||
mock_push: MagicMock,
|
||||
mock_clone: MagicMock,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
mapping_file = docs / "mapping.json"
|
||||
mapping_file.write_text(json.dumps({"index.md": "Home"}))
|
||||
monkeypatch.setenv("CI_GITEA_TOKEN", "fake")
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.MAPPING_FILE", mapping_file)
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.DOCS_DIR", docs)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--repo", "owner/repo"])
|
||||
assert result.exit_code == 0
|
||||
assert "No push needed" in result.output
|
||||
|
||||
@patch("devx.ci.sync_wiki.time.sleep")
|
||||
@patch("devx.ci.sync_wiki.clone_wiki", side_effect=[True, False])
|
||||
@patch("devx.ci.sync_wiki.commit_and_push", return_value=True)
|
||||
@patch("devx.ci.sync_wiki.sync_files", return_value=(1, 0))
|
||||
def test_verify_clone_fails(
|
||||
self,
|
||||
mock_sync: MagicMock,
|
||||
mock_push: MagicMock,
|
||||
mock_clone: MagicMock,
|
||||
mock_sleep: MagicMock,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
mapping_file = docs / "mapping.json"
|
||||
mapping_file.write_text(json.dumps({"index.md": "Home"}))
|
||||
monkeypatch.setenv("CI_GITEA_TOKEN", "fake")
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.MAPPING_FILE", mapping_file)
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.DOCS_DIR", docs)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--verify", "--repo", "owner/repo"])
|
||||
assert result.exit_code != 0
|
||||
assert "not found" in result.output
|
||||
assert "could not clone" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
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"}):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", return_value=" \n "):
|
||||
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={}):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--dry-run", "--repo", "owner/repo"])
|
||||
@patch("devx.ci.sync_wiki.time.sleep")
|
||||
@patch("devx.ci.sync_wiki.clone_wiki")
|
||||
@patch("devx.ci.sync_wiki.commit_and_push", return_value=True)
|
||||
@patch("devx.ci.sync_wiki.sync_files", return_value=(1, 0))
|
||||
def test_verify_missing_page(
|
||||
self,
|
||||
mock_sync: MagicMock,
|
||||
mock_push: MagicMock,
|
||||
mock_clone: MagicMock,
|
||||
mock_sleep: MagicMock,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
mapping_file = docs / "mapping.json"
|
||||
mapping_file.write_text(json.dumps({"index.md": "Home"}))
|
||||
monkeypatch.setenv("CI_GITEA_TOKEN", "fake")
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.MAPPING_FILE", mapping_file)
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.DOCS_DIR", docs)
|
||||
|
||||
# Mock clone_wiki to create the wiki dir WITHOUT the expected file
|
||||
def fake_clone(url: str, dest: Path) -> bool:
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
return True
|
||||
|
||||
mock_clone.side_effect = fake_clone
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--verify", "--repo", "owner/repo"])
|
||||
assert result.exit_code != 0
|
||||
assert "empty" in result.output.lower()
|
||||
assert "page(s) missing" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_create_and_update(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that pages are created and updated correctly (non-dry-run)."""
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
|
||||
mock_mapping.exists.return_value = True
|
||||
mapping = {"new.md": "New-Page", "existing.md": "Existing-Page"}
|
||||
with patch("devx.ci.sync_wiki.load_mapping", return_value=mapping):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Content"):
|
||||
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={"Existing-Page": "Existing-Page"}):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--repo", "owner/repo"])
|
||||
@patch("devx.ci.sync_wiki.clone_wiki", return_value=True)
|
||||
@patch("devx.ci.sync_wiki.commit_and_push", return_value=True)
|
||||
@patch("devx.ci.sync_wiki.sync_files", return_value=(1, 0))
|
||||
def test_auto_detect_repo(
|
||||
self,
|
||||
mock_sync: MagicMock,
|
||||
mock_push: MagicMock,
|
||||
mock_clone: MagicMock,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
mapping_file = docs / "mapping.json"
|
||||
mapping_file.write_text(json.dumps({"index.md": "Home"}))
|
||||
monkeypatch.setenv("CI_GITEA_TOKEN", "fake")
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.MAPPING_FILE", mapping_file)
|
||||
monkeypatch.setattr("devx.ci.sync_wiki.DOCS_DIR", docs)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, [])
|
||||
assert result.exit_code == 0
|
||||
assert "Created: 1" in result.output
|
||||
assert "Updated: 1" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_verify_passes(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that --verify passes when content matches."""
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
encoded = base64.b64encode(b"# Home Content").decode("ascii")
|
||||
# list_wiki_pages returns {"Home": "Home"}, fetch returns encoded content
|
||||
mock_client._request.return_value.json.return_value = {"content_base64": encoded}
|
||||
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={"index.md": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home Content"):
|
||||
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={"Home": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.verify_wiki_page", return_value=True):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--repo", "owner/repo", "--verify"])
|
||||
assert result.exit_code == 0
|
||||
assert "Verification passed" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_verify_fails_on_empty_content(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that --verify fails when wiki pages have empty content."""
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
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={"index.md": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home Content"):
|
||||
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={"Home": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.verify_wiki_page", return_value=False):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--repo", "owner/repo", "--verify"])
|
||||
assert result.exit_code == 1
|
||||
assert "FAIL" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_verify_skipped_in_dry_run(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that --verify is skipped during dry-run."""
|
||||
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={"index.md": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
|
||||
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={}):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--dry-run", "--verify", "--repo", "owner/repo"])
|
||||
assert result.exit_code == 0
|
||||
assert "Verification" not in result.output
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_strict_passes(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that --strict passes when integrity check succeeds."""
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
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={"index.md": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
|
||||
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={"Home": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.verify_wiki_integrity", return_value=[]):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--repo", "owner/repo", "--strict"])
|
||||
assert result.exit_code == 0
|
||||
assert "Integrity check passed" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_strict_fails_on_integrity_issues(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that --strict fails when integrity check finds issues."""
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
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={"index.md": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
|
||||
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={"Home": "Home"}):
|
||||
with patch(
|
||||
"devx.ci.sync_wiki.verify_wiki_integrity",
|
||||
return_value=["Missing page: FAQ", "Stale page: Old-Page"],
|
||||
):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--repo", "owner/repo", "--strict"])
|
||||
assert result.exit_code == 1
|
||||
assert "Integrity check FAILED" in result.output
|
||||
assert "Missing page: FAQ" in result.output
|
||||
assert "Stale page: Old-Page" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_strict_skipped_in_dry_run(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that --strict verification is skipped during dry-run."""
|
||||
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={"index.md": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
|
||||
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={}):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--dry-run", "--strict", "--repo", "owner/repo"])
|
||||
assert result.exit_code == 0
|
||||
assert "Integrity check" not in result.output
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_initial_list_api_error_aborts(self, mock_client_cls: MagicMock) -> None:
|
||||
"""When the initial page list fails after retries, sync aborts to avoid duplicate pages."""
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
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={"index.md": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
|
||||
with patch("devx.ci.sync_wiki._list_wiki_pages_with_retry", side_effect=APIError(0, "timeout")):
|
||||
with patch("devx.ci.sync_wiki.sync_page", return_value="created"):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--repo", "owner/repo"])
|
||||
assert result.exit_code != 0
|
||||
assert "Failed to list existing wiki pages" in result.output
|
||||
assert "Aborting" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
|
||||
@patch("devx.ci.sync_wiki.GiteaClient")
|
||||
def test_verify_skips_when_refetch_fails(self, mock_client_cls: MagicMock) -> None:
|
||||
"""When --verify re-fetch fails after retries, verification is skipped gracefully."""
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
# Initial list succeeds, but verify re-fetch fails
|
||||
list_side_effect = [{"Home": "Home"}, APIError(0, "timeout")]
|
||||
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={"index.md": "Home"}):
|
||||
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
|
||||
with patch("devx.ci.sync_wiki._list_wiki_pages_with_retry", side_effect=list_side_effect):
|
||||
with patch("devx.ci.sync_wiki.sync_page", return_value="updated"):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--repo", "owner/repo", "--verify"])
|
||||
assert result.exit_code == 0
|
||||
assert "Skipping content verification" in result.output
|
||||
|
||||
Reference in New Issue
Block a user