Compare commits

..
24 Commits
Author SHA1 Message Date
devx-ci-bot 22c2d7c925 release: v0.12.0 [skip ci] 2026-06-24 23:05:23 +00:00
emil b4350751f1 DEVX-47: feat: add Polish as officially supported language
Post-merge / detect-type (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 7s
Post-merge / validate-commit-msg (push) Successful in 14s
Post-merge / release (push) Failing after 58s
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / badges (push) Successful in 45s
2026-06-24 23:04:20 +00:00
gitea-actions-bot a987b63da7 chore: update badge URLs to commit 85b1ae90 [skip ci] 2026-06-24 22:50:19 +00:00
emil 82d613e23b DEVX-46: docs: add pyproject.toml dependency and pip.conf instructions for devx
Post-merge / detect-type (push) Successful in 7s
Post-merge / validate-commit-msg (push) Successful in 6s
Post-merge / configure-repo (push) Successful in 13s
Post-merge / release (push) Successful in 1m1s
Post-merge / vikunja (push) Successful in 15s
Post-merge / sync-wiki (push) Successful in 1m2s
Post-merge / badges (push) Successful in 1m1s
2026-06-24 22:48:06 +00:00
gitea-actions-bot 6b6c9d40f1 chore: update badge URLs to commit 90c2559f [skip ci] 2026-06-25 00:35:25 +02:00
emil d17854296a DEVX-45: docs: remove stale .taskid file fallback references
Post-merge / detect-type (push) Successful in 12s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / configure-repo (push) Successful in 13s
Post-merge / release (push) Successful in 43s
Post-merge / vikunja (push) Successful in 7s
Post-merge / sync-wiki (push) Successful in 43s
Post-merge / badges (push) Successful in 50s
2026-06-24 22:33:35 +00:00
gitea-actions-bot 2fff7ed271 chore: update badge URLs to commit c2065ac1 [skip ci] 2026-06-25 00:28:01 +02:00
devx-ci-bot 0a4d66ab5c release: v0.11.1 [skip ci] 2026-06-25 00:25:51 +02:00
emil 14c9200179 DEVX-44: fix: add build/twine to ci deps, activate venv in notify_failure
Post-merge / detect-type (push) Successful in 7s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / release (push) Successful in 1m37s
Post-merge / vikunja (push) Successful in 12s
Post-merge / sync-wiki (push) Successful in 1m34s
Post-merge / badges (push) Successful in 1m16s
2026-06-24 22:24:33 +00:00
gitea-actions-bot 85ae272b1f chore: update badge URLs to commit 389da217 [skip ci] 2026-06-24 22:18:25 +00:00
devx-ci-bot a0c4c1c7f0 release: v0.11.0 [skip ci] 2026-06-24 22:17:36 +00:00
emil fd0c4de31e DEVX-43: feat: add publish step to post-merge release job, make publish idempotent
Post-merge / detect-type (push) Successful in 12s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / release (push) Failing after 44s
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / badges (push) Successful in 44s
2026-06-24 22:16:37 +00:00
gitea-actions-bot 8f7af97335 chore: update badge URLs to commit 0bb83e89 [skip ci] 2026-06-24 22:57:46 +02:00
devx-ci-bot a14d838564 release: v0.10.2 [skip ci] 2026-06-24 20:56:35 +00:00
emil 7dcb9c03c0 DEVX-42: fix: badge generation respects pyproject.toml testpaths, shows stdout in warnings
Post-merge / detect-type (push) Successful in 13s
Post-merge / validate-commit-msg (push) Successful in 6s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / release (push) Successful in 45s
Post-merge / vikunja (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 51s
Post-merge / badges (push) Successful in 1m5s
2026-06-24 20:55:35 +00:00
gitea-actions-bot 6f2b110c17 chore: update badge URLs to commit 55e25b06 [skip ci] 2026-06-24 20:34:59 +00:00
devx-ci-bot cfb856ff75 release: v0.10.1 [skip ci] 2026-06-24 20:34:06 +00:00
emil 95adf86895 DEVX-41: fix: badge generation REPO_ROOT, auto-detect package, error feedback
Post-merge / detect-type (push) Successful in 7s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / release (push) Successful in 45s
Post-merge / vikunja (push) Successful in 8s
Post-merge / sync-wiki (push) Successful in 40s
Post-merge / badges (push) Successful in 48s
2026-06-24 20:33:12 +00:00
gitea-actions-bot 7cf039ebbe chore: update badge URLs to commit 16bc9c20 [skip ci] 2026-06-24 22:04:42 +02:00
devx-ci-bot a17982f2cf release: v0.10.0 [skip ci] 2026-06-24 22:03:14 +02:00
emil cf85964877 DEVX-40: feat: remove .taskid file fallback, use branch name only
Post-merge / detect-type (push) Successful in 13s
Post-merge / validate-commit-msg (push) Successful in 13s
Post-merge / configure-repo (push) Successful in 13s
Post-merge / release (push) Successful in 39s
Post-merge / vikunja (push) Successful in 7s
Post-merge / sync-wiki (push) Successful in 56s
Post-merge / badges (push) Successful in 1m25s
2026-06-24 20:02:19 +00:00
gitea-actions-bot fbb1fc3134 chore: update badge URLs to commit ffc2c8a9 [skip ci] 2026-06-24 19:27:17 +00:00
emil a8f86aca68 DEVX-39: fix: use raw/branch/badges/ URLs for badges in README and docs
Post-merge / detect-type (push) Successful in 6s
Post-merge / configure-repo (push) Successful in 7s
Post-merge / validate-commit-msg (push) Successful in 13s
Post-merge / release (push) Successful in 34s
Post-merge / vikunja (push) Successful in 7s
Post-merge / sync-wiki (push) Successful in 39s
Post-merge / badges (push) Successful in 40s
2026-06-24 19:25:50 +00:00
gitea-actions-bot 13bed1d99c chore: update badge URLs to commit 329cfc69 [skip ci] 2026-06-24 19:03:43 +00:00
26 changed files with 952 additions and 358 deletions
+1 -2
View File
@@ -120,8 +120,7 @@ jobs:
auto-merge: auto-merge:
# Auto-merge runs after all CI checks pass. It reads the task ID # Auto-merge runs after all CI checks pass. It reads the task ID
# from the branch name (falling back to .taskid file), validates # from the branch name, validates the PR title, and squash-merges.
# the PR title, and squash-merges.
needs: [quality, detect-changes, pr-review] needs: [quality, detect-changes, pr-review]
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
runs-on: docker runs-on: docker
+21 -1
View File
@@ -75,7 +75,7 @@ jobs:
needs: [detect-type] needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false' if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker runs-on: docker
timeout-minutes: 10 timeout-minutes: 15
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@@ -94,12 +94,32 @@ jobs:
. .venv/bin/activate . .venv/bin/activate
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release python3 -m devx.ci.release
- name: Publish release
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate
export PATH="$HOME/.local/bin:$PATH"
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$TAG" ]; then
echo "No tag found — skipping publish"
exit 0
fi
HEAD_MSG=$(git log -1 --format=%s)
if echo "$HEAD_MSG" | grep -q "^release: ${TAG}"; then
echo "Publishing release $TAG..."
python3 -m devx.ci.publish "$TAG" "${{ github.repository }}"
else
echo "HEAD is not a release commit for $TAG — skipping publish"
fi
- name: Notify on failure - name: Notify on failure
if: failure() if: failure()
env: env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }} REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.tools.install_tools --tool tea python3 -m devx.tools.install_tools --tool tea
tea login add --name devx --url "${{ github.server_url }}" --token "$REPO_TOKEN" || true tea login add --name devx --url "${{ github.server_url }}" --token "$REPO_TOKEN" || true
+3
View File
@@ -35,3 +35,6 @@ Thumbs.db
# Badges # Badges
.badges/ .badges/
# Deprecated CI task tracking (branch name is the sole source of truth)
.taskid
-1
View File
@@ -1 +0,0 @@
DEVX-30
+4 -9
View File
@@ -287,15 +287,10 @@ setuptools via `dynamic = ["version"]` in `pyproject.toml`.
### Task ID Resolution ### Task ID Resolution
`auto_merge` resolves the task ID from the branch name first (e.g. `auto_merge` resolves the task ID solely from the branch name (e.g.
`DEVX-12-fix-foo``DEVX-12`), falling back to the `.taskid` file `DEVX-12-fix-foo``DEVX-12`). Branch names must include the task ID
for branches without a task ID prefix. If both exist and disagree, prefix — there is no `.taskid` file fallback. If a stale `.taskid` file
a warning is printed and the branch task ID is preferred. exists in the repo, a deprecation warning is printed advising its removal.
**When creating a new branch from an existing branch**, the `.taskid`
file may be stale (it contains the old branch's task ID). Either:
1. Update `.taskid` to match the new branch's task ID, or
2. Delete `.taskid` — the branch name is the primary source of truth
### Workflow `auto-merge` Job and `always()` ### Workflow `auto-merge` Job and `always()`
+40
View File
@@ -2,6 +2,46 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.12.0] - 2026-06-24
### Features
- Add Polish as officially supported language
## [0.11.1] - 2026-06-24
### Bug Fixes
- Add build/twine to ci deps, activate venv in notify_failure
## [0.11.0] - 2026-06-24
### Features
- Add publish step to post-merge release job, make publish idempotent
## [0.10.2] - 2026-06-24
### Bug Fixes
- Badge generation respects pyproject.toml testpaths, shows stdout in warnings
## [0.10.1] - 2026-06-24
### Bug Fixes
- Badge generation REPO_ROOT, auto-detect package, error feedback
## [0.10.0] - 2026-06-24
### Features
- Remove .taskid file fallback, use branch name only
### Bug Fixes
- Use raw/branch/badges/ URLs for badges in README and docs
## [0.9.12] - 2026-06-24 ## [0.9.12] - 2026-06-24
### Bug Fixes ### Bug Fixes
+46 -13
View File
@@ -16,12 +16,12 @@ quality badges.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE) [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85d87b6f36cec86531a04f895d67bb8ff212dd4a/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85b1ae902404330b7fb8d1ecc6751ce65f17b0fc/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85d87b6f36cec86531a04f895d67bb8ff212dd4a/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85b1ae902404330b7fb8d1ecc6751ce65f17b0fc/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85d87b6f36cec86531a04f895d67bb8ff212dd4a/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki) [![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85b1ae902404330b7fb8d1ecc6751ce65f17b0fc/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85d87b6f36cec86531a04f895d67bb8ff212dd4a/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85b1ae902404330b7fb8d1ecc6751ce65f17b0fc/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85d87b6f36cec86531a04f895d67bb8ff212dd4a/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases) [![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85b1ae902404330b7fb8d1ecc6751ce65f17b0fc/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85d87b6f36cec86531a04f895d67bb8ff212dd4a/python.svg)](https://www.python.org/downloads/) [![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85b1ae902404330b7fb8d1ecc6751ce65f17b0fc/python.svg)](https://www.python.org/downloads/)
## Why devx? ## Why devx?
@@ -55,28 +55,61 @@ up by bumping their devx dependency.
- **Developer tools** — environment setup, CI tool installation, test speed - **Developer tools** — environment setup, CI tool installation, test speed
enforcement, repository configuration. enforcement, repository configuration.
- **i18n** — built-in translations for English, Bulgarian, German, Russian, - **i18n** — built-in translations for English, Bulgarian, German, Russian,
and Chinese; projects can extend with their own keys. Chinese, and Polish; projects can extend with their own keys.
## Installation ## Installation
Install from the Gitea PyPI registry: devx is published to the Gitea PyPI registry at
`https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple`.
The registry is publicly readable — no authentication required to install.
### Quick install (one-off)
```bash ```bash
pip install devx --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple pip install devx --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
``` ```
Or add the registry to your `pip.conf` / `pyproject.toml` and install normally: ### Persistent configuration (recommended)
Add the registry to `~/.pip/pip.conf` so `pip install devx` works without
specifying `--index-url` every time:
```ini
[global]
extra-index-url = https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
```
### As a dependency in another project
To use devx as a dependency in your `pyproject.toml`, add the registry as an
extra index and list devx in your dependencies:
```toml
[project]
dependencies = [
"devx>=0.11.1",
]
[tool.pip]
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
```
Then install normally:
```bash ```bash
pip install devx pip install -e .
``` ```
> **Note:** If your project requires a specific devx version, pin it in
> `dependencies` (e.g., `"devx==0.11.1"`) or use a version constraint
> (e.g., `"devx>=0.11.1,<0.12"`).
### Optional extras ### Optional extras
devx ships optional dependency groups for different use cases: devx ships optional dependency groups for different use cases:
```bash ```bash
pip install "devx[ci,lint]" # CI runners and linting (pytest, ruff, pyright, bandit) pip install "devx[ci,lint]" # CI runners and linting (pytest, ruff, pyright, bandit, build, twine)
pip install "devx[molecule]" # Molecule testing for Ansible projects pip install "devx[molecule]" # Molecule testing for Ansible projects
pip install "devx[dev]" # Full local development (ci + lint + build + twine) pip install "devx[dev]" # Full local development (ci + lint + build + twine)
``` ```
@@ -291,7 +324,7 @@ The config system loads `.env` automatically via `python-dotenv`.
| `DEVX_REPO_NAME` | **(none — must be set)** | Repository name (or `owner/repo`) | | `DEVX_REPO_NAME` | **(none — must be set)** | Repository name (or `owner/repo`) |
| `DEVX_TASK_PREFIX` | `DEVX` | Task ID prefix (GRM, OBL-INFRA, etc.) | | `DEVX_TASK_PREFIX` | `DEVX` | Task ID prefix (GRM, OBL-INFRA, etc.) |
| `DEVX_VIKUNJA_PROJECT_ID` | `6` | Vikunja project ID | | `DEVX_VIKUNJA_PROJECT_ID` | `6` | Vikunja project ID |
| `DEVX_LANG` | `en` | Language for i18n (en, bg, de, ru, zh) | | `DEVX_LANG` | `en` | Language for i18n (en, bg, de, ru, zh, pl) |
| `DEVX_TRANSLATIONS_PATH` | — | Path to a custom JSON translations file | | `DEVX_TRANSLATIONS_PATH` | — | Path to a custom JSON translations file |
| `DEVX_VERSION_FILE` | `src/devx/__init__.py` | Version source file (used by release) | | `DEVX_VERSION_FILE` | `src/devx/__init__.py` | Version source file (used by release) |
| `DEVX_DOCS_DIR` | `docs` | Documentation directory (used by sync_wiki) | | `DEVX_DOCS_DIR` | `docs` | Documentation directory (used by sync_wiki) |
@@ -398,7 +431,7 @@ src/devx/
├── i18n.py # Translation system (gettext-based, translations.json) ├── i18n.py # Translation system (gettext-based, translations.json)
├── exceptions.py # Custom exception types (DevxError, APIError) ├── exceptions.py # Custom exception types (DevxError, APIError)
├── opentofu.py # OpenTofu output helpers ├── opentofu.py # OpenTofu output helpers
├── translations.json # Translation strings (en, bg, de, ru, zh) ├── translations.json # Translation strings (en, bg, de, ru, zh, pl)
├── ci/ # CI/CD automation modules (run by workflows) ├── ci/ # CI/CD automation modules (run by workflows)
├── tools/ # Developer tooling modules (run locally or by CI) ├── tools/ # Developer tooling modules (run locally or by CI)
└── molecule/ # Optional molecule testing helpers (for Ansible projects) └── molecule/ # Optional molecule testing helpers (for Ansible projects)
+43 -12
View File
@@ -12,12 +12,12 @@ project to be reusable across all oblachno-oss repositories.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE) [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85d87b6f36cec86531a04f895d67bb8ff212dd4a/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85b1ae902404330b7fb8d1ecc6751ce65f17b0fc/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85d87b6f36cec86531a04f895d67bb8ff212dd4a/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85b1ae902404330b7fb8d1ecc6751ce65f17b0fc/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85d87b6f36cec86531a04f895d67bb8ff212dd4a/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki) [![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85b1ae902404330b7fb8d1ecc6751ce65f17b0fc/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85d87b6f36cec86531a04f895d67bb8ff212dd4a/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85b1ae902404330b7fb8d1ecc6751ce65f17b0fc/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85d87b6f36cec86531a04f895d67bb8ff212dd4a/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases) [![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85b1ae902404330b7fb8d1ecc6751ce65f17b0fc/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85d87b6f36cec86531a04f895d67bb8ff212dd4a/python.svg)](https://www.python.org/downloads/) [![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/85b1ae902404330b7fb8d1ecc6751ce65f17b0fc/python.svg)](https://www.python.org/downloads/)
## Overview ## Overview
@@ -42,22 +42,53 @@ via environment variables and `pyproject.toml`, and inherit:
- **Developer tools** — environment setup, CI tool installation, test speed - **Developer tools** — environment setup, CI tool installation, test speed
enforcement, repository configuration. enforcement, repository configuration.
- **i18n** — built-in translations for English, Bulgarian, German, Russian, - **i18n** — built-in translations for English, Bulgarian, German, Russian,
and Chinese; projects can extend with their own keys. Chinese, and Polish; projects can extend with their own keys.
## Installation ## Installation
Install from the Gitea PyPI registry: devx is published to the Gitea PyPI registry at
`https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple`.
The registry is publicly readable — no authentication required to install.
### Quick install (one-off)
```bash ```bash
pip install devx --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple pip install devx --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
``` ```
Optional extras: ### Persistent configuration (recommended)
Add the registry to `~/.pip/pip.conf`:
```ini
[global]
extra-index-url = https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
```
Then `pip install devx` works without specifying `--index-url`.
### As a dependency in another project
Add devx to your `pyproject.toml` dependencies and configure the registry:
```toml
[project]
dependencies = [
"devx>=0.11.1",
]
[tool.pip]
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
```
Pin a specific version if needed: `"devx==0.11.1"` or `"devx>=0.11.1,<0.12"`.
### Optional extras
```bash ```bash
pip install "devx[ci,lint]" # CI runners and linting pip install "devx[ci,lint]" # CI runners and linting (pytest, ruff, pyright, bandit, build, twine)
pip install "devx[molecule]" # Molecule testing for Ansible projects pip install "devx[molecule]" # Molecule testing for Ansible projects
pip install "devx[dev]" # Full local development pip install "devx[dev]" # Full local development (ci + lint + build + twine)
``` ```
## Architecture ## Architecture
@@ -117,7 +148,7 @@ fallback. Key variables:
| `DEVX_REPO_OWNER` | **(must be set)** | Repository owner | | `DEVX_REPO_OWNER` | **(must be set)** | Repository owner |
| `DEVX_REPO_NAME` | **(must be set)** | Repository name | | `DEVX_REPO_NAME` | **(must be set)** | Repository name |
| `DEVX_TASK_PREFIX` | `DEVX` | Task ID prefix (GRM, OBL-INFRA, etc.) | | `DEVX_TASK_PREFIX` | `DEVX` | Task ID prefix (GRM, OBL-INFRA, etc.) |
| `DEVX_LANG` | `en` | Language for i18n (en, bg, de, ru, zh) | | `DEVX_LANG` | `en` | Language for i18n (en, bg, de, ru, zh, pl) |
| `REPO_TOKEN` | — | Gitea API token | | `REPO_TOKEN` | — | Gitea API token |
| `VIKUNJA_TOKEN` | — | Vikunja API token | | `VIKUNJA_TOKEN` | — | Vikunja API token |
+2 -2
View File
@@ -16,7 +16,7 @@ src/devx/
├── i18n.py # Translation system (JSON-based, translations.json) ├── i18n.py # Translation system (JSON-based, translations.json)
├── exceptions.py # Custom exception types (DevxError, APIError) ├── exceptions.py # Custom exception types (DevxError, APIError)
├── opentofu.py # OpenTofu output helpers ├── opentofu.py # OpenTofu output helpers
├── translations.json # Translation strings (en, bg, de, ru, zh) ├── translations.json # Translation strings (en, bg, de, ru, zh, pl)
├── ci/ # CI/CD automation modules (run by workflows) ├── ci/ # CI/CD automation modules (run by workflows)
│ ├── __init__.py │ ├── __init__.py
│ ├── _shared.py # Shared utilities (get_latest_tag) │ ├── _shared.py # Shared utilities (get_latest_tag)
@@ -188,7 +188,7 @@ repos that only need a Gitea release.
### `auto_merge.py` ### `auto_merge.py`
Auto-merges a PR when all CI checks pass. Reads the task ID from the branch Auto-merges a PR when all CI checks pass. Reads the task ID from the branch
name (falling back to `.taskid` file), validates the PR title format against name, validates the PR title format against
the Vikunja task title, extracts the conventional commit message from PR the Vikunja task title, extracts the conventional commit message from PR
commits, and squash-merges with title `{PREFIX}-N <conventional commit>`. commits, and squash-merges with title `{PREFIX}-N <conventional commit>`.
+2 -4
View File
@@ -93,8 +93,7 @@ Depends on `quality`, `detect-changes`, and `pr-review`. The final job in the
CI workflow. Runs `python -m devx.ci.auto_merge` with the branch name, PR CI workflow. Runs `python -m devx.ci.auto_merge` with the branch name, PR
title, repository, and PR number: title, repository, and PR number:
1. **Read task ID** from branch name (e.g., `DEVX-12-fix-foo` → `DEVX-12`), 1. **Read task ID** from branch name (e.g., `DEVX-12-fix-foo` → `DEVX-12`)
falling back to `.taskid` file for branches without a task ID prefix
2. **Validate PR title format** — must be `{PREFIX}-N: <vikunja task title>` 2. **Validate PR title format** — must be `{PREFIX}-N: <vikunja task title>`
3. **Validate PR title matches Vikunja task** — fetches the Vikunja task and 3. **Validate PR title matches Vikunja task** — fetches the Vikunja task and
compares the title compares the title
@@ -330,8 +329,7 @@ On failure, the `notify_failure` step creates a Gitea issue.
### `auto_merge.py` ### `auto_merge.py`
Auto-merge PR when all CI checks pass. Reads task ID from the branch name Auto-merge PR when all CI checks pass. Reads task ID from the branch name
(e.g., `DEVX-12-fix-foo` → `DEVX-12`), falling back to `.taskid` file for (e.g., `DEVX-12-fix-foo` → `DEVX-12`). Validates PR title format, checks the
branches without a task ID prefix. Validates PR title format, checks the
Vikunja task exists and the title matches, extracts the conventional commit Vikunja task exists and the title matches, extracts the conventional commit
message from PR commits, and squash-merges with message from PR commits, and squash-merges with
`{PREFIX}-N <conventional commit>` title. `{PREFIX}-N <conventional commit>` title.
+3 -3
View File
@@ -18,7 +18,7 @@ devx molecule --help # show molecule commands
### `devx ci auto-merge` ### `devx ci auto-merge`
Auto-merge a PR when all CI checks pass. Reads the task ID from the branch Auto-merge a PR when all CI checks pass. Reads the task ID from the branch
name (falling back to `.taskid` file), validates the PR title format against name, validates the PR title format against
the Vikunja task title, extracts the conventional commit message from PR the Vikunja task title, extracts the conventional commit message from PR
commits, and squash-merges with `{PREFIX}-N <conventional commit>` title. commits, and squash-merges with `{PREFIX}-N <conventional commit>` title.
@@ -40,8 +40,8 @@ checks `src/devx/translations.json` against `src/devx/**/*.py`.
Checks performed: Checks performed:
- **Missing keys** — a `_()` call in code has no entry in the translations file - **Missing keys** — a `_()` call in code has no entry in the translations file
- **Dead keys** — a key in the translations file is not used in any code - **Dead keys** — a key in the translations file is not used in any code
- **Missing languages** — a key exists but is missing one of the five - **Missing languages** — a key exists but is missing one of the six
supported languages (en, bg, de, ru, zh) supported languages (en, bg, de, ru, zh, pl)
```bash ```bash
devx ci check-translations devx ci check-translations
+2
View File
@@ -30,6 +30,8 @@ version = {attr = "devx.__version__"}
ci = [ ci = [
"pytest>=9.1.0", "pytest>=9.1.0",
"pytest-cov>=7.1.0", "pytest-cov>=7.1.0",
"build>=1.5.0",
"twine>=6.2.0",
] ]
# Lint and type-checking tools (quality job) # Lint and type-checking tools (quality job)
lint = [ lint = [
+1 -1
View File
@@ -1,3 +1,3 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects.""" """devx — reusable development and CI/CD tools for oblachno-oss projects."""
__version__ = "0.9.12" __version__ = "0.12.0"
+15 -27
View File
@@ -2,9 +2,9 @@
"""Auto-merge PR when all CI checks pass. """Auto-merge PR when all CI checks pass.
Runs as the final job in ci.yml. Reads the task ID from the branch name Runs as the final job in ci.yml. Reads the task ID from the branch name
(falling back to ``.taskid`` file for branches without a task ID prefix), (e.g., ``DEVX-31-fix-foo`` → ``DEVX-31``), validates the PR title against
validates the PR title, and squash-merges with a conventional commit the Vikunja task, and squash-merges with a conventional commit message
message prefixed by the task ID. prefixed by the task ID.
PR title format: ``{PREFIX}-N: <vikunja task title>`` PR title format: ``{PREFIX}-N: <vikunja task title>``
Merge commit format: ``{PREFIX}-N <conventional commit message>`` Merge commit format: ``{PREFIX}-N <conventional commit message>``
@@ -42,7 +42,7 @@ from devx.config import (
from devx.exceptions import APIError from devx.exceptions import APIError
from devx.i18n import _ from devx.i18n import _
TASKID_FILE = ".taskid" TASKID_FILE = ".taskid" # Deprecated, kept for backward-compat warnings
PR_TITLE_RE = re.compile(rf"^{TASK_PREFIX}-\d+:\s+.+") PR_TITLE_RE = re.compile(rf"^{TASK_PREFIX}-\d+:\s+.+")
load_dotenv() load_dotenv()
@@ -63,44 +63,31 @@ def run_cmd(args: list[str], check: bool = True) -> subprocess.CompletedProcess[
def read_taskid(branch: str) -> str: def read_taskid(branch: str) -> str:
"""Read task ID from branch name, falling back to .taskid file. """Read task ID from branch name.
The branch name is the primary source of truth for the task ID The branch name is the sole source of truth for the task ID
(e.g., ``DEVX-31-fix-foo`` → ``DEVX-31``). The ``.taskid`` file (e.g., ``DEVX-31-fix-foo`` → ``DEVX-31``). Branches must include
is a legacy fallback for branches without a task ID prefix. the task ID prefix — there is no ``.taskid`` file fallback.
If both sources exist and disagree, a warning is printed and the If a stale ``.taskid`` file exists and disagrees with the branch
branch task ID is preferred (it is the current source of truth). name, a deprecation warning is printed advising its removal.
""" """
branch_task_id = extract_task_id(branch) branch_task_id = extract_task_id(branch)
if branch_task_id: if branch_task_id:
# Check for stale .taskid file that disagrees with branch name # Warn about stale .taskid file if it exists and disagrees
path = Path(TASKID_FILE) path = Path(TASKID_FILE)
if path.exists(): if path.exists():
file_task_id = path.read_text(encoding="utf-8").strip() file_task_id = path.read_text(encoding="utf-8").strip()
if file_task_id and file_task_id != branch_task_id: if file_task_id and file_task_id != branch_task_id:
click.echo( click.echo(
_( _(
"WARNING: .taskid file ({file_id}) disagrees with branch name ({branch_id}). " "WARNING: .taskid file ({file_id}) is deprecated and disagrees with branch name ({branch_id}). "
"Using branch task ID. Update or delete .taskid to silence this warning.", "Delete .taskid from the repo — branch name is the sole source of truth.",
file_id=file_task_id, file_id=file_task_id,
branch_id=branch_task_id, branch_id=branch_task_id,
) )
) )
return branch_task_id return branch_task_id
# Fallback: read from .taskid file
path = Path(TASKID_FILE)
if path.exists():
task_id = path.read_text(encoding="utf-8").strip()
if task_id:
click.echo(
_(
"Task ID from .taskid file: {task_id} (not found in branch name '{branch}')",
task_id=task_id,
branch=branch,
)
)
return task_id
return "" return ""
@@ -229,7 +216,8 @@ def main(branch: str, pr_title: str, repo: str, pr_number: str) -> None:
if not task_id: if not task_id:
raise click.ClickException( raise click.ClickException(
_( _(
"Oops! No task ID found in .taskid file or branch name '{branch}'.", "Oops! No task ID found in branch name '{branch}'. "
"Branch names must include the task ID prefix (e.g., DEVX-31-fix-bug).",
branch=branch, branch=branch,
) )
) )
+6 -6
View File
@@ -11,8 +11,8 @@ Checks performed (all fail with exit code 1 on error):
- **Missing keys**: a ``_()`` call in code has no entry in the corresponding - **Missing keys**: a ``_()`` call in code has no entry in the corresponding
translations file. translations file.
- **Dead keys**: a key in a translations file is not used in any code. - **Dead keys**: a key in a translations file is not used in any code.
- **Missing languages**: a key exists but is missing one of the 5 supported - **Missing languages**: a key exists but is missing one of the 6 supported
languages (en, bg, de, ru, zh). This is an error — all supported languages languages (en, bg, de, ru, zh, pl). This is an error — all supported languages
must have translations for every key. must have translations for every key.
Usage:: Usage::
@@ -33,7 +33,7 @@ import click
REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
SUPPORTED_LANGS = ("en", "bg", "de", "ru", "zh") SUPPORTED_LANGS = ("en", "bg", "de", "ru", "zh", "pl")
# Default translation set: devx package itself # Default translation set: devx package itself
DEFAULT_TRANS_FILE = REPO_ROOT / "src" / "devx" / "translations.json" DEFAULT_TRANS_FILE = REPO_ROOT / "src" / "devx" / "translations.json"
@@ -101,9 +101,9 @@ def collect_keys(src_dir: Path) -> set[str]:
if pyfile.name == "i18n.py": if pyfile.name == "i18n.py":
continue continue
keys |= extract_keys(pyfile) keys |= extract_keys(pyfile)
# Add dynamic keys for the default source directory # Dynamic keys are common status strings used via _(variable) that
if src_dir == DEFAULT_SRC_DIR: # can't be detected by AST scanning. Include them for all projects.
keys |= DYNAMIC_KEYS keys |= DYNAMIC_KEYS
return keys return keys
-2
View File
@@ -298,8 +298,6 @@ DEFAULT_INFRASTRUCTURE: list[str] = [
"activate.sh", "activate.sh",
"activate.fish", "activate.fish",
"activate.zsh", "activate.zsh",
# CI task tracking file (written by CI, not by developers)
".taskid",
] ]
+11
View File
@@ -211,6 +211,17 @@ def main(tag: str, repo: str, registry_url: str | None, skip_build: bool) -> Non
click.echo(_("--skip-build: skipping package build and PyPI publish.")) click.echo(_("--skip-build: skipping package build and PyPI publish."))
tea = TeaCLI(repo=repo) tea = TeaCLI(repo=repo)
# Check if release already exists (idempotent — avoids failure when
# called multiple times, e.g. by both post-merge and publish workflows)
try:
releases = tea.list_releases(repo)
if any(r.get("tag_name") == tag for r in releases):
click.echo(_("Gitea release {tag} already exists — skipping creation.", tag=tag))
return
except TeaCLIError:
pass # If listing fails, proceed to create
release_body = generate_release_notes(tag) release_body = generate_release_notes(tag)
try: try:
+12 -2
View File
@@ -18,6 +18,7 @@ Usage::
from __future__ import annotations from __future__ import annotations
import contextlib import contextlib
import os
import re import re
import subprocess # nosec B404 import subprocess # nosec B404
import sys import sys
@@ -27,7 +28,16 @@ from typing import Any
import click import click
REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
def _repo_root() -> Path:
"""Resolve repo root from GITHUB_WORKSPACE or cwd."""
workspace = os.environ.get("GITHUB_WORKSPACE")
if workspace:
path = Path(workspace)
if path.is_dir():
return path
return Path.cwd()
# Badge filenames that get pushed to the badges branch # Badge filenames that get pushed to the badges branch
BADGE_FILES = ["coverage.svg", "tests.svg", "docs.svg", "quality.svg", "version.svg", "python.svg"] BADGE_FILES = ["coverage.svg", "tests.svg", "docs.svg", "quality.svg", "version.svg", "python.svg"]
@@ -116,7 +126,7 @@ def update_readme_with_badge_sha(badges_sha: str, repo_root: Path | None = None)
Switches back to master, replaces ``raw/branch/badges/`` URLs with Switches back to master, replaces ``raw/branch/badges/`` URLs with
``raw/commit/<sha>/`` URLs, commits and pushes. ``raw/commit/<sha>/`` URLs, commits and pushes.
""" """
root = repo_root or REPO_ROOT root = repo_root or _repo_root()
# Switch back to master # Switch back to master
_run(["git", "checkout", "master"]) # nosec B607 _run(["git", "checkout", "master"]) # nosec B607
+2 -2
View File
@@ -1,7 +1,7 @@
"""Simple i18n for devx scripts and tools. """Simple i18n for devx scripts and tools.
Set DEVX_LANG environment variable to override the default English. Set DEVX_LANG environment variable to override the default English.
Supported: en, bg, de, ru, zh. Supported: en, bg, de, ru, zh, pl.
Projects can extend translations by setting DEVX_TRANSLATIONS_PATH to a Projects can extend translations by setting DEVX_TRANSLATIONS_PATH to a
JSON file with additional keys. Keys from the project's file are merged JSON file with additional keys. Keys from the project's file are merged
@@ -45,7 +45,7 @@ def _(key: str, **kwargs: object) -> str:
If unset, English is always returned regardless of system locale. If unset, English is always returned regardless of system locale.
""" """
lang = os.getenv("DEVX_LANG", "en") lang = os.getenv("DEVX_LANG", "en")
if lang not in ("en", "bg", "de", "ru", "zh"): if lang not in ("en", "bg", "de", "ru", "zh", "pl"):
lang = "en" lang = "en"
template = TRANSLATIONS.get(key, {}).get(lang, key) template = TRANSLATIONS.get(key, {}).get(lang, key)
return template.format(**kwargs) return template.format(**kwargs)
+231 -60
View File
@@ -5,12 +5,21 @@ Runs pytest-cov, doc-coverage, lint checks, and version extraction,
then writes SVG badge files that can be served as static files from then writes SVG badge files that can be served as static files from
the Gitea raw file API. the Gitea raw file API.
The repo root is resolved from ``GITHUB_WORKSPACE`` or ``os.getcwd()``,
so this module works correctly both when run from a source checkout
and when devx is installed as a pip package in CI.
The package name and coverage target are auto-detected from the
``src/`` directory structure, making this module reusable across
all oblachno-oss repos without per-repo configuration.
Usage: Usage:
python3 -m devx.tools.generate_badges --output-dir .badges/ python3 -m devx.tools.generate_badges --output-dir .badges/
""" """
from __future__ import annotations from __future__ import annotations
import os
import re import re
import subprocess # nosec B404 import subprocess # nosec B404
import sys import sys
@@ -18,8 +27,7 @@ from pathlib import Path
import click import click
REPO_ROOT = Path(__file__).resolve().parents[4] # Coverage regex matches "TOTAL ... NN%" or "TOTAL ... NN.NN%"
_COVERAGE_RE = re.compile(r"TOTAL.*?(\d+(?:\.\d+)?)%") _COVERAGE_RE = re.compile(r"TOTAL.*?(\d+(?:\.\d+)?)%")
_PASSED_RE = re.compile(r"(\d+) passed") _PASSED_RE = re.compile(r"(\d+) passed")
_DOC_COVERAGE_RE = re.compile(r"Doc coverage:\s+\d+/\d+\s+\((\d+)%") _DOC_COVERAGE_RE = re.compile(r"Doc coverage:\s+\d+/\d+\s+\((\d+)%")
@@ -37,18 +45,59 @@ COLOR_HEX: dict[str, str] = {
} }
def _find_package_init() -> Path | None: def resolve_repo_root() -> Path:
"""Find the first package __init__.py under src/ that defines __version__.""" """Resolve the repository root directory.
src_dir = REPO_ROOT / "src"
if not src_dir.exists(): Uses ``GITHUB_WORKSPACE`` env var (set by Gitea Actions) or
falls back to ``os.getcwd()``. This ensures the correct repo
root is used even when devx is installed as a pip package.
"""
workspace = os.environ.get("GITHUB_WORKSPACE")
if workspace:
path = Path(workspace)
if path.is_dir():
return path
return Path.cwd()
def detect_package_name(repo_root: Path) -> str | None:
"""Auto-detect the Python package name from ``src/`` directory.
Looks for the first subdirectory under ``src/`` that contains
an ``__init__.py`` file with ``__version__``.
Returns the package directory name (e.g., ``devx``,
``gitea_runner_manager``) or ``None`` if no package is found.
"""
src_dir = repo_root / "src"
if not src_dir.is_dir():
return None return None
for init_file in src_dir.rglob("__init__.py"): for entry in sorted(src_dir.iterdir()):
try: if not entry.is_dir():
content = init_file.read_text()
except OSError:
continue continue
if "__version__" in content: init_file = entry / "__init__.py"
return init_file if init_file.exists():
return entry.name
return None
def detect_coverage_target(repo_root: Path) -> str | None:
"""Auto-detect the pytest-cov target from pyproject.toml.
Parses ``addopts`` in ``[tool.pytest.ini_options]`` for
``--cov=src/<package>``. Falls back to ``src/<package>`` if
the package is detected but no explicit cov target is found.
"""
pyproject = repo_root / "pyproject.toml"
if pyproject.exists():
content = pyproject.read_text()
match = re.search(r"--cov=(\S+)", content)
if match:
return match.group(1)
# Fallback: derive from package name
pkg = detect_package_name(repo_root)
if pkg:
return f"src/{pkg}"
return None return None
@@ -57,14 +106,15 @@ def _xml_escape(text: str) -> str:
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;") return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
def run_command(cmd: list[str]) -> tuple[int, str, str]: def run_command(cmd: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
"""Run a command and return (returncode, stdout, stderr).""" """Run a command and return (returncode, stdout, stderr)."""
root = str(cwd or resolve_repo_root())
result = subprocess.run( # nosec B603 result = subprocess.run( # nosec B603
cmd, cmd,
capture_output=True, capture_output=True,
text=True, text=True,
check=False, check=False,
cwd=str(REPO_ROOT), cwd=root,
) )
return result.returncode, result.stdout, result.stderr return result.returncode, result.stdout, result.stderr
@@ -139,15 +189,25 @@ def extract_doc_coverage(output: str) -> int | None:
return None return None
def read_version() -> str: def read_version(repo_root: Path) -> str:
"""Read __version__ from the package __init__.py.""" """Read __version__ from the package __init__.py under src/.
init_file = _find_package_init()
if init_file is None: Auto-detects the package directory and reads ``__version__``
from its ``__init__.py``.
"""
pkg = detect_package_name(repo_root)
if pkg is None:
click.echo(" WARNING: No Python package found under src/ — version badge will show 'unknown'")
return "unknown"
init_file = repo_root / "src" / pkg / "__init__.py"
if not init_file.exists():
click.echo(f" WARNING: {init_file} not found — version badge will show 'unknown'")
return "unknown" return "unknown"
content = init_file.read_text() content = init_file.read_text()
match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content) match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content)
if match: if match:
return match.group(1) return match.group(1)
click.echo(f" WARNING: No __version__ found in {init_file} — version badge will show 'unknown'")
return "unknown" return "unknown"
@@ -179,65 +239,170 @@ def doc_coverage_color(pct: int) -> str:
return "orange" return "orange"
def generate_badges(output_dir: Path) -> dict[str, dict[str, str | int]]: def detect_testpaths(repo_root: Path) -> list[str]:
"""Generate all badge SVG files and return badge data as a dict.""" """Detect test paths from pyproject.toml or filesystem.
badges: dict[str, dict[str, str | int]] = {}
# 1. Code coverage + test count (single pytest-cov run) Parses ``testpaths`` in ``[tool.pytest.ini_options]`` from
rc, stdout, stderr = run_command( pyproject.toml. Falls back to ``["tests"]`` if the tests/
[ directory exists. Returns an empty list if no test paths
sys.executable, are found (pytest will use its own defaults).
"-m", """
"pytest", pyproject = repo_root / "pyproject.toml"
"tests/", if pyproject.exists():
"-v", content = pyproject.read_text()
"--cov=src/devx", # Match: testpaths = ["dir1", "dir2"]
"--cov-report=term-missing", match = re.search(r"testpaths\s*=\s*\[([^\]]+)\]", content)
"--cov-fail-under=0", if match:
] paths = re.findall(r'["\']([^"\']+)["\']', match.group(1))
) resolved = []
for p in paths:
p = p.strip()
if (repo_root / p).exists():
resolved.append(p)
if resolved:
return resolved
# Fallback: tests/ directory
tests_dir = repo_root / "tests"
if tests_dir.is_dir():
return ["tests"]
return []
def collect_coverage_and_tests(repo_root: Path) -> tuple[dict[str, str | int], dict[str, str | int]]:
"""Run pytest-cov and collect coverage + test count badges.
Returns (coverage_badge, tests_badge). If pytest is not
available or no tests are found, returns 'unknown' badges
with a clear warning explaining the failure.
"""
cov_target = detect_coverage_target(repo_root)
if cov_target is None:
click.echo(" WARNING: No coverage target detected (no src/ package, no --cov in pyproject.toml)")
return make_badge("coverage", "unknown", "lightgrey"), make_badge("tests", "unknown", "lightgrey")
testpaths = detect_testpaths(repo_root)
click.echo(f" Test paths: {testpaths or '(pytest defaults)'}")
cmd = [
sys.executable,
"-m",
"pytest",
*testpaths,
"--cov",
cov_target,
"--cov-report=term-missing",
"--cov-fail-under=0",
"-q",
]
rc, stdout, stderr = run_command(cmd, cwd=repo_root)
combined = stdout + "\n" + stderr combined = stdout + "\n" + stderr
coverage = extract_coverage(combined) coverage = extract_coverage(combined)
if coverage is not None: if coverage is not None:
badges["coverage"] = make_badge("coverage", f"{coverage:.0f}%", coverage_color(coverage)) cov_badge = make_badge("coverage", f"{coverage:.0f}%", coverage_color(coverage))
else: else:
badges["coverage"] = make_badge("coverage", "unknown", "red") click.echo(f" WARNING: Could not extract coverage from pytest output (rc={rc})")
click.echo(f" pytest stdout (last 300 chars): {stdout.strip()[-300:]}")
click.echo(f" pytest stderr (last 300 chars): {stderr.strip()[-300:]}")
cov_badge = make_badge("coverage", "unknown", "red")
test_count = extract_test_count(combined) test_count = extract_test_count(combined)
if test_count is not None: if test_count is not None:
badges["tests"] = make_badge("tests", f"{test_count} passing", "brightgreen" if rc == 0 else "red") tests_badge = make_badge("tests", f"{test_count} passing", "brightgreen" if rc == 0 else "red")
else: else:
badges["tests"] = make_badge("tests", "unknown", "red") click.echo(f" WARNING: Could not extract test count from pytest output (rc={rc})")
click.echo(f" pytest stdout (last 300 chars): {stdout.strip()[-300:]}")
click.echo(f" pytest stderr (last 300 chars): {stderr.strip()[-300:]}")
tests_badge = make_badge("tests", "unknown", "red")
# 2. Documentation coverage return cov_badge, tests_badge
rc, stdout, _ = run_command(
[
sys.executable, def collect_doc_coverage(repo_root: Path) -> dict[str, str | int]:
"-m", """Run doc_coverage and collect the docs badge."""
"devx.ci.doc_coverage", rc, stdout, stderr = run_command(
] [sys.executable, "-m", "devx.ci.doc_coverage"],
cwd=repo_root,
) )
doc_pct = extract_doc_coverage(stdout) doc_pct = extract_doc_coverage(stdout)
if doc_pct is not None: if doc_pct is not None:
badges["docs"] = make_badge("docs", f"{doc_pct}%", doc_coverage_color(doc_pct)) return make_badge("docs", f"{doc_pct}%", doc_coverage_color(doc_pct))
else: click.echo(f" WARNING: Could not extract doc coverage (rc={rc})")
badges["docs"] = make_badge("docs", "unknown", "red") click.echo(f" stderr: {stderr.strip()[:200]}")
return make_badge("docs", "unknown", "red")
# 3. Code quality (ruff + pyright + bandit all pass)
lint_rc, _, _ = run_command([sys.executable, "-m", "ruff", "check", "src/", "tests/"])
format_rc, _, _ = run_command([sys.executable, "-m", "ruff", "format", "--check", "src/", "tests/"])
type_rc, _, _ = run_command([sys.executable, "-m", "pyright"])
bandit_rc, _, _ = run_command([sys.executable, "-m", "bandit", "-r", "src/"])
all_pass = all(rc == 0 for rc in [lint_rc, format_rc, type_rc, bandit_rc]) def collect_quality(repo_root: Path) -> dict[str, str | int]:
badges["quality"] = make_badge("code quality", "A" if all_pass else "F", "brightgreen" if all_pass else "red") """Run lint checks and collect the quality badge.
Runs ruff check, ruff format --check, pyright, and bandit.
If any tool is not installed, it is skipped with a warning.
"""
results: list[bool] = []
tool_names: list[str] = []
for cmd, name in [
([sys.executable, "-m", "ruff", "check", "src/", "tests/"], "ruff check"),
([sys.executable, "-m", "ruff", "format", "--check", "src/", "tests/"], "ruff format"),
([sys.executable, "-m", "pyright"], "pyright"),
([sys.executable, "-m", "bandit", "-r", "src/"], "bandit"),
]:
rc, _, stderr = run_command(cmd, cwd=repo_root)
if rc == 0:
results.append(True)
tool_names.append(f"{name}: pass")
else:
results.append(False)
# Distinguish "tool not installed" from "tool found issues"
if "No module named" in stderr or "not found" in stderr.lower():
click.echo(f" WARNING: {name} not installed — skipping (counted as pass)")
results[-1] = True
tool_names.append(f"{name}: not installed (skipped)")
else:
tool_names.append(f"{name}: FAIL")
click.echo(f" WARNING: {name} failed (rc={rc})")
click.echo(f" stderr: {stderr.strip()[:200]}")
all_pass = all(results)
click.echo(f" Quality checks: {', '.join(tool_names)}")
return make_badge("code quality", "A" if all_pass else "F", "brightgreen" if all_pass else "red")
def generate_badges(output_dir: Path, repo_root: Path | None = None) -> dict[str, dict[str, str | int]]:
"""Generate all badge SVG files and return badge data as a dict.
Args:
output_dir: Directory to write SVG files.
repo_root: Repository root (auto-detected if None).
"""
root = repo_root or resolve_repo_root()
click.echo(f" Repo root: {root}")
pkg = detect_package_name(root)
click.echo(f" Package: {pkg or 'none'}")
badges: dict[str, dict[str, str | int]] = {}
# 1. Code coverage + test count (single pytest-cov run)
click.echo(" Collecting coverage and tests...")
cov_badge, tests_badge = collect_coverage_and_tests(root)
badges["coverage"] = cov_badge
badges["tests"] = tests_badge
# 2. Documentation coverage
click.echo(" Collecting doc coverage...")
badges["docs"] = collect_doc_coverage(root)
# 3. Code quality (ruff + pyright + bandit)
click.echo(" Collecting code quality...")
badges["quality"] = collect_quality(root)
# 4. Version # 4. Version
version = read_version() click.echo(" Collecting version...")
version = read_version(root)
badges["version"] = make_badge("version", f"v{version}", "blue") badges["version"] = make_badge("version", f"v{version}", "blue")
# 5. Python version (static but nice) # 5. Python version (static)
badges["python"] = make_badge("python", "3.12", "blue") badges["python"] = make_badge("python", "3.12", "blue")
# Write SVG files # Write SVG files
@@ -254,14 +419,20 @@ def generate_badges(output_dir: Path) -> dict[str, dict[str, str | int]]:
@click.command() @click.command()
@click.option( @click.option(
"--output-dir", "--output-dir",
default=str(REPO_ROOT / ".badges"), default=".badges",
help="Directory to write badge SVG files.", help="Directory to write badge SVG files.",
) )
def cli(output_dir: str) -> None: @click.option(
"--repo-root",
default=None,
help="Repository root (auto-detected if not specified).",
)
def cli(output_dir: str, repo_root: str | None) -> None:
"""Generate self-contained SVG badge files from project metrics.""" """Generate self-contained SVG badge files from project metrics."""
out = Path(output_dir) out = Path(output_dir)
root = Path(repo_root) if repo_root else None
click.echo(f"Generating badges in {out}...") click.echo(f"Generating badges in {out}...")
badges = generate_badges(out) badges = generate_badges(out, repo_root=root)
click.echo(f"\nGenerated {len(badges)} badges:") click.echo(f"\nGenerated {len(badges)} badges:")
for name, badge in badges.items(): for name, badge in badges.items():
click.echo(f" {name}: {badge['label']}={badge['message']} ({badge['color']})") click.echo(f" {name}: {badge['label']}={badge['message']} ({badge['color']})")
+206 -26
View File
@@ -3,6 +3,7 @@
"bg": "\n=== Summary ===", "bg": "\n=== Summary ===",
"de": "\n=== Summary ===", "de": "\n=== Summary ===",
"en": "\n=== Summary ===", "en": "\n=== Summary ===",
"pl": "\n=== Podsumowanie ===",
"ru": "\n=== Summary ===", "ru": "\n=== Summary ===",
"zh": "\n=== Summary ===" "zh": "\n=== Summary ==="
}, },
@@ -10,6 +11,7 @@
"bg": "\nAll documentation coverage checks passed!", "bg": "\nAll documentation coverage checks passed!",
"de": "\nAll documentation coverage checks passed!", "de": "\nAll documentation coverage checks passed!",
"en": "\nAll documentation coverage checks passed!", "en": "\nAll documentation coverage checks passed!",
"pl": "\nWszystkie kontrole pokrycia dokumentacji zakończone pomyślnie!",
"ru": "\nAll documentation coverage checks passed!", "ru": "\nAll documentation coverage checks passed!",
"zh": "\nAll documentation coverage checks passed!" "zh": "\nAll documentation coverage checks passed!"
}, },
@@ -17,6 +19,7 @@
"bg": "\nCHANGELOG version ordering:", "bg": "\nCHANGELOG version ordering:",
"de": "\nCHANGELOG version ordering:", "de": "\nCHANGELOG version ordering:",
"en": "\nCHANGELOG version ordering:", "en": "\nCHANGELOG version ordering:",
"pl": "\nKolejność wersji w CHANGELOG:",
"ru": "\nCHANGELOG version ordering:", "ru": "\nCHANGELOG version ordering:",
"zh": "\nCHANGELOG version ordering:" "zh": "\nCHANGELOG version ordering:"
}, },
@@ -24,6 +27,7 @@
"bg": "\nChecking CI script documentation in ci-cd-workflow.md...", "bg": "\nChecking CI script documentation in ci-cd-workflow.md...",
"de": "\nChecking CI script documentation in ci-cd-workflow.md...", "de": "\nChecking CI script documentation in ci-cd-workflow.md...",
"en": "\nChecking CI script documentation in ci-cd-workflow.md...", "en": "\nChecking CI script documentation in ci-cd-workflow.md...",
"pl": "\nSprawdzanie dokumentacji skryptów CI w ci-cd-workflow.md...",
"ru": "\nChecking CI script documentation in ci-cd-workflow.md...", "ru": "\nChecking CI script documentation in ci-cd-workflow.md...",
"zh": "\nChecking CI script documentation in ci-cd-workflow.md..." "zh": "\nChecking CI script documentation in ci-cd-workflow.md..."
}, },
@@ -31,6 +35,7 @@
"bg": "\nChecking module documentation in architecture.md...", "bg": "\nChecking module documentation in architecture.md...",
"de": "\nChecking module documentation in architecture.md...", "de": "\nChecking module documentation in architecture.md...",
"en": "\nChecking module documentation in architecture.md...", "en": "\nChecking module documentation in architecture.md...",
"pl": "\nSprawdzanie dokumentacji modułów w architecture.md...",
"ru": "\nChecking module documentation in architecture.md...", "ru": "\nChecking module documentation in architecture.md...",
"zh": "\nChecking module documentation in architecture.md..." "zh": "\nChecking module documentation in architecture.md..."
}, },
@@ -38,6 +43,7 @@
"bg": "\nDoc coverage: {covered}/{total} ({pct}%)", "bg": "\nDoc coverage: {covered}/{total} ({pct}%)",
"de": "\nDoc coverage: {covered}/{total} ({pct}%)", "de": "\nDoc coverage: {covered}/{total} ({pct}%)",
"en": "\nDoc coverage: {covered}/{total} ({pct}%)", "en": "\nDoc coverage: {covered}/{total} ({pct}%)",
"pl": "\nPokrycie dokumentacji: {covered}/{total} ({pct}%)",
"ru": "\nDoc coverage: {covered}/{total} ({pct}%)", "ru": "\nDoc coverage: {covered}/{total} ({pct}%)",
"zh": "\nDoc coverage: {covered}/{total} ({pct}%)" "zh": "\nDoc coverage: {covered}/{total} ({pct}%)"
}, },
@@ -45,6 +51,7 @@
"bg": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}", "bg": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
"de": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}", "de": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
"en": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}", "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}", "ru": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}",
"zh": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}" "zh": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}"
}, },
@@ -52,6 +59,7 @@
"bg": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.", "bg": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.",
"de": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.", "de": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.",
"en": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.", "en": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.",
"pl": "\nBŁĄD: Pokrycie dokumentacji nie wynosi 100%. Użyj --fail-on-missing, aby to wymusić.",
"ru": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.", "ru": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.",
"zh": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce." "zh": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce."
}, },
@@ -59,6 +67,7 @@
"bg": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.", "bg": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
"de": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.", "de": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
"en": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.", "en": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
"pl": "\nNapraw niezgodne tagi przed utworzeniem nowych wydań. Uruchom 'python3 -m devx.ci.release --verify', aby uzyskać pełny raport.",
"ru": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.", "ru": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
"zh": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report." "zh": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report."
}, },
@@ -66,6 +75,7 @@
"bg": "\nIntegrity check FAILED ({count} issues):", "bg": "\nIntegrity check FAILED ({count} issues):",
"de": "\nIntegrity check FAILED ({count} issues):", "de": "\nIntegrity check FAILED ({count} issues):",
"en": "\nIntegrity check FAILED ({count} issues):", "en": "\nIntegrity check FAILED ({count} issues):",
"pl": "\nKontrola integralności NIEUDANA ({count} problemów):",
"ru": "\nIntegrity check FAILED ({count} issues):", "ru": "\nIntegrity check FAILED ({count} issues):",
"zh": "\nIntegrity check FAILED ({count} issues):" "zh": "\nIntegrity check FAILED ({count} issues):"
}, },
@@ -73,6 +83,7 @@
"bg": "\nIntegrity check passed — all {count} pages verified.", "bg": "\nIntegrity check passed — all {count} pages verified.",
"de": "\nIntegrity check passed — all {count} pages verified.", "de": "\nIntegrity check passed — all {count} pages verified.",
"en": "\nIntegrity check passed — all {count} pages verified.", "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.", "ru": "\nIntegrity check passed — all {count} pages verified.",
"zh": "\nIntegrity check passed — all {count} pages verified." "zh": "\nIntegrity check passed — all {count} pages verified."
}, },
@@ -80,6 +91,7 @@
"bg": "\nLatest tag: {tag}", "bg": "\nLatest tag: {tag}",
"de": "\nLatest tag: {tag}", "de": "\nLatest tag: {tag}",
"en": "\nLatest tag: {tag}", "en": "\nLatest tag: {tag}",
"pl": "\nNajnowszy tag: {tag}",
"ru": "\nLatest tag: {tag}", "ru": "\nLatest tag: {tag}",
"zh": "\nLatest tag: {tag}" "zh": "\nLatest tag: {tag}"
}, },
@@ -87,6 +99,7 @@
"bg": "\nMissing documentation:", "bg": "\nMissing documentation:",
"de": "\nMissing documentation:", "de": "\nMissing documentation:",
"en": "\nMissing documentation:", "en": "\nMissing documentation:",
"pl": "\nBrakująca dokumentacja:",
"ru": "\nMissing documentation:", "ru": "\nMissing documentation:",
"zh": "\nMissing documentation:" "zh": "\nMissing documentation:"
}, },
@@ -94,6 +107,7 @@
"bg": "\nResult: {status}", "bg": "\nResult: {status}",
"de": "\nResult: {status}", "de": "\nResult: {status}",
"en": "\nResult: {status}", "en": "\nResult: {status}",
"pl": "\nWynik: {status}",
"ru": "\nResult: {status}", "ru": "\nResult: {status}",
"zh": "\nResult: {status}" "zh": "\nResult: {status}"
}, },
@@ -101,6 +115,7 @@
"bg": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).", "bg": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).",
"de": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).", "de": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).",
"en": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).", "en": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).",
"pl": "\nRecenzja #{review_id} opublikowana na PR #{pr_number} ze zdarzeniem '{event}' ({num_comments} komentarzy w tekście).",
"ru": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).", "ru": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).",
"zh": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments)." "zh": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments)."
}, },
@@ -108,6 +123,7 @@
"bg": "\nRunning full wiki integrity check...", "bg": "\nRunning full wiki integrity check...",
"de": "\nRunning full wiki integrity check...", "de": "\nRunning full wiki integrity check...",
"en": "\nRunning full wiki integrity check...", "en": "\nRunning full wiki integrity check...",
"pl": "\nUruchamianie pełnej kontroli integralności wiki...",
"ru": "\nRunning full wiki integrity check...", "ru": "\nRunning full wiki integrity check...",
"zh": "\nRunning full wiki integrity check..." "zh": "\nRunning full wiki integrity check..."
}, },
@@ -115,6 +131,7 @@
"bg": "\nTag → Commit alignment:", "bg": "\nTag → Commit alignment:",
"de": "\nTag → Commit alignment:", "de": "\nTag → Commit alignment:",
"en": "\nTag → Commit alignment:", "en": "\nTag → Commit alignment:",
"pl": "\nTag → Commit: zgodność:",
"ru": "\nTag → Commit alignment:", "ru": "\nTag → Commit alignment:",
"zh": "\nTag → Commit alignment:" "zh": "\nTag → Commit alignment:"
}, },
@@ -122,6 +139,7 @@
"bg": "\nUntagged release commits:", "bg": "\nUntagged release commits:",
"de": "\nUntagged release commits:", "de": "\nUntagged release commits:",
"en": "\nUntagged release commits:", "en": "\nUntagged release commits:",
"pl": "\nCommity wydania bez tagu:",
"ru": "\nUntagged release commits:", "ru": "\nUntagged release commits:",
"zh": "\nUntagged release commits:" "zh": "\nUntagged release commits:"
}, },
@@ -129,6 +147,7 @@
"bg": "\nUser-facing changes ({count}):", "bg": "\nUser-facing changes ({count}):",
"de": "\nUser-facing changes ({count}):", "de": "\nUser-facing changes ({count}):",
"en": "\nUser-facing changes ({count}):", "en": "\nUser-facing changes ({count}):",
"pl": "\nZmiany widoczne dla użytkownika ({count}):",
"ru": "\nUser-facing changes ({count}):", "ru": "\nUser-facing changes ({count}):",
"zh": "\nUser-facing changes ({count}):" "zh": "\nUser-facing changes ({count}):"
}, },
@@ -136,6 +155,7 @@
"bg": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!", "bg": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!",
"de": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!", "de": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!",
"en": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!", "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!", "ru": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!",
"zh": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!" "zh": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!"
}, },
@@ -143,6 +163,7 @@
"bg": "\nVerification passed — all wiki pages have correct content.", "bg": "\nVerification passed — all wiki pages have correct content.",
"de": "\nVerification passed — all wiki pages have correct content.", "de": "\nVerification passed — all wiki pages have correct content.",
"en": "\nVerification passed — all wiki pages have correct content.", "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.", "ru": "\nVerification passed — all wiki pages have correct content.",
"zh": "\nVerification passed — all wiki pages have correct content." "zh": "\nVerification passed — all wiki pages have correct content."
}, },
@@ -150,6 +171,7 @@
"bg": "\nVerifying wiki pages have content...", "bg": "\nVerifying wiki pages have content...",
"de": "\nVerifying wiki pages have content...", "de": "\nVerifying wiki pages have content...",
"en": "\nVerifying wiki pages have content...", "en": "\nVerifying wiki pages have content...",
"pl": "\nWeryfikowanie, czy strony wiki mają treść...",
"ru": "\nVerifying wiki pages have content...", "ru": "\nVerifying wiki pages have content...",
"zh": "\nVerifying wiki pages have content..." "zh": "\nVerifying wiki pages have content..."
}, },
@@ -157,6 +179,7 @@
"bg": "\nWorkflow-only changes ({count}):", "bg": "\nWorkflow-only changes ({count}):",
"de": "\nWorkflow-only changes ({count}):", "de": "\nWorkflow-only changes ({count}):",
"en": "\nWorkflow-only changes ({count}):", "en": "\nWorkflow-only changes ({count}):",
"pl": "\nZmiany tylko w workflow ({count}):",
"ru": "\nWorkflow-only changes ({count}):", "ru": "\nWorkflow-only changes ({count}):",
"zh": "\nWorkflow-only changes ({count}):" "zh": "\nWorkflow-only changes ({count}):"
}, },
@@ -164,6 +187,7 @@
"bg": "\n[dry-run] Changelog:\n{changelog}", "bg": "\n[dry-run] Changelog:\n{changelog}",
"de": "\n[dry-run] Changelog:\n{changelog}", "de": "\n[dry-run] Changelog:\n{changelog}",
"en": "\n[dry-run] Changelog:\n{changelog}", "en": "\n[dry-run] Changelog:\n{changelog}",
"pl": "\n[dry-run] Changelog:\n{changelog}",
"ru": "\n[dry-run] Changelog:\n{changelog}", "ru": "\n[dry-run] Changelog:\n{changelog}",
"zh": "\n[dry-run] Changelog:\n{changelog}" "zh": "\n[dry-run] Changelog:\n{changelog}"
}, },
@@ -171,6 +195,7 @@
"bg": "\n{label} files changed ({count}):", "bg": "\n{label} files changed ({count}):",
"de": "\n{label} files changed ({count}):", "de": "\n{label} files changed ({count}):",
"en": "\n{label} files changed ({count}):", "en": "\n{label} files changed ({count}):",
"pl": "\n{label} plików zmienionych ({count}):",
"ru": "\n{label} files changed ({count}):", "ru": "\n{label} files changed ({count}):",
"zh": "\n{label} files changed ({count}):" "zh": "\n{label} files changed ({count}):"
}, },
@@ -178,6 +203,7 @@
"bg": "\n{tag} files ({count}):", "bg": "\n{tag} files ({count}):",
"de": "\n{tag} files ({count}):", "de": "\n{tag} files ({count}):",
"en": "\n{tag} files ({count}):", "en": "\n{tag} files ({count}):",
"pl": "\nPliki {tag} ({count}):",
"ru": "\n{tag} files ({count}):", "ru": "\n{tag} files ({count}):",
"zh": "\n{tag} files ({count}):" "zh": "\n{tag} files ({count}):"
}, },
@@ -185,6 +211,7 @@
"bg": " - Автоматично изтриване на клон след сливане: да", "bg": " - Автоматично изтриване на клон след сливане: да",
"de": " - Branch nach Merge automatisch löschen: ja", "de": " - Branch nach Merge automatisch löschen: ja",
"en": " - Auto-delete branch after merge: yes", "en": " - Auto-delete branch after merge: yes",
"pl": " - Auto-usuwanie gałęzi po scaleniu: tak",
"ru": " - Автоудаление ветки после слияния: да", "ru": " - Автоудаление ветки после слияния: да",
"zh": " - 合并后自动删除分支: 是" "zh": " - 合并后自动删除分支: 是"
}, },
@@ -192,6 +219,7 @@
"bg": " - Блокиране на остарели клонове: да", "bg": " - Блокиране на остарели клонове: да",
"de": " - Veraltete Branches blockieren: ja", "de": " - Veraltete Branches blockieren: ja",
"en": " - Block outdated branches: yes", "en": " - Block outdated branches: yes",
"pl": " - Blokowanie nieaktualnych gałęzi: tak",
"ru": " - Блокировать устаревшие ветки: да", "ru": " - Блокировать устаревшие ветки: да",
"zh": " - 阻止过时分支: 是" "zh": " - 阻止过时分支: 是"
}, },
@@ -199,6 +227,7 @@
"bg": " - Блокиране на отхвърлени рецензии: да", "bg": " - Блокиране на отхвърлени рецензии: да",
"de": " - Abgelehnte Reviews blockieren: ja", "de": " - Abgelehnte Reviews blockieren: ja",
"en": " - Block rejected reviews: yes", "en": " - Block rejected reviews: yes",
"pl": " - Blokowanie odrzuconych recenzji: tak",
"ru": " - Блокировать отклонённые ревью: да", "ru": " - Блокировать отклонённые ревью: да",
"zh": " - 阻止被拒绝的审查: 是" "zh": " - 阻止被拒绝的审查: 是"
}, },
@@ -206,6 +235,7 @@
"bg": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)", "bg": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)",
"de": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)", "de": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)",
"en": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)", "en": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)",
"pl": " - Bezpośrednie push-e: ZABLOKOWANE (wymagają PR, użytkownicy z białej listy mogą pushować)",
"ru": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)", "ru": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)",
"zh": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)" "zh": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)"
}, },
@@ -213,6 +243,7 @@
"bg": " - Анулиране на остарели одобрения: да", "bg": " - Анулиране на остарели одобрения: да",
"de": " - Veraltete Genehmigungen ablehnen: ja", "de": " - Veraltete Genehmigungen ablehnen: ja",
"en": " - Dismiss stale approvals: yes", "en": " - Dismiss stale approvals: yes",
"pl": " - Odrzucanie nieaktualnych zatwierdzeń: tak",
"ru": " - Отклонять устаревшие одобрения: да", "ru": " - Отклонять устаревшие одобрения: да",
"zh": " - 忽略过时审批: 是" "zh": " - 忽略过时审批: 是"
}, },
@@ -220,6 +251,7 @@
"bg": " - Необходими одобрения: {count}", "bg": " - Необходими одобрения: {count}",
"de": " - Erforderliche Genehmigungen: {count}", "de": " - Erforderliche Genehmigungen: {count}",
"en": " - Required approvals: {count}", "en": " - Required approvals: {count}",
"pl": " - Wymagane zatwierdzenia: {count}",
"ru": " - Требуемые одобрения: {count}", "ru": " - Требуемые одобрения: {count}",
"zh": " - 必需审批数: {count}" "zh": " - 必需审批数: {count}"
}, },
@@ -227,6 +259,7 @@
"bg": " - Необходими проверки на състоянието: {checks}", "bg": " - Необходими проверки на състоянието: {checks}",
"de": " - Erforderliche Status-Checks: {checks}", "de": " - Erforderliche Status-Checks: {checks}",
"en": " - Required status checks: {checks}", "en": " - Required status checks: {checks}",
"pl": " - Wymagane kontrole statusu: {checks}",
"ru": " - Требуемые проверки статуса: {checks}", "ru": " - Требуемые проверки статуса: {checks}",
"zh": " - 必需状态检查: {checks}" "zh": " - 必需状态检查: {checks}"
}, },
@@ -234,6 +267,7 @@
"bg": " Created: {title}", "bg": " Created: {title}",
"de": " Created: {title}", "de": " Created: {title}",
"en": " Created: {title}", "en": " Created: {title}",
"pl": " Utworzono: {title}",
"ru": " Created: {title}", "ru": " Created: {title}",
"zh": " Created: {title}" "zh": " Created: {title}"
}, },
@@ -241,6 +275,7 @@
"bg": " FAIL: {title} — content mismatch or empty!", "bg": " FAIL: {title} — content mismatch or empty!",
"de": " FAIL: {title} — content mismatch or empty!", "de": " FAIL: {title} — content mismatch or empty!",
"en": " FAIL: {title} — content mismatch or empty!", "en": " FAIL: {title} — content mismatch or empty!",
"pl": " BŁĄD: {title} — treść niezgodna lub pusta!",
"ru": " FAIL: {title} — content mismatch or empty!", "ru": " FAIL: {title} — content mismatch or empty!",
"zh": " FAIL: {title} — content mismatch or empty!" "zh": " FAIL: {title} — content mismatch or empty!"
}, },
@@ -248,6 +283,7 @@
"bg": " ЛИПСВА: devx {cmd}", "bg": " ЛИПСВА: devx {cmd}",
"de": " FEHLT: devx {cmd}", "de": " FEHLT: devx {cmd}",
"en": " MISSING: devx {cmd}", "en": " MISSING: devx {cmd}",
"pl": " BRAK: devx {cmd}",
"ru": " ОТСУТСТВУЕТ: devx {cmd}", "ru": " ОТСУТСТВУЕТ: devx {cmd}",
"zh": " 缺失: devx {cmd}" "zh": " 缺失: devx {cmd}"
}, },
@@ -255,6 +291,7 @@
"bg": " MISSING: {module}", "bg": " MISSING: {module}",
"de": " MISSING: {module}", "de": " MISSING: {module}",
"en": " MISSING: {module}", "en": " MISSING: {module}",
"pl": " BRAK: {module}",
"ru": " MISSING: {module}", "ru": " MISSING: {module}",
"zh": " MISSING: {module}" "zh": " MISSING: {module}"
}, },
@@ -262,6 +299,7 @@
"bg": " MISSING: {script}", "bg": " MISSING: {script}",
"de": " MISSING: {script}", "de": " MISSING: {script}",
"en": " MISSING: {script}", "en": " MISSING: {script}",
"pl": " BRAK: {script}",
"ru": " MISSING: {script}", "ru": " MISSING: {script}",
"zh": " MISSING: {script}" "zh": " MISSING: {script}"
}, },
@@ -269,6 +307,7 @@
"bg": " ОК: devx {cmd}", "bg": " ОК: devx {cmd}",
"de": " OK: devx {cmd}", "de": " OK: devx {cmd}",
"en": " OK: devx {cmd}", "en": " OK: devx {cmd}",
"pl": " OK: devx {cmd}",
"ru": " ОК: devx {cmd}", "ru": " ОК: devx {cmd}",
"zh": " 正常: devx {cmd}" "zh": " 正常: devx {cmd}"
}, },
@@ -276,6 +315,7 @@
"bg": " OK: {module}", "bg": " OK: {module}",
"de": " OK: {module}", "de": " OK: {module}",
"en": " OK: {module}", "en": " OK: {module}",
"pl": " OK: {module}",
"ru": " OK: {module}", "ru": " OK: {module}",
"zh": " OK: {module}" "zh": " OK: {module}"
}, },
@@ -283,6 +323,7 @@
"bg": " OK: {script}", "bg": " OK: {script}",
"de": " OK: {script}", "de": " OK: {script}",
"en": " OK: {script}", "en": " OK: {script}",
"pl": " OK: {script}",
"ru": " OK: {script}", "ru": " OK: {script}",
"zh": " OK: {script}" "zh": " OK: {script}"
}, },
@@ -290,6 +331,7 @@
"bg": " OK: {title} ({chars} chars)", "bg": " OK: {title} ({chars} chars)",
"de": " OK: {title} ({chars} chars)", "de": " OK: {title} ({chars} chars)",
"en": " OK: {title} ({chars} chars)", "en": " OK: {title} ({chars} chars)",
"pl": " OK: {title} ({chars} znaków)",
"ru": " OK: {title} ({chars} chars)", "ru": " OK: {title} ({chars} chars)",
"zh": " OK: {title} ({chars} chars)" "zh": " OK: {title} ({chars} chars)"
}, },
@@ -297,6 +339,7 @@
"bg": " Updated: {title}", "bg": " Updated: {title}",
"de": " Updated: {title}", "de": " Updated: {title}",
"en": " Updated: {title}", "en": " Updated: {title}",
"pl": " Zaktualizowano: {title}",
"ru": " Updated: {title}", "ru": " Updated: {title}",
"zh": " Updated: {title}" "zh": " Updated: {title}"
}, },
@@ -304,6 +347,7 @@
"bg": "--skip-build: skipping package build and PyPI publish.", "bg": "--skip-build: skipping package build and PyPI publish.",
"de": "--skip-build: skipping package build and PyPI publish.", "de": "--skip-build: skipping package build and PyPI publish.",
"en": "--skip-build: skipping package build and PyPI publish.", "en": "--skip-build: skipping package build and PyPI publish.",
"pl": "--skip-build: pomijanie budowania pakietu i publikacji PyPI.",
"ru": "--skip-build: skipping package build and PyPI publish.", "ru": "--skip-build: skipping package build and PyPI publish.",
"zh": "--skip-build: skipping package build and PyPI publish." "zh": "--skip-build: skipping package build and PyPI publish."
}, },
@@ -311,6 +355,7 @@
"bg": "=== Release Alignment Verification ===\n", "bg": "=== Release Alignment Verification ===\n",
"de": "=== Release Alignment Verification ===\n", "de": "=== Release Alignment Verification ===\n",
"en": "=== Release Alignment Verification ===\n", "en": "=== Release Alignment Verification ===\n",
"pl": "=== Weryfikacja zgodności wydań ===\n",
"ru": "=== Release Alignment Verification ===\n", "ru": "=== Release Alignment Verification ===\n",
"zh": "=== Release Alignment Verification ===\n" "zh": "=== Release Alignment Verification ===\n"
}, },
@@ -318,6 +363,7 @@
"bg": "API poll warning: {exc}", "bg": "API poll warning: {exc}",
"de": "API poll warning: {exc}", "de": "API poll warning: {exc}",
"en": "API poll warning: {exc}", "en": "API poll warning: {exc}",
"pl": "Ostrzeżenie sondowania API: {exc}",
"ru": "API poll warning: {exc}", "ru": "API poll warning: {exc}",
"zh": "API poll warning: {exc}" "zh": "API poll warning: {exc}"
}, },
@@ -325,6 +371,7 @@
"bg": "All molecule tests passed.", "bg": "All molecule tests passed.",
"de": "All molecule tests passed.", "de": "All molecule tests passed.",
"en": "All molecule tests passed.", "en": "All molecule tests passed.",
"pl": "Wszystkie testy molecule zakończone pomyślnie.",
"ru": "All molecule tests passed.", "ru": "All molecule tests passed.",
"zh": "All molecule tests passed." "zh": "All molecule tests passed."
}, },
@@ -332,6 +379,7 @@
"bg": "Another molecule runner failed. Stopping this runner early.", "bg": "Another molecule runner failed. Stopping this runner early.",
"de": "Another molecule runner failed. Stopping this runner early.", "de": "Another molecule runner failed. Stopping this runner early.",
"en": "Another molecule runner failed. Stopping this runner early.", "en": "Another molecule runner failed. Stopping this runner early.",
"pl": "Inny runner molecule zakończył się niepowodzeniem. Wczesne zatrzymanie tego runnera.",
"ru": "Another molecule runner failed. Stopping this runner early.", "ru": "Another molecule runner failed. Stopping this runner early.",
"zh": "Another molecule runner failed. Stopping this runner early." "zh": "Another molecule runner failed. Stopping this runner early."
}, },
@@ -339,6 +387,7 @@
"bg": "Bumping version: {current} -> v{new_version}", "bg": "Bumping version: {current} -> v{new_version}",
"de": "Bumping version: {current} -> v{new_version}", "de": "Bumping version: {current} -> v{new_version}",
"en": "Bumping version: {current} -> v{new_version}", "en": "Bumping version: {current} -> v{new_version}",
"pl": "Zmiana wersji: {current} -> v{new_version}",
"ru": "Bumping version: {current} -> v{new_version}", "ru": "Bumping version: {current} -> v{new_version}",
"zh": "Bumping version: {current} -> v{new_version}" "zh": "Bumping version: {current} -> v{new_version}"
}, },
@@ -346,6 +395,7 @@
"bg": "Checking CLI command documentation...", "bg": "Checking CLI command documentation...",
"de": "Checking CLI command documentation...", "de": "Checking CLI command documentation...",
"en": "Checking CLI command documentation...", "en": "Checking CLI command documentation...",
"pl": "Sprawdzanie dokumentacji poleceń CLI...",
"ru": "Checking CLI command documentation...", "ru": "Checking CLI command documentation...",
"zh": "Checking CLI command documentation..." "zh": "Checking CLI command documentation..."
}, },
@@ -353,6 +403,7 @@
"bg": "Command failed ({cmd}): {stderr}", "bg": "Command failed ({cmd}): {stderr}",
"de": "Command failed ({cmd}): {stderr}", "de": "Command failed ({cmd}): {stderr}",
"en": "Command failed ({cmd}): {stderr}", "en": "Command failed ({cmd}): {stderr}",
"pl": "Polecenie nie powiodło się ({cmd}): {stderr}",
"ru": "Command failed ({cmd}): {stderr}", "ru": "Command failed ({cmd}): {stderr}",
"zh": "Command failed ({cmd}): {stderr}" "zh": "Command failed ({cmd}): {stderr}"
}, },
@@ -360,6 +411,7 @@
"bg": "Comparing {base}..{head} ({count} files changed)", "bg": "Comparing {base}..{head} ({count} files changed)",
"de": "Comparing {base}..{head} ({count} files changed)", "de": "Comparing {base}..{head} ({count} files changed)",
"en": "Comparing {base}..{head} ({count} files changed)", "en": "Comparing {base}..{head} ({count} files changed)",
"pl": "Porównywanie {base}..{head} ({count} zmienionych plików)",
"ru": "Comparing {base}..{head} ({count} files changed)", "ru": "Comparing {base}..{head} ({count} files changed)",
"zh": "Comparing {base}..{head} ({count} files changed)" "zh": "Comparing {base}..{head} ({count} files changed)"
}, },
@@ -367,6 +419,7 @@
"bg": "Конфигуриране на защита на клона {branch}...", "bg": "Конфигуриране на защита на клона {branch}...",
"de": "Konfiguriere Branch-Schutz für {branch}...", "de": "Konfiguriere Branch-Schutz für {branch}...",
"en": "Configuring branch protection for {branch}...", "en": "Configuring branch protection for {branch}...",
"pl": "Konfigurowanie ochrony gałęzi dla {branch}...",
"ru": "Настройка защиты ветки {branch}...", "ru": "Настройка защиты ветки {branch}...",
"zh": "正在配置 {branch} 的分支保护..." "zh": "正在配置 {branch} 的分支保护..."
}, },
@@ -374,6 +427,7 @@
"bg": "Конфигуриране на настройките на хранилището...", "bg": "Конфигуриране на настройките на хранилището...",
"de": "Repository-Einstellungen konfigurieren...", "de": "Repository-Einstellungen konfigurieren...",
"en": "Configuring repository settings...", "en": "Configuring repository settings...",
"pl": "Konfigurowanie ustawień repozytorium...",
"ru": "Настройка параметров репозитория...", "ru": "Настройка параметров репозитория...",
"zh": "正在配置仓库设置..." "zh": "正在配置仓库设置..."
}, },
@@ -381,6 +435,7 @@
"bg": "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.", "de": "Could not extract conventional commit message from PR commits.",
"en": "Could not extract conventional commit message from PR commits.", "en": "Could not extract conventional commit message from PR commits.",
"pl": "Nie udało się wyodrębnić konwencjonalnej wiadomości commit z commitów PR.",
"ru": "Could not extract conventional commit message from PR commits.", "ru": "Could not extract conventional commit message from PR commits.",
"zh": "Could not extract conventional commit message from PR commits." "zh": "Could not extract conventional commit message from PR commits."
}, },
@@ -388,6 +443,7 @@
"bg": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.", "bg": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
"de": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.", "de": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
"en": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.", "en": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
"pl": "Nie znaleziono zadania Vikunja {task_id} w projekcie {project_id}. Każdy PR musi mieć odpowiadające zadanie Vikunja.",
"ru": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.", "ru": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
"zh": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task." "zh": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task."
}, },
@@ -395,6 +451,7 @@
"bg": "Could not find __version__ in {file}", "bg": "Could not find __version__ in {file}",
"de": "Could not find __version__ in {file}", "de": "Could not find __version__ in {file}",
"en": "Could not find __version__ in {file}", "en": "Could not find __version__ in {file}",
"pl": "Nie znaleziono __version__ w {file}",
"ru": "Could not find __version__ in {file}", "ru": "Could not find __version__ in {file}",
"zh": "Could not find __version__ in {file}" "zh": "Could not find __version__ in {file}"
}, },
@@ -402,6 +459,7 @@
"bg": "Could not parse test execution time from output.", "bg": "Could not parse test execution time from output.",
"de": "Could not parse test execution time from output.", "de": "Could not parse test execution time from output.",
"en": "Could not parse test execution time from output.", "en": "Could not parse test execution time from output.",
"pl": "Nie udało się przeanalizować czasu wykonania testu z wyjścia.",
"ru": "Could not parse test execution time from output.", "ru": "Could not parse test execution time from output.",
"zh": "Could not parse test execution time from output." "zh": "Could not parse test execution time from output."
}, },
@@ -409,6 +467,7 @@
"bg": "Created issue #{issue_id}: {title}", "bg": "Created issue #{issue_id}: {title}",
"de": "Created issue #{issue_id}: {title}", "de": "Created issue #{issue_id}: {title}",
"en": "Created issue #{issue_id}: {title}", "en": "Created issue #{issue_id}: {title}",
"pl": "Utworzono zgłoszenie #{issue_id}: {title}",
"ru": "Created issue #{issue_id}: {title}", "ru": "Created issue #{issue_id}: {title}",
"zh": "Created issue #{issue_id}: {title}" "zh": "Created issue #{issue_id}: {title}"
}, },
@@ -416,6 +475,7 @@
"bg": "Created release commit.", "bg": "Created release commit.",
"de": "Created release commit.", "de": "Created release commit.",
"en": "Created release commit.", "en": "Created release commit.",
"pl": "Utworzono commit wydania.",
"ru": "Created release commit.", "ru": "Created release commit.",
"zh": "Created release commit." "zh": "Created release commit."
}, },
@@ -423,6 +483,7 @@
"bg": "Докер демонът вече работи", "bg": "Докер демонът вече работи",
"de": "Docker-Daemon läuft bereits", "de": "Docker-Daemon läuft bereits",
"en": "Docker daemon already running", "en": "Docker daemon already running",
"pl": "Demon Docker już uruchomiony",
"ru": "Демон Docker уже работает", "ru": "Демон Docker уже работает",
"zh": "Docker 守护进程已在运行" "zh": "Docker 守护进程已在运行"
}, },
@@ -430,6 +491,7 @@
"bg": "Docker daemon failed to start", "bg": "Docker daemon failed to start",
"de": "Docker-Daemon konnte nicht gestartet werden", "de": "Docker-Daemon konnte nicht gestartet werden",
"en": "Docker daemon failed to start", "en": "Docker daemon failed to start",
"pl": "Nie udało się uruchomić demona Docker",
"ru": "Не удалось запустить Docker-демон", "ru": "Не удалось запустить Docker-демон",
"zh": "Docker 守护进程启动失败" "zh": "Docker 守护进程启动失败"
}, },
@@ -437,6 +499,7 @@
"bg": "Docker daemon started", "bg": "Docker daemon started",
"de": "Docker-Daemon gestartet", "de": "Docker-Daemon gestartet",
"en": "Docker daemon started", "en": "Docker daemon started",
"pl": "Demon Docker uruchomiony",
"ru": "Docker-демон запущен", "ru": "Docker-демон запущен",
"zh": "Docker 守护进程已启动" "zh": "Docker 守护进程已启动"
}, },
@@ -444,6 +507,7 @@
"bg": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.", "bg": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
"de": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.", "de": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
"en": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.", "en": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
"pl": "Tryb dry-run: na gałęzi '{branch}' (nie master). Niektóre kontrole mogą zachowywać się inaczej.",
"ru": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.", "ru": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
"zh": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently." "zh": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently."
}, },
@@ -451,6 +515,7 @@
"bg": "ГРЕШКА: REPO_TOKEN не е зададен.", "bg": "ГРЕШКА: REPO_TOKEN не е зададен.",
"de": "FEHLER: REPO_TOKEN ist nicht gesetzt.", "de": "FEHLER: REPO_TOKEN ist nicht gesetzt.",
"en": "ERROR: REPO_TOKEN is not set.", "en": "ERROR: REPO_TOKEN is not set.",
"pl": "BŁĄD: REPO_TOKEN nie jest ustawiony.",
"ru": "ОШИБКА: REPO_TOKEN не задан.", "ru": "ОШИБКА: REPO_TOKEN не задан.",
"zh": "错误:未设置 REPO_TOKEN。" "zh": "错误:未设置 REPO_TOKEN。"
}, },
@@ -458,6 +523,7 @@
"bg": "ГРЕШКА: Името на хранилището не е указано. Използвайте --repo или задайте DEVX_REPO_NAME.", "bg": "ГРЕШКА: Името на хранилището не е указано. Използвайте --repo или задайте DEVX_REPO_NAME.",
"de": "FEHLER: Repository-Name nicht angegeben. Verwenden Sie --repo oder setzen Sie DEVX_REPO_NAME.", "de": "FEHLER: Repository-Name nicht angegeben. Verwenden Sie --repo oder setzen Sie DEVX_REPO_NAME.",
"en": "ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME.", "en": "ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME.",
"pl": "BŁĄD: Nazwa repozytorium nie jest określona. Użyj --repo lub ustaw DEVX_REPO_NAME.",
"ru": "ОШИБКА: Имя репозитория не указано. Используйте --repo или задайте DEVX_REPO_NAME.", "ru": "ОШИБКА: Имя репозитория не указано. Используйте --repo или задайте DEVX_REPO_NAME.",
"zh": "错误:未指定仓库名称。请使用 --repo 或设置 DEVX_REPO_NAME。" "zh": "错误:未指定仓库名称。请使用 --repo 或设置 DEVX_REPO_NAME。"
}, },
@@ -465,6 +531,7 @@
"bg": "ERROR: Tag consistency check failed. Existing tags are misaligned:", "bg": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
"de": "ERROR: Tag consistency check failed. Existing tags are misaligned:", "de": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
"en": "ERROR: Tag consistency check failed. Existing tags are misaligned:", "en": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
"pl": "BŁĄD: Kontrola zgodności tagów nie powiodła się. Istniejące tagi są niezgodne:",
"ru": "ERROR: Tag consistency check failed. Existing tags are misaligned:", "ru": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
"zh": "ERROR: Tag consistency check failed. Existing tags are misaligned:" "zh": "ERROR: Tag consistency check failed. Existing tags are misaligned:"
}, },
@@ -472,6 +539,7 @@
"bg": "ГРЕШКА: VIKUNJA_TOKEN не е зададен.", "bg": "ГРЕШКА: VIKUNJA_TOKEN не е зададен.",
"de": "FEHLER: VIKUNJA_TOKEN ist nicht gesetzt.", "de": "FEHLER: VIKUNJA_TOKEN ist nicht gesetzt.",
"en": "ERROR: VIKUNJA_TOKEN is not set.", "en": "ERROR: VIKUNJA_TOKEN is not set.",
"pl": "BŁĄD: VIKUNJA_TOKEN nie jest ustawiony.",
"ru": "ОШИБКА: VIKUNJA_TOKEN не задан.", "ru": "ОШИБКА: VIKUNJA_TOKEN не задан.",
"zh": "错误:未设置 VIKUNJA_TOKEN。" "zh": "错误:未设置 VIKUNJA_TOKEN。"
}, },
@@ -479,6 +547,7 @@
"bg": "ERROR: mapping.json not found at {path}", "bg": "ERROR: mapping.json not found at {path}",
"de": "ERROR: mapping.json not found at {path}", "de": "ERROR: mapping.json not found at {path}",
"en": "ERROR: mapping.json not found at {path}", "en": "ERROR: mapping.json not found at {path}",
"pl": "BŁĄD: mapping.json nie znaleziono w {path}",
"ru": "ERROR: mapping.json not found at {path}", "ru": "ERROR: mapping.json not found at {path}",
"zh": "ERROR: mapping.json not found at {path}" "zh": "ERROR: mapping.json not found at {path}"
}, },
@@ -486,6 +555,7 @@
"bg": "FAILED: {pair} exited with code {code}", "bg": "FAILED: {pair} exited with code {code}",
"de": "FAILED: {pair} exited with code {code}", "de": "FAILED: {pair} exited with code {code}",
"en": "FAILED: {pair} exited with code {code}", "en": "FAILED: {pair} exited with code {code}",
"pl": "NIEUDANE: {pair} zakończone kodem {code}",
"ru": "FAILED: {pair} exited with code {code}", "ru": "FAILED: {pair} exited with code {code}",
"zh": "FAILED: {pair} exited with code {code}" "zh": "FAILED: {pair} exited with code {code}"
}, },
@@ -493,6 +563,7 @@
"bg": "Failed to create issue via tea: {error}", "bg": "Failed to create issue via tea: {error}",
"de": "Failed to create issue via tea: {error}", "de": "Failed to create issue via tea: {error}",
"en": "Failed to create issue via tea: {error}", "en": "Failed to create issue via tea: {error}",
"pl": "Nie udało się utworzyć zgłoszenia przez tea: {error}",
"ru": "Failed to create issue via tea: {error}", "ru": "Failed to create issue via tea: {error}",
"zh": "Failed to create issue via tea: {error}" "zh": "Failed to create issue via tea: {error}"
}, },
@@ -500,6 +571,7 @@
"bg": "Found {count} existing wiki pages.", "bg": "Found {count} existing wiki pages.",
"de": "Found {count} existing wiki pages.", "de": "Found {count} existing wiki pages.",
"en": "Found {count} existing wiki pages.", "en": "Found {count} existing wiki pages.",
"pl": "Znaleziono {count} istniejących stron wiki.",
"ru": "Found {count} existing wiki pages.", "ru": "Found {count} existing wiki pages.",
"zh": "Found {count} existing wiki pages." "zh": "Found {count} existing wiki pages."
}, },
@@ -507,6 +579,7 @@
"bg": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.", "bg": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"de": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.", "de": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"en": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.", "en": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"pl": "GITEA_URL/REPO_TOKEN/RUN_ID nie ustawione; uruchamianie bez anulowania między runnerami.",
"ru": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.", "ru": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"zh": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation." "zh": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation."
}, },
@@ -514,13 +587,23 @@
"bg": "Generated {file} with prefix '{prefix}'.", "bg": "Generated {file} with prefix '{prefix}'.",
"de": "Generated {file} with prefix '{prefix}'.", "de": "Generated {file} with prefix '{prefix}'.",
"en": "Generated {file} with prefix '{prefix}'.", "en": "Generated {file} with prefix '{prefix}'.",
"pl": "Wygenerowano {file} z prefiksem '{prefix}'.",
"ru": "Generated {file} with prefix '{prefix}'.", "ru": "Generated {file} with prefix '{prefix}'.",
"zh": "Generated {file} with prefix '{prefix}'." "zh": "Generated {file} with prefix '{prefix}'."
}, },
"Gitea release {tag} already exists — skipping creation.": {
"bg": "Gitea release {tag} вече съществува — прескачане на създаването.",
"de": "Gitea-Release {tag} existiert bereits — Erstellung übersprungen.",
"en": "Gitea release {tag} already exists — skipping creation.",
"pl": "Wydanie Gitea {tag} już istnieje — pomijanie tworzenia.",
"ru": "Gitea release {tag} уже существует — пропуск создания.",
"zh": "Gitea release {tag} 已存在 — 跳过创建。"
},
"HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.": { "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.": {
"bg": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.", "bg": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
"de": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.", "de": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
"en": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.", "en": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
"pl": "HEAD jest commitem wydania ('{msg}') ale tag {tag} brakuje. Naprawa przez utworzenie tagu.",
"ru": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.", "ru": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
"zh": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag." "zh": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag."
}, },
@@ -528,6 +611,7 @@
"bg": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.", "bg": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.",
"de": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.", "de": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.",
"en": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.", "en": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.",
"pl": "HEAD jest commitem wydania dla v{version} ale tag {tag} wskazuje na inny commit ({tag_commit} vs HEAD {head_commit}). Wskazuje to na niezgodność tag/commit.",
"ru": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.", "ru": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment.",
"zh": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment." "zh": "HEAD is a release commit for v{version} but tag {tag} points to a different commit ({tag_commit} vs HEAD {head_commit}). This indicates a tag/commit misalignment."
}, },
@@ -535,6 +619,7 @@
"bg": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.", "bg": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
"de": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.", "de": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
"en": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.", "en": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
"pl": "HEAD jest już commitem wydania ('{msg}') a tag {tag} wskazuje na HEAD. Pomijanie.",
"ru": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.", "ru": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping.",
"zh": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping." "zh": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping."
}, },
@@ -542,6 +627,7 @@
"bg": "HTTP грешка: {status} — {message}", "bg": "HTTP грешка: {status} — {message}",
"de": "HTTP-Fehler: {status} — {message}", "de": "HTTP-Fehler: {status} — {message}",
"en": "HTTP error: {status} — {message}", "en": "HTTP error: {status} — {message}",
"pl": "Błąd HTTP: {status} — {message}",
"ru": "Ошибка HTTP: {status} — {message}", "ru": "Ошибка HTTP: {status} — {message}",
"zh": "HTTP 错误: {status} — {message}" "zh": "HTTP 错误: {status} — {message}"
}, },
@@ -549,6 +635,7 @@
"bg": "HTTP {status} Забранено — вашият токен няма администраторски права.\nУверете се, че токенът принадлежи на собственик на хранилище или администратор на организация.\nАлтернативно, конфигурирайте защитата на клона ръчно в Настройки → Клонове.", "bg": "HTTP {status} Забранено — вашият токен няма администраторски права.\nУверете се, че токенът принадлежи на собственик на хранилище или администратор на организация.\nАлтернативно, конфигурирайте защитата на клона ръчно в Настройки → Клонове.",
"de": "HTTP {status} Verboten — Ihr Token hat keine Admin-Rechte.\nStellen Sie sicher, dass das Token einem Repository-Besitzer oder Organisations-Admin gehört.\nAlternativ können Sie den Branch-Schutz manuell unter Einstellungen → Branches konfigurieren.", "de": "HTTP {status} Verboten — Ihr Token hat keine Admin-Rechte.\nStellen Sie sicher, dass das Token einem Repository-Besitzer oder Organisations-Admin gehört.\nAlternativ können Sie den Branch-Schutz manuell unter Einstellungen → Branches konfigurieren.",
"en": "HTTP {status} Forbidden — your token lacks admin rights.\nMake sure the token belongs to a repo owner or organisation admin.\nAlternatively, configure branch protection manually in Settings → Branches.", "en": "HTTP {status} Forbidden — your token lacks admin rights.\nMake sure the token belongs to a repo owner or organisation admin.\nAlternatively, configure branch protection manually in Settings → Branches.",
"pl": "HTTP {status} Forbidden — twój token nie ma uprawnień administratora.\nUpewnij się, że token należy do właściciela repozytorium lub administratora organizacji.\nAlternatywnie skonfiguruj ochronę gałęzi ręcznie w Ustawienia → Gałęzie.",
"ru": "HTTP {status} Запрещено — у вашего токена нет прав администратора.\nУбедитесь, что токен принадлежит владельцу репозитория или администратору организации.\nЛибо настройте защиту ветки вручную в разделе Настройки → Ветки.", "ru": "HTTP {status} Запрещено — у вашего токена нет прав администратора.\nУбедитесь, что токен принадлежит владельцу репозитория или администратору организации.\nЛибо настройте защиту ветки вручную в разделе Настройки → Ветки.",
"zh": "HTTP {status} 禁止访问 — 您的令牌缺少管理员权限。\n请确保令牌属于仓库所有者或组织管理员。\n或者,您可以在 设置 → 分支 中手动配置分支保护。" "zh": "HTTP {status} 禁止访问 — 您的令牌缺少管理员权限。\n请确保令牌属于仓库所有者或组织管理员。\n或者,您可以在 设置 → 分支 中手动配置分支保护。"
}, },
@@ -556,6 +643,7 @@
"bg": "Head branch is behind master. Pulling and rebasing...", "bg": "Head branch is behind master. Pulling and rebasing...",
"de": "Head branch is behind master. Pulling and rebasing...", "de": "Head branch is behind master. Pulling and rebasing...",
"en": "Head branch is behind master. Pulling and rebasing...", "en": "Head branch is behind master. Pulling and rebasing...",
"pl": "Gałąź head jest w tyle za master. Pobieranie i rebasing...",
"ru": "Head branch is behind master. Pulling and rebasing...", "ru": "Head branch is behind master. Pulling and rebasing...",
"zh": "Head branch is behind master. Pulling and rebasing..." "zh": "Head branch is behind master. Pulling and rebasing..."
}, },
@@ -563,6 +651,7 @@
"bg": "Хост Docker не е наличен, стартиране на локален dockerd...", "bg": "Хост Docker не е наличен, стартиране на локален dockerd...",
"de": "Host-Docker nicht verfügbar, lokaler dockerd wird gestartet...", "de": "Host-Docker nicht verfügbar, lokaler dockerd wird gestartet...",
"en": "Host Docker not available, starting local dockerd...", "en": "Host Docker not available, starting local dockerd...",
"pl": "Host Docker niedostępny, uruchamianie lokalnego dockerd...",
"ru": "Хост Docker недоступен, запускается локальный dockerd...", "ru": "Хост Docker недоступен, запускается локальный dockerd...",
"zh": "主机 Docker 不可用,正在启动本地 dockerd..." "zh": "主机 Docker 不可用,正在启动本地 dockerd..."
}, },
@@ -570,6 +659,7 @@
"bg": "Инфраструктурен commit (без идентификатор на задача DEVX-N), пропускаме обновяването на Vikunja: {msg}", "bg": "Инфраструктурен commit (без идентификатор на задача DEVX-N), пропускаме обновяването на Vikunja: {msg}",
"de": "Infrastruktur-Commit (keine DEVX-N Task-ID), Vikunja-Update wird übersprungen: {msg}", "de": "Infrastruktur-Commit (keine DEVX-N Task-ID), Vikunja-Update wird übersprungen: {msg}",
"en": "Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}", "en": "Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}",
"pl": "Commit infrastruktury (bez ID zadania DEVX-N), pomijanie aktualizacji Vikunja: {msg}",
"ru": "Инфраструктурный коммит (без ID задачи DEVX-N), пропуск обновления Vikunja: {msg}", "ru": "Инфраструктурный коммит (без ID задачи DEVX-N), пропуск обновления Vikunja: {msg}",
"zh": "基础设施提交(无 DEVX-N 任务 ID),跳过 Vikunja 更新: {msg}" "zh": "基础设施提交(无 DEVX-N 任务 ID),跳过 Vikunja 更新: {msg}"
}, },
@@ -577,6 +667,7 @@
"bg": "Integration tests cancelled — another runner failed.", "bg": "Integration tests cancelled — another runner failed.",
"de": "Integration tests cancelled — another runner failed.", "de": "Integration tests cancelled — another runner failed.",
"en": "Integration tests cancelled — another runner failed.", "en": "Integration tests cancelled — another runner failed.",
"pl": "Testy integracyjne anulowane — inny runner zakończył się niepowodzeniem.",
"ru": "Integration tests cancelled — another runner failed.", "ru": "Integration tests cancelled — another runner failed.",
"zh": "Integration tests cancelled — another runner failed." "zh": "Integration tests cancelled — another runner failed."
}, },
@@ -584,6 +675,7 @@
"bg": "Integration tests failed with exit code {code}", "bg": "Integration tests failed with exit code {code}",
"de": "Integration tests failed with exit code {code}", "de": "Integration tests failed with exit code {code}",
"en": "Integration tests failed with exit code {code}", "en": "Integration tests failed with exit code {code}",
"pl": "Testy integracyjne zakończone niepowodzeniem z kodem {code}",
"ru": "Integration tests failed with exit code {code}", "ru": "Integration tests failed with exit code {code}",
"zh": "Integration tests failed with exit code {code}" "zh": "Integration tests failed with exit code {code}"
}, },
@@ -591,6 +683,7 @@
"bg": "Integration tests passed.", "bg": "Integration tests passed.",
"de": "Integration tests passed.", "de": "Integration tests passed.",
"en": "Integration tests passed.", "en": "Integration tests passed.",
"pl": "Testy integracyjne zakończone pomyślnie.",
"ru": "Integration tests passed.", "ru": "Integration tests passed.",
"zh": "Integration tests passed." "zh": "Integration tests passed."
}, },
@@ -598,6 +691,7 @@
"bg": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}", "bg": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
"de": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}", "de": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
"en": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}", "en": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
"pl": "Lint nie powiódł się — odmowa wydania. Najpierw napraw błędy lint.\n{stderr}",
"ru": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}", "ru": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
"zh": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}" "zh": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}"
}, },
@@ -605,6 +699,7 @@
"bg": "Lint passed.", "bg": "Lint passed.",
"de": "Lint passed.", "de": "Lint passed.",
"en": "Lint passed.", "en": "Lint passed.",
"pl": "Lint zakończony pomyślnie.",
"ru": "Lint passed.", "ru": "Lint passed.",
"zh": "Lint passed." "zh": "Lint passed."
}, },
@@ -612,6 +707,7 @@
"bg": "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.", "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.", "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.", "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." "zh": "Mapped file {file} is empty. Update the content or remove from mapping.json."
}, },
@@ -619,6 +715,7 @@
"bg": "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.", "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.", "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.", "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." "zh": "Mapped file {file} not found. Update mapping.json or create the file."
}, },
@@ -626,6 +723,7 @@
"bg": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.", "bg": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
"de": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.", "de": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
"en": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.", "en": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
"pl": "Scalanie nie powiodło się po ponownej próbie rebase: {error}\nProszę wykonać rebase PR ręcznie.",
"ru": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.", "ru": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually.",
"zh": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually." "zh": "Merge failed after rebase retry: {error}\nPlease rebase the PR manually."
}, },
@@ -633,6 +731,7 @@
"bg": "Сливането неуспешно с HTTP {status}: {message}\nПроверете дали PR е готов и имате права за сливане.", "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.", "de": "Merge fehlgeschlagen mit HTTP {status}: {message}\nBitte prüfen Sie, ob der PR bereit ist und Sie Merge-Rechte haben.",
"en": "Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.", "en": "Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.",
"pl": "Scalanie nie powiodło się z HTTP {status}: {message}\nSprawdź czy PR jest gotowy i masz uprawnienia do scalania.",
"ru": "Слияние не удалось: HTTP {status}: {message}\nПроверьте, что PR готов и у вас есть права на слияние.", "ru": "Слияние не удалось: HTTP {status}: {message}\nПроверьте, что PR готов и у вас есть права на слияние.",
"zh": "合并失败: HTTP {status}: {message}\n请检查 PR 是否准备就绪且您具有合并权限。" "zh": "合并失败: HTTP {status}: {message}\n请检查 PR 是否准备就绪且您具有合并权限。"
}, },
@@ -640,6 +739,7 @@
"bg": "Merged {count} reports: {tests} tests, {failures} failures → {output}", "bg": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
"de": "Merged {count} reports: {tests} tests, {failures} failures → {output}", "de": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
"en": "Merged {count} reports: {tests} tests, {failures} failures → {output}", "en": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
"pl": "Scalono {count} raportów: {tests} testów, {failures} niepowodzeń → {output}",
"ru": "Merged {count} reports: {tests} tests, {failures} failures → {output}", "ru": "Merged {count} reports: {tests} tests, {failures} failures → {output}",
"zh": "Merged {count} reports: {tests} tests, {failures} failures → {output}" "zh": "Merged {count} reports: {tests} tests, {failures} failures → {output}"
}, },
@@ -647,6 +747,7 @@
"bg": "Модул {mod} няма функция main()", "bg": "Модул {mod} няма функция main()",
"de": "Modul {mod} hat keine main()-Funktion", "de": "Modul {mod} hat keine main()-Funktion",
"en": "Module {mod} has no main() function", "en": "Module {mod} has no main() function",
"pl": "Moduł {mod} nie ma funkcji main()",
"ru": "Модуль {mod} не имеет функции main()", "ru": "Модуль {mod} не имеет функции main()",
"zh": "模块 {mod} 没有 main() 函数" "zh": "模块 {mod} 没有 main() 函数"
}, },
@@ -654,6 +755,7 @@
"bg": "Директорията на molecule не е намерена: {path}", "bg": "Директорията на molecule не е намерена: {path}",
"de": "Molecule-Verzeichnis nicht gefunden: {path}", "de": "Molecule-Verzeichnis nicht gefunden: {path}",
"en": "Molecule directory not found: {path}", "en": "Molecule directory not found: {path}",
"pl": "Katalog molecule nie znaleziony: {path}",
"ru": "Директория molecule не найдена: {path}", "ru": "Директория molecule не найдена: {path}",
"zh": "未找到 molecule 目录: {path}" "zh": "未找到 molecule 目录: {path}"
}, },
@@ -661,6 +763,7 @@
"bg": "Отлично! Gitea release {tag} е създаден.", "bg": "Отлично! Gitea release {tag} е създаден.",
"de": "Prima! Gitea-Release {tag} erstellt.", "de": "Prima! Gitea-Release {tag} erstellt.",
"en": "Nice! Gitea release {tag} created.", "en": "Nice! Gitea release {tag} created.",
"pl": "Świetnie! Wydanie Gitea {tag} utworzone.",
"ru": "Отлично! Gitea release {tag} создан.", "ru": "Отлично! Gitea release {tag} создан.",
"zh": "不错!Gitea release {tag} 已创建。" "zh": "不错!Gitea release {tag} 已创建。"
}, },
@@ -668,6 +771,7 @@
"bg": "Отлично! PR #{pr_number} е squash-merge-нат със заглавие: {merge_title}", "bg": "Отлично! PR #{pr_number} е squash-merge-нат със заглавие: {merge_title}",
"de": "Prima! PR #{pr_number} wurde mit Titel {merge_title} squash-gemergt.", "de": "Prima! PR #{pr_number} wurde mit Titel {merge_title} squash-gemergt.",
"en": "Nice! PR #{pr_number} squash-merged with title: {merge_title}", "en": "Nice! PR #{pr_number} squash-merged with title: {merge_title}",
"pl": "Świetnie! PR #{pr_number} squash-merged z tytułem: {merge_title}",
"ru": "Отлично! PR #{pr_number} squash-merge с заголовком: {merge_title}", "ru": "Отлично! PR #{pr_number} squash-merge с заголовком: {merge_title}",
"zh": "不错!PR #{pr_number} 已 squash 合并,标题: {merge_title}" "zh": "不错!PR #{pr_number} 已 squash 合并,标题: {merge_title}"
}, },
@@ -675,6 +779,7 @@
"bg": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.", "bg": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
"de": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.", "de": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
"en": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.", "en": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
"pl": "Świetnie! Wydanie v{version} otagowane i wypchnięte. Workflow publikacji zostanie uruchomiony.",
"ru": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.", "ru": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
"zh": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered." "zh": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered."
}, },
@@ -682,6 +787,7 @@
"bg": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) е обновена и маркирана като готова.", "bg": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) е обновена и маркирана като готова.",
"de": "Prima! Vikunja-Aufgabe {task_id} (ID {vikunja_id}) aktualisiert und als erledigt markiert.", "de": "Prima! Vikunja-Aufgabe {task_id} (ID {vikunja_id}) aktualisiert und als erledigt markiert.",
"en": "Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.", "en": "Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.",
"pl": "Świetnie! Zadanie Vikunja {task_id} (ID {vikunja_id}) zaktualizowane i oznaczone jako ukończone.",
"ru": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) обновлена и отмечена как выполненная.", "ru": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) обновлена и отмечена как выполненная.",
"zh": "不错!Vikunja 任务 {task_id} (ID {vikunja_id}) 已更新并标记为完成。" "zh": "不错!Vikunja 任务 {task_id} (ID {vikunja_id}) 已更新并标记为完成。"
}, },
@@ -689,6 +795,7 @@
"bg": "No JUnit reports found matching {pattern} — skipping merge.", "bg": "No JUnit reports found matching {pattern} — skipping merge.",
"de": "No JUnit reports found matching {pattern} — skipping merge.", "de": "No JUnit reports found matching {pattern} — skipping merge.",
"en": "No JUnit reports found matching {pattern} — skipping merge.", "en": "No JUnit reports found matching {pattern} — skipping merge.",
"pl": "Nie znaleziono raportów JUnit pasujących do {pattern} — pomijanie scalania.",
"ru": "No JUnit reports found matching {pattern} — skipping merge.", "ru": "No JUnit reports found matching {pattern} — skipping merge.",
"zh": "No JUnit reports found matching {pattern} — skipping merge." "zh": "No JUnit reports found matching {pattern} — skipping merge."
}, },
@@ -696,6 +803,7 @@
"bg": "No changes between {base} and {head}.", "bg": "No changes between {base} and {head}.",
"de": "No changes between {base} and {head}.", "de": "No changes between {base} and {head}.",
"en": "No changes between {base} and {head}.", "en": "No changes between {base} and {head}.",
"pl": "Brak zmian między {base} i {head}.",
"ru": "No changes between {base} and {head}.", "ru": "No changes between {base} and {head}.",
"zh": "No changes between {base} and {head}." "zh": "No changes between {base} and {head}."
}, },
@@ -703,6 +811,7 @@
"bg": "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.", "de": "No staged changes — version and changelog already up to date.",
"en": "No staged changes — version and changelog already up to date.", "en": "No staged changes — version and changelog already up to date.",
"pl": "Brak zmian w staging — wersja i changelog są już aktualne.",
"ru": "No staged changes — version and changelog already up to date.", "ru": "No staged changes — version and changelog already up to date.",
"zh": "No staged changes — version and changelog already up to date." "zh": "No staged changes — version and changelog already up to date."
}, },
@@ -710,6 +819,7 @@
"bg": "No tags found — treating all changes as user-facing.", "bg": "No tags found — treating all changes as user-facing.",
"de": "No tags found — treating all changes as user-facing.", "de": "No tags found — treating all changes as user-facing.",
"en": "No tags found — treating all changes as user-facing.", "en": "No tags found — treating all changes as user-facing.",
"pl": "Nie znaleziono tagów — traktowanie wszystkich zmian jako widocznych dla użytkownika.",
"ru": "No tags found — treating all changes as user-facing.", "ru": "No tags found — treating all changes as user-facing.",
"zh": "No tags found — treating all changes as user-facing." "zh": "No tags found — treating all changes as user-facing."
}, },
@@ -717,6 +827,7 @@
"bg": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.", "bg": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
"de": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.", "de": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
"en": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.", "en": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
"pl": "Nie znaleziono ID zadania ({prefix}-N) w wiadomości commit: {msg}. Każdy commit nie-infrastrukturalny musi mieć ID zadania.",
"ru": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.", "ru": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
"zh": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID." "zh": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID."
}, },
@@ -724,6 +835,7 @@
"bg": "No unreleased changes found. Nothing to release.", "bg": "No unreleased changes found. Nothing to release.",
"de": "No unreleased changes found. Nothing to release.", "de": "No unreleased changes found. Nothing to release.",
"en": "No unreleased changes found. Nothing to release.", "en": "No unreleased changes found. Nothing to release.",
"pl": "Nie znaleziono nieopublikowanych zmian. Nic do wydania.",
"ru": "No unreleased changes found. Nothing to release.", "ru": "No unreleased changes found. Nothing to release.",
"zh": "No unreleased changes found. Nothing to release." "zh": "No unreleased changes found. Nothing to release."
}, },
@@ -731,6 +843,7 @@
"bg": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.", "bg": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
"de": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.", "de": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
"en": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.", "en": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
"pl": "Brak zmian widocznych dla użytkownika od {tag} — tylko pliki workflow/infrastruktury uległy zmianie. Pomijanie wydania.",
"ru": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.", "ru": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
"zh": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release." "zh": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release."
}, },
@@ -738,6 +851,7 @@
"bg": "Note: Self-approval not allowed. Posting COMMENT instead.", "bg": "Note: Self-approval not allowed. Posting COMMENT instead.",
"de": "Note: Self-approval not allowed. Posting COMMENT instead.", "de": "Note: Self-approval not allowed. Posting COMMENT instead.",
"en": "Note: Self-approval not allowed. Posting COMMENT instead.", "en": "Note: Self-approval not allowed. Posting COMMENT instead.",
"pl": "Uwaga: Samo-zatwierdzenie niedozwolone. Publikowanie COMMENT zamiast tego.",
"ru": "Note: Self-approval not allowed. Posting COMMENT instead.", "ru": "Note: Self-approval not allowed. Posting COMMENT instead.",
"zh": "Note: Self-approval not allowed. Posting COMMENT instead." "zh": "Note: Self-approval not allowed. Posting COMMENT instead."
}, },
@@ -745,6 +859,7 @@
"bg": "Опа! Съобщението за commit трябва да следва конвенционален формат.\n Очаква се: <type>: <description>\n Получено: {subject}\n Разрешени типове: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "bg": "Опа! Съобщението за commit трябва да следва конвенционален формат.\n Очаква се: <type>: <description>\n Получено: {subject}\n Разрешени типове: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
"de": "Ups! Commit-Nachricht muss dem konventionellen Commit-Format folgen.\n Erwartet: <type>: <description>\n Erhalten: {subject}\n Erlaubte Typen: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "de": "Ups! Commit-Nachricht muss dem konventionellen Commit-Format folgen.\n Erwartet: <type>: <description>\n Erhalten: {subject}\n Erlaubte Typen: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
"en": "Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "en": "Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
"pl": "Ups! Wiadomość commit musi być w formacie conventional commit.\n Oczekiwano: <typ>: <opis>\n Otrzymano: {subject}\n Dozwolone typy: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
"ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
"zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE" "zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE"
}, },
@@ -752,6 +867,7 @@
"bg": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.", "bg": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.",
"de": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.", "de": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.",
"en": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.", "en": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.",
"pl": "Ups! Nie dołączaj ID zadania ({prefix}-N) w commitach gałęzi feature.\n ID zadania zostanie dodane automatycznie przy scaleniu przez CI.",
"ru": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.", "ru": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.",
"zh": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI." "zh": "Oops! Do not include task ID ({prefix}-N) in feature branch commits.\n The task ID will be added automatically on merge via CI."
}, },
@@ -759,6 +875,7 @@
"bg": "Опа! Публикуването в Gitea PyPI registry неуспешно:\n{stderr}", "bg": "Опа! Публикуването в Gitea PyPI registry неуспешно:\n{stderr}",
"de": "Ups! Veröffentlichung in der Gitea PyPI-Registry fehlgeschlagen:\n{stderr}", "de": "Ups! Veröffentlichung in der Gitea PyPI-Registry fehlgeschlagen:\n{stderr}",
"en": "Oops! Gitea PyPI registry publish failed:\n{stderr}", "en": "Oops! Gitea PyPI registry publish failed:\n{stderr}",
"pl": "Ups! Publikacja w rejestrze Gitea PyPI nie powiodła się:\n{stderr}",
"ru": "Ой! Публикация в Gitea PyPI registry не удалась:\n{stderr}", "ru": "Ой! Публикация в Gitea PyPI registry не удалась:\n{stderr}",
"zh": "哎呀!Gitea PyPI registry 发布失败:\n{stderr}" "zh": "哎呀!Gitea PyPI registry 发布失败:\n{stderr}"
}, },
@@ -766,6 +883,7 @@
"bg": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}", "bg": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
"de": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}", "de": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
"en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}", "en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
"pl": "Ups! Commit gałęzi master musi być w formacie conventional po ID zadania.\n Oczekiwano: {prefix}-N: <typ>: <opis>\n Otrzymano: {subject}",
"ru": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}", "ru": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}",
"zh": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}" "zh": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}"
}, },
@@ -773,20 +891,23 @@
"bg": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}", "bg": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
"de": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}", "de": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
"en": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}", "en": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
"pl": "Ups! Commity gałęzi master muszą zaczynać się od ID zadania.\n Oczekiwano: {prefix}-N: <conwencjonalna wiadomość commit>\n Otrzymano: {subject}",
"ru": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}", "ru": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}",
"zh": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}" "zh": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}"
}, },
"Oops! No task ID found in .taskid file or branch name '{branch}'.": { "Oops! No task ID found in branch name '{branch}'. Branch names must include the task ID prefix (e.g., DEVX-31-fix-bug).": {
"bg": "Oops! No task ID found in .taskid file or branch name '{branch}'.", "bg": "Ой! Не е намерен ID на задача в името на клона '{branch}'. Имената на клонове трябва да включват префикса за ID на задача (напр. DEVX-31-fix-bug).",
"de": "Oops! No task ID found in .taskid file or branch name '{branch}'.", "de": "Hoppla! Keine Task-ID im Branch-Namen '{branch}' gefunden. Branch-Namen müssen das Task-ID-Präfix enthalten (z.B. DEVX-31-fix-bug).",
"en": "Oops! No task ID found in .taskid file or branch name '{branch}'.", "en": "Oops! No task ID found in branch name '{branch}'. Branch names must include the task ID prefix (e.g., DEVX-31-fix-bug).",
"ru": "Oops! No task ID found in .taskid file or branch name '{branch}'.", "pl": "Ups! Nie znaleziono ID zadania w nazwie gałęzi '{branch}'. Nazwy gałęzi muszą zawierać prefiks ID zadania (np., DEVX-31-fix-bug).",
"zh": "Oops! No task ID found in .taskid file or branch name '{branch}'." "ru": "Ой! ID задачи не найден в имени ветки '{branch}'. Имена веток должны включать префикс ID задачи (например, DEVX-31-fix-bug).",
"zh": "哎呀!在分支名称 '{branch}' 中未找到任务 ID。分支名称必须包含任务 ID 前缀(例如 DEVX-31-fix-bug)。"
}, },
"Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}": { "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}": {
"bg": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}", "bg": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
"de": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}", "de": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
"en": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}", "en": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
"pl": "Ups! Tytuł PR musi być w formacie '{prefix}-N: <tytuł zadania>'.\n Oczekiwano: {task_id}: <tytuł zadania>\n Otrzymano: {pr_title}",
"ru": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}", "ru": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
"zh": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}" "zh": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}"
}, },
@@ -794,6 +915,7 @@
"bg": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}", "bg": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
"de": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}", "de": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
"en": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}", "en": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
"pl": "Ups! Niezgodność ID zadania w tytule PR.\n ID zadania z gałęzi: {task_id}\n Tytuł PR: {pr_title}",
"ru": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}", "ru": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}",
"zh": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}" "zh": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}"
}, },
@@ -801,6 +923,7 @@
"bg": "Опа! Сборката на пакета неуспешна:\n{stderr}", "bg": "Опа! Сборката на пакета неуспешна:\n{stderr}",
"de": "Ups! Paket-Build fehlgeschlagen:\n{stderr}", "de": "Ups! Paket-Build fehlgeschlagen:\n{stderr}",
"en": "Oops! Package build failed:\n{stderr}", "en": "Oops! Package build failed:\n{stderr}",
"pl": "Ups! Budowanie pakietu nie powiodło się:\n{stderr}",
"ru": "Ой! Сборка пакета не удалась:\n{stderr}", "ru": "Ой! Сборка пакета не удалась:\n{stderr}",
"zh": "哎呀!包构建失败:\n{stderr}" "zh": "哎呀!包构建失败:\n{stderr}"
}, },
@@ -808,20 +931,15 @@
"bg": "Опа! Публикуването в PyPI неуспешно:\n{stderr}", "bg": "Опа! Публикуването в PyPI неуспешно:\n{stderr}",
"de": "Ups! PyPI-Veröffentlichung fehlgeschlagen:\n{stderr}", "de": "Ups! PyPI-Veröffentlichung fehlgeschlagen:\n{stderr}",
"en": "Oops! PyPI publish failed:\n{stderr}", "en": "Oops! PyPI publish failed:\n{stderr}",
"pl": "Ups! Publikacja PyPI nie powiodła się:\n{stderr}",
"ru": "Ой! Публикация в PyPI не удалась:\n{stderr}", "ru": "Ой! Публикация в PyPI не удалась:\n{stderr}",
"zh": "哎呀!PyPI 发布失败:\n{stderr}" "zh": "哎呀!PyPI 发布失败:\n{stderr}"
}, },
"Parsed owner={owner}, repo={repo} from DEVX_REPO_NAME": {
"bg": "Разбор на owner={owner}, repo={repo} от DEVX_REPO_NAME",
"de": "Owner={owner}, repo={repo} aus DEVX_REPO_NAME analysiert",
"en": "Parsed owner={owner}, repo={repo} from DEVX_REPO_NAME",
"ru": "Извлечён owner={owner}, repo={repo} из DEVX_REPO_NAME",
"zh": "从 DEVX_REPO_NAME 解析 owner={owner}, repo={repo}"
},
"PASSED: {pair}": { "PASSED: {pair}": {
"bg": "PASSED: {pair}", "bg": "PASSED: {pair}",
"de": "PASSED: {pair}", "de": "PASSED: {pair}",
"en": "PASSED: {pair}", "en": "PASSED: {pair}",
"pl": "UDANE: {pair}",
"ru": "PASSED: {pair}", "ru": "PASSED: {pair}",
"zh": "PASSED: {pair}" "zh": "PASSED: {pair}"
}, },
@@ -829,6 +947,7 @@
"bg": "PR number must be an integer, got: {pr_number}", "bg": "PR number must be an integer, got: {pr_number}",
"de": "PR number must be an integer, got: {pr_number}", "de": "PR number must be an integer, got: {pr_number}",
"en": "PR number must be an integer, got: {pr_number}", "en": "PR number must be an integer, got: {pr_number}",
"pl": "Numer PR musi być liczbą całkowitą, otrzymano: {pr_number}",
"ru": "PR number must be an integer, got: {pr_number}", "ru": "PR number must be an integer, got: {pr_number}",
"zh": "PR number must be an integer, got: {pr_number}" "zh": "PR number must be an integer, got: {pr_number}"
}, },
@@ -836,6 +955,7 @@
"bg": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}", "bg": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
"de": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}", "de": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
"en": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}", "en": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
"pl": "Tytuł PR nie pasuje do tytułu zadania Vikunja.\n Oczekiwano: {expected}\n Otrzymano: {pr_title}",
"ru": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}", "ru": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
"zh": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}" "zh": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}"
}, },
@@ -843,13 +963,23 @@
"bg": "PYPI_TOKEN не е зададен и няма конфигуриран URL на registry — пропускаме публикуването в PyPI. Без притеснения, просто ще създадем Gitea release.", "bg": "PYPI_TOKEN не е зададен и няма конфигуриран URL на registry — пропускаме публикуването в PyPI. Без притеснения, просто ще създадем Gitea release.",
"de": "PYPI_TOKEN nicht gesetzt und keine Registry-URL konfiguriert — PyPI-Veröffentlichung wird übersprungen. Keine Sorge, wir erstellen einfach das Gitea-Release.", "de": "PYPI_TOKEN nicht gesetzt und keine Registry-URL konfiguriert — PyPI-Veröffentlichung wird übersprungen. Keine Sorge, wir erstellen einfach das Gitea-Release.",
"en": "PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.", "en": "PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.",
"pl": "PYPI_TOKEN nie ustawiony i brak URL rejestru — pomijanie publikacji PyPI. Bez obaw, utworzymy tylko wydanie Gitea.",
"ru": "PYPI_TOKEN не задан и URL registry не настроен — пропускаем публикацию в PyPI. Не беспокойтесь, мы просто создадим Gitea release.", "ru": "PYPI_TOKEN не задан и URL registry не настроен — пропускаем публикацию в PyPI. Не беспокойтесь, мы просто создадим Gitea release.",
"zh": "未设置 PYPI_TOKEN 且未配置 registry URL — 跳过 PyPI 发布。别担心,我们直接创建 Gitea release。" "zh": "未设置 PYPI_TOKEN 且未配置 registry URL — 跳过 PyPI 发布。别担心,我们直接创建 Gitea release。"
}, },
"Parsed owner={owner}, repo={repo} from DEVX_REPO_NAME": {
"bg": "Разбор на owner={owner}, repo={repo} от DEVX_REPO_NAME",
"de": "Owner={owner}, repo={repo} aus DEVX_REPO_NAME analysiert",
"en": "Parsed owner={owner}, repo={repo} from DEVX_REPO_NAME",
"pl": "Przeanalizowano owner={owner}, repo={repo} z DEVX_REPO_NAME",
"ru": "Извлечён owner={owner}, repo={repo} из DEVX_REPO_NAME",
"zh": "从 DEVX_REPO_NAME 解析 owner={owner}, repo={repo}"
},
"Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.": { "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.": {
"bg": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.", "bg": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
"de": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.", "de": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
"en": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.", "en": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
"pl": "Kontrola szybkości pojedynczego testu NIEUDANA: {count} test(ów) przekracza limit {limit}s.",
"ru": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.", "ru": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
"zh": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit." "zh": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit."
}, },
@@ -857,6 +987,7 @@
"bg": "Публикувано в Gitea PyPI registry.", "bg": "Публикувано в Gitea PyPI registry.",
"de": "In der Gitea PyPI-Registry veröffentlicht.", "de": "In der Gitea PyPI-Registry veröffentlicht.",
"en": "Published to Gitea PyPI registry.", "en": "Published to Gitea PyPI registry.",
"pl": "Opublikowano w rejestrze Gitea PyPI.",
"ru": "Опубликовано в Gitea PyPI registry.", "ru": "Опубликовано в Gitea PyPI registry.",
"zh": "已发布到 Gitea PyPI registry。" "zh": "已发布到 Gitea PyPI registry。"
}, },
@@ -864,6 +995,7 @@
"bg": "Публикувано в PyPI.", "bg": "Публикувано в PyPI.",
"de": "In PyPI veröffentlicht.", "de": "In PyPI veröffentlicht.",
"en": "Published to PyPI.", "en": "Published to PyPI.",
"pl": "Opublikowano w PyPI.",
"ru": "Опубликовано в PyPI.", "ru": "Опубликовано в PyPI.",
"zh": "已发布到 PyPI。" "zh": "已发布到 PyPI。"
}, },
@@ -871,6 +1003,7 @@
"bg": "Pushed release commit to master.", "bg": "Pushed release commit to master.",
"de": "Pushed release commit to master.", "de": "Pushed release commit to master.",
"en": "Pushed release commit to master.", "en": "Pushed release commit to master.",
"pl": "Wypchnięto commit wydania do master.",
"ru": "Pushed release commit to master.", "ru": "Pushed release commit to master.",
"zh": "Pushed release commit to master." "zh": "Pushed release commit to master."
}, },
@@ -878,6 +1011,7 @@
"bg": "Rebased and pushed. Retrying merge...", "bg": "Rebased and pushed. Retrying merge...",
"de": "Rebased and pushed. Retrying merge...", "de": "Rebased and pushed. Retrying merge...",
"en": "Rebased and pushed. Retrying merge...", "en": "Rebased and pushed. Retrying merge...",
"pl": "Rebase i wypchnięto. Ponowna próba scalenia...",
"ru": "Rebased and pushed. Retrying merge...", "ru": "Rebased and pushed. Retrying merge...",
"zh": "Rebased and pushed. Retrying merge..." "zh": "Rebased and pushed. Retrying merge..."
}, },
@@ -885,6 +1019,7 @@
"bg": "Release creation failed: {error}", "bg": "Release creation failed: {error}",
"de": "Release creation failed: {error}", "de": "Release creation failed: {error}",
"en": "Release creation failed: {error}", "en": "Release creation failed: {error}",
"pl": "Tworzenie wydania nie powiodło się: {error}",
"ru": "Release creation failed: {error}", "ru": "Release creation failed: {error}",
"zh": "Release creation failed: {error}" "zh": "Release creation failed: {error}"
}, },
@@ -892,6 +1027,7 @@
"bg": "Release must be run on master, currently on '{branch}'.", "bg": "Release must be run on master, currently on '{branch}'.",
"de": "Release must be run on master, currently on '{branch}'.", "de": "Release must be run on master, currently on '{branch}'.",
"en": "Release must be run on master, currently on '{branch}'.", "en": "Release must be run on master, currently on '{branch}'.",
"pl": "Wydanie musi być uruchomione na master, obecnie na '{branch}'.",
"ru": "Release must be run on master, currently on '{branch}'.", "ru": "Release must be run on master, currently on '{branch}'.",
"zh": "Release must be run on master, currently on '{branch}'." "zh": "Release must be run on master, currently on '{branch}'."
}, },
@@ -899,6 +1035,7 @@
"bg": "Repo must be in 'owner/name' format, got: {repo}", "bg": "Repo must be in 'owner/name' format, got: {repo}",
"de": "Repo must be in 'owner/name' format, got: {repo}", "de": "Repo must be in 'owner/name' format, got: {repo}",
"en": "Repo must be in 'owner/name' format, got: {repo}", "en": "Repo must be in 'owner/name' format, got: {repo}",
"pl": "Repo musi być w formacie 'owner/name', otrzymano: {repo}",
"ru": "Repo must be in 'owner/name' format, got: {repo}", "ru": "Repo must be in 'owner/name' format, got: {repo}",
"zh": "Repo must be in 'owner/name' format, got: {repo}" "zh": "Repo must be in 'owner/name' format, got: {repo}"
}, },
@@ -906,6 +1043,7 @@
"bg": "Конфигурирането на хранилището е завършено.", "bg": "Конфигурирането на хранилището е завършено.",
"de": "Repository-Konfiguration abgeschlossen.", "de": "Repository-Konfiguration abgeschlossen.",
"en": "Repository configuration complete.", "en": "Repository configuration complete.",
"pl": "Konfiguracja repozytorium zakończona.",
"ru": "Конфигурация репозитория завершена.", "ru": "Конфигурация репозитория завершена.",
"zh": "仓库配置完成。" "zh": "仓库配置完成。"
}, },
@@ -913,6 +1051,7 @@
"bg": "Roles directory not found: {path}", "bg": "Roles directory not found: {path}",
"de": "Roles directory not found: {path}", "de": "Roles directory not found: {path}",
"en": "Roles directory not found: {path}", "en": "Roles directory not found: {path}",
"pl": "Katalog ról nie znaleziony: {path}",
"ru": "Roles directory not found: {path}", "ru": "Roles directory not found: {path}",
"zh": "Roles directory not found: {path}" "zh": "Roles directory not found: {path}"
}, },
@@ -920,6 +1059,7 @@
"bg": "Индексът на runner {index} е извън диапазона (0..{max})", "bg": "Индексът на runner {index} е извън диапазона (0..{max})",
"de": "Runner-Index {index} außerhalb des Bereichs (0..{max})", "de": "Runner-Index {index} außerhalb des Bereichs (0..{max})",
"en": "Runner index {index} out of range (0..{max})", "en": "Runner index {index} out of range (0..{max})",
"pl": "Indeks runnera {index} poza zakresem (0..{max})",
"ru": "Индекс runner {index} вне диапазона (0..{max})", "ru": "Индекс runner {index} вне диапазона (0..{max})",
"zh": "Runner 索引 {index} 超出范围 (0..{max})" "zh": "Runner 索引 {index} 超出范围 (0..{max})"
}, },
@@ -927,6 +1067,7 @@
"bg": "Running lint checks...", "bg": "Running lint checks...",
"de": "Running lint checks...", "de": "Running lint checks...",
"en": "Running lint checks...", "en": "Running lint checks...",
"pl": "Uruchamianie kontroli lint...",
"ru": "Running lint checks...", "ru": "Running lint checks...",
"zh": "Running lint checks..." "zh": "Running lint checks..."
}, },
@@ -934,6 +1075,7 @@
"bg": "Running tests...", "bg": "Running tests...",
"de": "Running tests...", "de": "Running tests...",
"en": "Running tests...", "en": "Running tests...",
"pl": "Uruchamianie testów...",
"ru": "Running tests...", "ru": "Running tests...",
"zh": "Running tests..." "zh": "Running tests..."
}, },
@@ -941,6 +1083,7 @@
"bg": "Running: {scenario} on {platform}", "bg": "Running: {scenario} on {platform}",
"de": "Running: {scenario} on {platform}", "de": "Running: {scenario} on {platform}",
"en": "Running: {scenario} on {platform}", "en": "Running: {scenario} on {platform}",
"pl": "Uruchamianie: {scenario} na {platform}",
"ru": "Running: {scenario} on {platform}", "ru": "Running: {scenario} on {platform}",
"zh": "Running: {scenario} on {platform}" "zh": "Running: {scenario} on {platform}"
}, },
@@ -948,6 +1091,7 @@
"bg": "Skipping commit push — no staged changes.", "bg": "Skipping commit push — no staged changes.",
"de": "Skipping commit push — no staged changes.", "de": "Skipping commit push — no staged changes.",
"en": "Skipping commit push — no staged changes.", "en": "Skipping commit push — no staged changes.",
"pl": "Pomijanie wypchnięcia commit — brak zmian w staging.",
"ru": "Skipping commit push — no staged changes.", "ru": "Skipping commit push — no staged changes.",
"zh": "Skipping commit push — no staged changes." "zh": "Skipping commit push — no staged changes."
}, },
@@ -955,6 +1099,7 @@
"bg": "Syncing {count} documentation pages to wiki...", "bg": "Syncing {count} documentation pages to wiki...",
"de": "Syncing {count} documentation pages to wiki...", "de": "Syncing {count} documentation pages to wiki...",
"en": "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...", "ru": "Syncing {count} documentation pages to wiki...",
"zh": "Syncing {count} documentation pages to wiki..." "zh": "Syncing {count} documentation pages to wiki..."
}, },
@@ -962,6 +1107,7 @@
"bg": "Tag consistency check failed.", "bg": "Tag consistency check failed.",
"de": "Tag consistency check failed.", "de": "Tag consistency check failed.",
"en": "Tag consistency check failed.", "en": "Tag consistency check failed.",
"pl": "Kontrola zgodności tagów nie powiodła się.",
"ru": "Tag consistency check failed.", "ru": "Tag consistency check failed.",
"zh": "Tag consistency check failed." "zh": "Tag consistency check failed."
}, },
@@ -969,6 +1115,7 @@
"bg": "Tag v{version} already existed. Publish workflow should already have been triggered.", "bg": "Tag v{version} already existed. Publish workflow should already have been triggered.",
"de": "Tag v{version} already existed. Publish workflow should already have been triggered.", "de": "Tag v{version} already existed. Publish workflow should already have been triggered.",
"en": "Tag v{version} already existed. Publish workflow should already have been triggered.", "en": "Tag v{version} already existed. Publish workflow should already have been triggered.",
"pl": "Tag v{version} już istniał. Workflow publikacji powinien już być uruchomiony.",
"ru": "Tag v{version} already existed. Publish workflow should already have been triggered.", "ru": "Tag v{version} already existed. Publish workflow should already have been triggered.",
"zh": "Tag v{version} already existed. Publish workflow should already have been triggered." "zh": "Tag v{version} already existed. Publish workflow should already have been triggered."
}, },
@@ -976,6 +1123,7 @@
"bg": "Tag {tag} already exists and points to HEAD. Skipping creation.", "bg": "Tag {tag} already exists and points to HEAD. Skipping creation.",
"de": "Tag {tag} already exists and points to HEAD. Skipping creation.", "de": "Tag {tag} already exists and points to HEAD. Skipping creation.",
"en": "Tag {tag} already exists and points to HEAD. Skipping creation.", "en": "Tag {tag} already exists and points to HEAD. Skipping creation.",
"pl": "Tag {tag} już istnieje i wskazuje na HEAD. Pomijanie tworzenia.",
"ru": "Tag {tag} already exists and points to HEAD. Skipping creation.", "ru": "Tag {tag} already exists and points to HEAD. Skipping creation.",
"zh": "Tag {tag} already exists and points to HEAD. Skipping creation." "zh": "Tag {tag} already exists and points to HEAD. Skipping creation."
}, },
@@ -983,6 +1131,7 @@
"bg": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.", "bg": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.",
"de": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.", "de": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.",
"en": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.", "en": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.",
"pl": "Tag {tag} już istnieje ale wskazuje na {tag_commit} (oczekiwano HEAD {head_commit}). Wskazuje to na niezgodność tag/commit. Uruchom 'python3 -m devx.ci.release --verify', aby uzyskać szczegóły.",
"ru": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.", "ru": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details.",
"zh": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details." "zh": "Tag {tag} already exists but points to {tag_commit} (expected HEAD {head_commit}). This indicates a tag/commit misalignment. Run 'python3 -m devx.ci.release --verify' for details."
}, },
@@ -990,20 +1139,15 @@
"bg": "Task ID: {task_id}", "bg": "Task ID: {task_id}",
"de": "Task ID: {task_id}", "de": "Task ID: {task_id}",
"en": "Task ID: {task_id}", "en": "Task ID: {task_id}",
"pl": "ID zadania: {task_id}",
"ru": "Task ID: {task_id}", "ru": "Task ID: {task_id}",
"zh": "Task ID: {task_id}" "zh": "Task ID: {task_id}"
}, },
"Task ID from .taskid file: {task_id} (not found in branch name '{branch}')": {
"bg": "Task ID от .taskid файл: {task_id} (не е намерен в името на клона '{branch}')",
"de": "Task ID aus .taskid-Datei: {task_id} (nicht im Branch-Namen '{branch}' gefunden)",
"en": "Task ID from .taskid file: {task_id} (not found in branch name '{branch}')",
"ru": "Task ID из файла .taskid: {task_id} (не найден в имени ветки '{branch}')",
"zh": "来自 .taskid 文件的 Task ID: {task_id}(在分支名 '{branch}' 中未找到)"
},
"Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.": { "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.": {
"bg": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.", "bg": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.",
"de": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.", "de": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.",
"en": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.", "en": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.",
"pl": "Test '{name}' trwał {elapsed:.2f}s (limit: {limit}s). Optymalizuj: użyj lżejszych fixtures, zmniejsz I/O, lub mockuj zewnętrzne wywołania.",
"ru": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.", "ru": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls.",
"zh": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls." "zh": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls."
}, },
@@ -1011,6 +1155,7 @@
"bg": "Tests failed — refusing to release. Fix test failures first.\n{stderr}", "bg": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
"de": "Tests failed — refusing to release. Fix test failures first.\n{stderr}", "de": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
"en": "Tests failed — refusing to release. Fix test failures first.\n{stderr}", "en": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
"pl": "Testy nie powiodły się — odmowa wydania. Najpierw napraw niepowodzenia testów.\n{stderr}",
"ru": "Tests failed — refusing to release. Fix test failures first.\n{stderr}", "ru": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
"zh": "Tests failed — refusing to release. Fix test failures first.\n{stderr}" "zh": "Tests failed — refusing to release. Fix test failures first.\n{stderr}"
}, },
@@ -1018,6 +1163,7 @@
"bg": "Tests passed.", "bg": "Tests passed.",
"de": "Tests passed.", "de": "Tests passed.",
"en": "Tests passed.", "en": "Tests passed.",
"pl": "Testy zakończone pomyślnie.",
"ru": "Tests passed.", "ru": "Tests passed.",
"zh": "Tests passed." "zh": "Tests passed."
}, },
@@ -1025,6 +1171,7 @@
"bg": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).", "bg": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).",
"de": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).", "de": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).",
"en": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).", "en": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).",
"pl": "Testy jednostkowe zakończone pomyślnie w {duration:.2f}s (poniżej limitu {max}s, wszystkie testy poniżej limitu {single}s na test).",
"ru": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).", "ru": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit).",
"zh": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit)." "zh": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit)."
}, },
@@ -1032,6 +1179,7 @@
"bg": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.", "bg": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.",
"de": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.", "de": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.",
"en": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.", "en": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.",
"pl": "Testy jednostkowe zbyt wolne: {duration:.2f}s (maks. dozwolone: {max}s).\n Naprawa: uruchom 'make pytest-cov' do profilowania, następnie zoptymalizuj wolne testy.\n Wskazówka: unikaj niepotrzebnych importów, użyj lżejszych mocków, lub buforuj fixtures.",
"ru": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.", "ru": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.",
"zh": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures." "zh": "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures."
}, },
@@ -1039,6 +1187,7 @@
"bg": "Unknown check category '{check}'. Available: all, user-facing{tags}", "bg": "Unknown check category '{check}'. Available: all, user-facing{tags}",
"de": "Unknown check category '{check}'. Available: all, user-facing{tags}", "de": "Unknown check category '{check}'. Available: all, user-facing{tags}",
"en": "Unknown check category '{check}'. Available: all, user-facing{tags}", "en": "Unknown check category '{check}'. Available: all, user-facing{tags}",
"pl": "Nieznana kategoria kontroli '{check}'. Dostępne: all, user-facing{tags}",
"ru": "Unknown check category '{check}'. Available: all, user-facing{tags}", "ru": "Unknown check category '{check}'. Available: all, user-facing{tags}",
"zh": "Unknown check category '{check}'. Available: all, user-facing{tags}" "zh": "Unknown check category '{check}'. Available: all, user-facing{tags}"
}, },
@@ -1046,6 +1195,7 @@
"bg": "Updated version in {init}", "bg": "Updated version in {init}",
"de": "Updated version in {init}", "de": "Updated version in {init}",
"en": "Updated version in {init}", "en": "Updated version in {init}",
"pl": "Zaktualizowano wersję w {init}",
"ru": "Updated version in {init}", "ru": "Updated version in {init}",
"zh": "Updated version in {init}" "zh": "Updated version in {init}"
}, },
@@ -1053,6 +1203,7 @@
"bg": "Updated {changelog_file}", "bg": "Updated {changelog_file}",
"de": "Updated {changelog_file}", "de": "Updated {changelog_file}",
"en": "Updated {changelog_file}", "en": "Updated {changelog_file}",
"pl": "Zaktualizowano {changelog_file}",
"ru": "Updated {changelog_file}", "ru": "Updated {changelog_file}",
"zh": "Updated {changelog_file}" "zh": "Updated {changelog_file}"
}, },
@@ -1060,6 +1211,7 @@
"bg": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.", "bg": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
"de": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.", "de": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
"en": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.", "en": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
"pl": "VIKUNJA_TOKEN nie jest ustawiony. Jest to wymagane w CI do walidacji tytułów PR.",
"ru": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.", "ru": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
"zh": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles." "zh": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles."
}, },
@@ -1067,6 +1219,7 @@
"bg": "Version file: {file}", "bg": "Version file: {file}",
"de": "Version file: {file}", "de": "Version file: {file}",
"en": "Version file: {file}", "en": "Version file: {file}",
"pl": "Plik wersji: {file}",
"ru": "Version file: {file}", "ru": "Version file: {file}",
"zh": "Version file: {file}" "zh": "Version file: {file}"
}, },
@@ -1074,6 +1227,7 @@
"bg": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.", "bg": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.",
"de": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.", "de": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.",
"en": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.", "en": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.",
"pl": "Błąd API Vikunja (HTTP {status}): {message}. Zadanie {task_id} NIE zostało zaktualizowane. Scalenie powiodło się ale zadanie Vikunja wymaga ręcznej aktualizacji.",
"ru": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.", "ru": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.",
"zh": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update." "zh": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update."
}, },
@@ -1081,20 +1235,23 @@
"bg": "WARNING: --skip-tests passed — skipping test verification.", "bg": "WARNING: --skip-tests passed — skipping test verification.",
"de": "WARNING: --skip-tests passed — skipping test verification.", "de": "WARNING: --skip-tests passed — skipping test verification.",
"en": "WARNING: --skip-tests passed — skipping test verification.", "en": "WARNING: --skip-tests passed — skipping test verification.",
"pl": "OSTRZEŻENIE: --skip-tests przekazane — pomijanie weryfikacji testów.",
"ru": "WARNING: --skip-tests passed — skipping test verification.", "ru": "WARNING: --skip-tests passed — skipping test verification.",
"zh": "WARNING: --skip-tests passed — skipping test verification." "zh": "WARNING: --skip-tests passed — skipping test verification."
}, },
"WARNING: .taskid file ({file_id}) disagrees with branch name ({branch_id}). Using branch task ID. Update or delete .taskid to silence this warning.": { "WARNING: .taskid file ({file_id}) is deprecated and disagrees with branch name ({branch_id}). Delete .taskid from the repo — branch name is the sole source of truth.": {
"bg": "ВНИМАНИЕ: .taskid файл ({file_id}) не съвпада с името на клона ({branch_id}). Използва се task ID от клона. Актуализирайте или изтрийте .taskid за да премахнете това предупреждение.", "bg": "ВНИМАНИЕ: Файлът .taskid ({file_id}) е остарял и не съвпада с името на клона ({branch_id}). Изтрийте .taskid от хранилището — името на клона е единственият източник на истината.",
"de": "WARNUNG: .taskid-Datei ({file_id}) stimmt nicht mit Branch-Namen ({branch_id}) überein. Branch-Task-ID wird verwendet. Aktualisieren oder löschen Sie .taskid, um diese Warnung zu unterdrücken.", "de": "WARNUNG: Die Datei .taskid ({file_id}) ist veraltet und stimmt nicht mit dem Branch-Namen ({branch_id}) überein. Löschen Sie .taskid aus dem Repo — der Branch-Name ist die einzige Wahrheitsquelle.",
"en": "WARNING: .taskid file ({file_id}) disagrees with branch name ({branch_id}). Using branch task ID. Update or delete .taskid to silence this warning.", "en": "WARNING: .taskid file ({file_id}) is deprecated and disagrees with branch name ({branch_id}). Delete .taskid from the repo — branch name is the sole source of truth.",
"ru": "ВНИМАНИЕ: файл .taskid ({file_id}) не совпадает с именем ветки ({branch_id}). Используется Task ID из ветки. Обновите или удалите .taskid, чтобы скрыть это предупреждение.", "pl": "OSTRZEŻENIE: plik .taskid ({file_id}) jest przestarzały i niezgodny z nazwą gałęzi ({branch_id}). Usuń .taskid z repozytorium — nazwa gałęzi jest jedynym źródłem prawdy.",
"zh": "警告:.taskid 文件 ({file_id}) 与分支名 ({branch_id}) 不一致。使用分支 Task ID。更新或删除 .taskid 以消除此警告。" "ru": "ВНИМАНИЕ: Файл .taskid ({file_id}) устарел и не совпадает с именем ветки ({branch_id}). Удалите .taskid из репозитория — имя ветки — единственный источник истины.",
"zh": "警告:.taskid 文件 ({file_id}) 已弃用,与分支名称 ({branch_id}) 不一致。请从仓库中删除 .taskid — 分支名称是唯一的真实来源。"
}, },
"Warning: could not fetch tags from origin.": { "Warning: could not fetch tags from origin.": {
"bg": "Warning: could not fetch tags from origin.", "bg": "Warning: could not fetch tags from origin.",
"de": "Warning: could not fetch tags from origin.", "de": "Warning: could not fetch tags from origin.",
"en": "Warning: could not fetch tags from origin.", "en": "Warning: could not fetch tags from origin.",
"pl": "Ostrzeżenie: nie udało się pobrać tagów z origin.",
"ru": "Warning: could not fetch tags from origin.", "ru": "Warning: could not fetch tags from origin.",
"zh": "Warning: could not fetch tags from origin." "zh": "Warning: could not fetch tags from origin."
}, },
@@ -1102,6 +1259,7 @@
"bg": "Wiki integrity check failed — {count} issue(s)", "bg": "Wiki integrity check failed — {count} issue(s)",
"de": "Wiki integrity check failed — {count} issue(s)", "de": "Wiki integrity check failed — {count} issue(s)",
"en": "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)", "ru": "Wiki integrity check failed — {count} issue(s)",
"zh": "Wiki integrity check failed — {count} issue(s)" "zh": "Wiki integrity check failed — {count} issue(s)"
}, },
@@ -1109,6 +1267,7 @@
"bg": "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", "de": "Wiki verification failed — {failures} page(s) empty or mismatched",
"en": "Wiki verification failed — {failures} page(s) empty or mismatched", "en": "Wiki verification failed — {failures} page(s) empty or mismatched",
"pl": "Weryfikacja wiki nie powiodła się — {failures} strona(y) pusta lub niezgodna",
"ru": "Wiki verification failed — {failures} page(s) empty or mismatched", "ru": "Wiki verification failed — {failures} page(s) empty or mismatched",
"zh": "Wiki verification failed — {failures} page(s) empty or mismatched" "zh": "Wiki verification failed — {failures} page(s) empty or mismatched"
}, },
@@ -1116,6 +1275,7 @@
"bg": "[dry-run] Would commit: release: v{version}", "bg": "[dry-run] Would commit: release: v{version}",
"de": "[dry-run] Would commit: release: v{version}", "de": "[dry-run] Would commit: release: v{version}",
"en": "[dry-run] Would commit: release: v{version}", "en": "[dry-run] Would commit: release: v{version}",
"pl": "[dry-run] Utworzono by commit: release: v{version}",
"ru": "[dry-run] Would commit: release: v{version}", "ru": "[dry-run] Would commit: release: v{version}",
"zh": "[dry-run] Would commit: release: v{version}" "zh": "[dry-run] Would commit: release: v{version}"
}, },
@@ -1123,6 +1283,7 @@
"bg": "[dry-run] Would create tag: v{version}", "bg": "[dry-run] Would create tag: v{version}",
"de": "[dry-run] Would create tag: v{version}", "de": "[dry-run] Would create tag: v{version}",
"en": "[dry-run] Would create tag: v{version}", "en": "[dry-run] Would create tag: v{version}",
"pl": "[dry-run] Utworzono by tag: v{version}",
"ru": "[dry-run] Would create tag: v{version}", "ru": "[dry-run] Would create tag: v{version}",
"zh": "[dry-run] Would create tag: v{version}" "zh": "[dry-run] Would create tag: v{version}"
}, },
@@ -1130,6 +1291,7 @@
"bg": "[dry-run] Would create tag: {tag}", "bg": "[dry-run] Would create tag: {tag}",
"de": "[dry-run] Would create tag: {tag}", "de": "[dry-run] Would create tag: {tag}",
"en": "[dry-run] Would create tag: {tag}", "en": "[dry-run] Would create tag: {tag}",
"pl": "[dry-run] Utworzono by tag: {tag}",
"ru": "[dry-run] Would create tag: {tag}", "ru": "[dry-run] Would create tag: {tag}",
"zh": "[dry-run] Would create tag: {tag}" "zh": "[dry-run] Would create tag: {tag}"
}, },
@@ -1137,6 +1299,7 @@
"bg": "[dry-run] Would push commit to master", "bg": "[dry-run] Would push commit to master",
"de": "[dry-run] Would push commit to master", "de": "[dry-run] Would push commit to master",
"en": "[dry-run] Would push commit to master", "en": "[dry-run] Would push commit to master",
"pl": "[dry-run] Wypchnięto by commit do master",
"ru": "[dry-run] Would push commit to master", "ru": "[dry-run] Would push commit to master",
"zh": "[dry-run] Would push commit to master" "zh": "[dry-run] Would push commit to master"
}, },
@@ -1144,6 +1307,7 @@
"bg": "[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)", "de": "[dry-run] Would sync page: {title} ({chars} chars)",
"en": "[dry-run] Would sync page: {title} ({chars} chars)", "en": "[dry-run] Would sync page: {title} ({chars} chars)",
"pl": "[dry-run] Zsynchronizowano by stronę: {title} ({chars} znaków)",
"ru": "[dry-run] Would sync page: {title} ({chars} chars)", "ru": "[dry-run] Would sync page: {title} ({chars} chars)",
"zh": "[dry-run] Would sync page: {title} ({chars} chars)" "zh": "[dry-run] Would sync page: {title} ({chars} chars)"
}, },
@@ -1151,6 +1315,7 @@
"bg": "[dry-run] Would update {changelog_file}", "bg": "[dry-run] Would update {changelog_file}",
"de": "[dry-run] Would update {changelog_file}", "de": "[dry-run] Would update {changelog_file}",
"en": "[dry-run] Would update {changelog_file}", "en": "[dry-run] Would update {changelog_file}",
"pl": "[dry-run] Zaktualizowano by {changelog_file}",
"ru": "[dry-run] Would update {changelog_file}", "ru": "[dry-run] Would update {changelog_file}",
"zh": "[dry-run] Would update {changelog_file}" "zh": "[dry-run] Would update {changelog_file}"
}, },
@@ -1158,6 +1323,7 @@
"bg": "[dry-run] Would update {init}", "bg": "[dry-run] Would update {init}",
"de": "[dry-run] Would update {init}", "de": "[dry-run] Would update {init}",
"en": "[dry-run] Would update {init}", "en": "[dry-run] Would update {init}",
"pl": "[dry-run] Zaktualizowano by {init}",
"ru": "[dry-run] Would update {init}", "ru": "[dry-run] Would update {init}",
"zh": "[dry-run] Would update {init}" "zh": "[dry-run] Would update {init}"
}, },
@@ -1165,6 +1331,7 @@
"bg": "активен", "bg": "активен",
"de": "aktiv", "de": "aktiv",
"en": "active", "en": "active",
"pl": "aktywny",
"ru": "активен", "ru": "активен",
"zh": "活跃" "zh": "活跃"
}, },
@@ -1172,6 +1339,7 @@
"bg": "завършен", "bg": "завършен",
"de": "abgeschlossen", "de": "abgeschlossen",
"en": "completed", "en": "completed",
"pl": "ukończony",
"ru": "завершён", "ru": "завершён",
"zh": "已完成" "zh": "已完成"
}, },
@@ -1179,6 +1347,7 @@
"bg": "неуспешен", "bg": "неуспешен",
"de": "fehlgeschlagen", "de": "fehlgeschlagen",
"en": "failed", "en": "failed",
"pl": "nieudany",
"ru": "неудачный", "ru": "неудачный",
"zh": "失败" "zh": "失败"
}, },
@@ -1186,6 +1355,7 @@
"bg": "git command failed ({cmd}): {stderr}", "bg": "git command failed ({cmd}): {stderr}",
"de": "git command failed ({cmd}): {stderr}", "de": "git command failed ({cmd}): {stderr}",
"en": "git command failed ({cmd}): {stderr}", "en": "git command failed ({cmd}): {stderr}",
"pl": "polecenie git nie powiodło się ({cmd}): {stderr}",
"ru": "git command failed ({cmd}): {stderr}", "ru": "git command failed ({cmd}): {stderr}",
"zh": "git command failed ({cmd}): {stderr}" "zh": "git command failed ({cmd}): {stderr}"
}, },
@@ -1193,6 +1363,7 @@
"bg": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.", "bg": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
"de": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.", "de": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
"en": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.", "en": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
"pl": "git-cliff wygenerował pusty changelog dla v{version}. Sprawdź cliff.toml i historię commitów.",
"ru": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.", "ru": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history.",
"zh": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history." "zh": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history."
}, },
@@ -1200,6 +1371,7 @@
"bg": "git-cliff returned empty version.", "bg": "git-cliff returned empty version.",
"de": "git-cliff returned empty version.", "de": "git-cliff returned empty version.",
"en": "git-cliff returned empty version.", "en": "git-cliff returned empty version.",
"pl": "git-cliff zwrócił pustą wersję.",
"ru": "git-cliff returned empty version.", "ru": "git-cliff returned empty version.",
"zh": "git-cliff returned empty version." "zh": "git-cliff returned empty version."
}, },
@@ -1207,6 +1379,7 @@
"bg": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).", "bg": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
"de": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).", "de": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
"en": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).", "en": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
"pl": "git-cliff zwrócił nieprawidłowy format wersji: {version}. Oczekiwano semver (np., 0.4.1).",
"ru": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).", "ru": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1).",
"zh": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1)." "zh": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1)."
}, },
@@ -1214,6 +1387,7 @@
"bg": "в процес", "bg": "в процес",
"de": "in Bearbeitung", "de": "in Bearbeitung",
"en": "in progress", "en": "in progress",
"pl": "w toku",
"ru": "в процессе", "ru": "в процессе",
"zh": "进行中" "zh": "进行中"
}, },
@@ -1221,6 +1395,7 @@
"bg": "неактивен", "bg": "неактивен",
"de": "inaktiv", "de": "inaktiv",
"en": "inactive", "en": "inactive",
"pl": "nieaktywny",
"ru": "неактивен", "ru": "неактивен",
"zh": "未激活" "zh": "未激活"
}, },
@@ -1228,6 +1403,7 @@
"bg": "mapping.json keys and values must be strings, got {k}={v}", "bg": "mapping.json keys and values must be strings, got {k}={v}",
"de": "mapping.json keys and values must be strings, got {k}={v}", "de": "mapping.json keys and values must be strings, got {k}={v}",
"en": "mapping.json keys and values must be strings, got {k}={v}", "en": "mapping.json keys and values must be strings, got {k}={v}",
"pl": "klucze i wartości mapping.json muszą być ciągami znaków, otrzymano {k}={v}",
"ru": "mapping.json keys and values must be strings, got {k}={v}", "ru": "mapping.json keys and values must be strings, got {k}={v}",
"zh": "mapping.json keys and values must be strings, got {k}={v}" "zh": "mapping.json keys and values must be strings, got {k}={v}"
}, },
@@ -1235,6 +1411,7 @@
"bg": "mapping.json must be a dict of file-path -> page-title, got {type}", "bg": "mapping.json must be a dict of file-path -> page-title, got {type}",
"de": "mapping.json must be a dict of file-path -> page-title, got {type}", "de": "mapping.json must be a dict of file-path -> page-title, got {type}",
"en": "mapping.json must be a dict of file-path -> page-title, got {type}", "en": "mapping.json must be a dict of file-path -> page-title, got {type}",
"pl": "mapping.json musi być słownikiem ścieżka-pliku -> tytuł-strony, otrzymano {type}",
"ru": "mapping.json must be a dict of file-path -> page-title, got {type}", "ru": "mapping.json must be a dict of file-path -> page-title, got {type}",
"zh": "mapping.json must be a dict of file-path -> page-title, got {type}" "zh": "mapping.json must be a dict of file-path -> page-title, got {type}"
}, },
@@ -1242,6 +1419,7 @@
"bg": "в очакване", "bg": "в очакване",
"de": "ausstehend", "de": "ausstehend",
"en": "pending", "en": "pending",
"pl": "oczekujący",
"ru": "ожидает", "ru": "ожидает",
"zh": "待处理" "zh": "待处理"
}, },
@@ -1249,6 +1427,7 @@
"bg": "неизвестен", "bg": "неизвестен",
"de": "unbekannt", "de": "unbekannt",
"en": "unknown", "en": "unknown",
"pl": "nieznany",
"ru": "неизвестно", "ru": "неизвестно",
"zh": "未知" "zh": "未知"
}, },
@@ -1256,6 +1435,7 @@
"bg": "{file} already exists. Use --force to overwrite.", "bg": "{file} already exists. Use --force to overwrite.",
"de": "{file} already exists. Use --force to overwrite.", "de": "{file} already exists. Use --force to overwrite.",
"en": "{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.", "ru": "{file} already exists. Use --force to overwrite.",
"zh": "{file} already exists. Use --force to overwrite." "zh": "{file} already exists. Use --force to overwrite."
} }
+15 -32
View File
@@ -21,40 +21,31 @@ from devx.exceptions import APIError
class TestReadTaskid: class TestReadTaskid:
def test_prefers_branch_name_over_file(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def] def test_extracts_from_branch_name(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("DEVX-60\n")
# Branch name takes priority over .taskid file
assert read_taskid("DEVX-19-fix-bug") == "DEVX-19"
def test_falls_back_to_file_when_no_branch_match(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("DEVX-60\n")
# No task ID in branch name → fall back to .taskid
assert read_taskid("some-branch") == "DEVX-60"
def test_falls_back_to_branch_name(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
assert read_taskid("DEVX-19-fix-bug") == "DEVX-19" assert read_taskid("DEVX-19-fix-bug") == "DEVX-19"
def test_returns_empty_when_no_file_no_match(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def] def test_returns_empty_when_no_match(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
assert read_taskid("feature-branch") == "" assert read_taskid("feature-branch") == ""
def test_empty_file_falls_back_to_branch(self, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("\n")
assert read_taskid("DEVX-42-test") == "DEVX-42"
def test_warns_on_stale_taskid_file(self, tmp_path, monkeypatch, capsys) -> None: # type: ignore[no-untyped-def] def test_warns_on_stale_taskid_file(self, tmp_path, monkeypatch, capsys) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("DEVX-60\n") (tmp_path / ".taskid").write_text("DEVX-60\n")
# Branch name takes priority, but stale .taskid should produce a warning # Branch name takes priority, stale .taskid should produce deprecation warning
assert read_taskid("DEVX-19-fix-bug") == "DEVX-19" assert read_taskid("DEVX-19-fix-bug") == "DEVX-19"
captured = capsys.readouterr() captured = capsys.readouterr()
assert "WARNING" in captured.out combined = captured.out + captured.err
assert "DEVX-60" in captured.out assert "WARNING" in combined
assert "DEVX-19" in captured.out assert "deprecated" in combined
assert "DEVX-60" in combined
assert "DEVX-19" in combined
def test_no_warning_when_taskid_file_absent(self, tmp_path, monkeypatch, capsys) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path)
assert read_taskid("DEVX-42-test") == "DEVX-42"
captured = capsys.readouterr()
assert "WARNING" not in captured.out
# -- extract_task_id (legacy fallback) -- # -- extract_task_id (legacy fallback) --
@@ -217,7 +208,6 @@ class TestMain:
self, mock_client_cls: MagicMock, _mock_validate: MagicMock, tmp_path, monkeypatch self, mock_client_cls: MagicMock, _mock_validate: MagicMock, tmp_path, monkeypatch
) -> None: # type: ignore[no-untyped-def] ) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("DEVX-19\n")
mock_client = MagicMock() mock_client = MagicMock()
mock_client.get_pr_commits.return_value = [ mock_client.get_pr_commits.return_value = [
@@ -244,7 +234,7 @@ class TestMain:
@patch("devx.ci.auto_merge.GiteaClient") @patch("devx.ci.auto_merge.GiteaClient")
def test_no_task_id_raises(self, mock_client_cls: MagicMock, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def] def test_no_task_id_raises(self, mock_client_cls: MagicMock, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
# No .taskid file, no DEVX-N in branch name # No DEVX-N in branch name
runner = CliRunner() runner = CliRunner()
result = runner.invoke(main, ["feature-branch", "DEVX-19: test", "owner/repo", "7"]) result = runner.invoke(main, ["feature-branch", "DEVX-19: test", "owner/repo", "7"])
assert result.exit_code != 0 assert result.exit_code != 0
@@ -254,7 +244,6 @@ class TestMain:
@patch("devx.ci.auto_merge.GiteaClient") @patch("devx.ci.auto_merge.GiteaClient")
def test_invalid_pr_title_raises(self, mock_client_cls: MagicMock, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def] def test_invalid_pr_title_raises(self, mock_client_cls: MagicMock, tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("DEVX-19\n")
runner = CliRunner() runner = CliRunner()
result = runner.invoke(main, ["DEVX-19-fix", "Bad title", "owner/repo", "7"]) result = runner.invoke(main, ["DEVX-19-fix", "Bad title", "owner/repo", "7"])
@@ -268,7 +257,6 @@ class TestMain:
self, mock_client_cls: MagicMock, _mock_validate: MagicMock, tmp_path, monkeypatch self, mock_client_cls: MagicMock, _mock_validate: MagicMock, tmp_path, monkeypatch
) -> None: # type: ignore[no-untyped-def] ) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("DEVX-19\n")
mock_client = MagicMock() mock_client = MagicMock()
mock_client.get_pr_commits.return_value = [ mock_client.get_pr_commits.return_value = [
@@ -298,7 +286,6 @@ class TestMain:
self, mock_client_cls: MagicMock, _mock_validate: MagicMock, tmp_path, monkeypatch self, mock_client_cls: MagicMock, _mock_validate: MagicMock, tmp_path, monkeypatch
) -> None: # type: ignore[no-untyped-def] ) -> None: # type: ignore[no-untyped-def]
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("DEVX-19\n")
mock_client = MagicMock() mock_client = MagicMock()
mock_client.get_pr_commits.return_value = [ mock_client.get_pr_commits.return_value = [
@@ -323,7 +310,6 @@ class TestMain:
) -> None: # type: ignore[no-untyped-def] ) -> None: # type: ignore[no-untyped-def]
"""When no conventional commit message is found in PR commits, raises.""" """When no conventional commit message is found in PR commits, raises."""
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("DEVX-19\n")
mock_client = MagicMock() mock_client = MagicMock()
mock_client.get_pr_commits.return_value = [] mock_client.get_pr_commits.return_value = []
@@ -341,7 +327,6 @@ class TestMain:
def test_invalid_pr_number_raises(self, tmp_path, monkeypatch) -> None: def test_invalid_pr_number_raises(self, tmp_path, monkeypatch) -> None:
"""Non-integer PR number should raise.""" """Non-integer PR number should raise."""
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("DEVX-19\n")
runner = CliRunner() runner = CliRunner()
result = runner.invoke(main, ["DEVX-19-fix", "DEVX-19: Test", "owner/repo", "not-a-number"]) result = runner.invoke(main, ["DEVX-19-fix", "DEVX-19: Test", "owner/repo", "not-a-number"])
assert result.exit_code != 0 assert result.exit_code != 0
@@ -351,7 +336,6 @@ class TestMain:
def test_invalid_repo_format_raises(self, tmp_path, monkeypatch) -> None: def test_invalid_repo_format_raises(self, tmp_path, monkeypatch) -> None:
"""Repo without owner/name should raise.""" """Repo without owner/name should raise."""
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("DEVX-19\n")
runner = CliRunner() runner = CliRunner()
result = runner.invoke(main, ["DEVX-19-fix", "DEVX-19: Test", "invalidrepo", "7"]) result = runner.invoke(main, ["DEVX-19-fix", "DEVX-19: Test", "invalidrepo", "7"])
assert result.exit_code != 0 assert result.exit_code != 0
@@ -365,7 +349,6 @@ class TestMain:
) -> None: # type: ignore[no-untyped-def] ) -> None: # type: ignore[no-untyped-def]
"""When rebase retry also fails, raises with helpful message.""" """When rebase retry also fails, raises with helpful message."""
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
(tmp_path / ".taskid").write_text("DEVX-19\n")
mock_client = MagicMock() mock_client = MagicMock()
mock_client.get_pr_commits.return_value = [ mock_client.get_pr_commits.return_value = [
+29 -11
View File
@@ -41,9 +41,12 @@ class TestCheckTranslationSet:
src_dir.mkdir() src_dir.mkdir()
(src_dir / "mod.py").write_text('_("Hello")\n') (src_dir / "mod.py").write_text('_("Hello")\n')
trans_file = tmp_path / "translations.json" trans_file = tmp_path / "translations.json"
trans_file.write_text( all_langs = {"en": "Hello", "bg": "Здравей", "de": "Hallo", "ru": "Привет", "zh": "你好", "pl": "Cześć"}
json.dumps({"Hello": {"en": "Hello", "bg": "Здравей", "de": "Hallo", "ru": "Привет", "zh": "你好"}}) # Include dynamic keys since collect_keys now adds them for all dirs
) data = {"Hello": all_langs}
for dk in check_translations.DYNAMIC_KEYS:
data[dk] = all_langs
trans_file.write_text(json.dumps(data))
result = check_translations.check_translation_set("test", src_dir, trans_file) result = check_translations.check_translation_set("test", src_dir, trans_file)
assert not result.errors assert not result.errors
@@ -170,9 +173,11 @@ class TestMain:
def test_translations_flag(self, tmp_path: Path) -> None: def test_translations_flag(self, tmp_path: Path) -> None:
"""--translations flag should check a specific file.""" """--translations flag should check a specific file."""
trans_file = tmp_path / "translations.json" trans_file = tmp_path / "translations.json"
trans_file.write_text( all_langs = {"en": "Hello", "bg": "Здравей", "de": "Hallo", "ru": "Привет", "zh": "你好", "pl": "Cześć"}
json.dumps({"Hello": {"en": "Hello", "bg": "Здравей", "de": "Hallo", "ru": "Привет", "zh": "你好"}}) data = {"Hello": all_langs}
) for dk in check_translations.DYNAMIC_KEYS:
data[dk] = all_langs
trans_file.write_text(json.dumps(data))
(tmp_path / "mod.py").write_text('_("Hello")\n') (tmp_path / "mod.py").write_text('_("Hello")\n')
runner = CliRunner() runner = CliRunner()
@@ -251,6 +256,19 @@ class TestDevxI18n:
monkeypatch.delenv("DEVX_LANG", raising=False) monkeypatch.delenv("DEVX_LANG", raising=False)
importlib.reload(devx.i18n) importlib.reload(devx.i18n)
def test_polish_translation(self, monkeypatch: pytest.MonkeyPatch) -> None:
import importlib
monkeypatch.setenv("DEVX_LANG", "pl")
import devx.i18n
importlib.reload(devx.i18n)
result = devx.i18n._("Running tests...")
assert "Uruchamianie testów" in result
monkeypatch.delenv("DEVX_LANG", raising=False)
importlib.reload(devx.i18n)
def test_unsupported_lang_fallback(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_unsupported_lang_fallback(self, monkeypatch: pytest.MonkeyPatch) -> None:
import importlib import importlib
@@ -290,14 +308,14 @@ class TestCollectKeys:
assert "should_appear" in keys assert "should_appear" in keys
assert "should_not_appear" not in keys assert "should_not_appear" not in keys
def test_non_default_dir_no_dynamic_keys(self, tmp_path: Path) -> None: def test_non_default_dir_includes_dynamic_keys(self, tmp_path: Path) -> None:
"""Non-default source dirs should not include DYNAMIC_KEYS.""" """Non-default source dirs should also include DYNAMIC_KEYS."""
(tmp_path / "mod.py").write_text('_("mykey")\n') (tmp_path / "mod.py").write_text('_("mykey")\n')
keys = check_translations.collect_keys(tmp_path) keys = check_translations.collect_keys(tmp_path)
assert "mykey" in keys assert "mykey" in keys
# Dynamic keys should NOT be present for non-default dirs # Dynamic keys should be present for all dirs
assert "completed" not in keys assert "completed" in keys
assert "pending" not in keys assert "pending" in keys
def test_default_dir_includes_dynamic_keys(self) -> None: def test_default_dir_includes_dynamic_keys(self) -> None:
"""The default source dir should include DYNAMIC_KEYS.""" """The default source dir should include DYNAMIC_KEYS."""
+198 -142
View File
@@ -1,4 +1,4 @@
"""Unit tests for scripts/generate_badges.py.""" """Unit tests for devx/tools/generate_badges.py."""
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@@ -8,7 +8,13 @@ from click.testing import CliRunner
from devx.tools.generate_badges import ( from devx.tools.generate_badges import (
COLOR_HEX, COLOR_HEX,
cli, cli,
collect_coverage_and_tests,
collect_doc_coverage,
collect_quality,
coverage_color, coverage_color,
detect_coverage_target,
detect_package_name,
detect_testpaths,
doc_coverage_color, doc_coverage_color,
extract_coverage, extract_coverage,
extract_doc_coverage, extract_doc_coverage,
@@ -17,10 +23,107 @@ from devx.tools.generate_badges import (
make_badge, make_badge,
read_version, read_version,
render_svg, render_svg,
resolve_repo_root,
run_command, run_command,
) )
class TestResolveRepoRoot:
def test_uses_github_workspace_when_set(self, tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.setenv("GITHUB_WORKSPACE", str(tmp_path))
assert resolve_repo_root() == tmp_path
def test_falls_back_to_cwd_when_no_workspace(self, tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.delenv("GITHUB_WORKSPACE", raising=False)
monkeypatch.chdir(tmp_path)
assert resolve_repo_root() == tmp_path
def test_falls_back_to_cwd_when_workspace_invalid(self, monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.setenv("GITHUB_WORKSPACE", "/nonexistent/path")
result = resolve_repo_root()
assert result == Path.cwd()
class TestDetectPackageName:
def test_detects_package_with_init(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
src = tmp_path / "src"
pkg = src / "mypkg"
pkg.mkdir(parents=True)
(pkg / "__init__.py").write_text('__version__ = "1.0.0"\n')
assert detect_package_name(tmp_path) == "mypkg"
def test_returns_none_when_no_src(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
assert detect_package_name(tmp_path) is None
def test_returns_none_when_no_init(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
src = tmp_path / "src"
pkg = src / "mypkg"
pkg.mkdir(parents=True)
# No __init__.py
assert detect_package_name(tmp_path) is None
def test_picks_first_package_alphabetically(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
src = tmp_path / "src"
for name in ["zpkg", "apkg"]:
d = src / name
d.mkdir(parents=True)
(d / "__init__.py").write_text("")
assert detect_package_name(tmp_path) == "apkg"
def test_skips_non_dir_entries(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
src = tmp_path / "src"
src.mkdir(parents=True)
(src / "README.md").write_text("not a package")
pkg = src / "mypkg"
pkg.mkdir()
(pkg / "__init__.py").write_text("")
assert detect_package_name(tmp_path) == "mypkg"
class TestDetectCoverageTarget:
def test_parses_from_pyproject(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
(tmp_path / "pyproject.toml").write_text(
'[tool.pytest.ini_options]\naddopts = "--cov=src/devx --cov-report=term-missing"\n'
)
assert detect_coverage_target(tmp_path) == "src/devx"
def test_falls_back_to_src_package(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
src = tmp_path / "src"
pkg = src / "mypkg"
pkg.mkdir(parents=True)
(pkg / "__init__.py").write_text('__version__ = "1.0"\n')
assert detect_coverage_target(tmp_path) == "src/mypkg"
def test_returns_none_when_no_package(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
assert detect_coverage_target(tmp_path) is None
class TestDetectTestpaths:
def test_parses_from_pyproject(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
(tmp_path / "scripts" / "tests").mkdir(parents=True)
(tmp_path / "tests" / "unit").mkdir(parents=True)
(tmp_path / "pyproject.toml").write_text(
'[tool.pytest.ini_options]\ntestpaths = ["scripts/tests", "tests/unit"]\n'
)
assert detect_testpaths(tmp_path) == ["scripts/tests", "tests/unit"]
def test_filters_nonexistent_paths(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
(tmp_path / "tests").mkdir()
(tmp_path / "pyproject.toml").write_text('[tool.pytest.ini_options]\ntestpaths = ["tests", "nonexistent"]\n')
assert detect_testpaths(tmp_path) == ["tests"]
def test_falls_back_to_tests_dir(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
(tmp_path / "tests").mkdir()
assert detect_testpaths(tmp_path) == ["tests"]
def test_returns_empty_when_no_tests_dir(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
assert detect_testpaths(tmp_path) == []
def test_returns_empty_when_pyproject_has_no_testpaths(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
(tmp_path / "pyproject.toml").write_text("[tool.pytest.ini_options]\naddopts = '-ra'\n")
assert detect_testpaths(tmp_path) == []
class TestRunCommand: class TestRunCommand:
@patch("devx.tools.generate_badges.subprocess.run") @patch("devx.tools.generate_badges.subprocess.run")
def test_returns_returncode_stdout_stderr(self, mock_run: MagicMock) -> None: def test_returns_returncode_stdout_stderr(self, mock_run: MagicMock) -> None:
@@ -157,100 +260,122 @@ class TestDocCoverageColor:
class TestReadVersion: class TestReadVersion:
@patch("devx.tools.generate_badges._find_package_init") def test_reads_version_from_init(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
def test_reads_version_from_init(self, mock_find: MagicMock) -> None: src = tmp_path / "src" / "mypkg"
mock_init = MagicMock() src.mkdir(parents=True)
mock_init.read_text.return_value = '__version__ = "0.5.0"\n' (src / "__init__.py").write_text('__version__ = "0.5.0"\n')
mock_find.return_value = mock_init assert read_version(tmp_path) == "0.5.0"
assert read_version() == "0.5.0"
@patch("devx.tools.generate_badges._find_package_init") def test_returns_unknown_when_no_version(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
def test_returns_unknown_when_no_version(self, mock_find: MagicMock) -> None: src = tmp_path / "src" / "mypkg"
mock_init = MagicMock() src.mkdir(parents=True)
mock_init.read_text.return_value = "no version here\n" (src / "__init__.py").write_text("no version here\n")
mock_find.return_value = mock_init assert read_version(tmp_path) == "unknown"
assert read_version() == "unknown"
@patch("devx.tools.generate_badges._find_package_init", return_value=None) def test_returns_unknown_when_no_package(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
def test_returns_unknown_when_no_init(self, mock_find: MagicMock) -> None: assert read_version(tmp_path) == "unknown"
assert read_version() == "unknown"
@patch("devx.tools.generate_badges.detect_package_name", return_value="mypkg")
def test_returns_unknown_when_init_missing(self, mock_pkg: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
# Package detected but __init__.py doesn't exist (edge case)
assert read_version(tmp_path) == "unknown"
class TestFindPackageInit: class TestCollectCoverageAndTests:
@patch("devx.tools.generate_badges.REPO_ROOT") @patch("devx.tools.generate_badges.run_command")
def test_no_src_dir(self, mock_root: MagicMock) -> None: @patch("devx.tools.generate_badges.detect_testpaths", return_value=["tests"])
"""Returns None when src/ directory doesn't exist.""" @patch("devx.tools.generate_badges.detect_coverage_target", return_value="src/devx")
from devx.tools.generate_badges import _find_package_init def test_extracts_coverage_and_tests(
self, mock_target: MagicMock, mock_testpaths: MagicMock, mock_run: MagicMock, tmp_path: Path
) -> None: # type: ignore[no-untyped-def]
mock_run.return_value = (0, "1018 passed in 4.23s\nTOTAL 3546 0 100%", "")
cov, tests = collect_coverage_and_tests(tmp_path)
assert cov["message"] == "100%"
assert tests["message"] == "1018 passing"
mock_src = MagicMock() @patch("devx.tools.generate_badges.run_command")
mock_src.exists.return_value = False @patch("devx.tools.generate_badges.detect_testpaths", return_value=["tests"])
mock_root.__truediv__ = MagicMock(return_value=mock_src) @patch("devx.tools.generate_badges.detect_coverage_target", return_value="src/devx")
assert _find_package_init() is None def test_returns_unknown_when_no_match(
self, mock_target: MagicMock, mock_testpaths: MagicMock, mock_run: MagicMock, tmp_path: Path
) -> None: # type: ignore[no-untyped-def]
mock_run.return_value = (1, "garbled output", "some error")
cov, tests = collect_coverage_and_tests(tmp_path)
assert cov["message"] == "unknown"
assert tests["message"] == "unknown"
@patch("devx.tools.generate_badges.REPO_ROOT") @patch("devx.tools.generate_badges.detect_coverage_target", return_value=None)
def test_no_version_in_init_files(self, mock_root: MagicMock, tmp_path: Path) -> None: def test_returns_lightgrey_when_no_target(self, mock_target: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
"""Returns None when no __init__.py has __version__.""" cov, tests = collect_coverage_and_tests(tmp_path)
from devx.tools.generate_badges import _find_package_init assert cov["message"] == "unknown"
assert cov["color"] == "lightgrey"
assert tests["message"] == "unknown"
assert tests["color"] == "lightgrey"
src_dir = tmp_path / "src"
src_dir.mkdir()
(src_dir / "__init__.py").write_text("# no version here\n")
mock_root.__truediv__ = MagicMock(return_value=src_dir)
assert _find_package_init() is None
@patch("devx.tools.generate_badges.REPO_ROOT") class TestCollectDocCoverage:
def test_finds_init_with_version(self, mock_root: MagicMock, tmp_path: Path) -> None: @patch("devx.tools.generate_badges.run_command")
"""Returns the __init__.py that has __version__.""" def test_extracts_doc_coverage(self, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
from devx.tools.generate_badges import _find_package_init mock_run.return_value = (0, "Doc coverage: 20/20 (100%)", "")
badge = collect_doc_coverage(tmp_path)
assert badge["message"] == "100%"
src_dir = tmp_path / "src" @patch("devx.tools.generate_badges.run_command")
pkg_dir = src_dir / "mypkg" def test_returns_unknown_when_no_match(self, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
pkg_dir.mkdir(parents=True) mock_run.return_value = (1, "no doc coverage", "error")
(src_dir / "__init__.py").write_text("# no version\n") badge = collect_doc_coverage(tmp_path)
(pkg_dir / "__init__.py").write_text('__version__ = "1.0.0"\n') assert badge["message"] == "unknown"
mock_root.__truediv__ = MagicMock(return_value=src_dir)
result = _find_package_init()
assert result is not None
assert "__version__" in result.read_text()
@patch("devx.tools.generate_badges.REPO_ROOT")
def test_handles_oserror(self, mock_root: MagicMock, tmp_path: Path) -> None:
"""Handles OSError when reading init files."""
from devx.tools.generate_badges import _find_package_init
src_dir = tmp_path / "src" class TestCollectQuality:
src_dir.mkdir() @patch("devx.tools.generate_badges.run_command")
init_file = src_dir / "__init__.py" def test_all_pass_returns_a(self, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
init_file.write_text('__version__ = "1.0.0"\n') mock_run.return_value = (0, "", "")
mock_root.__truediv__ = MagicMock(return_value=src_dir) badge = collect_quality(tmp_path)
# Patch Path.read_text to raise OSError assert badge["message"] == "A"
with patch.object(Path, "read_text", side_effect=OSError("permission denied")): assert badge["color"] == "brightgreen"
result = _find_package_init()
assert result is None @patch("devx.tools.generate_badges.run_command")
def test_lint_failure_returns_f(self, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
mock_run.return_value = (1, "", "some error")
badge = collect_quality(tmp_path)
assert badge["message"] == "F"
assert badge["color"] == "red"
@patch("devx.tools.generate_badges.run_command")
def test_tool_not_installed_counts_as_pass(self, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
mock_run.return_value = (1, "", "No module named ruff")
badge = collect_quality(tmp_path)
assert badge["message"] == "A"
class TestGenerateBadges: class TestGenerateBadges:
@patch("devx.tools.generate_badges.run_command") @patch("devx.tools.generate_badges.collect_quality")
@patch("devx.tools.generate_badges.collect_doc_coverage")
@patch("devx.tools.generate_badges.collect_coverage_and_tests")
@patch("devx.tools.generate_badges.read_version", return_value="0.5.0") @patch("devx.tools.generate_badges.read_version", return_value="0.5.0")
@patch("devx.tools.generate_badges.extract_coverage", return_value=100.0) @patch("devx.tools.generate_badges.detect_package_name", return_value="devx")
@patch("devx.tools.generate_badges.extract_test_count", return_value=573)
@patch("devx.tools.generate_badges.extract_doc_coverage", return_value=100)
def test_generates_all_badge_files( def test_generates_all_badge_files(
self, self,
mock_doc_cov: MagicMock, mock_pkg: MagicMock,
mock_test_count: MagicMock,
mock_cov: MagicMock,
mock_version: MagicMock, mock_version: MagicMock,
mock_run: MagicMock, mock_cov_tests: MagicMock,
mock_doc: MagicMock,
mock_quality: MagicMock,
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None: # type: ignore[no-untyped-def]
mock_run.return_value = (0, "output", "") mock_cov_tests.return_value = (
badges = generate_badges(tmp_path) make_badge("coverage", "100%", "brightgreen"),
make_badge("tests", "573 passing", "brightgreen"),
)
mock_doc.return_value = make_badge("docs", "100%", "brightgreen")
mock_quality.return_value = make_badge("code quality", "A", "brightgreen")
badges = generate_badges(tmp_path, repo_root=tmp_path)
expected = {"coverage", "tests", "docs", "quality", "version", "python"} expected = {"coverage", "tests", "docs", "quality", "version", "python"}
assert set(badges.keys()) == expected assert set(badges.keys()) == expected
# Verify SVG files were written
for name in expected: for name in expected:
svg_file = tmp_path / f"{name}.svg" svg_file = tmp_path / f"{name}.svg"
assert svg_file.exists() assert svg_file.exists()
@@ -258,78 +383,10 @@ class TestGenerateBadges:
assert content.startswith("<svg") assert content.startswith("<svg")
assert "</svg>" in content assert "</svg>" in content
@patch("devx.tools.generate_badges.run_command")
@patch("devx.tools.generate_badges.read_version", return_value="0.5.0")
@patch("devx.tools.generate_badges.extract_coverage", return_value=100.0)
@patch("devx.tools.generate_badges.extract_test_count", return_value=573)
@patch("devx.tools.generate_badges.extract_doc_coverage", return_value=100)
def test_quality_badge_pass_when_all_lint_passes(
self,
mock_doc_cov: MagicMock,
mock_test_count: MagicMock,
mock_cov: MagicMock,
mock_version: MagicMock,
mock_run: MagicMock,
tmp_path: Path,
) -> None:
mock_run.return_value = (0, "output", "")
badges = generate_badges(tmp_path)
assert badges["quality"]["message"] == "A"
assert badges["quality"]["color"] == "brightgreen"
@patch("devx.tools.generate_badges.run_command")
@patch("devx.tools.generate_badges.read_version", return_value="0.5.0")
@patch("devx.tools.generate_badges.extract_coverage", return_value=100.0)
@patch("devx.tools.generate_badges.extract_test_count", return_value=573)
@patch("devx.tools.generate_badges.extract_doc_coverage", return_value=100)
def test_quality_badge_fails_when_lint_fails(
self,
mock_doc_cov: MagicMock,
mock_test_count: MagicMock,
mock_cov: MagicMock,
mock_version: MagicMock,
mock_run: MagicMock,
tmp_path: Path,
) -> None:
mock_run.side_effect = [
(0, "output", ""),
(0, "output", ""),
(1, "error", ""),
(0, "output", ""),
(0, "output", ""),
(0, "output", ""),
]
badges = generate_badges(tmp_path)
assert badges["quality"]["message"] == "F"
assert badges["quality"]["color"] == "red"
@patch("devx.tools.generate_badges.run_command")
@patch("devx.tools.generate_badges.read_version", return_value="0.5.0")
@patch("devx.tools.generate_badges.extract_coverage", return_value=None)
@patch("devx.tools.generate_badges.extract_test_count", return_value=None)
@patch("devx.tools.generate_badges.extract_doc_coverage", return_value=None)
def test_badges_show_unknown_when_extraction_fails(
self,
mock_doc_cov: MagicMock,
mock_test_count: MagicMock,
mock_cov: MagicMock,
mock_version: MagicMock,
mock_run: MagicMock,
tmp_path: Path,
) -> None:
mock_run.return_value = (1, "garbled output", "")
badges = generate_badges(tmp_path)
assert badges["coverage"]["message"] == "unknown"
assert badges["coverage"]["color"] == "red"
assert badges["tests"]["message"] == "unknown"
assert badges["tests"]["color"] == "red"
assert badges["docs"]["message"] == "unknown"
assert badges["docs"]["color"] == "red"
class TestCli: class TestCli:
@patch("devx.tools.generate_badges.generate_badges") @patch("devx.tools.generate_badges.generate_badges")
def test_cli_generates_badges(self, mock_gen: MagicMock, tmp_path: Path) -> None: def test_cli_generates_badges(self, mock_gen: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
mock_gen.return_value = { mock_gen.return_value = {
"coverage": make_badge("coverage", "100%", "brightgreen"), "coverage": make_badge("coverage", "100%", "brightgreen"),
"tests": make_badge("tests", "573 passing", "brightgreen"), "tests": make_badge("tests", "573 passing", "brightgreen"),
@@ -339,7 +396,6 @@ class TestCli:
assert result.exit_code == 0 assert result.exit_code == 0
assert "Generating badges" in result.output assert "Generating badges" in result.output
assert "Generated 2 badges" in result.output assert "Generated 2 badges" in result.output
mock_gen.assert_called_once_with(tmp_path)
def test_main_module_block() -> None: def test_main_module_block() -> None:
+43
View File
@@ -162,6 +162,7 @@ class TestMain:
mock_notes: MagicMock, mock_notes: MagicMock,
) -> None: ) -> None:
mock_tea = MagicMock() mock_tea = MagicMock()
mock_tea.list_releases.return_value = []
mock_tea_cls.return_value = mock_tea mock_tea_cls.return_value = mock_tea
runner = CliRunner() runner = CliRunner()
result = runner.invoke(main, ["v1.0.0", "owner/repo"]) result = runner.invoke(main, ["v1.0.0", "owner/repo"])
@@ -187,6 +188,7 @@ class TestMain:
) -> None: ) -> None:
"""When no PYPI_TOKEN, publishes to Gitea PyPI registry.""" """When no PYPI_TOKEN, publishes to Gitea PyPI registry."""
mock_tea = MagicMock() mock_tea = MagicMock()
mock_tea.list_releases.return_value = []
mock_tea_cls.return_value = mock_tea mock_tea_cls.return_value = mock_tea
runner = CliRunner() runner = CliRunner()
result = runner.invoke(main, ["v1.0.0", "owner/repo"]) result = runner.invoke(main, ["v1.0.0", "owner/repo"])
@@ -209,6 +211,7 @@ class TestMain:
) -> None: ) -> None:
"""--registry-url flag publishes to the specified Gitea registry.""" """--registry-url flag publishes to the specified Gitea registry."""
mock_tea = MagicMock() mock_tea = MagicMock()
mock_tea.list_releases.return_value = []
mock_tea_cls.return_value = mock_tea mock_tea_cls.return_value = mock_tea
runner = CliRunner() runner = CliRunner()
result = runner.invoke( result = runner.invoke(
@@ -236,6 +239,7 @@ class TestMain:
) -> None: ) -> None:
"""DEVX_PYPI_REGISTRY_URL env var sets the registry URL.""" """DEVX_PYPI_REGISTRY_URL env var sets the registry URL."""
mock_tea = MagicMock() mock_tea = MagicMock()
mock_tea.list_releases.return_value = []
mock_tea_cls.return_value = mock_tea mock_tea_cls.return_value = mock_tea
runner = CliRunner() runner = CliRunner()
result = runner.invoke(main, ["v1.0.0", "owner/repo"]) result = runner.invoke(main, ["v1.0.0", "owner/repo"])
@@ -256,6 +260,7 @@ class TestMain:
) -> None: ) -> None:
"""When no PYPI_TOKEN and no registry URL, skips publish and creates release only.""" """When no PYPI_TOKEN and no registry URL, skips publish and creates release only."""
mock_tea = MagicMock() mock_tea = MagicMock()
mock_tea.list_releases.return_value = []
mock_tea_cls.return_value = mock_tea mock_tea_cls.return_value = mock_tea
runner = CliRunner() runner = CliRunner()
result = runner.invoke(main, ["v1.0.0", "owner/repo", "--registry-url", ""]) result = runner.invoke(main, ["v1.0.0", "owner/repo", "--registry-url", ""])
@@ -307,6 +312,7 @@ class TestMain:
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
) -> None: ) -> None:
mock_tea = MagicMock() mock_tea = MagicMock()
mock_tea.list_releases.return_value = []
mock_tea.create_release.side_effect = TeaCLIError("server error") mock_tea.create_release.side_effect = TeaCLIError("server error")
mock_tea_cls.return_value = mock_tea mock_tea_cls.return_value = mock_tea
runner = CliRunner() runner = CliRunner()
@@ -323,6 +329,7 @@ class TestMain:
) -> None: ) -> None:
"""--skip-build skips build_package and PyPI publish, only creates Gitea release.""" """--skip-build skips build_package and PyPI publish, only creates Gitea release."""
mock_tea = MagicMock() mock_tea = MagicMock()
mock_tea.list_releases.return_value = []
mock_tea_cls.return_value = mock_tea mock_tea_cls.return_value = mock_tea
runner = CliRunner() runner = CliRunner()
result = runner.invoke(main, ["v1.0.0", "owner/repo", "--skip-build"]) result = runner.invoke(main, ["v1.0.0", "owner/repo", "--skip-build"])
@@ -330,3 +337,39 @@ class TestMain:
assert "skip" in result.output.lower() assert "skip" in result.output.lower()
mock_build.assert_not_called() mock_build.assert_not_called()
mock_tea.create_release.assert_called_once() mock_tea.create_release.assert_called_once()
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.publish_to_pypi")
@patch("devx.ci.publish.build_package")
def test_skips_release_creation_when_already_exists(
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
) -> None:
"""If the Gitea release already exists, skip creation (idempotent)."""
mock_tea = MagicMock()
mock_tea.list_releases.return_value = [{"tag_name": "v1.0.0"}]
mock_tea_cls.return_value = mock_tea
runner = CliRunner()
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
assert result.exit_code == 0
assert "already exists" in result.output
mock_tea.create_release.assert_not_called()
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.publish_to_pypi")
@patch("devx.ci.publish.build_package")
def test_proceeds_to_create_when_list_releases_fails(
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
) -> None:
"""If list_releases raises TeaCLIError, proceed to create the release."""
mock_tea = MagicMock()
mock_tea.list_releases.side_effect = TeaCLIError("api error")
mock_tea_cls.return_value = mock_tea
runner = CliRunner()
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
assert result.exit_code == 0
assert "Gitea release v1.0.0 created" in result.output
mock_tea.create_release.assert_called_once()
+16
View File
@@ -287,3 +287,19 @@ class TestMain:
result = runner.invoke(push_badges.main, ["--no-readme-update"]) result = runner.invoke(push_badges.main, ["--no-readme-update"])
assert result.exit_code != 0 assert result.exit_code != 0
mock_sleep.assert_not_called() mock_sleep.assert_not_called()
class TestRepoRoot:
def test_uses_github_workspace(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("GITHUB_WORKSPACE", str(tmp_path))
assert push_badges._repo_root() == tmp_path
def test_falls_back_to_cwd(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("GITHUB_WORKSPACE", raising=False)
monkeypatch.chdir(tmp_path)
assert push_badges._repo_root() == tmp_path
def test_falls_back_when_workspace_invalid(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("GITHUB_WORKSPACE", "/nonexistent")
monkeypatch.chdir(tmp_path)
assert push_badges._repo_root() == tmp_path