Compare commits

..
44 Commits
Author SHA1 Message Date
devx-ci-bot 4d57780f40 release: v0.51.10 [skip ci] 2026-09-17 09:44:23 +00:00
kireto 27ea8903eb DEVX-162: fix: read Gitea repo-variable data field; fail closed on unknown nightly status
Post-merge / detect-and-configure (push) Successful in 2m9s
Post-merge / release-and-maintain (push) Successful in 1m30s
2026-09-17 09:41:36 +00:00
gitea-actions-bot 87d46226f6 chore: update badge URLs to commit 09be7662 [skip ci] 2026-09-05 14:18:15 +00:00
emil 4638e334b5 DEVX-167: docs: add dependency-graph, deployment-coordination, and skill-creation skills
Post-merge / detect-and-configure (push) Successful in 20s
Post-merge / release-and-maintain (push) Successful in 54s
Co-authored-by: emil User <emil.simeonov@tutanota.com>
2026-09-05 14:16:52 +00:00
gitea-actions-bot c6bd4e8f63 chore: update badge URLs to commit 27d2b933 [skip ci] 2026-08-26 18:12:15 +00:00
devx-ci-bot d47e3833bc release: v0.51.9 [skip ci] 2026-08-26 18:11:18 +00:00
emil c00e9e3d7b DEVX-166: fix: exclude docs/plans/* from PR size check
Post-merge / detect-and-configure (push) Successful in 13s
Post-merge / release-and-maintain (push) Successful in 1m25s
Co-authored-by: emil User <emil.simeonov@tutanota.com>
2026-08-26 18:10:34 +00:00
gitea-actions-bot 40e95bc96f chore: update badge URLs to commit ae0cb5b8 [skip ci] 2026-08-26 18:06:00 +00:00
devx-ci-bot 3aa9681404 release: v0.51.8 [skip ci] 2026-08-26 18:04:45 +00:00
emil e96f63cb40 DEVX-165: fix: accept deps: as valid conventional commit type
Post-merge / detect-and-configure (push) Successful in 14s
Post-merge / release-and-maintain (push) Successful in 1m46s
Co-authored-by: emil User <emil.simeonov@tutanota.com>
2026-08-26 18:03:57 +00:00
gitea-actions-bot 6d6c8cceec chore: update badge URLs to commit 2f14bdfd [skip ci] 2026-08-26 14:29:30 +00:00
devx-ci-bot d9489b5387 release: v0.51.7 [skip ci] 2026-08-26 14:28:39 +00:00
kireto 35f7bc92cd DEVX-164: fix: increase HTTP 500 retry count to 5 with longer backoff and visible logging
Post-merge / detect-and-configure (push) Successful in 11s
Post-merge / release-and-maintain (push) Successful in 1m20s
2026-08-26 14:27:57 +00:00
gitea-actions-bot 55bcd8fa01 chore: update badge URLs to commit f474ba2f [skip ci] 2026-08-26 14:11:33 +00:00
devx-ci-bot 990f70845c release: v0.51.6 [skip ci] 2026-08-26 14:10:44 +00:00
kireto 149e8846b8 DEVX-163: fix: use stderr=STDOUT to capture all docker push output in one stream
Post-merge / detect-and-configure (push) Successful in 13s
Post-merge / release-and-maintain (push) Successful in 1m18s
2026-08-26 14:09:59 +00:00
gitea-actions-bot a7cdebc0dc chore: update badge URLs to commit 73dcdbaf [skip ci] 2026-08-26 13:42:00 +00:00
devx-ci-bot 012f0979ce release: v0.51.5 [skip ci] 2026-08-26 13:41:09 +00:00
kireto 62412755cb DEVX-163: fix: check stdout for HTTP 500 in _run_push (docker sends to stdout)
Post-merge / release-and-maintain (push) Successful in 1m20s
Post-merge / detect-and-configure (push) Successful in 12s
2026-08-26 13:40:24 +00:00
gitea-actions-bot 25f00335df chore: update badge URLs to commit 7660d501 [skip ci] 2026-08-26 13:14:22 +00:00
devx-ci-bot fa32df2f22 release: v0.51.4 [skip ci] 2026-08-26 13:13:27 +00:00
kireto 2d7b4bdac3 DEVX-162: fix: serialize registry uploads and retry on HTTP 500
Post-merge / detect-and-configure (push) Successful in 12s
Post-merge / release-and-maintain (push) Successful in 1m22s
2026-08-26 13:12:45 +00:00
gitea-actions-bot 5986b5b9ed chore: update badge URLs to commit b1023118 [skip ci] 2026-08-26 08:27:13 +00:00
emil 34c7f3782c DEVX-160: refactor: remove cross-repo contract tests from devx
Post-merge / detect-and-configure (push) Successful in 11s
Post-merge / release-and-maintain (push) Successful in 53s
Co-authored-by: emil User <emil.simeonov@tutanota.com>
2026-08-26 08:26:05 +00:00
gitea-actions-bot e123d7050f chore: update badge URLs to commit c133ea40 [skip ci] 2026-08-26 08:24:10 +00:00
devx-ci-bot 8b8e7eb7f5 release: v0.51.3 [skip ci] 2026-08-26 08:13:00 +00:00
emil dcfbd4c0c2 DEVX-161: fix: fall back to CI bot token for auto-merge approval
Post-merge / detect-and-configure (push) Successful in 14s
Post-merge / release-and-maintain (push) Canceled after 13m38s
Co-authored-by: emil User <emil.simeonov@tutanota.com>
2026-08-26 08:12:00 +00:00
gitea-actions-bot 48122876ba chore: update badge URLs to commit 1a5ea7fe [skip ci] 2026-08-26 07:48:15 +00:00
devx-ci-bot 5f0b9d3a71 release: v0.51.2 [skip ci] 2026-08-26 07:47:20 +00:00
emil 816f27ed7a DEVX-159: fix: push-first strategy in build_image to avoid losing latest tag
Post-merge / detect-and-configure (push) Successful in 12s
Post-merge / release-and-maintain (push) Successful in 1m26s
Co-authored-by: emil User <emil.simeonov@tutanota.com>
2026-08-26 07:46:35 +00:00
gitea-actions-bot 7101908a78 chore: update badge URLs to commit eaad6892 [skip ci] 2026-08-25 17:27:42 +00:00
devx-ci-bot a637448f83 release: v0.51.1 [skip ci] 2026-08-25 17:26:52 +00:00
emo f94ce03a04 DEVX-158: fix: delete existing manifest before push (Gitea #31964 workaround)
Post-merge / release-and-maintain (push) Successful in 1m22s
Post-merge / detect-and-configure (push) Successful in 12s
Co-authored-by: emo <emo@oblachno.com>
2026-08-25 17:26:04 +00:00
gitea-actions-bot 92a14c8e68 chore: update badge URLs to commit b1e8ed35 [skip ci] 2026-08-25 16:39:20 +00:00
devx-ci-bot 5f08e23e09 release: v0.51.0 [skip ci] 2026-08-25 16:38:30 +00:00
emo 9fa41457f2 DEVX-157: feat: add role defaults path to create_dependency_pr search
Post-merge / detect-and-configure (push) Successful in 14s
Post-merge / release-and-maintain (push) Successful in 1m20s
Co-authored-by: emo <emo@oblachno.com>
2026-08-25 16:37:45 +00:00
gitea-actions-bot f62fe16c1b chore: update badge URLs to commit c35a676f [skip ci] 2026-08-24 22:23:22 +00:00
devx-ci-bot f90eeeb550 release: v0.50.2 [skip ci] 2026-08-24 22:22:33 +00:00
emil f9462bc939 DEVX-156: fix: update check_pr_size usage example with --repo and --pr-number args
Post-merge / release-and-maintain (push) Successful in 1m17s
Post-merge / detect-and-configure (push) Successful in 11s
Co-authored-by: emil User <emil.simeonov@tutanota.com>
2026-08-24 22:21:49 +00:00
gitea-actions-bot 7eb11e3261 chore: update badge URLs to commit 3e50818d [skip ci] 2026-08-24 21:53:46 +00:00
emil 0ac2bf4a8c DEVX-156: docs: add consumer repo reference to validate_spec docstring
Post-merge / detect-and-configure (push) Successful in 31s
Post-merge / release-and-maintain (push) Successful in 53s
Co-authored-by: emil User <emil.simeonov@tutanota.com>
2026-08-24 21:52:18 +00:00
gitea-actions-bot d0e3f3918b chore: update badge URLs to commit 041c1730 [skip ci] 2026-08-24 21:48:07 +00:00
emil 2704ec45b5 DEVX-156: docs: expand package docstring with CI module overview
Post-merge / release-and-maintain (push) Successful in 1m0s
Post-merge / detect-and-configure (push) Successful in 31s
Co-authored-by: emil User <emil.simeonov@tutanota.com>
2026-08-24 21:42:10 +00:00
emil 11c4a1fc9e DEVX-155: Replace pr_review with spec-driven CI gates and pr-review skill
Post-merge / detect-and-configure (push) Failing after 18s
Post-merge / release-and-maintain (push) Skipped
2026-08-24 20:39:09 +00:00
56 changed files with 5764 additions and 3367 deletions
+135
View File
@@ -0,0 +1,135 @@
# dependency-graph
Map of the oblachno ecosystem. Knows which repo produces what, which
repos depend on which, and the correct order for cross-repo changes.
## When to Invoke
Invoke this skill when:
- Changes span multiple repos
- A change in one repo requires version bumps in downstream repos
- Deploying infrastructure that depends on published packages/images
- Verifying the ecosystem is in a consistent state before deployment
- Determining which repos to update and in what order
## Prerequisites
- All repos cloned under `/home/emo/dev/ideas/oblachno/`
- `.env` with `DEVELOPER_GITEA_API_TOKEN` in each repo
## Ecosystem Map
```
devx (PyPI package)
/ | \
/ | \
grm sso-bridge infra
(PyPI) (PyPI+Docker) (deploys all)
| | |
v v v
infra bump infra bump staging
(auto PR) (auto PR) production
|
mattermost-oidc (Docker image)
(infra pulls :latest at deploy)
```
## Repositories
| Repo | Produces | Consumers | Release Trigger |
|------|----------|-----------|-----------------|
| `devx` | PyPI package `devx` | grm, sso-bridge, infra | User-facing changes to `src/devx/**` |
| `grm` | PyPI package `grm` | infra | User-facing changes to `src/grm/**` or `ansible/**` |
| `sso-bridge` | PyPI package `sso_bridge` + Docker image | infra | User-facing changes to `src/sso_bridge/**` or `ansible/**` |
| `infra` | Staging/production deployment | (end users) | User-facing changes + nightly gate |
| `mattermost-oidc` | Docker image `mattermost-oidc` | infra (pulls at deploy) | `Dockerfile` or `build.yml` changes |
## Dependency Chain
### devx → all repos
devx publishes to the Gitea PyPI registry. grm, sso-bridge, and infra
pin devx in `pyproject.toml`:
```toml
"devx @ git+https://git.oblachno.oblachno.fyi/oblachno-oss/devx.git@vX.Y.Z"
```
When devx publishes a new version:
1. grm, sso-bridge, and infra must bump their pinned devx version
2. This is currently manual — no auto-dependency-PR from devx
3. Each repo must `make setup` to pick up the new version
### grm → infra
grm publishes to PyPI. Its post-merge workflow auto-creates an infra
dependency PR via `devx.ci.create_dependency_pr --repo oblachno/infra
--package grm`. The PR bumps the pinned grm version in infra's
`pyproject.toml`.
### sso-bridge → infra
sso-bridge publishes to PyPI AND builds a Docker image. Its post-merge
workflow auto-creates an infra dependency PR via
`devx.ci.create_dependency_pr --repo oblachno/infra --package
sso_bridge`. The PR bumps the pinned sso_bridge version.
The Docker image is pulled by infra at deploy time (`sso-bridge:latest`).
### mattermost-oidc → infra
mattermost-oidc builds a Docker image tagged `:latest` and `:MM_VERSION`.
infra pulls `mattermost-oidc:latest` at deploy time. There is no
auto-dependency-PR — infra simply pulls the latest image.
### infra → staging/production
infra deploys to staging and production. The deployment:
1. Provisions VMs from golden images
2. Runs Ansible roles (including grm and sso-bridge roles)
3. Pulls Docker images (sso-bridge, mattermost-oidc)
4. Configures services
## Correct Order for Cross-Repo Changes
When a change spans multiple repos, follow this order:
1. **devx first** — if the change starts in devx, merge and publish devx
first. Wait for the PyPI publish job to complete.
2. **Bump devx in consumers** — in grm/sso-bridge/infra, bump the pinned
devx version, run `make setup`, verify tests pass, merge.
3. **grm/sso-bridge second** — merge and publish grm/sso-bridge. Wait
for the PyPI publish + Docker image build to complete.
4. **Auto-dependency-PRs** — grm/sso-bridge post-merge auto-creates infra
PRs to bump pinned versions. Wait for these PRs to appear.
5. **Merge infra dependency PRs** — review and merge the auto-created
infra PRs.
6. **infra last** — deploy to staging, validate, promote to production.
## State Verification Before Deployment
Before deploying infra, verify:
1. **devx version consistent** — all repos pin the same devx version
2. **grm published** — latest grm tag exists in PyPI
3. **sso-bridge published** — latest sso_bridge tag exists in PyPI
4. **sso-bridge image built** — latest sso-bridge Docker image exists
5. **mattermost-oidc image built** — latest mattermost-oidc image exists
6. **infra pins match published versions** — no stale pins
7. **Nightly gate green**`NIGHTLY_STATUS` is not `failed`
## Quick Check Commands
```bash
# Check latest devx version
curl -sS https://git.oblachno.oblachno.fyi/api/v1/repos/oblachno-oss/devx/releases/latest | python3 -c "import json,sys; print(json.load(sys.stdin).get('tag_name','?'))"
# Check pinned devx version in each repo
for repo in grm sso-bridge infra; do
echo -n "$repo: "; grep 'devx @' /home/emo/dev/ideas/oblachno/$repo/pyproject.toml | grep -oP 'v[\d.]+'
done
# Check latest sso-bridge image build
curl -sS -H "Authorization: token $DEVELOPER_GITEA_API_TOKEN" \
"https://git.oblachno.oblachno.fyi/api/v1/repos/oblachno/sso-bridge/actions/runs?per_page=5" \
| python3 -c "import json,sys; [print(r['id'],r['status'],r['conclusion']) for r in json.load(sys.stdin).get('workflow_runs',[]) if r.get('event')=='push']"
```
@@ -0,0 +1,90 @@
# deployment-coordination
How devx releases propagate to downstream repos. devx is the base
package — all other repos pin it. A devx release must complete before
consumers can bump.
## When to Invoke
Invoke this skill when:
- Changes to devx affect downstream repos (grm, sso-bridge, infra)
- Preparing a devx release that other repos depend on
- Verifying downstream repos have bumped to the latest devx version
- Coordinating a multi-repo change that starts in devx
## Prerequisites
- devx repo at `/home/emo/dev/ideas/oblachno/devx`
- `.env` with `DEVELOPER_GITEA_API_TOKEN`
- See `dependency-graph` skill for the full ecosystem map
## What devx Produces
devx publishes a Python package to the Gitea PyPI registry. Downstream
repos pin it:
```toml
"devx @ git+https://git.oblachno.oblachno.fyi/oblachno-oss/devx.git@vX.Y.Z"
```
## Release Flow
1. PR merged to master
2. Post-merge workflow runs `devx.ci.release` — classifies changes
3. If user-facing changes: git-cliff bumps version, creates tag, pushes
4. `devx.ci.publish` builds and publishes to Gitea PyPI registry
5. Downstream repos must bump their pinned devx version
## Downstream Consumers
| Repo | Pin location | Auto-bump? |
|------|-------------|------------|
| grm | `pyproject.toml` | No — manual |
| sso-bridge | `pyproject.toml` | No — manual |
| infra | `pyproject.toml` | No — manual |
devx does NOT auto-create dependency PRs in downstream repos. Bumping
is manual: create a PR in each downstream repo to update the pinned
version.
## Coordinating a devx Change
When a change to devx affects downstream repos:
1. **Merge devx PR** — wait for post-merge publish to complete
2. **Verify publish** — check the new tag exists:
```bash
curl -sS -H "Authorization: token $DEVELOPER_GITEA_API_TOKEN" \
https://git.oblachno.oblachno.fyi/api/v1/repos/oblachno-oss/devx/releases/latest \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('tag_name','?'))"
```
3. **Bump downstream repos** — for each repo (grm, sso-bridge, infra):
- Update `devx @ ...@vX.Y.Z` in `pyproject.toml`
- Run `make setup` to install the new version
- Run `make pytest-cov` to verify compatibility
- Create and merge a PR
4. **Verify infra deploys** — after infra bumps, verify staging deploy
picks up the new devx version
## State Verification
Before starting a devx change, verify current state:
```bash
# Current devx version
grep '__version__' /home/emo/dev/ideas/oblachno/devx/src/devx/__init__.py
# What each repo pins
for repo in grm sso-bridge infra; do
echo -n "$repo pins: "
grep 'devx @' /home/emo/dev/ideas/oblachno/$repo/pyproject.toml | grep -oP 'v[\d.]+'
done
```
If pins are inconsistent across repos, bump them to the latest published
version before starting new work.
## Common Mistakes
- Merging a devx PR and immediately merging downstream PRs without
waiting for the publish job to complete
- Forgetting to bump infra (it has the most complex deploy pipeline)
- Bumping only one downstream repo when the change affects all three
+9 -1
View File
@@ -12,7 +12,6 @@ Quick reference for devx tools when working on the devx repo itself.
| Check CI status | `make devx-pr-status` or `make devx-pr-status PR=42 WAIT=1` |
| Fetch CI failure logs | `make devx-pr-logs` or `make devx-pr-logs PR=42 JOB=quality TAIL=50` |
| Add ready-to-merge label | `make devx-pr-label` or `make devx-pr-label PR=42` |
| Post PR review | `make devx-pr-review PR=42 EVENT=APPROVE BODY="..." CHECKLIST=1,2,3,4,5,6,7,8,9,10,11,12,13` |
| Rebase current branch | `make rebase` |
| Rebase PR via API | `make pr-rebase` or `make pr-rebase PR=42` |
@@ -24,6 +23,15 @@ When the `ready-to-merge` label is added and all CI checks pass:
3. The rebase triggers a new CI run; the next auto-merge attempt merges
4. No manual rebase needed unless the API rebase fails
## Spec-Driven CI Gates (Pre-merge)
Every PR must pass these gates before merge:
| Gate | Module | What it checks |
|------|--------|----------------|
| Spec validation | `devx.ci.validate_spec` | Spec file exists at `docs/specs/<TASK-ID>.md`, has REQ-IDs, all ACs checked |
| PR size | `devx.ci.check_pr_size` | Max 500 lines / 10 files (excludes CHANGELOG, badges, locks) |
## Key Rules
- Never manually merge via API — always use auto-merge with `ready-to-merge` label
+272
View File
@@ -0,0 +1,272 @@
# pr-review
Deep, critical PR review with auto-fix. This skill guides the agent
through a thorough review of a pull request, posting inline comments
for each issue found, auto-fixing them, resolving the discussion threads,
and marking the PR as ready-to-merge when no blocking issues remain.
## When to Invoke
Invoke this skill when asked to review a PR, or when a PR is open and
needs review before merge. Do NOT invoke automatically on every PR —
this is an on-demand deep review, not a CI gate.
## Prerequisites
- The PR must be open in a Gitea repo
- The agent needs Gitea MCP access (gitea server)
- The agent needs git push access to the PR's head branch
- The PR should have passed CI (validate job) before deep review
## Review Categories
Review every PR against these 8 categories. For each issue found, post
an inline comment on the specific line, then auto-fix it.
### 1. Functional Correctness
- Does the code actually do what the spec/PR title claims?
- Are edge cases handled? (empty input, null, boundary values, concurrent access)
- Are error paths tested? Not just happy path.
- Does the code handle all return values? (ignored errors, unchecked None)
- Are there off-by-one errors, wrong comparisons, inverted conditions?
- Do loops terminate correctly? (no infinite loops, correct break/continue)
- Are regex patterns correct? (anchored, escaped, non-greedy where needed)
- Are API responses validated before use? (status codes, response shape)
### 2. Completeness
- Are all requirements from the spec implemented? (check each REQ-ID)
- Are all acceptance criteria in the spec checked off?
- Are tests written for all new code paths?
- Are error messages user-facing (wrapped in `_()`)?
- Are new CLI commands documented in `docs/user/cli-commands.md`?
- Are new modules added to architecture docs?
- Are CHANGELOG entries added for user-facing changes?
- Are translations added for new user-facing strings?
### 3. Architecture
- Does the code follow the repo's layer separation? (no business logic in CLI, no direct subprocess in CLI)
- Are new dependencies justified? (no unnecessary new packages)
- Is configuration via env vars / config.py, not hardcoded?
- Are new modules placed in the correct directory? (ci/ vs tools/ vs molecule/)
- Does the code reuse existing utilities? (no reimplemented helpers)
- Are imports circular? (check import chains)
- Is the code testable? (injectable dependencies, no hidden global state)
- Does the code follow existing patterns in the codebase?
### 4. Reliability
- Are external API calls retried with backoff?
- Are timeouts set on all network operations?
- Are file operations atomic? (write to temp, rename)
- Are database operations transactional where needed?
- Are there race conditions? (check shared mutable state)
- Are resources cleaned up in all paths? (finally blocks, context managers)
- Can the code handle partial failures? (one service down, others up)
- Are idempotency guarantees maintained? (safe to retry)
### 5. Robustness
- Does the code fail gracefully? (meaningful error messages, not stack traces)
- Are unexpected inputs handled? (type checking, validation)
- Are there any crash-on-bad-input paths?
- Does the code degrade under load? (backpressure, queue limits)
- Are there resource leaks? (file handles, connections, memory)
- Does the code survive network partitions? (retry, circuit breaker)
- Are there any unhandled exceptions that could crash the process?
- Is logging sufficient to diagnose production issues?
### 6. Security
- Are there hardcoded secrets, tokens, or passwords?
- Is `shell=True` used with user input? (command injection)
- Is `eval()` or `exec()` used? (code injection)
- Are SQL queries parameterized? (no string concatenation)
- Are file paths validated? (no path traversal)
- Are user inputs sanitized before display? (XSS in web contexts)
- Are SSL/TLS verifications disabled without justification?
- Are secrets logged in error messages or debug output?
- Are permissions checked before privileged operations?
- Is sensitive data in memory longer than necessary?
### 7. Technical Excellence
- Are functions under 50 lines? (refactor if longer)
- Is cyclomatic complexity reasonable? (no deeply nested if/else chains)
- Are names meaningful? (no single-letter vars, no misleading names)
- Is dead code removed? (no commented-out blocks, no unused imports)
- Are comments explaining WHY, not WHAT?
- Is the code DRY? (no copy-pasted blocks that should be shared)
- Is the code SOLID? (single responsibility, open/closed)
- Are magic numbers extracted to named constants?
- Is the code formatted per the repo's linter config?
- Are type hints present on all function signatures?
### 8. Test Quality
- Do tests actually test the behavior? (not just that code runs)
- Are tests independent? (no shared mutable state, no order dependency)
- Are tests fast? (no real sleeps, no real network calls, mocked)
- Are edge cases tested? (empty, None, boundary, error paths)
- Are test names descriptive? (test_what_condition_expected_result)
- Are mocks set up correctly? (mocking the right object, not too broad)
- Is coverage 100% for new code? (every branch, every line)
- Are integration tests added for cross-module changes?
- Do tests clean up after themselves? (tmp_path, fixtures)
## Review Procedure
### Step 1: Gather Context
```
1. Read the PR spec (if exists): docs/specs/<TASK-ID>.md
2. Fetch PR details via Gitea MCP: pull_request_read (get_pr, list_pr_files)
3. Read the full diff: git diff origin/master...HEAD
4. Read the PR description and any existing review comments
5. Identify the repo's task prefix (OBL-INFRA, GRM, SSO, DEVX)
```
### Step 2: Review Each File
For each changed file in the PR:
1. Read the full file (not just the diff) to understand context
2. Go through all 8 review categories
3. For each issue found, note: file path, line number, category, severity, description, suggested fix
### Step 3: Post Inline Comments
For each issue found, post an inline review comment using the Gitea MCP:
```
mcp_call_tool: gitea / pull_request_review_write
method: create
owner: <owner>
repo: <repo>
pull_number: <PR number>
state: PENDING (accumulate comments before submitting)
body: "" (empty for now, summary added on submit)
comments: [
{
path: "<file path>",
new_line_num: <line number>,
body: "**[<category>] [<severity>]** <description>\n\n**Suggested fix:**\n```<lang>\n<fixed code>\n```"
}
]
```
Comment format:
```
**[Security] [error]** `shell=True` used with user input — command injection risk.
**Suggested fix:**
```python
subprocess.run(["git", "log", commit], check=True)
```
```
Severity levels:
- `error` — must fix before merge (security, correctness, crash)
- `warning` — should fix before merge (reliability, best practice)
- `info` — consider fixing (style, minor improvement)
### Step 4: Auto-Fix Issues
For each issue that can be safely auto-fixed:
1. Edit the file using the `edit` tool
2. Commit with message: `fix: address review comment — <short description>`
3. Push to the PR's head branch: `git push origin HEAD`
4. Wait for CI to re-run on the push
Auto-fix ALL issues unless:
- The fix requires an architectural decision (ask the user)
- The fix changes public API behavior (ask the user)
- The fix is ambiguous (multiple valid approaches, ask the user)
### Step 5: Resolve Discussion Threads
After auto-fixing an issue and CI passes:
1. Find the review comment thread for that issue
2. Post a reply: `Fixed in <commit-sha>. Closing this thread.`
3. Resolve the discussion (if Gitea supports it via API)
4. If resolving via API is not available, the reply comment serves as resolution
### Step 6: Submit Final Review
After all issues are addressed (fixed or discussed):
```
mcp_call_tool: gitea / pull_request_review_write
method: submit
owner: <owner>
repo: <repo>
pull_number: <PR number>
review_id: <from step 3 create>
state: COMMENT (or APPROVED if no blocking issues remain)
body: <summary — see below>
```
### Step 7: Post Summary
Post a brief summary as a PR comment (via `issue_write / add_comment`):
```
## Deep Review Summary
- **Files reviewed:** N
- **Issues found:** N (N auto-fixed, N require attention)
- **Categories:** security (N), correctness (N), architecture (N), ...
**Outcome:** ✅ Ready to merge — all issues addressed.
**OR**
**Outcome:** ⚠️ N blocking issue(s) remain — see inline comments.
```
Keep the summary to 5-10 bullet points. Do not paste the full review.
### Step 8: Mark PR Ready
If all issues are addressed and no blocking issues remain:
```
mcp_call_tool: gitea / issue_write
method: add_labels
owner: <owner>
repo: <repo>
issue_number: <PR number>
labels: [<label_id for "ready-to-merge">]
```
If blocking issues remain, do NOT add the label. Post a comment
explaining what needs to be resolved before the PR can merge.
## Gitea MCP Tools Reference
| Action | MCP tool | Method |
|--------|----------|--------|
| Get PR details | `pull_request_read` | `get_pr` |
| List PR files | `pull_request_read` | `list_pr_files` |
| Get PR diff | `pull_request_read` | `get_pr_diff` |
| Create review (pending) | `pull_request_review_write` | `create` (state: PENDING) |
| Submit review | `pull_request_review_write` | `submit` (state: APPROVED/COMMENT/REQUEST_CHANGES) |
| Post PR comment | `issue_write` | `add_comment` |
| Add label | `issue_write` | `add_labels` |
| List labels | `label_read` | `list_repo_labels` |
| Merge PR | `pull_request_write` | `merge` (do NOT use — auto-merge handles this) |
## Important Rules
- **Never merge the PR yourself.** Add the `ready-to-merge` label and let
the auto-merge workflow handle it. This ensures CI passes and the
commit message follows the `<PREFIX>-N: <conventional>` format.
- **Never approve your own PR.** If the agent created the PR, post
COMMENT state, not APPROVED.
- **Always push fixes to the PR branch**, not directly to master.
- **Wait for CI after each push** before resolving the discussion thread.
- **Post one review with all comments**, not multiple reviews.
- **The summary must be brief** — 5-10 bullet points max.
- **Severity matters**: only `error` severity blocks the `ready-to-merge` label.
+142
View File
@@ -0,0 +1,142 @@
# skill-creation
How to create, validate, and maintain Devin skills. Skills must be
clear, succinct, and actionable — no AI slop.
## When to Invoke
Invoke this skill when:
- Creating a new skill
- Amending an existing skill
- Evaluating whether a skill is needed
- Reviewing a PR that adds or modifies skills
## Prerequisites
- Skill directory: `.devin/skills/<skill-name>/SKILL.md`
- Validator: `tests/test_skills.py` (infra) or reference to it
- Tests: `tests/unit/test_skills_validation.py` (infra)
## When to Create a Skill
Create a skill when:
- An agent struggles with a task repeatedly (branch hygiene, PR order)
- A workflow has non-obvious ordering constraints (deployment coordination)
- A task requires specific tool usage over raw commands (CI monitoring)
- Multiple agents need shared context (dependency graph)
Do NOT create a skill for:
- One-off tasks (use a spec instead)
- Tasks already covered by AGENTS.md
- Tasks that are obvious from the Makefile or README
- Tasks that change frequently (skills should be stable)
## Skill Structure
Every skill MUST have:
```markdown
# <skill-name>
One-line description of what the skill does.
## When to Invoke
2-4 bullet points describing when to use this skill.
## Prerequisites
What must exist before using the skill (venv, .env, tools).
## <Core Content>
The actual guidance. Keep it actionable.
## Verification (if applicable)
How to verify the skill's guidance works.
## Common Mistakes (if applicable)
What agents get wrong without this skill.
```
## Quality Standards
### Do
- **Be specific.** Reference exact make targets, file paths, commands.
- **Be concise.** Each section should be scannable in under 30 seconds.
- **Be actionable.** Every paragraph should tell the agent what to DO.
- **Use tables** for command reference, mappings, and comparisons.
- **Use code blocks** for commands the agent should run.
- **Link to other skills** when related (e.g., "See `dependency-graph` skill").
### Don't
- **No preamble.** Don't start with "This skill helps agents..." — just state what it does.
- **No filler.** Don't repeat information from AGENTS.md or other skills.
- **No vague advice.** "Be careful with branches" is useless. "Run `git branch --show-current` before every commit" is useful.
- **No AI slop.** Don't write "In this comprehensive guide, we will explore..." — just give the guidance.
- **No redundant sections.** If "Common Mistakes" would repeat "When to Invoke", skip it.
- **No marketing.** Don't describe the skill as "powerful" or "comprehensive".
## Scope Rules
- **One skill per concern.** Don't mix branch hygiene with CI monitoring.
- **Project-specific, not generic.** Skills reference this repo's make targets, file paths, and conventions — not abstract advice.
- **Shared skills must be identical across repos.** Use `SHARED_SKILLS` in the validator to enforce this.
- **Per-repo skills must reflect that repo's reality.** Don't copy infra-specific targets to sso-bridge.
## Automated Validation
Every skill must pass the validator (`tests/test_skills.py`). The validator checks:
1. **Structure** — H1 title, "When to Invoke" section, "Prerequisites" section
2. **Commands** — referenced `make <target>` commands exist in Makefile or devx.mak
3. **Paths** — referenced file paths exist in the repo
4. **Shared skills** — identical content across repos (SHA-256 comparison)
5. **No drift** — no references to nonexistent commands or files
Run the validator:
```bash
python3 tests/test_skills.py --repo infra --repo sso-bridge
```
## Effectiveness Evaluation
### Static Checks (automated, CI)
The validator runs in CI as part of `make pytest-cov`. A failing skill
test blocks the PR. This catches:
- Missing sections
- Invalid commands
- Broken file references
- Cross-repo drift
### Runtime Metrics (manual, periodic)
Track these signals to evaluate skill effectiveness:
- **Skill invocation frequency** — how often agents invoke the skill
- **Success rate when invoked** — did the skill prevent the mistake it targets?
- **Feedback issues** — agents create Gitea issues with `feedback` label when a skill is unclear or wrong
- **Mistake recurrence** — if agents still make the mistake the skill targets, the skill needs improvement
### Retrospective Review
Periodically (monthly or after major incidents) review skills:
1. List all skills and their last-modified dates
2. Check for feedback issues tagged `skill-improvement`
3. Verify referenced commands still exist (run validator)
4. Remove skills that are no longer relevant
5. Update skills where mistakes still recur
6. Document lessons in this skill's "Common Mistakes" section
## Creating a New Skill — Checklist
- [ ] Identify the repeated struggle or non-obvious workflow
- [ ] Check no existing skill covers it
- [ ] Write the skill following the structure above
- [ ] Run `python3 tests/test_skills.py` — must pass
- [ ] Run `make pytest-cov` — must pass with 100% coverage
- [ ] If shared across repos, copy identical content to each repo
- [ ] Add the skill to `SHARED_SKILLS` in the validator if shared
- [ ] Create PR, verify CI passes, merge
@@ -0,0 +1,130 @@
# Spec-Driven Development
## Overview
Every change starts with a spec. No spec, no code. No code, no PR.
The spec is a markdown file at `docs/specs/<TASK-ID>.md` in the repo.
It contains structured requirements (REQ-IDs) and acceptance criteria
(AC checklist) that CI validates before merge.
## Workflow
1. **Create Vikunja task**`make create-task -- --title "Title" --description "..."`
2. **Write spec** — Create `docs/specs/<TASK-ID>.md` (see template below)
3. **Create branch**`git checkout -b <PREFIX>-N-short-description`
4. **Implement** — Write code with `# Implements: REQ-N` comments
5. **Check ACs** — Tick all acceptance criteria checkboxes in the spec
6. **Push and create PR**`make push-with-pr`
7. **CI validates** — Spec validation, PR size check, fast molecule, lint, tests
8. **Auto-merge** — Add `ready-to-merge` label after review
9. **Auto-deploy** — Post-merge deploys to staging (if nightly gate is green)
## Spec Template
```markdown
# <TASK-ID>: <Title>
## Problem
<What is broken or missing? Why does this change exist?>
## Approach
<How will you solve it? What are the key design decisions?>
REQ-1: <First requirement description>
REQ-2: <Second requirement description>
REQ-3: <Third requirement description>
## Test Plan
- <How will you verify each REQ is implemented correctly?>
- <Include unit tests, molecule scenarios, integration tests>
## Deploy Plan
- <How will this change be deployed?>
- <What order do components need to deploy in?>
- <Are there migrations or one-time operations?>
## Rollback Plan
- <How do you revert if something goes wrong?>
- <What data/state changes are irreversible?>
## Acceptance Criteria
- [ ] REQ-1: <criterion that proves REQ-1 is done>
- [ ] REQ-2: <criterion that proves REQ-2 is done>
- [ ] REQ-3: <criterion that proves REQ-3 is done>
```
## CI Validation
The `devx.ci.validate_spec` module checks:
1. **Spec file exists** at `docs/specs/<TASK-ID>.md` (TASK-ID from branch name)
2. **Required sections present**: Problem, Approach, Test Plan, Deploy Plan, Rollback Plan, Acceptance Criteria
3. **At least one REQ-ID** line (format: `REQ-N: <description>`)
4. **All AC checkboxes checked** (`- [x]`, not `- [ ]`)
If any check fails, CI blocks the PR before expensive jobs run.
## PR Size Limits
CI enforces max 500 lines / 10 files changed (excluding CHANGELOG.md,
README.md, badges, lock files). Oversized PRs are rejected. Split your
work into smaller PRs.
## Code-to-Spec Linking
Each function, task, or template that implements a requirement should
have a comment:
```python
# Implements: REQ-1
def install_sso_bridge():
...
```
```yaml
# Implements: REQ-2
- name: Clone infra repo
git:
...
```
## Fast Molecule (Pre-merge)
CI runs molecule only for **changed roles** (detected via git diff),
with converge + verify only, single platform. This gives quick feedback
(~5-10 min) without the full molecule suite.
## Full Molecule (Nightly)
The complete molecule suite (all scenarios, all platforms) runs nightly
at 02:00 CET on master. If it fails:
- A Gitea issue is created with the `feedback` label
- The `NIGHTLY_STATUS` repo variable is set to `failed:<run_id>`
- All staging deploys are blocked until nightly passes again
## Auto-Deploy on Merge
Every merged PR auto-deploys to staging (if nightly gate is green).
No manual trigger needed. The deploy runs the full pipeline:
provision → deploy-observability → deploy-customer → configure-oidc.
For grm/sso-bridge: post-merge publishes the package, then auto-creates
an infra PR to bump the pinned version. That infra PR auto-deploys when
merged.
## Key Commands
```bash
# Validate spec locally (before pushing)
python -m devx.ci.validate_spec --branch <PREFIX>-N-description
# Check PR size locally
python -m devx.ci.check_pr_size --base origin/master --head HEAD
# See which roles need fast molecule
python -m devx.ci.fast_molecule --base origin/master --head HEAD
# Check nightly gate status
python -m devx.ci.nightly_gate --repo oblachno/infra --action check
```
@@ -45,6 +45,13 @@ This runs `lint-all` + `pytest-cov`. The pre-push git hook only
validates the Vikunja task exists — it does NOT run tests. You must
run `make pre-push` manually.
### Spec-Driven Workflow
Every PR requires a spec file at `docs/specs/<TASK-ID>.md`. See the
`spec-driven-development` skill for the full workflow and template.
CI validates the spec (via `devx.ci.validate_spec`) and checks PR size
(via `devx.ci.check_pr_size`) before running expensive jobs.
## CI Failure Investigation
When investigating a CI failure:
+3
View File
@@ -77,6 +77,9 @@ jobs:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
PYTHONPATH: src
# Serialize blob uploads to avoid Gitea registry race condition
# (BlobUploader.Append offset mismatch — see DEVX-162).
DOCKER_MAX_CONCURRENT_UPLOADS: "1"
run: |
. .venv/bin/activate
export PATH="$HOME/.local/bin:$PATH"
+42 -13
View File
@@ -106,14 +106,30 @@ jobs:
--pr-title "$PR_TITLE" \
--repo "$REPOSITORY" \
--pr-number "$PR_NUMBER"
- name: Run automated PR review
- name: Validate spec file
if: github.event_name == 'pull_request'
env:
DEVX_TASK_PREFIX: DEVX
PYTHONPATH: ${{ env.PYTHONPATH }}
HEAD_REF: ${{ github.head_ref }}
run: |
. .venv/bin/activate 2>/dev/null || true
set -euo pipefail
python3 -m devx.ci.pr_review \
"${{ github.event.number }}" \
"${{ github.repository }}"
python3 -m devx.ci.validate_spec \
--branch "$HEAD_REF" \
--github-output
- name: Check PR size
if: github.event_name == 'pull_request'
env:
PYTHONPATH: ${{ env.PYTHONPATH }}
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.check_pr_size \
--base "origin/master" \
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
--repo "${{ github.repository }}" \
--pr-number "${{ github.event.number }}" \
--github-output
# --- release-dry-run step (conditional) ---
- name: Release dry-run validation
if: steps.detect.outputs.user-facing-changed == 'true'
@@ -167,16 +183,29 @@ jobs:
REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PR_NUMBER: ${{ github.event.number }}
REPOSITORY: ${{ github.repository }}
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.pr_review \
"$PR_NUMBER" \
"$REPOSITORY" \
--event APPROVE \
--checklist-confirmed \
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
--body "Auto-approved: all CI checks passed (validate job)."
# Post APPROVE review via Gitea API to satisfy branch protection.
# Try REVIEWER_GITEA_API_TOKEN first; fall back to CI_GITEA_API_TOKEN
# (CI bot account) if the reviewer token is the same user as the PR
# creator (Gitea rejects self-approvals).
for TOKEN in "${REVIEWER_GITEA_API_TOKEN}" "${CI_GITEA_API_TOKEN}"; do
[ -z "$TOKEN" ] && continue
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"event":"APPROVED","body":"Auto-approved: all CI checks passed (validate job)."}')
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | head -n -1)
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
echo "Approval posted successfully (HTTP $HTTP_CODE)."
break
fi
echo "::warning::Approval with token failed (HTTP $HTTP_CODE): ${BODY}"
done
- name: Squash merge with task ID
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
+30 -2
View File
@@ -83,7 +83,6 @@ src/devx/
│ ├── classify_changes.py # User-facing vs infrastructure change detection
│ ├── detect_release_commit.py # Detect release commits on master
│ ├── validate_commit_msg.py # Conventional commit validation
│ ├── pr_review.py # Automated PR review + manual reviews (--event, --body, --checklist-confirmed)
│ ├── post_merge.py # Vikunja task updates after merge
│ ├── sync_wiki.py # Sync documentation to Gitea wiki
│ ├── push_badges.py # Generate and push quality badges (--retries for retry on git push failures)
@@ -158,8 +157,37 @@ src/devx/
- **PYTHONPATH: src** — Workflows set `PYTHONPATH: src` (NOT `.:src` since there are no scripts at repo root)
- **Config via env vars** — `DEVX_*` environment variables with `.env` file fallback
## Spec-Driven Development
Every change starts with a spec. No spec, no code.
**Workflow:**
1. Create Vikunja task → get `<PREFIX>-N` task ID
2. Write spec at `docs/specs/<TASK-ID>.md` (see template in `.devin/skills/spec-driven-development/SKILL.md`)
3. Create branch, implement with `# Implements: REQ-N` comments
4. Tick all acceptance criteria checkboxes in spec
5. Push and create PR — CI validates spec before expensive jobs
**CI gates (pre-merge):**
- `devx.ci.validate_spec` — checks spec exists, has required sections, REQ-IDs, all ACs checked
- `devx.ci.check_pr_size` — max 500 lines / 10 files (excludes CHANGELOG, badges, locks)
- `devx.ci.fast_molecule` — converge+verify only for changed roles, single platform
**Nightly (infra only):**
- Full molecule suite (all scenarios, all platforms) + staging deploy + integration tests
- On failure: sets `NIGHTLY_STATUS=failed`, blocks staging deploys
- Post-merge auto-deploy to staging checks this gate before deploying
**Post-merge:**
- Infra: auto-deploys to staging (if nightly gate is green)
- GRM/sso-bridge: auto-publishes package, auto-creates infra dependency PR to bump pinned version
**Skill:** `.devin/skills/spec-driven-development/SKILL.md` — full template and workflow details.
## PR Workflow (Mandatory)
Every change to master goes through this workflow. No exceptions.
### Branch Protection (Required Gitea Settings)
@@ -210,7 +238,7 @@ docs: update README
### 6. Review the PR
**Automated review (CI `validate` job):** Every PR triggers an automated
review via `python -m devx.ci.pr_review` as a step in the `validate` job.
review via the `pr-review` skill (agent-invoked, not a CI step).
This posts a review with
`COMMENT` (no issues) or `REQUEST_CHANGES` (issues found):
+76
View File
@@ -2,6 +2,82 @@
All notable changes to this project will be documented in this file.
## [0.51.10] - 2026-09-17
### Bug Fixes
- Read Gitea repo-variable data field; fail closed on unknown nightly status
## [0.51.9] - 2026-08-26
### Bug Fixes
- Exclude docs/plans/* from PR size check
## [0.51.8] - 2026-08-26
### Bug Fixes
- Accept deps: as valid conventional commit type
## [0.51.7] - 2026-08-26
### Bug Fixes
- Increase HTTP 500 retry count to 5 with longer backoff and visible logging
## [0.51.6] - 2026-08-26
### Bug Fixes
- Use stderr=STDOUT to capture all docker push output in one stream
## [0.51.5] - 2026-08-26
### Bug Fixes
- Check stdout for HTTP 500 in _run_push (docker sends to stdout)
## [0.51.4] - 2026-08-26
### Bug Fixes
- Serialize registry uploads and retry on HTTP 500
### Refactor
- Remove cross-repo contract tests from devx
## [0.51.3] - 2026-08-26
### Bug Fixes
- Fall back to CI bot token for auto-merge approval
## [0.51.2] - 2026-08-26
### Bug Fixes
- Push-first strategy in build_image to avoid losing latest tag
## [0.51.1] - 2026-08-25
### Bug Fixes
- Delete existing manifest before push (Gitea #31964 workaround)
## [0.51.0] - 2026-08-25
### Features
- Add role defaults path to create_dependency_pr search
## [0.50.2] - 2026-08-24
### Bug Fixes
- Update check_pr_size usage example with --repo and --pr-number args
## [0.50.1] - 2026-08-15
### Bug Fixes
+9 -9
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)
[![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/a506e1130cab09747bb881e0fe98093e6bcd8110/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/a506e1130cab09747bb881e0fe98093e6bcd8110/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/a506e1130cab09747bb881e0fe98093e6bcd8110/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/a506e1130cab09747bb881e0fe98093e6bcd8110/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/a506e1130cab09747bb881e0fe98093e6bcd8110/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/a506e1130cab09747bb881e0fe98093e6bcd8110/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/09be76620c0a26418c614fb26b7feec2a4f457d3/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/09be76620c0a26418c614fb26b7feec2a4f457d3/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/09be76620c0a26418c614fb26b7feec2a4f457d3/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/09be76620c0a26418c614fb26b7feec2a4f457d3/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/09be76620c0a26418c614fb26b7feec2a4f457d3/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/09be76620c0a26418c614fb26b7feec2a4f457d3/python.svg)](https://www.python.org/downloads/)
## Why devx?
@@ -87,7 +87,7 @@ extra index and list devx in your dependencies:
```toml
[project]
dependencies = [
"devx>=0.50.1",
"devx>=0.51.10",
]
[tool.pip]
@@ -101,8 +101,8 @@ pip install -e .
```
> **Note:** If your project requires a specific devx version, pin it in
> `dependencies` (for example, `"devx==0.50.1"`) or use a version constraint
> (for example, `"devx>=0.50.1,<0.51"`).
> `dependencies` (for example, `"devx==0.51.10"`) or use a version constraint
> (for example, `"devx>=0.51.10,<0.52"`).
### Optional extras
+8 -8
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)
[![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/a506e1130cab09747bb881e0fe98093e6bcd8110/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/a506e1130cab09747bb881e0fe98093e6bcd8110/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/a506e1130cab09747bb881e0fe98093e6bcd8110/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/a506e1130cab09747bb881e0fe98093e6bcd8110/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/a506e1130cab09747bb881e0fe98093e6bcd8110/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/a506e1130cab09747bb881e0fe98093e6bcd8110/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/09be76620c0a26418c614fb26b7feec2a4f457d3/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/09be76620c0a26418c614fb26b7feec2a4f457d3/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/09be76620c0a26418c614fb26b7feec2a4f457d3/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/09be76620c0a26418c614fb26b7feec2a4f457d3/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/09be76620c0a26418c614fb26b7feec2a4f457d3/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/09be76620c0a26418c614fb26b7feec2a4f457d3/python.svg)](https://www.python.org/downloads/)
## Overview
@@ -74,14 +74,14 @@ Add devx to your `pyproject.toml` dependencies and configure the registry:
```toml
[project]
dependencies = [
"devx>=0.50.1",
"devx>=0.51.10",
]
[tool.pip]
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
```
Pin a specific version if needed: `"devx==0.50.1"` or `"devx>=0.50.1,<0.51"`.
Pin a specific version if needed: `"devx==0.51.10"` or `"devx>=0.51.10,<0.52"`.
### Optional extras
+46
View File
@@ -0,0 +1,46 @@
# DEVX-155: Replace pr_review with spec-driven CI gates and pr-review skill
## Problem
The `devx.ci.pr_review` module was a monolithic automated PR review tool that
ran in CI and posted COMMENT/REQUEST_CHANGES reviews. It duplicated logic now
better handled by an agent-invoked skill, and it blocked the introduction of
spec-driven development gates (validate_spec, check_pr_size) that should run
before expensive CI jobs.
## Approach
Remove `pr_review` and replace it with lightweight, focused CI gates plus a
new `pr-review` skill for deep agent-invoked reviews.
REQ-1: Add `devx.ci.validate_spec` — validates spec file exists, has required sections, REQ-IDs, all ACs checked
REQ-2: Add `devx.ci.check_pr_size` — enforces max 500 lines / 10 files (excludes CHANGELOG, badges, locks)
REQ-3: Add `devx.ci.fast_molecule` — detects changed roles, outputs fast molecule commands (converge+verify, single platform)
REQ-4: Add `devx.ci.nightly_gate` — checks/sets NIGHTLY_STATUS repo variable to block staging deploys on nightly failure
REQ-5: Add `devx.ci.create_dependency_pr` — auto-creates infra PR to bump pinned package version after grm/sso-bridge release
REQ-6: Remove `devx.ci.pr_review` module and `tests/unit/test_pr_review.py`
REQ-7: Update CI workflows to replace pr_review steps with validate_spec + check_pr_size + curl-based APPROVE
REQ-8: Add `spec-driven-development` and `pr-review` skills under `.devin/skills/`
REQ-9: Update AGENTS.md and skill docs to document the new spec-driven workflow
## Test Plan
- Unit tests for each new module (test_validate_spec, test_check_pr_size, test_fast_molecule, test_nightly_gate, test_create_dependency_pr, test_spec_driven_workflows)
- Remove test_pr_review.py and pr_review references from test_cli.py (pr_review.py deleted from source)
- Verify CI workflow YAML passes actionlint
## Deploy Plan
- Merge to master via auto-merge workflow
- devx post-merge publishes new version; downstream repos (grm, infra, sso-bridge) bump their devx pin
## Rollback Plan
- Revert the merge commit; downstream repos keep their current devx pin
- pr_review.py can be restored from git history if needed
## Acceptance Criteria
- [x] REQ-1: `devx.ci.validate_spec` module exists with `--branch` and `--github-output` options
- [x] REQ-2: `devx.ci.check_pr_size` module exists with `--base`, `--head`, `--github-output` options
- [x] REQ-3: `devx.ci.fast_molecule` module exists and outputs changed roles + commands
- [x] REQ-4: `devx.ci.nightly_gate` module exists with `--action check/set-passed/set-failed`
- [x] REQ-5: `devx.ci.create_dependency_pr` module exists with `--repo`, `--package`, `--new-version` options
- [x] REQ-6: The pr_review CI module and its test file are deleted from source tree
- [x] REQ-7: CI workflow uses validate_spec + check_pr_size + curl APPROVE instead of pr_review
- [x] REQ-8: `.devin/skills/spec-driven-development/SKILL.md` and `.devin/skills/pr-review/SKILL.md` exist
- [x] REQ-9: AGENTS.md documents spec-driven development workflow and pr-review skill
+34
View File
@@ -0,0 +1,34 @@
# DEVX-156: Fix commit message format and release new CI modules
## Problem
The DEVX-155 merge commit on master has an invalid format
('DEVX-155: Replace...' missing conventional commit type). This blocks
the post-merge release workflow's `validate_commit_msg` step, preventing
`validate_spec`, `check_pr_size`, `nightly_gate`, and `create_dependency_pr`
from being published to the Gitea PyPI registry. All downstream repos
(grm, infra, sso-bridge) are blocked — their CI fails with
`No module named devx.ci.validate_spec`.
## Approach
Add a trivial user-facing change (version doc comment) with a proper
conventional commit format to trigger the post-merge release workflow.
The release will publish the new CI modules that DEVX-155 introduced.
REQ-1: Add a user-facing change to src/devx/ to trigger release
REQ-2: Ensure the commit message follows conventional format (type: description)
## Test Plan
- Verify post-merge workflow runs successfully after merge
- Verify a new release tag is created (v0.51.0 or similar)
- Verify devx.ci.validate_spec is importable from the published package
## Deploy Plan
- Merge to master via auto-merge workflow
- Post-merge workflow auto-releases and publishes
## Rollback Plan
- Revert the merge commit if release fails
## Acceptance Criteria
- [x] REQ-1: A user-facing change is added to src/devx/
- [x] REQ-2: Commit message follows conventional format
+24
View File
@@ -0,0 +1,24 @@
# DEVX-157: Add role defaults path to create_dependency_pr search
## Problem
`create_dependency_pr` only searches `pyproject.toml` and the infra images vars file for pinned versions. The sso-bridge role pins its version in its role defaults file via `sso_bridge_version`, which is not searched.
## Approach
Add the sso-bridge role defaults path to the search paths.
REQ-1: Add ROLE_DEFAULTS_PATH constant pointing to the sso-bridge role defaults file
REQ-2: Include ROLE_DEFAULTS_PATH in the search loop
## Test Plan
- Verify existing tests pass
- Verify find_pinned_version finds sso_bridge_version in the defaults file
## Deploy Plan
- Merge to master, auto-release new devx version
## Rollback Plan
- Revert the merge commit
## Acceptance Criteria
- [x] REQ-1: ROLE_DEFAULTS_PATH constant added
- [x] REQ-2: search loop includes ROLE_DEFAULTS_PATH
+38
View File
@@ -0,0 +1,38 @@
# DEVX-158: Fix build-images workflow: delete existing manifest before push
## Problem
Gitea 1.27 has a known bug (#31964) where pushing a Docker image tag that
already exists in the container registry fails with HTTP 500 "package
version already exists." The build-images workflow has been failing for weeks because
every push to `ci-base:latest`, `ci-quality:latest`, and `ci-full:latest`
hits this error.
## Approach
Add a `delete_remote_manifest` function that deletes the existing manifest
via the Docker registry v2 API before pushing. This works around the Gitea
bug by ensuring the tag doesn't exist when the push starts.
REQ-1: Add `delete_remote_manifest` function using Docker registry v2 API
REQ-2: Call `delete_remote_manifest` before each `docker push` in `push_image`
REQ-3: Pass registry credentials from `main` to `push_image`
REQ-4: Handle errors gracefully — never block the push if delete fails
REQ-5: 100% test coverage for new code
## Test Plan
- Unit tests for `delete_remote_manifest` (success, 404, 500, network error)
- Unit tests for `push_image` with and without credentials
- Verify existing tests still pass
## Deploy Plan
- Merge to master, auto-release new devx version
- The build-images workflow will use the new code on the next run
## Rollback Plan
- Revert the merge commit
## Acceptance Criteria
- [x] REQ-1: `delete_remote_manifest` function added
- [x] REQ-2: Called before each push in `push_image`
- [x] REQ-3: Credentials passed from `main` to `push_image`
- [x] REQ-4: Errors don't block the push (returns True on failure)
- [x] REQ-5: 100% test coverage
+41
View File
@@ -0,0 +1,41 @@
# DEVX-159: Fix build_image push-first strategy to avoid losing latest tag
## Problem
The `push_image` function in `build_image.py` deletes the existing
manifest *before* pushing (Gitea #31964 workaround). When the push
fails for other reasons (HTTP 500), the old tag is lost, breaking all
CI jobs that use that image.
This caused `ci-base:latest` to disappear from the registry when
build-images run #4104 failed with HTTP 500 on push, after already
deleting the old `latest` manifest.
## Approach
Switch to a push-first strategy:
1. Try pushing directly
2. Only if push fails with "already exists" (Gitea #31964), delete
the old manifest and retry
3. If push fails for any other reason, the old manifest is preserved
REQ-1: Push first, no pre-emptive delete
REQ-2: Delete + retry only on "already exists" error
REQ-3: Old manifest preserved on non-already-exists failures
REQ-4: 100% test coverage of new logic
## Test Plan
- Unit tests for all push paths (success, already-exists retry,
non-already-exists failure, retry-also-fails)
- Verify existing tests still pass
## Deploy Plan
- Merge to master, build-images workflow uses new push logic on next
image rebuild
## Rollback Plan
- Revert the merge commit
## Acceptance Criteria
- [x] REQ-1: Push first, no pre-emptive delete
- [x] REQ-2: Delete + retry only on "already exists" error
- [x] REQ-3: Old manifest preserved on non-already-exists failures
- [x] REQ-4: 100% test coverage of new logic
+31
View File
@@ -0,0 +1,31 @@
# DEVX-160: Remove cross-repo contract tests from devx
## Problem
devx unit tests (`test_spec_driven_workflows.py`) were validating workflow
YAML and skill files in infra, grm, sso-bridge, and Mattermost OIDC repos.
This is an architecture violation — devx must not be aware of other repos.
Those repos consume devx; devx does not test them.
## Approach
Rewrite `test_spec_driven_workflows.py` to only test devx's own workflows
and skills. Remove all references to `_OBLACHNO_ROOT`, `_INFRA`, `_GRM`,
`_SSO_BRIDGE`, and parametrized repo lists.
REQ-1: No references to other repos in devx tests
REQ-2: All devx workflow/skill tests still pass
REQ-3: 100% coverage maintained
## Test Plan
- Run `pytest tests/unit/test_spec_driven_workflows.py` — all pass
- Run full test suite with coverage — 100%
## Deploy Plan
- Merge to master
## Rollback Plan
- Revert the merge commit
## Acceptance Criteria
- [x] REQ-1: No references to other repos in devx tests
- [x] REQ-2: All devx workflow/skill tests still pass
- [x] REQ-3: 100% coverage maintained
+27
View File
@@ -0,0 +1,27 @@
# DEVX-161: Fix auto-merge self-approval: use CI bot token fallback
## Problem
The auto-merge workflow posts an APPROVE review using
`REVIEWER_GITEA_API_TOKEN`. When this token belongs to the same user
who created the PR, Gitea rejects the self-approval, causing the merge
to fail with HTTP 405 "Does not have enough approvals."
## Approach
Try `REVIEWER_GITEA_API_TOKEN` first; if it fails (self-approval
rejection), fall back to `CI_GITEA_API_TOKEN` (kireto — CI bot account).
REQ-1: Auto-merge posts approval with fallback to CI bot token
REQ-2: Approval step reports which token succeeded
## Test Plan
- Create a PR and observe auto-merge succeeds
## Deploy Plan
- Merge to master
## Rollback Plan
- Revert the merge commit
## Acceptance Criteria
- [x] REQ-1: Auto-merge posts approval with fallback to CI bot token
- [x] REQ-2: Approval step reports which token succeeded
@@ -0,0 +1,41 @@
# DEVX-162: Fix registry push race condition: serialize uploads + retry on HTTP 500
## Problem
The Gitea container registry (v1.27.2) has a known race condition in
`BlobUploader.Append()` where concurrent blob uploads cause the file
offset and DB model to get out of sync, producing HTTP 500 "offset
mismatch between file and model" errors. This causes the build-images
workflow to fail intermittently when pushing runner images.
The `package_blob_upload` table accumulates stale entries from failed
uploads that worsen the problem over time.
## Approach
Two fixes in devx (a third fix — scheduled cleanup — is tracked
separately as OBL-INFRA-537):
1. Set `DOCKER_MAX_CONCURRENT_UPLOADS=1` in the build-images workflow
to serialize blob uploads and avoid the race condition.
2. Add HTTP 500 retry logic to `push_image` in `build_image.py`.
When a push fails with HTTP 500 (not "already exists"), retry up
to 3 times with exponential backoff (5s, 10s, 20s).
REQ-1: Build-images workflow sets DOCKER_MAX_CONCURRENT_UPLOADS=1
REQ-2: push_image retries on HTTP 500 with exponential backoff
REQ-3: All existing tests pass with 100% coverage
## Test Plan
- Unit tests for retry logic (mock subprocess)
- Manual: trigger build-images workflow and verify push succeeds
## Deploy Plan
- Merge to master
## Rollback Plan
- Revert the merge commit
## Acceptance Criteria
- [x] REQ-1: Build-images workflow sets DOCKER_MAX_CONCURRENT_UPLOADS=1
- [x] REQ-2: push_image retries on HTTP 500 with exponential backoff
- [x] REQ-3: All existing tests pass with 100% coverage
+58
View File
@@ -0,0 +1,58 @@
# DEVX-162: Fix Gitea repo-variable read contract and fail closed on unknown nightly status
## Problem
`GiteaClient.get_repo_variable()` reads `body["value"]`, but the deployed
Gitea returns the variable payload in the `data` field:
```json
{"owner_id":0,"repo_id":1,"name":"NIGHTLY_STATUS","data":"passed:5842","description":""}
```
Every read therefore returns `None`. `devx.ci.nightly_gate --action check`
interprets `None` as "bootstrap — allow deploy," so a real `failed:<run>` status
is invisible and the gate is permanently fail-open. Infra nightly run 5800 set
`NIGHTLY_STATUS=passed:5800` while platform/customer integration tests were
still failing — and even a correct `failed` value would have been ignored.
Additionally, an unrecognized non-empty status currently allows deploys
(fail-open instead of fail-closed).
Verified against the live API: `POST`/`PUT` accept `{"value": ...}` and work;
only the GET response uses `data`. The earlier `DEVX-162` spec (registry push
race) is preserved as `DEVX-162-registry-push-race-historical.md`.
## Approach
REQ-1: `src/devx/api_clients.py``get_repo_variable` reads `data` first
and falls back to `value` for older server/fixture compatibility. Write
path unchanged (PUT/POST `{"value": ...}` verified live: 201/204).
REQ-2: `src/devx/ci/nightly_gate.py` — unknown non-empty status blocks the
deploy (fail closed) instead of allowing it. Unset (bootstrap) still
allows.
REQ-3: Tests cover the `data` field, the `value` fallback, and fail-closed
unknown status.
## Test Plan
- `pytest tests/unit/test_api_clients.py tests/unit/test_nightly_gate.py`
- `make lint-all` (ruff, pyright, bandit, translations)
## Deploy Plan
Merge via auto-merge; post-merge workflow publishes a new devx package to the
Gitea PyPI registry and opens the infra dependency-bump PR automatically.
## Rollback Plan
Revert the commit; infra's pinned devx version keeps the previous behavior
until the dependency PR lands.
## Acceptance Criteria
- [x] `get_repo_variable` returns the `data` field and falls back to `value`.
- [x] Unknown nightly status exits non-zero and writes `nightly-gate-passed=false`.
- [x] Unit tests cover `data`, `value` fallback and fail-closed unknown status.
- [x] `make lint-all` and unit tests pass.
+33
View File
@@ -0,0 +1,33 @@
# DEVX-163: Fix _run_push to check stdout for HTTP 500
## Problem
`_run_push` only checked `result.stderr` for HTTP 500, but docker push
sends the "received unexpected HTTP status: 500 Internal Server Error"
message to **stdout**, not stderr. This means the tenacity retry logic
added in DEVX-162 never triggered — the push failed immediately without
retrying.
## Approach
Check both `result.stdout` and `result.stderr` for the "500" status code.
Also update the "already exists" check in `push_image` to check both
streams, since docker may send that message to stdout as well.
REQ-1: _run_push checks both stdout and stderr for HTTP 500
REQ-2: push_image "already exists" check uses combined stdout+stderr
REQ-3: All existing tests pass with 100% coverage
## Test Plan
- Unit tests for stdout 500 detection
- Unit tests for stderr 500 detection
- Manual: trigger build-images workflow and verify retry works
## Deploy Plan
- Merge to master
## Rollback Plan
- Revert the merge commit
## Acceptance Criteria
- [x] REQ-1: _run_push checks both stdout and stderr for HTTP 500
- [x] REQ-2: push_image "already exists" check uses combined stdout+stderr
- [x] REQ-3: All existing tests pass with 100% coverage
+34
View File
@@ -0,0 +1,34 @@
# DEVX-164: Increase HTTP 500 retry count and backoff for docker push
## Problem
The HTTP 500 retry logic (DEVX-162, DEVX-163) works correctly — 3 retry
attempts are made. But all 3 attempts fail because the Gitea registry's
"offset mismatch" race condition needs more than ~15s to recover. The
current backoff is 5s-20s with 3 attempts (total ~15s of waiting).
## Approach
Increase retry count from 3 to 5 and backoff from 5-20s to 10-60s,
giving the registry up to ~2 minutes to recover. Add visible logging
between retry attempts so the CI logs show the retry happening.
REQ-1: Increase retry count from 3 to 5
REQ-2: Increase backoff from 5-20s to 10-60s exponential
REQ-3: Add visible logging between retry attempts (click.echo)
REQ-4: All tests pass with 100% coverage
## Test Plan
- Unit tests verify retry count and backoff parameters
- Unit tests verify logging output on retry
- Manual: trigger build-images workflow and verify retries visible in logs
## Deploy Plan
- Merge to master
## Rollback Plan
- Revert the merge commit
## Acceptance Criteria
- [x] REQ-1: Increase retry count from 3 to 5
- [x] REQ-2: Increase backoff from 5-20s to 10-60s exponential
- [x] REQ-3: Add visible logging between retry attempts (click.echo)
- [x] REQ-4: All tests pass with 100% coverage
+31
View File
@@ -0,0 +1,31 @@
# DEVX-165: Accept deps: as valid conventional commit type
## Problem
The commit validator rejects `deps:` as a conventional commit type, causing
post-merge CI failures on grm and sso-bridge repos where automated dependency
bump PRs use `deps: bump devx...` as the commit message.
## Approach
REQ-1: Add `deps` to `CONVENTIONAL_RE` in `src/devx/config.py`
REQ-2: Update the allowed types list in the error message in
`src/devx/ci/validate_commit_msg.py`
REQ-3: Add test coverage for `deps:` type in `tests/unit/test_config.py`
and `tests/unit/test_validate_commit_msg.py`
## Test Plan
- `make pytest-cov` passes with 100% coverage
- `make lint-all` passes
## Deploy Plan
- Merge to master → post-merge auto-publishes new devx version
- grm and sso-bridge bump devx version to pick up the fix
## Rollback Plan
- Revert the merge commit
## Acceptance Criteria
- [x] REQ-1: Add `deps` to `CONVENTIONAL_RE` in `src/devx/config.py`
- [x] REQ-2: Update the allowed types list in the error message in
`src/devx/ci/validate_commit_msg.py`
- [x] REQ-3: Add test coverage for `deps:` type in `tests/unit/test_config.py`
and `tests/unit/test_validate_commit_msg.py`
+27
View File
@@ -0,0 +1,27 @@
# DEVX-166: Exclude docs/plans/* from PR size check
## Problem
Planning docs in `docs/plans/` are legitimately large (700+ lines) but
fail the PR size check (max 500 lines). This blocks PRs that only add
planning documents.
## Approach
REQ-1: Add `docs/plans/*` to `DEFAULT_EXCLUDED_PATTERNS` in
`src/devx/ci/check_pr_size.py`
REQ-2: Add test coverage for the new exclusion pattern
## Test Plan
- `make pytest-cov` passes with 100% coverage
- `make lint-all` passes
## Deploy Plan
- Merge to master → post-merge auto-publishes new devx version
- Infra PR #1179 picks up the fix once devx is bumped
## Rollback Plan
- Revert the merge commit
## Acceptance Criteria
- [x] REQ-1: Add `docs/plans/*` to `DEFAULT_EXCLUDED_PATTERNS` in
`src/devx/ci/check_pr_size.py`
- [x] REQ-2: Add test coverage for the new exclusion pattern
+64
View File
@@ -0,0 +1,64 @@
# DEVX-167: Add dependency-graph, deployment-coordination, and skill-creation skills
## Problem
Agents working across the oblachno ecosystem lack shared, persistent
context for three recurring pain points:
1. **Cross-repo dependency ordering** — agents frequently merge
downstream PRs before the upstream publish job completes, or forget
to bump infra. There is no single reference for which repo produces
what and in what order changes must propagate.
2. **devx release coordination** — devx is the base package pinned by
grm, sso-bridge, and infra. Agents repeatedly merge devx PRs and
immediately merge downstream bumps without waiting for the PyPI
publish job, or bump only one consumer when a change affects all
three.
3. **Skill quality drift** — skills are created ad hoc with inconsistent
structure, vague advice, and no automated validation reference. New
skills miss required sections, reference nonexistent make targets,
and drift across repos.
## Approach
Add three SKILL.md files under `.devin/skills/`:
REQ-1: `dependency-graph` — shared skill mapping the oblachno ecosystem
(repos, what each produces, consumers, release triggers, correct
cross-repo change order, state verification checklist)
REQ-2: `deployment-coordination` — devx-specific skill covering the
devx release flow, downstream consumers, manual bump procedure, and
common mistakes when coordinating a devx change
REQ-3: `skill-creation` — shared skill defining skill structure,
quality standards, scope rules, automated validation reference, and a
creation checklist
## Files Affected
- `.devin/skills/dependency-graph/SKILL.md` (new)
- `.devin/skills/deployment-coordination/SKILL.md` (new)
- `.devin/skills/skill-creation/SKILL.md` (new)
- `docs/specs/DEVX-167.md` (new)
## Test Plan
- Verify all three SKILL.md files follow the required structure (H1
title, When to Invoke, Prerequisites sections)
- Verify referenced make targets and file paths exist
- Run `make pytest-cov` — skill validation tests must pass
## Deploy Plan
- Merge to master; skills are consumed by agents immediately on next
invocation — no build or deploy step required
## Rollback Plan
- Revert the merge commit; remove the three skill directories
## Acceptance Criteria
- [x] REQ-1: dependency-graph skill exists with ecosystem map, repo
table, dependency chain, cross-repo change order, and state
verification checklist
- [x] REQ-2: deployment-coordination skill exists with devx release
flow, downstream consumer table, coordination steps, and common
mistakes
- [x] REQ-3: skill-creation skill exists with structure template,
quality standards, scope rules, validation reference, and
creation checklist
+2 -2
View File
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
```toml
[project]
dependencies = [
"devx>=0.50.1",
"devx>=0.51.10",
]
[project.optional-dependencies]
dev = [
"devx>=0.50.1",
"devx>=0.51.10",
]
```
+8 -2
View File
@@ -1,3 +1,9 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
"""devx — reusable development and CI/CD tools for oblachno-oss projects.
__version__ = "0.50.1"
Provides CI/CD automation (validate_spec, check_pr_size, nightly_gate,
create_dependency_pr, auto_merge, release, publish), developer tooling
(setup, install_tools, configure_repo, create_task, create_pr), and
molecule testing helpers for Ansible projects.
"""
__version__ = "0.51.10"
+4 -1
View File
@@ -392,7 +392,10 @@ class GiteaClient:
"""
try:
r = self._request("GET", f"/actions/variables/{name}")
return r.json().get("value")
body = r.json()
if "data" in body:
return body["data"]
return body.get("value")
except APIError as e:
if e.status == 404:
return None
+213
View File
@@ -0,0 +1,213 @@
#!/usr/bin/env python3
# Implements: REQ-2
"""Check PR size and reject oversized PRs.
Enforces max lines changed and max files changed to keep PRs small
and deployable. Generated/excluded files are not counted.
PRs with the ``refactoring`` label bypass the size check large but
legitimate refactoring PRs that touch many files in a coordinated way.
Usage:
python -m devx.ci.check_pr_size --base origin/master --head HEAD \\
--repo oblachno-oss/grm --pr-number 123
In CI, pass ``--github-output`` to set ``pr-size-ok`` and ``pr-size-detail``
for downstream steps.
"""
from __future__ import annotations
import subprocess # nosec B404
import click
from dotenv import load_dotenv
from devx.api_clients import GiteaClient
from devx.ci._shared import write_github_output
from devx.config import GITEA_API_URL
from devx.i18n import _
from devx.tokens import get_ci_token
load_dotenv()
# Files/patterns excluded from size counting (generated, badges, locks, etc.)
DEFAULT_EXCLUDED_PATTERNS = [
"CHANGELOG.md",
"README.md",
"docs/index.md",
"docs/plans/*",
"*.svg",
"uv.lock",
"poetry.lock",
"Pipfile.lock",
"package-lock.json",
"yarn.lock",
"go.sum",
]
DEFAULT_MAX_LINES = 500
DEFAULT_MAX_FILES = 10
REFACTORING_LABEL = "refactoring"
def has_refactoring_label(repo: str, pr_number: int) -> bool:
"""Check if a PR has the 'refactoring' label (bypasses size check)."""
try:
token = get_ci_token()
owner, repo_name = repo.split("/", 1)
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
pr = client.get_pr(pr_number)
labels = pr.get("labels", [])
return any(label.get("name") == REFACTORING_LABEL for label in labels)
except Exception:
return False
def get_diff_stats(base: str, head: str) -> list[tuple[str, int, int]]:
"""Get per-file diff stats (additions, deletions) between base and head.
Returns a list of (filename, additions, deletions) tuples.
"""
result = subprocess.run( # nosec B603 B607
["git", "diff", "--numstat", base, head],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
raise click.ClickException(_("git diff --numstat failed: {stderr}", stderr=result.stderr.strip()))
stats: list[tuple[str, int, int]] = []
for line in result.stdout.strip().split("\n"):
if not line:
continue
parts = line.split("\t")
if len(parts) != 3:
continue
additions_s, deletions_s, filename = parts
# Binary files show "-" for additions/deletions
additions = int(additions_s) if additions_s.isdigit() else 0
deletions = int(deletions_s) if deletions_s.isdigit() else 0
stats.append((filename, additions, deletions))
return stats
def is_excluded(filename: str, excluded_patterns: list[str]) -> bool:
"""Check if a filename matches any excluded pattern."""
from fnmatch import fnmatch
return any(fnmatch(filename, pat) for pat in excluded_patterns)
def check_size(
stats: list[tuple[str, int, int]],
max_lines: int,
max_files: int,
excluded_patterns: list[str],
) -> tuple[bool, str]:
"""Check diff stats against limits.
Returns (is_ok, detail_message).
"""
included = [(f, a, d) for f, a, d in stats if not is_excluded(f, excluded_patterns)]
total_lines = sum(a + d for _, a, d in included)
total_files = len(included)
if total_files == 0:
return True, "No non-excluded files changed"
if total_files > max_files:
return False, _(
"PR has {file_count} files changed (max {max_files}). Excluded: {excluded_count} files.",
file_count=total_files,
max_files=max_files,
excluded_count=len(stats) - total_files,
)
if total_lines > max_lines:
return False, _(
"PR has {line_count} lines changed (max {max_lines}). Excluded: {excluded_count} files.",
line_count=total_lines,
max_lines=max_lines,
excluded_count=len(stats) - total_files,
)
return True, _(
"PR size OK: {file_count} files, {line_count} lines (max {max_files} files, {max_lines} lines).",
file_count=total_files,
line_count=total_lines,
max_files=max_files,
max_lines=max_lines,
)
@click.command()
@click.option("--base", default="origin/master", help=_("Base ref for diff"))
@click.option("--head", default="HEAD", help=_("Head ref for diff"))
@click.option(
"--max-lines",
type=int,
default=DEFAULT_MAX_LINES,
help=_("Max lines changed (excluded files not counted)"),
)
@click.option(
"--max-files",
type=int,
default=DEFAULT_MAX_FILES,
help=_("Max files changed (excluded files not counted)"),
)
@click.option(
"--github-output",
"github_output",
is_flag=True,
default=False,
help=_("Write results to $GITHUB_OUTPUT"),
)
@click.option(
"--excluded",
"excluded",
multiple=True,
help=_("Additional excluded patterns (in addition to defaults)"),
)
@click.option("--repo", default=None, help=_("Repo (owner/name) for label check"))
@click.option("--pr-number", type=int, default=None, help=_("PR number for label check"))
def cli(
base: str,
head: str,
max_lines: int,
max_files: int,
github_output: bool,
excluded: tuple[str, ...],
repo: str | None,
pr_number: int | None,
) -> None:
"""Check PR size and reject oversized PRs."""
# Check for refactoring label bypass
if repo and pr_number and has_refactoring_label(repo, pr_number):
detail = _("PR has 'refactoring' label — size check bypassed.")
if github_output:
write_github_output("pr-size-ok", "true")
write_github_output("pr-size-detail", detail)
click.echo(f"[pr-size] {detail}")
return
excluded_patterns = list(DEFAULT_EXCLUDED_PATTERNS) + list(excluded)
stats = get_diff_stats(base, head)
is_ok, detail = check_size(stats, max_lines, max_files, excluded_patterns)
if github_output:
write_github_output("pr-size-ok", "true" if is_ok else "false")
write_github_output("pr-size-detail", detail)
if is_ok:
click.echo(f"[pr-size] {detail}")
else:
click.echo(f"[pr-size] FAILED: {detail}", err=True)
click.echo("", err=True)
click.echo("Oversized PRs cannot be reliably reviewed or deployed independently.", err=True)
click.echo("Split your work into smaller PRs, each addressing one concern.", err=True)
raise click.ClickException(_("PR size check failed."))
if __name__ == "__main__": # pragma: no cover
cli()
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
# Implements: REQ-5
"""Auto-create an infra PR to bump a pinned dependency version.
After grm or sso-bridge publishes a new package version, this module
creates a PR in the infra repo to bump the pinned version in
``pyproject.toml`` or ``ansible/group_vars/all/images.yml``.
Reuses ``devx.tools.create_pr`` for PR creation and Vikunja task linking.
Usage:
python -m devx.ci.create_dependency_pr \
--repo oblachno/infra \
--package grm \
--new-version 0.5.2 \
--source-repo oblachno/grm \
--source-run-id 12345
"""
from __future__ import annotations
import re
import subprocess # nosec B404
from pathlib import Path
import click
from dotenv import load_dotenv
from devx.api_clients import GiteaClient
from devx.config import GITEA_API_URL, VIKUNJA_API_URL, VIKUNJA_PROJECT_ID
from devx.exceptions import APIError
from devx.i18n import _
from devx.tokens import get_ci_token, get_vikunja_token
from devx.tools.create_pr import find_existing_pr
load_dotenv()
# Where infra pins dependency versions
PYPROJECT_PATH = "pyproject.toml"
IMAGES_YML_PATH = "ansible/group_vars/all/images.yml"
ROLE_DEFAULTS_PATH = "ansible/roles/sso_bridge/defaults/main.yml"
def find_pinned_version(package: str, file_path: str) -> str | None:
"""Find the currently pinned version of a package in a file.
Looks for patterns like:
- ``"grm @ git+...@v0.5.1"``
- ``grm = "0.5.1"``
- ``grm_version: "0.5.1"``
- ``grm_image_version: "0.5.1"``
"""
path = Path(file_path)
if not path.exists():
return None
content = path.read_text(encoding="utf-8")
# Match various pinning patterns
patterns = [
rf"{package}\s*@\s*git\+[^@]+@v?([\d.]+)", # pip: package @ git+url@vX.Y.Z
rf'{package}\s*=\s*"([\d.]+)"', # pyproject: package = "X.Y.Z"
rf'{package}_version:\s*"([\d.]+)"', # ansible vars: package_version: "X.Y.Z"
rf'{package}_image_version:\s*"([\d.]+)"', # ansible vars: package_image_version: "X.Y.Z"
]
for pat in patterns:
match = re.search(pat, content)
if match:
return match.group(1)
return None
def update_pinned_version(file_path: str, package: str, old_version: str, new_version: str) -> bool:
"""Update the pinned version in a file. Returns True if changed."""
path = Path(file_path)
if not path.exists():
return False
content = path.read_text(encoding="utf-8")
# Replace old version with new version in package-related lines
patterns = [
(rf"({package}\s*@\s*git\+[^@]+@v?){old_version}", rf"\g<1>{new_version}"),
(rf'({package}\s*=\s*"){old_version}(")', rf"\g<1>{new_version}\g<2>"),
(rf'({package}_version:\s*"){old_version}(")', rf"\g<1>{new_version}\g<2>"),
(rf'({package}_image_version:\s*"){old_version}(")', rf"\g<1>{new_version}\g<2>"),
]
new_content = content
changed = False
for pat, replacement in patterns:
new_content, n = re.subn(pat, replacement, new_content)
if n > 0:
changed = True
if changed:
path.write_text(new_content, encoding="utf-8")
return changed
def create_vikunja_task(title: str, description: str) -> str | None:
"""Create a Vikunja task and return its identifier (e.g., OBL-INFRA-531)."""
try:
token = get_vikunja_token()
except click.ClickException:
return None
from devx.api_clients import VikunjaClient
client = VikunjaClient(VIKUNJA_API_URL, token)
task = client.create_task(VIKUNJA_PROJECT_ID, title=title, description=description)
return str(task.get("identifier", ""))
@click.command()
@click.option("--repo", default="oblachno/infra", help=_("Target repo (owner/name) to create PR in"))
@click.option("--package", required=True, help=_("Package name to bump (e.g., grm, sso-bridge)"))
@click.option("--new-version", required=True, help=_("New version to pin"))
@click.option("--source-repo", required=True, help=_("Source repo that published (owner/name)"))
@click.option("--source-run-id", default="", help=_("CI run ID that triggered the publish"))
@click.option("--dry-run", is_flag=True, default=False, help=_("Show what would be done without creating PR"))
def cli(
repo: str,
package: str,
new_version: str,
source_repo: str,
source_run_id: str,
dry_run: bool,
) -> None:
"""Create an infra PR to bump a pinned dependency version."""
token = get_ci_token()
if "/" not in repo:
raise click.ClickException(_("Invalid repo format: {repo}", repo=repo))
owner, repo_name = repo.split("/", 1)
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
# Find current pinned version
old_version = None
changed_file = None
for f in [PYPROJECT_PATH, IMAGES_YML_PATH, ROLE_DEFAULTS_PATH]:
old_version = find_pinned_version(package, f)
if old_version:
changed_file = f
break
if not old_version:
click.echo(_("[dep-pr] Could not find pinned version for {pkg} in infra repo.", pkg=package))
if dry_run:
return
raise click.ClickException(_("Could not find pinned version for {pkg}", pkg=package))
if old_version == new_version:
click.echo(_("[dep-pr] {pkg} already at {version} — no PR needed.", pkg=package, version=new_version))
return
click.echo(
_(
"[dep-pr] Bumping {pkg} from {old} to {new} in {file}",
pkg=package,
old=old_version,
new=new_version,
file=changed_file,
)
)
if dry_run:
click.echo(f"[dep-pr] DRY RUN: would update {changed_file} and create PR")
return
# Create a branch
branch_name = f"deps/{package}-{new_version}"
base_branch = "master"
# Check for existing PR (reuse from tools.create_pr)
existing = find_existing_pr(client, branch_name)
if existing:
click.echo(_("[dep-pr] PR already exists: #{number}", number=existing.get("number", "?")))
return
# Create branch via API
try:
master_ref = client._request("GET", "/git/refs/heads/master").json()
master_sha = master_ref.get("object", {}).get("sha", "")
if not master_sha:
raise click.ClickException("Could not get master SHA")
client._request("POST", "/git/refs", json={"ref": f"refs/heads/{branch_name}", "sha": master_sha})
except APIError as e:
if "already exists" in str(e).lower():
click.echo(f"[dep-pr] Branch {branch_name} already exists")
else:
raise click.ClickException(_("Failed to create branch: {error}", error=str(e))) from None
# Clone, update file, commit, push
subprocess.run(["git", "fetch", "origin", f"{branch_name}"], check=False, capture_output=True) # nosec B603 B607
subprocess.run(["git", "checkout", branch_name], check=False, capture_output=True) # nosec B603 B607
if not changed_file or not update_pinned_version(changed_file, package, old_version, new_version):
raise click.ClickException(_("Failed to update {file}", file=changed_file))
subprocess.run(["git", "add", changed_file], check=True) # nosec B603 B607
commit_msg = f"deps: bump {package} from {old_version} to {new_version}"
subprocess.run(["git", "commit", "-m", commit_msg], check=True) # nosec B603 B607
subprocess.run(["git", "push", "origin", branch_name], check=True) # nosec B603 B607
# Create Vikunja task for tracking
task_title = f"Bump {package} to {new_version}"
task_desc = (
f"<p>Auto-created dependency bump PR.</p>"
f"<p>Package: {package}</p>"
f"<p>Version: {old_version}{new_version}</p>"
f"<p>Source: {source_repo} (run #{source_run_id})</p>"
)
task_id = create_vikunja_task(task_title, task_desc)
# Create PR directly (dependency PRs have custom titles, not Vikunja-derived)
pr_title = f"{task_id}: {task_title}" if task_id else task_title
pr_body = (
f"## Dependency Bump\n\n"
f"Bumps **{package}** from `{old_version}` to `{new_version}`.\n\n"
f"- **Source**: {source_repo}\n"
f"- **Triggered by**: CI run #{source_run_id}\n"
f"- **Changed file**: `{changed_file}`\n\n"
f"This PR was auto-created by `devx.ci.create_dependency_pr`.\n"
)
if task_id:
pr_body += f"\nCloses {task_id}"
pr = client.create_pr(title=pr_title, head=branch_name, base=base_branch, body=pr_body)
click.echo(_("[dep-pr] Created PR #{number}: {title}", number=pr.get("number", "?"), title=pr_title))
if __name__ == "__main__": # pragma: no cover
cli()
-1
View File
@@ -44,7 +44,6 @@ REQUIRED_SCRIPTS = [
"auto_merge.py",
"release.py",
"publish.py",
"pr_review.py",
"notify_failure.py",
"post_merge.py",
"classify_changes.py",
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
# Implements: REQ-3
"""Detect changed Ansible roles and output fast molecule test commands.
Reuses ``devx.molecule.molecule_changed`` for role detection (which handles
playbookrole mapping and shared infrastructure paths).
Fast molecule = converge + verify only, single platform, no idempotence
check. Used in pre-merge CI to get quick feedback on Ansible changes
without running the full molecule suite (which runs nightly).
Usage:
python -m devx.ci.fast_molecule --base origin/master --head HEAD
Outputs the list of changed roles and the molecule commands to run.
In CI, pass ``--github-output`` to set ``fast-molecule-roles`` (space-
separated) and ``fast-molecule-needed`` (true/false) for downstream steps.
"""
from __future__ import annotations
from pathlib import Path
import click
from dotenv import load_dotenv
from devx.ci._shared import write_github_output
from devx.i18n import _
from devx.molecule.molecule_changed import detect_changed_roles, get_changed_files
load_dotenv()
def get_molecule_scenarios(role_name: str, roles_dir: str = "ansible/roles") -> list[str]:
"""Get list of molecule scenario names for a role."""
mol_dir = Path(roles_dir) / role_name / "molecule"
if not mol_dir.is_dir():
return []
scenarios = []
for p in mol_dir.iterdir():
if p.is_dir() and (p / "molecule.yml").exists():
scenarios.append(p.name)
return sorted(scenarios)
def build_molecule_commands(
roles: set[str],
roles_dir: str = "ansible/roles",
platform: str = "ubuntu-2604",
) -> list[str]:
"""Build molecule test commands for changed roles.
For each role, runs each scenario with converge + verify only
(skip create/destroy between scenarios, skip idempotence).
"""
commands: list[str] = []
for role in sorted(roles):
scenarios = get_molecule_scenarios(role, roles_dir)
if not scenarios:
continue
for scenario in scenarios:
cmd = f"molecule test -s {scenario} --destroy=never --platform-name={platform}"
commands.append(cmd)
return commands
@click.command()
@click.option("--base", default="origin/master", help=_("Base ref for diff"))
@click.option("--head", default="HEAD", help=_("Head ref for diff"))
@click.option("--roles-dir", default="ansible/roles", help=_("Directory containing Ansible roles"))
@click.option("--platform", default="ubuntu-2604", help=_("Single platform to test against"))
@click.option(
"--github-output",
"github_output",
is_flag=True,
default=False,
help=_("Write results to $GITHUB_OUTPUT"),
)
def cli(
base: str,
head: str,
roles_dir: str,
platform: str,
github_output: bool,
) -> None:
"""Detect changed roles and output fast molecule test commands."""
# Use molecule_changed for role detection (handles playbooks, shared infra)
files = get_changed_files(base)
if not files:
click.echo("[fast-molecule] No files changed.")
if github_output:
write_github_output("fast-molecule-needed", "false")
write_github_output("fast-molecule-roles", "")
return
roles = detect_changed_roles(files)
if not roles:
click.echo("[fast-molecule] No Ansible roles changed.")
if github_output:
write_github_output("fast-molecule-needed", "false")
write_github_output("fast-molecule-roles", "")
return
commands = build_molecule_commands(roles, roles_dir, platform)
if github_output:
write_github_output("fast-molecule-needed", "true" if commands else "false")
write_github_output("fast-molecule-roles", " ".join(sorted(roles)))
click.echo(_("[fast-molecule] Changed roles: {roles}", roles=", ".join(sorted(roles))))
if not commands:
click.echo("[fast-molecule] No molecule scenarios found for changed roles.")
return
click.echo(f"[fast-molecule] {len(commands)} scenario(s) to run:")
for cmd in commands:
click.echo(f" {cmd}")
if __name__ == "__main__": # pragma: no cover
cli()
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
# Implements: REQ-4
"""Check if the nightly CI gate has passed; block staging deploys if it failed.
The nightly gate stores its status as a Gitea Actions repository variable
named ``NIGHTLY_STATUS`` on the infra repo. Values:
- ``passed`` nightly molecule + staging deploy + integration tests passed.
- ``failed:<run_id>`` nightly failed. Staging deploys are blocked until
the nightly passes again.
- (not set) nightly hasn't run yet. First deploy is allowed (bootstrap).
Usage:
python -m devx.ci.nightly_gate --repo oblachno/infra --action check
python -m devx.ci.nightly_gate --repo oblachno/infra --action set-passed --run-id 12345
python -m devx.ci.nightly_gate --repo oblachno/infra --action set-failed --run-id 12345
"""
from __future__ import annotations
import click
from dotenv import load_dotenv
from devx.api_clients import GiteaClient
from devx.ci._shared import write_github_output
from devx.config import GITEA_API_URL
from devx.i18n import _
from devx.tokens import get_ci_token
load_dotenv()
NIGHTLY_STATUS_VAR = "NIGHTLY_STATUS"
def get_nightly_status(client: GiteaClient) -> str:
"""Get the nightly status variable. Returns empty string if not set."""
val = client.get_repo_variable(NIGHTLY_STATUS_VAR)
return val or ""
def set_nightly_status(client: GiteaClient, status: str) -> None:
"""Set the nightly status variable."""
client.set_repo_variable(NIGHTLY_STATUS_VAR, status)
@click.command()
@click.option("--repo", required=True, help=_("Repository in owner/name format"))
@click.option(
"--action",
type=click.Choice(["check", "set-passed", "set-failed"]),
required=True,
help=_("Action to perform"),
)
@click.option("--run-id", default="", help=_("CI run ID (for set-failed/set-passed)"))
@click.option(
"--github-output",
"github_output",
is_flag=True,
default=False,
help=_("Write results to $GITHUB_OUTPUT"),
)
def cli(repo: str, action: str, run_id: str, github_output: bool) -> None:
"""Check or set the nightly CI gate status."""
token = get_ci_token()
if "/" not in repo:
raise click.ClickException(_("Invalid repo format: {repo}. Expected owner/name.", repo=repo))
owner, repo_name = repo.split("/", 1)
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
if action == "check":
status = get_nightly_status(client)
if not status:
# Bootstrap: no nightly has run yet, allow deploy
click.echo("[nightly-gate] No nightly status set — allowing deploy (bootstrap).")
if github_output:
write_github_output("nightly-gate-passed", "true")
write_github_output("nightly-status", "")
return
if status.startswith("passed"):
click.echo("[nightly-gate] Nightly passed. Deploy allowed.")
if github_output:
write_github_output("nightly-gate-passed", "true")
write_github_output("nightly-status", status)
elif status.startswith("failed"):
run_part = status.split(":", 1)[1] if ":" in status else ""
run_link = f" (run #{run_part})" if run_part else ""
click.echo(
_(
"[nightly-gate] Nightly FAILED{run}. Staging deploys are blocked until nightly passes.",
run=run_link,
),
err=True,
)
if github_output:
write_github_output("nightly-gate-passed", "false")
write_github_output("nightly-status", status)
raise click.ClickException(_("Nightly gate failed — staging deploy blocked."))
else:
click.echo(
_(
"[nightly-gate] Unknown nightly status: {status} — blocking deploy (fail closed).",
status=status,
),
err=True,
)
if github_output:
write_github_output("nightly-gate-passed", "false")
write_github_output("nightly-status", status)
raise click.ClickException(_("Unknown nightly status — staging deploy blocked."))
elif action == "set-passed":
set_nightly_status(client, f"passed:{run_id}" if run_id else "passed")
click.echo(_("[nightly-gate] Set NIGHTLY_STATUS=passed{run}", run=f":{run_id}" if run_id else ""))
if github_output:
write_github_output("nightly-status", f"passed:{run_id}" if run_id else "passed")
elif action == "set-failed":
set_nightly_status(client, f"failed:{run_id}" if run_id else "failed")
click.echo(_("[nightly-gate] Set NIGHTLY_STATUS=failed{run}", run=f":{run_id}" if run_id else ""))
if github_output:
write_github_output("nightly-status", f"failed:{run_id}" if run_id else "failed")
if __name__ == "__main__": # pragma: no cover
cli()
-715
View File
@@ -1,715 +0,0 @@
#!/usr/bin/env python3
"""Automated PR review: check architecture compliance, best practices, and quality.
Fetches the PR diff via the Gitea API, runs a series of automated checks,
and posts a structured review using GiteaClient.create_review.
Checks performed:
1. Architecture compliance no business logic in CLI, no direct subprocess
calls outside executor, no hardcoded config that should be in config.py
2. Best practices no bare except, no print() (use click.echo), no TODO/FIXME
left in merged code, no functions > 50 lines
3. Security no secrets in code, no shell=True, no eval/exec
4. i18n no raw English strings in click.echo() without _() wrapper
5. Resource management no open() without with statement, no subprocess without cleanup
6. Documentation new CLI commands documented, new modules in architecture.md
7. Test coverage 100% enforced by pytest-cov (checked in quality job)
8. Commit conventions conventional commit format on branch commits
Usage:
CI_GITEA_API_TOKEN=<token> [REVIEWER_GITEA_API_TOKEN=<token>] python3 -m devx.ci.pr_review <pr_number> <owner/repo>
"""
from __future__ import annotations
import os
import re
from dataclasses import dataclass, field
from typing import Any
import click
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
from devx.api_clients import GiteaClient
from devx.config import GITEA_API_URL
from devx.exceptions import APIError
from devx.i18n import _
from devx.tokens import get_ci_token, get_reviewer_token
load_dotenv()
# Files that are exempt from certain checks
WORKFLOW_ONLY_SUFFIXES = (".yml", ".yaml", ".md", ".json", ".toml", ".cfg", ".ini", ".txt")
PYTHON_SUFFIX = ".py"
# Architecture rules
CLI_FILE = "src/devx/cli.py"
EXECUTOR_FILE = "src/devx/executor.py"
CONFIG_FILE = "src/devx/config.py"
# Patterns that indicate business logic in CLI (should be in runner_manager.py)
BUSINESS_LOGIC_IN_CLI = [
(r"subprocess\.(run|call|Popen|check_output|check_call)", "subprocess call in CLI — delegate to executor.py"),
(r"\bos\.system\b", "os.system call in CLI — delegate to executor.py"),
(r"\bansible-playbook\b", "ansible-playbook reference in CLI — delegate to executor.py"),
]
# Patterns that indicate bad practices
BAD_PRACTICES = [
(r"\bprint\s*\(", "print() found — use click.echo() for user output"),
(r"\beval\s*\(", "eval() found — security risk, avoid dynamic code execution"),
(r"\bexec\s*\(", "exec() found — security risk, avoid dynamic code execution"),
(r"shell\s*=\s*True", "shell=True found — security risk, use shell=False with list args"),
(r"except\s*:", "bare except found — catch specific exceptions"),
(r"except\s+Exception\s*:", "broad Exception catch — catch specific exceptions"),
(r"#\s*(TODO|FIXME|HACK|XXX)", "TODO/FIXME found — resolve before merging"),
]
# Patterns for hardcoded config values that should be in config.py
HARDCODED_CONFIG = [
(r"https?://[a-z]+\.[a-z]+\.[a-z]+", "hardcoded URL — move to config.py with env var override"),
]
@dataclass
class ReviewResult:
"""Result of automated review checks."""
issues: list[dict[str, Any]] = field(default_factory=list)
summary: list[str] = field(default_factory=list)
@property
def has_issues(self) -> bool:
return bool(self.issues)
def add_issue(self, file_path: str, line: int, message: str, severity: str = "warning") -> None:
self.issues.append(
{
"path": file_path,
"body": f"[{severity}] {message}",
"new_position": line,
}
)
def add_summary(self, text: str) -> None:
self.summary.append(text)
def is_python_file(path: str) -> bool:
"""Check if a file is a Python source file."""
return path.endswith(PYTHON_SUFFIX) and not path.startswith("tests/")
def is_workflow_only(path: str) -> bool:
"""Check if a file is workflow/config/docs only (not Python source)."""
return path.endswith(WORKFLOW_ONLY_SUFFIXES) or path.startswith((".gitea/", "docs/", "ansible/"))
def check_architecture_compliance(files: list[dict[str, Any]], result: ReviewResult) -> None:
"""Check that changes follow the documented architecture."""
for f in files:
path = f.get("filename", "")
if not is_python_file(path):
continue
patch = f.get("patch", "")
if not patch:
continue
lines = patch.split("\n")
current_line = 0
for line in lines:
if line.startswith("@@"):
match = re.search(r"\+(\d+)", line)
if match:
current_line = int(match.group(1)) - 1
continue
if line.startswith("+") and not line.startswith("+++"):
current_line += 1
content = line[1:]
# Check for business logic in CLI
if path == CLI_FILE:
for pattern, msg in BUSINESS_LOGIC_IN_CLI:
if re.search(pattern, content):
result.add_issue(path, current_line, msg, "error")
if not result.issues:
result.add_summary("- Architecture compliance: OK")
def check_best_practices(files: list[dict[str, Any]], result: ReviewResult) -> None:
"""Check for common code quality issues."""
for f in files:
path = f.get("filename", "")
if not is_python_file(path):
continue
patch = f.get("patch", "")
if not patch:
continue
lines = patch.split("\n")
current_line = 0
for line in lines:
if line.startswith("@@"):
match = re.search(r"\+(\d+)", line)
if match:
current_line = int(match.group(1)) - 1
continue
if line.startswith("+") and not line.startswith("+++"):
current_line += 1
content = line[1:]
for pattern, msg in BAD_PRACTICES:
if re.search(pattern, content):
result.add_issue(path, current_line, msg, "warning")
if not any(i["body"].startswith("[warning]") for i in result.issues):
result.add_summary("- Best practices: OK")
def check_security(files: list[dict[str, Any]], result: ReviewResult) -> None:
"""Check for security issues in changed files."""
for f in files:
path = f.get("filename", "")
if not is_python_file(path):
continue
patch = f.get("patch", "")
if not patch:
continue
lines = patch.split("\n")
current_line = 0
for line in lines:
if line.startswith("@@"):
match = re.search(r"\+(\d+)", line)
if match:
current_line = int(match.group(1)) - 1
continue
if line.startswith("+") and not line.startswith("+++"):
current_line += 1
content = line[1:]
# Check for hardcoded secrets
secret_re = r'(token|password|secret|key)\s*=\s*["\'][^"\']{8,}["\']' # nosec B105
is_secret = re.search(secret_re, content, re.IGNORECASE)
is_comment = content.strip().startswith("#")
is_example = "your-" in content or "example" in content
if is_secret and not is_comment and not is_example:
result.add_issue(
path,
current_line,
"potential hardcoded secret — use environment variable",
"error",
)
if not any(i["body"].startswith("[error]") and "secret" in i["body"] for i in result.issues):
result.add_summary("- Security: OK")
def check_i18n(files: list[dict[str, Any]], result: ReviewResult) -> None:
"""Check that user-facing strings are wrapped in _().
Detects ``click.echo()`` calls with raw string literals that are not
wrapped in ``_()``. Only checks ``src/`` files, not tests or scripts.
"""
# Pattern: click.echo("...") or click.echo(f"...") without _() wrapper
raw_echo_re = re.compile(r'click\.echo\s*\(\s*["\']([^"\']+)["\']')
raw_fstring_re = re.compile(r'click\.echo\s*\(\s*f["\']')
# Also check click.ClickException and raise with string
raw_exception_re = re.compile(r'click\.ClickException\s*\(\s*["\']([^"\']+)["\']')
for f in files:
path = f.get("filename", "")
if not is_python_file(path) or not path.startswith("src/"):
continue
patch = f.get("patch", "")
if not patch:
continue
lines = patch.split("\n")
current_line = 0
for line in lines:
if line.startswith("@@"):
match = re.search(r"\+(\d+)", line)
if match:
current_line = int(match.group(1)) - 1
continue
if line.startswith("+") and not line.startswith("+++"):
current_line += 1
content = line[1:]
# Skip comments and docstrings
stripped = content.strip()
if stripped.startswith("#") or stripped.startswith('"""') or stripped.startswith("'''"):
continue
# Check for raw strings in click.echo without _()
for regex, msg in [
(raw_echo_re, "click.echo() with raw string — wrap in _() for i18n"),
(raw_fstring_re, "click.echo() with f-string — wrap in _() for i18n"),
(raw_exception_re, "ClickException with raw string — wrap in _() for i18n"),
]:
if regex.search(content):
result.add_issue(path, current_line, msg, "warning")
if not any("i18n" in i["body"] for i in result.issues):
result.add_summary("- i18n: OK")
def check_resource_management(files: list[dict[str, Any]], result: ReviewResult) -> None:
"""Check for resource leaks: open() without with, subprocess without cleanup.
Detects:
- ``open()`` calls not in a ``with`` statement
- ``subprocess.Popen()`` without ``.wait()`` or ``.communicate()``
"""
# Pattern: open("...") not preceded by "with" on the same line
open_re = re.compile(r"(?<!with\s)\bopen\s*\(")
popen_re = re.compile(r"subprocess\.Popen\s*\(")
for f in files:
path = f.get("filename", "")
if not is_python_file(path):
continue
patch = f.get("patch", "")
if not patch:
continue
lines = patch.split("\n")
current_line = 0
for line in lines:
if line.startswith("@@"):
match = re.search(r"\+(\d+)", line)
if match:
current_line = int(match.group(1)) - 1
continue
if line.startswith("+") and not line.startswith("+++"):
current_line += 1
content = line[1:]
# Skip comments
if content.strip().startswith("#"):
continue
# Check for open() without with
if open_re.search(content) and "with " not in content:
result.add_issue(
path, current_line, "open() without with statement — potential resource leak", "warning"
)
# Check for Popen without communicate/wait on same line
if popen_re.search(content) and ".communicate" not in content and ".wait" not in content:
result.add_issue(
path,
current_line,
"subprocess.Popen() without immediate .communicate() or .wait() — ensure cleanup",
"warning",
)
if not any("resource" in i["body"].lower() for i in result.issues):
result.add_summary("- Resource management: OK")
def check_function_length(files: list[dict[str, Any]], result: ReviewResult) -> None:
"""Check that no new function is excessively long (> 50 lines)."""
for f in files:
path = f.get("filename", "")
if not is_python_file(path):
continue
patch = f.get("patch", "")
if not patch:
continue
# Count consecutive added lines within a function
lines = patch.split("\n")
current_line = 0
func_start = 0
func_name = ""
added_in_func = 0
for line in lines:
if line.startswith("@@"):
if func_name and added_in_func > 50:
result.add_issue(
path,
func_start,
f"function '{func_name}' adds {added_in_func} lines — consider splitting (> 50 lines)",
"warning",
)
match = re.search(r"\+(\d+)", line)
if match:
current_line = int(match.group(1)) - 1
func_name = ""
added_in_func = 0
continue
if line.startswith("+") and not line.startswith("+++"):
current_line += 1
content = line[1:]
func_match = re.match(r"\s*def\s+(\w+)\s*\(", content)
if func_match:
if func_name and added_in_func > 50:
result.add_issue(
path,
func_start,
f"function '{func_name}' adds {added_in_func} lines — consider splitting (> 50 lines)",
"warning",
)
func_name = func_match.group(1)
func_start = current_line
added_in_func = 0
else:
added_in_func += 1
elif line.startswith(" ") or line.startswith("-"):
pass # context or removed line
# Check last function
if func_name and added_in_func > 50:
result.add_issue(
path,
func_start,
f"function '{func_name}' adds {added_in_func} lines — consider splitting (> 50 lines)",
"warning",
)
def check_documentation(files: list[dict[str, Any]], result: ReviewResult) -> None:
"""Check that documentation is updated for relevant changes."""
has_src_changes = any(
is_python_file(f.get("filename", "")) and f.get("filename", "").startswith("src/") for f in files
)
has_doc_changes = any(
f.get("filename", "").startswith("docs/") or f.get("filename", "") in ("README.md", "AGENTS.md", "CHANGELOG.md")
for f in files
)
has_ansible_changes = any(f.get("filename", "").startswith("ansible/") for f in files)
has_tofu_changes = any(f.get("filename", "").startswith("tofu/") for f in files)
has_workflow_changes = any(f.get("filename", "").startswith(".gitea/") for f in files)
# Check for TODO/FIXME in changed docs
todo_issues: list[str] = []
for f in files:
filename = f.get("filename", "")
if filename.endswith(".md") and filename.startswith(("docs/", "README", "AGENTS")):
# Can't check file content from PR API easily, but flag if patch adds TODO
patch = f.get("patch", "")
if patch and re.search(r"^\+.*\b(TODO|FIXME|HACK|XXX)\b", patch, re.IGNORECASE):
todo_issues.append(f"{filename}: new TODO/FIXME added in documentation")
if has_src_changes and not has_doc_changes:
result.add_summary("- Documentation: WARNING — source files changed but no docs updated")
elif has_ansible_changes and not has_doc_changes:
result.add_summary("- Documentation: WARNING — Ansible role changed but no docs updated")
elif has_tofu_changes and not has_doc_changes:
result.add_summary("- Documentation: WARNING — OpenTofu changes but no docs updated")
elif has_workflow_changes and not has_doc_changes:
result.add_summary("- Documentation: INFO — workflow changes (consider updating CI docs if behavior changed)")
else:
result.add_summary("- Documentation: OK")
if todo_issues:
for issue in todo_issues:
result.add_summary(f"- Documentation: WARNING — {issue}")
def check_test_coverage(files: list[dict[str, Any]], result: ReviewResult) -> None:
"""Check that tests are updated for source changes."""
has_src_changes = any(
is_python_file(f.get("filename", "")) and f.get("filename", "").startswith("src/") for f in files
)
has_test_changes = any(f.get("filename", "").startswith("tests/") for f in files)
if has_src_changes and not has_test_changes:
result.add_summary("- Tests: WARNING — source files changed but no test files updated")
else:
result.add_summary("- Tests: OK")
def check_commit_conventions(client: GiteaClient, pr_number: str, result: ReviewResult) -> None:
"""Check that PR commits follow conventional commit format.
Verifies that at least one commit on the PR branch matches the
conventional commit pattern (type: description). Merge commits
and revert commits are exempt.
"""
try:
commits = client.get_pr_commits(pr_number)
except APIError as e:
result.add_summary(f"- Commit conventions: ERROR — could not fetch commits: {e.message}")
return
if not commits:
result.add_summary("- Commit conventions: OK (no commits to check)")
return
from devx.config import CONVENTIONAL_RE
has_conventional = False
non_conventional: list[str] = []
for commit in commits:
commit_info = commit.get("commit", {})
message = str(commit_info.get("message", "") if isinstance(commit_info, dict) else "").split("\n")[0]
# Skip merge commits and revert commits
if message.startswith(("Merge", "Revert")):
continue
if CONVENTIONAL_RE.match(message):
has_conventional = True
else:
non_conventional.append(message[:60])
if has_conventional:
result.add_summary("- Commit conventions: OK")
elif non_conventional:
result.add_summary(
f"- Commit conventions: WARNING — no conventional commit found. "
f"Non-conventional commits: {', '.join(non_conventional[:3])}"
)
else:
result.add_summary("- Commit conventions: OK (all commits are merges/reverts)")
def run_review(client: GiteaClient, pr_number: str) -> ReviewResult:
"""Run all review checks and return the result."""
result = ReviewResult()
try:
files = client.get_pr_files(pr_number)
except APIError as e:
result.add_summary(f"- ERROR: Could not fetch PR files: {e.message}")
return result
if not files:
result.add_summary("- No files changed in this PR")
return result
# Run all checks
check_architecture_compliance(files, result)
check_best_practices(files, result)
check_security(files, result)
check_i18n(files, result)
check_resource_management(files, result)
check_function_length(files, result)
check_documentation(files, result)
check_test_coverage(files, result)
check_commit_conventions(client, pr_number, result)
return result
def build_review_body(result: ReviewResult) -> str:
"""Build the review body text from the review result."""
lines = ["## Automated PR Review", ""]
for item in result.summary:
lines.append(item)
if result.issues:
lines.append("")
lines.append(f"**{len(result.issues)} issue(s) found:**")
lines.append("")
for issue in result.issues:
lines.append(f"- `{issue['path']}:{issue['new_position']}` — {issue['body']}")
else:
lines.append("")
lines.append("No issues found by automated checks.")
lines.append("")
lines.append("---")
lines.append("**Auto-merge:** If all CI checks pass, this PR will be merged automatically.")
return "\n".join(lines)
def post_review(client: GiteaClient, pr_number: str, result: ReviewResult) -> dict[str, Any]:
"""Post the review to the PR.
Uses REQUEST_CHANGES when issues are found, COMMENT otherwise.
Never uses APPROVE the bot shares the PR author's token, so
Gitea rejects self-approval. The actual APPROVE must come from
the manual review step.
"""
body = build_review_body(result)
event = "REQUEST_CHANGES" if result.has_issues else "COMMENT"
comments = result.issues if result.has_issues else []
return client.create_review(pr_number, event=event, body=body, comments=comments)
def _post_manual_review(
client: GiteaClient,
pr_number: str,
event: str,
body: str | None,
checklist_confirmed: bool,
checklist_categories: str | None,
dry_run: bool,
owner: str | None = None,
repo_name: str | None = None,
) -> None:
"""Post a manual review with validation for APPROVE events.
When self-approval is rejected (reviewer token belongs to PR author),
falls back to the CI token (different user) if available.
"""
if not body or len(body) < 50:
raise click.ClickException(_("Review body must be at least 50 characters."))
if event == "APPROVE":
if not checklist_confirmed:
raise click.ClickException(
_("--checklist-confirmed is required for APPROVE events."),
)
cats = [c.strip() for c in (checklist_categories or "").split(",") if c.strip()]
cat_nums: list[int] = []
for c in cats:
try:
cat_nums.append(int(c))
except ValueError:
raise click.ClickException(
_("Invalid checklist category: {cat}. Must be numbers.", cat=c),
) from None
if len(cat_nums) < 8:
raise click.ClickException(
_("--checklist-categories must list at least 8 of 13 categories. Got {count}.", count=len(cat_nums)),
)
click.echo(f"Manual review event: {event}")
click.echo(f"Body: {body[:80]}...")
if checklist_confirmed:
click.echo(f"Checklist confirmed: {checklist_categories}")
if dry_run:
click.echo("\n[dry-run] Review not posted.")
return
try:
review = client.create_review(pr_number, event=event, body=body)
except APIError as e:
if "approve" in e.message.lower() or "422" in str(e.status):
# Self-approval not allowed (reviewer token belongs to PR author).
# Fall back to CI token (different user) if available.
ci_token = os.environ.get("CI_GITEA_API_TOKEN", "").strip()
if ci_token and owner and repo_name:
click.echo(_("Note: Self-approval not allowed with reviewer token. Retrying with CI token."))
ci_client = GiteaClient(GITEA_API_URL, ci_token, owner, repo_name)
try:
review = ci_client.create_review(pr_number, event=event, body=body)
except APIError:
click.echo(_("Note: CI token also cannot approve. Posting COMMENT instead."))
review = client.create_review(pr_number, event="COMMENT", body=body)
else:
click.echo(_("Note: Self-approval not allowed. Posting COMMENT instead."))
review = client.create_review(pr_number, event="COMMENT", body=body)
else:
raise
review_id = review.get("id", "?")
click.echo(
_(
"\nReview #{review_id} posted on PR #{pr_number} with event '{event}'.",
review_id=review_id,
pr_number=pr_number,
event=event,
)
)
@click.command()
@click.argument("pr_number")
@click.argument("repo")
@click.option("--dry-run", is_flag=True, default=False, help="Print review without posting.")
@click.option(
"--event",
type=click.Choice(["APPROVE", "REQUEST_CHANGES", "COMMENT"], case_sensitive=False),
default=None,
help="Post a manual review with the given event (skips automated checks).",
)
@click.option("--body", default=None, help="Review body text (required with --event).")
@click.option(
"--checklist-confirmed",
is_flag=True,
default=False,
help="Attest that REVIEW_CHECKLIST.md categories were checked (required for APPROVE).",
)
@click.option(
"--checklist-categories",
default=None,
help="Comma-separated checklist category numbers (required for APPROVE, min 8 of 13).",
)
def main(
pr_number: str,
repo: str,
dry_run: bool,
event: str | None,
body: str | None,
checklist_confirmed: bool,
checklist_categories: str | None,
) -> None:
"""Run automated PR review and post results to Gitea.
Without --event: runs automated checks and posts COMMENT/REQUEST_CHANGES.
With --event: posts a manual review (skips automated checks).
"""
try:
token = get_reviewer_token() if (event and event.upper() == "APPROVE") else get_ci_token()
except click.ClickException:
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) from None
owner, repo_name = repo.split("/")
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
if event is not None:
_post_manual_review(
client,
pr_number,
event.upper(),
body,
checklist_confirmed,
checklist_categories,
dry_run,
owner=owner,
repo_name=repo_name,
)
return
result = run_review(client, pr_number)
body = build_review_body(result)
event = "REQUEST_CHANGES" if result.has_issues else "COMMENT"
click.echo(f"Review event: {event}")
click.echo(f"Issues found: {len(result.issues)}")
click.echo("")
click.echo(body)
if dry_run:
click.echo("\n[dry-run] Review not posted.")
return
try:
review = post_review(client, pr_number, result)
except APIError as e:
if "approve" in e.message.lower() or "422" in str(e.status):
click.echo(_("Note: Self-approval not allowed. Posting COMMENT instead."))
review = client.create_review(pr_number, event="COMMENT", body=body)
else:
raise
review_id = review.get("id", "?")
click.echo(
_(
"\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).",
review_id=review_id,
pr_number=pr_number,
event=event,
num_comments=len(result.issues),
)
)
if __name__ == "__main__": # pragma: no cover
main()
+1 -1
View File
@@ -118,7 +118,7 @@ def main(commit_msg_file: str | None, branch: str | None, from_git: bool) -> Non
" Expected: <type>: <description>\n"
" Got: {subject}\n"
" Allowed types: feat, fix, chore, docs, style, refactor,\n"
" perf, test, ci, build, revert, BREAKING CHANGE",
" perf, test, ci, build, deps, revert, BREAKING CHANGE",
subject=subject,
)
)
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
# Implements: REQ-1
"""Validate that a PR has a spec file with required sections and acceptance criteria.
Spec-driven development gate. Runs in CI before expensive jobs.
Used by grm, infra, sso-bridge, and devx itself.
Validates:
1. A spec file exists at ``docs/specs/<TASK-ID>.md`` (TASK-ID extracted from branch).
2. The spec contains required sections: Problem, Approach, Test Plan, Deploy Plan, Rollback Plan.
3. The spec contains REQ-ID lines (``REQ-N: <description>``).
4. The spec contains an Acceptance Criteria checklist with at least one item.
5. All acceptance criteria checkboxes are checked (``- [x]``).
Usage:
python -m devx.ci.validate_spec --branch OBL-INFRA-531-fix-foo
In CI, also pass ``--github-output`` to set ``spec-valid`` and ``spec-path``
for downstream steps.
"""
from __future__ import annotations
import re
from pathlib import Path
import click
from dotenv import load_dotenv
from devx.ci._shared import extract_task_id, write_github_output
from devx.i18n import _
load_dotenv()
REQUIRED_SECTIONS = [
"## Problem",
"## Approach",
"## Test Plan",
"## Deploy Plan",
"## Rollback Plan",
"## Acceptance Criteria",
]
REQ_ID_RE = re.compile(r"^REQ-\d+:\s+.+", re.MULTILINE)
AC_CHECKED_RE = re.compile(r"^\s*- \[x\]\s+.+", re.MULTILINE)
AC_UNCHECKED_RE = re.compile(r"^\s*- \[ \]\s+.+", re.MULTILINE)
def find_spec_file(task_id: str, specs_dir: str = "docs/specs") -> Path | None:
"""Find the spec file for the given task ID.
Looks for ``docs/specs/<TASK-ID>.md`` (case-insensitive filename).
Returns the Path if found, None otherwise.
"""
base = Path(specs_dir)
if not base.is_dir():
return None
# Exact match (case-insensitive)
for p in base.glob("*.md"):
if p.stem.upper() == task_id.upper():
return p
return None
def validate_spec_content(content: str) -> list[str]:
"""Validate spec content and return a list of error messages.
Returns an empty list if the spec is valid.
"""
errors: list[str] = []
# Check required sections
for section in REQUIRED_SECTIONS:
if section not in content:
errors.append(_("Missing required section: {section}", section=section))
# Check for at least one REQ-ID
req_ids = REQ_ID_RE.findall(content)
if not req_ids:
errors.append(_("No REQ-ID lines found. Each requirement must be labeled (e.g., 'REQ-1: <description>')."))
# Check acceptance criteria has at least one item
checked = AC_CHECKED_RE.findall(content)
unchecked = AC_UNCHECKED_RE.findall(content)
if not checked and not unchecked:
errors.append(_("Acceptance Criteria section has no checklist items. Add at least one '- [ ] item'."))
elif unchecked:
errors.append(
_(
"Acceptance Criteria has {count} unchecked item(s). All AC items must be checked (- [x]) before merge.",
count=len(unchecked),
)
)
return errors
@click.command()
@click.option("--branch", required=True, help=_("Branch name (e.g., OBL-INFRA-531-fix-foo)"))
@click.option("--specs-dir", default="docs/specs", help=_("Directory containing spec files"))
@click.option(
"--github-output",
"github_output",
is_flag=True,
default=False,
help=_("Write results to $GITHUB_OUTPUT"),
)
@click.option("--allow-missing", is_flag=True, default=False, help=_("Allow missing spec (warn only, don't fail)"))
def cli(branch: str, specs_dir: str, github_output: bool, allow_missing: bool) -> None:
"""Validate that a spec file exists and has required content."""
task_id = extract_task_id(branch)
if not task_id:
msg = _("No task ID found in branch name '{branch}'. Expected format: <PREFIX>-N-description.", branch=branch)
if allow_missing:
click.echo(f"WARNING: {msg}")
if github_output:
write_github_output("spec-valid", "false")
write_github_output("spec-path", "")
return
raise click.ClickException(msg)
spec_path = find_spec_file(task_id, specs_dir)
if spec_path is None:
msg = _(
"No spec file found for task {task_id} in {dir}/. Expected: {dir}/{task_id}.md",
task_id=task_id,
dir=specs_dir,
)
if allow_missing:
click.echo(f"WARNING: {msg}")
if github_output:
write_github_output("spec-valid", "false")
write_github_output("spec-path", "")
return
raise click.ClickException(msg)
content = spec_path.read_text(encoding="utf-8")
errors = validate_spec_content(content)
if github_output:
write_github_output("spec-valid", "true" if not errors else "false")
write_github_output("spec-path", str(spec_path))
if errors:
click.echo("", err=True)
click.echo("=" * 60, err=True)
click.echo(f"Spec validation FAILED for {spec_path}:", err=True)
click.echo("=" * 60, err=True)
for e in errors:
click.echo(f" - {e}", err=True)
raise click.ClickException(_("Spec validation failed."))
click.echo(_("[spec-check] Spec validated: {path}", path=spec_path))
if __name__ == "__main__": # pragma: no cover
cli()
-7
View File
@@ -116,13 +116,6 @@ def ci_post_merge(args: tuple[str, ...]) -> None:
_run_module("devx.ci.post_merge", list(args))
@ci.command("pr-review")
@click.argument("args", nargs=-1)
def ci_pr_review(args: tuple[str, ...]) -> None:
"""Run automated PR review."""
_run_module("devx.ci.pr_review", list(args))
@ci.command("publish")
@click.argument("args", nargs=-1)
def ci_publish(args: tuple[str, ...]) -> None:
+1 -1
View File
@@ -93,4 +93,4 @@ RETRY_BACKOFF_BASE = 2 # seconds: 2, 4, 8
RETRY_STATUS_CODES = {429, 500, 502, 503, 504}
# Conventional commit regex — used by validate_commit_msg.py
CONVENTIONAL_RE = re.compile(r"^(feat|fix|chore|docs|style|refactor|perf|test|ci|build|revert)(\(.+\))?: .+")
CONVENTIONAL_RE = re.compile(r"^(feat|fix|chore|docs|style|refactor|perf|test|ci|build|revert|deps)(\(.+\))?: .+")
+1 -11
View File
@@ -109,7 +109,7 @@ devx-ensure-venv:
fi
.PHONY: devx-create-task devx-create-pr devx-push devx-push-with-pr devx-check-config
.PHONY: devx-pr-status devx-pr-logs devx-pr-label devx-pr-review devx-rebase devx-pr-rebase
.PHONY: devx-pr-status devx-pr-logs devx-pr-label devx-rebase devx-pr-rebase
.PHONY: devx-configure-gitea-pypi devx-install-tools devx-install-checkmake devx-checkmake
.PHONY: devx-workflow-lint devx-workflow-dryrun devx-workflow-dryrun-safe devx-workflow-check
.PHONY: devx-notify-failure devx-install-hooks devx-activate-scripts devx-venv devx-ensure-venv
@@ -171,16 +171,6 @@ devx-pr-label:
$(if $(PR),--pr $(PR)) \
--label $(or $(LABEL),ready-to-merge)
# Usage: make devx-pr-review PR=42 EVENT=APPROVE BODY="..." CHECKLIST=1,2,3,4,5,6,7,8,9,10,11,12,13
# make devx-pr-review PR=42 EVENT=REQUEST_CHANGES BODY="..."
# make devx-pr-review PR=42 (auto review)
devx-pr-review:
@$(DEVX_PYTHON) -m devx.ci.pr_review \
$(PR) $(DEVX_REPO_OWNER)/$(DEVX_REPO_NAME) \
$(if $(EVENT),--event $(EVENT)) \
$(if $(BODY),--body "$(BODY)") \
$(if $(CHECKLIST),--checklist-confirmed --checklist-categories $(CHECKLIST))
# Rebase current branch onto origin/master and force-push
# Usage: make devx-rebase
# make devx-rebase NO_PUSH=1
+2 -16
View File
@@ -2,19 +2,16 @@
Centralizes Gitea/Vikunja token discovery with role-based environment
variable names and backwards compatibility with the legacy
``CI_GITEA_TOKEN`` / ``REVIEW_GITEA_TOKEN`` naming convention.
``CI_GITEA_TOKEN`` naming convention.
Roles:
- ``CI_GITEA_API_TOKEN``: CI workflows (read actions, post status, merge, etc.)
- ``REVIEWER_GITEA_API_TOKEN``: PR approval reviews (must be a different user
from the PR author for Gitea to accept the review as an approval)
- ``DEVELOPER_GITEA_API_TOKEN``: local development tools (create-task,
create-pr, setup, etc.)
Fallbacks:
- New role names are checked first.
- Legacy names (``CI_GITEA_TOKEN``, ``REVIEW_GITEA_TOKEN``) are accepted for
backwards compatibility.
- Legacy names (``CI_GITEA_TOKEN``) are accepted for backwards compatibility.
- If no role-specific token is set, the generic CI tokens are tried last.
"""
@@ -28,12 +25,6 @@ from devx.i18n import _
# Token environment variable names, in lookup priority order.
CI_TOKEN_NAMES = ["CI_GITEA_API_TOKEN", "CI_GITEA_TOKEN"]
REVIEWER_TOKEN_NAMES = [
"REVIEWER_GITEA_API_TOKEN",
# Legacy name used before role-based tokens.
"REVIEW_GITEA_TOKEN",
*CI_TOKEN_NAMES,
]
DEVELOPER_TOKEN_NAMES = ["DEVELOPER_GITEA_API_TOKEN", *CI_TOKEN_NAMES]
VIKUNJA_TOKEN_NAMES = ["VIKUNJA_TOKEN"]
@@ -61,11 +52,6 @@ def get_ci_token() -> str:
return get_token(*CI_TOKEN_NAMES)
def get_reviewer_token() -> str:
"""Resolve the reviewer Gitea API token used for PR approvals."""
return get_token(*REVIEWER_TOKEN_NAMES)
def get_developer_token() -> str:
"""Resolve the developer Gitea API token used for local tooling."""
return get_token(*DEVELOPER_TOKEN_NAMES)
+168 -10
View File
@@ -40,13 +40,17 @@ and ``CI_GITEA_USERNAME`` environment variables, matching the existing CI workfl
from __future__ import annotations
import base64
import json
import os
import subprocess # nosec B404
import urllib.error
import urllib.request
from dataclasses import dataclass, field
from pathlib import Path
import click
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
from devx.i18n import _
from devx.tokens import get_developer_token
@@ -188,38 +192,188 @@ def build_image(
return True
def delete_remote_manifest(
registry: str,
name: str,
tag: str,
username: str,
token: str,
*,
dry_run: bool = False,
) -> bool:
"""Delete an existing manifest from the Gitea container registry.
Gitea 1.27 has a bug (#31964) where pushing a tag that already exists
fails with HTTP 500 "package version already exists". This function
deletes the existing manifest before the push to work around it.
Returns True if deleted or not found, False on unexpected errors.
"""
manifest_url = f"https://{registry}/v2/{name}/manifests/{tag}"
if dry_run:
click.echo(f"[dry-run] DELETE {manifest_url}")
return True
# First, get the digest via HEAD
req = urllib.request.Request(manifest_url, method="HEAD") # nosec B310
auth_str = f"{username}:{token}"
req.add_header("Authorization", f"Basic {base64.b64encode(auth_str.encode()).decode()}")
req.add_header("Accept", "application/vnd.docker.distribution.manifest.v2+json")
try:
with urllib.request.urlopen(req, timeout=30) as resp: # nosec B310
digest = resp.headers.get("Docker-Content-Digest")
except urllib.error.HTTPError as e:
if e.code == 404:
return True # Tag doesn't exist — nothing to delete
if e.code == 405:
# HEAD not supported — try GET with a range
pass
else:
click.echo(f" Warning: HEAD {tag} returned {e.code}", err=True)
return True # Don't block the push
except urllib.error.URLError as e:
click.echo(f" Warning: HEAD {tag} failed: {e}", err=True)
return True # Don't block the push
else:
if not digest:
return True
# Delete by digest
del_url = f"https://{registry}/v2/{name}/manifests/{digest}"
del_req = urllib.request.Request(del_url, method="DELETE") # nosec B310
del_req.add_header("Authorization", f"Basic {base64.b64encode(auth_str.encode()).decode()}")
try:
with urllib.request.urlopen(del_req, timeout=30) as resp: # nosec B310
click.echo(f" Deleted existing {tag} (digest: {digest[:19]}...)")
except urllib.error.HTTPError as e:
if e.code == 404:
return True # Already gone
click.echo(f" Warning: DELETE {tag} returned {e.code}", err=True)
return True # Don't block the push
except urllib.error.URLError as e:
click.echo(f" Warning: DELETE {tag} failed: {e}", err=True)
return True
return True
class PushHTTP500Error(Exception):
"""Raised when docker push fails with an HTTP 500 from the registry."""
def _run_push(cmd: list[str]) -> subprocess.CompletedProcess[str]:
"""Run a docker push command, raising PushHTTP500Error on registry 500.
The Gitea container registry (v1.27.x) has a race condition in
BlobUploader.Append that causes intermittent HTTP 500 "offset
mismatch" errors during concurrent blob uploads. Retrying the
push gives the registry time to recover.
Docker sends push progress/errors to both stdout and stderr depending
on the error type, so both streams are checked for the 500 status.
Uses stderr=STDOUT to merge both streams into stdout, ensuring all
output is captured in one place (docker push output behavior varies
depending on TTY detection).
"""
result = subprocess.run( # nosec B603
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
check=False,
)
if result.returncode != 0 and "500" in (result.stdout or ""):
raise PushHTTP500Error(result.stdout.strip())
return result
def push_image(
spec: ImageSpec,
registry: str,
*,
dry_run: bool = False,
username: str = "",
token: str = "",
) -> bool:
"""Push all tags of a Docker image to the registry.
Returns True if all pushes succeed, False if any fail.
Push-first strategy: try pushing directly. Only if the push fails
with Gitea #31964 ("package version already exists") do we delete
the old manifest and retry. This avoids losing the existing tag
when the push fails for unrelated reasons (e.g. HTTP 500).
HTTP 500 errors from the Gitea registry race condition are retried
up to 3 times with exponential backoff (5s, 10s) via tenacity.
"""
full_tags = [build_full_tag(registry, spec.name, t) for t in spec.tags]
all_ok = True
for ft in full_tags:
for ft, tag in zip(full_tags, spec.tags, strict=False):
cmd = ["docker", "push", ft]
if dry_run:
click.echo(f"[dry-run] {' '.join(cmd)}")
continue
click.echo(f"Pushing {ft}...")
result = subprocess.run( # nosec B603
cmd,
capture_output=True,
text=True,
check=False,
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=10, min=10, max=60),
retry=retry_if_exception_type(PushHTTP500Error),
before_sleep=lambda retry_state: click.echo(
_(
" HTTP 500 from registry, retrying in {wait:.0f}s (attempt {attempt}/5)...",
wait=retry_state.next_action.sleep if retry_state.next_action else 0,
attempt=retry_state.attempt_number + 1,
),
err=True,
),
reraise=True,
)
if result.returncode != 0:
def _attempt(_cmd: list[str] = cmd) -> subprocess.CompletedProcess[str]:
return _run_push(_cmd)
try:
result = _attempt()
except PushHTTP500Error as e:
click.echo(
_("Push failed for {tag}: {error}", tag=ft, error=result.stderr.strip()),
_("Push failed for {tag}: {error}", tag=ft, error=str(e)),
err=True,
)
all_ok = False
else:
continue
if result.returncode == 0:
click.echo(f"Pushed {ft}")
continue
combined_output = (result.stdout or "").strip()
# Gitea #31964: push fails because tag already exists.
# Delete the old manifest and retry once.
if username and token and "already exists" in combined_output.lower():
click.echo(" Tag exists (Gitea #31964), deleting old manifest and retrying...")
delete_remote_manifest(
registry,
spec.name,
tag,
username,
token,
dry_run=dry_run,
)
click.echo(f" Retrying push {ft}...")
result = subprocess.run( # nosec B603
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
check=False,
)
if result.returncode == 0:
click.echo(f"Pushed {ft} (after retry)")
continue
combined_output = (result.stdout or "").strip()
click.echo(
_("Push failed for {tag}: {error}", tag=ft, error=combined_output),
err=True,
)
all_ok = False
return all_ok
@@ -320,11 +474,15 @@ def main(
raise click.ClickException(_("Registry login failed"))
failed: list[str] = []
push_username = "" # nosec B105
push_token = "" # nosec B105
if push:
push_username, push_token = _get_registry_creds()
for spec in specs:
if not build_image(spec, registry, dry_run=dry_run, pull=pull):
failed.append(spec.name)
continue
if push and not push_image(spec, registry, dry_run=dry_run): # type: ignore[arg-type]
if push and not push_image(spec, registry, dry_run=dry_run, username=push_username, token=push_token): # type: ignore[arg-type]
failed.append(spec.name)
if failed:
+1882 -1530
View File
@@ -1,51 +1,51 @@
{
"\n=== Summary ===": {
"bg": "\n=== Summary ===",
"de": "\n=== Summary ===",
"bg": "\n=== Обобщение ===",
"de": "\n=== Zusammenfassung ===",
"en": "\n=== Summary ===",
"pl": "\n=== Podsumowanie ===",
"ru": "\n=== Summary ===",
"zh": "\n=== Summary ==="
"ru": "\n=== Сводка ===",
"zh": "\n=== 摘要 ==="
},
"\nAll documentation coverage checks passed!": {
"bg": "\nAll documentation coverage checks passed!",
"de": "\nAll documentation coverage checks passed!",
"bg": "\nВсички проверки за покритие на документацията преминаха успешно!",
"de": "\nAlle Dokumentations-Abdeckungsprüfungen bestanden!",
"en": "\nAll documentation coverage checks passed!",
"pl": "\nWszystkie kontrole pokrycia dokumentacji zakończone pomyślnie!",
"ru": "\nAll documentation coverage checks passed!",
"zh": "\nAll documentation coverage checks passed!"
"ru": "\nВсе проверки покрытия документации пройдены!",
"zh": "\n所有文档覆盖率检查均已通过!"
},
"\nCHANGELOG version ordering:": {
"bg": "\nCHANGELOG version ordering:",
"de": "\nCHANGELOG version ordering:",
"bg": "\nПодреждане на версиите в CHANGELOG:",
"de": "\nReihenfolge der CHANGELOG-Versionen:",
"en": "\nCHANGELOG version ordering:",
"pl": "\nKolejność wersji w CHANGELOG:",
"ru": "\nCHANGELOG version ordering:",
"zh": "\nCHANGELOG version ordering:"
"ru": "\nПорядок версий в CHANGELOG:",
"zh": "\nCHANGELOG 版本顺序:"
},
"\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...",
"bg": "\nПроверка на документацията за CI скриптове в ci-cd-workflow.md...",
"de": "\nPrüfe CI-Skript-Dokumentation 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...",
"zh": "\nChecking CI script documentation in ci-cd-workflow.md..."
"ru": "\nПроверка документации CI-скриптов в ci-cd-workflow.md...",
"zh": "\n正在检查 ci-cd-workflow.md 中的 CI 脚本文档..."
},
"\nChecking module documentation in architecture.md...": {
"bg": "\nChecking module documentation in architecture.md...",
"de": "\nChecking module documentation in architecture.md...",
"bg": "\nПроверка на документацията за модулите в architecture.md...",
"de": "\nPrüfe Moduldokumentation 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...",
"zh": "\nChecking module documentation in architecture.md..."
"ru": "\nПроверка документации модулей в architecture.md...",
"zh": "\n正在检查 architecture.md 中的模块文档..."
},
"\nDoc coverage: {covered}/{total} ({pct}%)": {
"bg": "\nDoc coverage: {covered}/{total} ({pct}%)",
"de": "\nDoc coverage: {covered}/{total} ({pct}%)",
"bg": "\nПокритие на документацията: {covered}/{total} ({pct}%)",
"de": "\nDokumentationsabdeckung: {covered}/{total} ({pct}%)",
"en": "\nDoc coverage: {covered}/{total} ({pct}%)",
"pl": "\nPokrycie dokumentacji: {covered}/{total} ({pct}%)",
"ru": "\nDoc coverage: {covered}/{total} ({pct}%)",
"zh": "\nDoc coverage: {covered}/{total} ({pct}%)"
"ru": "\nПокрытие документации: {covered}/{total} ({pct}%)",
"zh": "\n文档覆盖率:{covered}/{total} ({pct}%)"
},
"\nDone! Synced: {synced}, Pruned: {pruned}": {
"bg": "",
@@ -56,20 +56,20 @@
"zh": ""
},
"\nDone. Deleted {deleted}, kept {kept}, failed {failed}.": {
"bg": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}.",
"de": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}.",
"bg": "\nГотово. Изтрити: {deleted}, запазени: {kept}, неуспешни: {failed}.",
"de": "\nFertig. Gelöscht: {deleted}, behalten: {kept}, fehlgeschlagen: {failed}.",
"en": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}.",
"pl": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}.",
"ru": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}.",
"zh": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}."
"pl": "\nGotowe. Usunięto: {deleted}, zachowano: {kept}, błędów: {failed}.",
"ru": "\nГотово. Удалено: {deleted}, сохранено: {kept}, ошибок: {failed}.",
"zh": "\n完成。已删除 {deleted},保留 {kept},失败 {failed}"
},
"\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.",
"bg": "\nГРЕШКА: Покритието на документацията не е 100%. Използвайте --fail-on-missing за налагане.",
"de": "\nFEHLER: Die Dokumentationsabdeckung beträgt nicht 100%. Mit --fail-on-missing erzwingen.",
"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.",
"zh": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce."
"ru": "\nОШИБКА: Покрытие документации не составляет 100%. Используйте --fail-on-missing для принудительной проверки.",
"zh": "\n错误:文档覆盖率未达到 100%。使用 --fail-on-missing 强制执行。"
},
"\nFAIL: {n} stale version reference(s) found:": {
"bg": "",
@@ -80,12 +80,12 @@
"zh": ""
},
"\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.": {
"bg": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
"de": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
"bg": "\nКоригирайте несъответстващите тагове преди създаване на нови версии. Изпълнете 'python3 -m devx.ci.release --verify' за пълен отчет.",
"de": "\nKorrigieren Sie die falsch zugeordneten Tags, bevor Sie neue Releases erstellen. Führen Sie 'python3 -m devx.ci.release --verify' für einen vollständigen Bericht aus.",
"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.",
"zh": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report."
"ru": "\nИсправьте несоответствующие теги перед созданием новых релизов. Выполните 'python3 -m devx.ci.release --verify' для полного отчёта.",
"zh": "\n请在创建新版本之前修正不匹配的标签。运行 'python3 -m devx.ci.release --verify' 获取完整报告。"
},
"\nFixed {n} stale version reference(s).": {
"bg": "",
@@ -96,36 +96,36 @@
"zh": ""
},
"\nGenerated {count} badges:": {
"bg": "\nGenerated {count} badges:",
"de": "\nGenerated {count} badges:",
"bg": "\nГенерирани {count} значка:",
"de": "\n{count} Badges generiert:",
"en": "\nGenerated {count} badges:",
"pl": "\nGenerated {count} badges:",
"ru": "\nGenerated {count} badges:",
"zh": "\nGenerated {count} badges:"
"pl": "\nWygenerowano {count} odznak:",
"ru": "\nСгенерировано значков: {count}:",
"zh": "\n已生成 {count} 个徽章:"
},
"\nKeeping {kept}, would delete {count}": {
"bg": "\nKeeping {kept}, would delete {count}",
"de": "\nKeeping {kept}, would delete {count}",
"bg": "\nЗапазени {kept}, ще бъдат изтрити {count}",
"de": "\nBehalte {kept}, würde {count} löschen",
"en": "\nKeeping {kept}, would delete {count}",
"pl": "\nKeeping {kept}, would delete {count}",
"ru": "\nKeeping {kept}, would delete {count}",
"zh": "\nKeeping {kept}, would delete {count}"
"pl": "\nZachowano {kept}, usunięto by {count}",
"ru": "\nСохранено {kept}, будет удалено {count}",
"zh": "\n保留 {kept},将删除 {count}"
},
"\nLatest tag: {tag}": {
"bg": "\nLatest tag: {tag}",
"de": "\nLatest tag: {tag}",
"bg": "\nПоследен таг: {tag}",
"de": "\nNeuestes Tag: {tag}",
"en": "\nLatest tag: {tag}",
"pl": "\nNajnowszy tag: {tag}",
"ru": "\nLatest tag: {tag}",
"zh": "\nLatest tag: {tag}"
"ru": "\nПоследний тег: {tag}",
"zh": "\n最新标签:{tag}"
},
"\nMissing documentation:": {
"bg": "\nMissing documentation:",
"de": "\nMissing documentation:",
"bg": "\nЛипсваща документация:",
"de": "\nFehlende Dokumentation:",
"en": "\nMissing documentation:",
"pl": "\nBrakująca dokumentacja:",
"ru": "\nMissing documentation:",
"zh": "\nMissing documentation:"
"ru": "\nОтсутствующая документация:",
"zh": "\n缺失的文档:"
},
"\nNo stale version references found.": {
"bg": "",
@@ -144,28 +144,12 @@
"zh": ""
},
"\nResult: {status}": {
"bg": "\nResult: {status}",
"de": "\nResult: {status}",
"bg": "\nРезултат: {status}",
"de": "\nErgebnis: {status}",
"en": "\nResult: {status}",
"pl": "\nWynik: {status}",
"ru": "\nResult: {status}",
"zh": "\nResult: {status}"
},
"\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).",
"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).",
"zh": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments)."
},
"\nReview #{review_id} posted on PR #{pr_number} with event '{event}'.": {
"bg": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}'.",
"de": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}'.",
"en": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}'.",
"pl": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}'.",
"ru": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}'.",
"zh": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}'."
"ru": "\nРезультат: {status}",
"zh": "\n结果:{status}"
},
"\nRun with --fix to auto-update version references.": {
"bg": "",
@@ -176,28 +160,36 @@
"zh": ""
},
"\nTag → Commit alignment:": {
"bg": "\nTag → Commit alignment:",
"de": "\nTagCommit alignment:",
"bg": "\nСъответствие таг → комит:",
"de": "\nTag-→-Commit-Zuordnung:",
"en": "\nTag → Commit alignment:",
"pl": "\nTag → Commit: zgodność:",
"ru": "\nTag → Commit alignment:",
"zh": "\nTag → Commit alignment:"
"ru": "\nСоответствие тег → коммит:",
"zh": "\n标签 → 提交对应关系:"
},
"\nTest isolation check FAILED: {count} violation(s) in {files} file(s).\n": {
"bg": "\nПроверката за изолация на тестовете СЕ ПРОВАЛИ: {count} нарушение(я) във {files} файл(а).\n",
"de": "\nTestisolierungsprüfung FEHLGESCHLAGEN: {count} Verstoß/Verstöße in {files} Datei(en).\n",
"en": "\nTest isolation check FAILED: {count} violation(s) in {files} file(s).\n",
"pl": "\nKontrola izolacji testów NIEUDANA: {count} naruszeń w {files} plikach.\n",
"ru": "\nПроверка изоляции тестов ПРОВАЛЕНА: {count} нарушение(й) в {files} файл(ах).\n",
"zh": "\n测试隔离检查失败:{files} 个文件中存在 {count} 处违规。\n"
},
"\nUntagged release commits:": {
"bg": "\nUntagged release commits:",
"de": "\nUntagged release commits:",
"bg": "\nРелийз комити без таг:",
"de": "\nRelease-Commits ohne Tag:",
"en": "\nUntagged release commits:",
"pl": "\nCommity wydania bez tagu:",
"ru": "\nUntagged release commits:",
"zh": "\nUntagged release commits:"
"ru": "\nРелизные коммиты без тега:",
"zh": "\n未打标签的发布提交:"
},
"\nUser-facing changes ({count}):": {
"bg": "\nUser-facing changes ({count}):",
"de": "\nUser-facing changes ({count}):",
"bg": "\nВидими за потребителя промени ({count}):",
"de": "\nNutzersichtbare Änderungen ({count}):",
"en": "\nUser-facing changes ({count}):",
"pl": "\nZmiany widoczne dla użytkownika ({count}):",
"ru": "\nUser-facing changes ({count}):",
"zh": "\nUser-facing changes ({count}):"
"ru": "\nПользовательские изменения ({count}):",
"zh": "\n面向用户的更改({count}):"
},
"\nVerification passed — all wiki pages exist.": {
"bg": "",
@@ -216,36 +208,36 @@
"zh": ""
},
"\nWorkflow-only changes ({count}):": {
"bg": "\nWorkflow-only changes ({count}):",
"de": "\nWorkflow-only changes ({count}):",
"bg": "\nПромени само в workflow ({count}):",
"de": "\nNur-Workflow-Änderungen ({count}):",
"en": "\nWorkflow-only changes ({count}):",
"pl": "\nZmiany tylko w workflow ({count}):",
"ru": "\nWorkflow-only changes ({count}):",
"zh": "\nWorkflow-only changes ({count}):"
"ru": "\nИзменения только в workflow ({count}):",
"zh": "\n仅工作流更改({count}):"
},
"\n[check_test_coverage] Fix: add the missing test file(s) before committing.": {
"bg": "\n[check_test_coverage] Fix: add the missing test file(s) before committing.",
"de": "\n[check_test_coverage] Fix: add the missing test file(s) before committing.",
"bg": "\n[check_test_coverage] Корекция: добавете липсващите тестови файл(ове) преди комит.",
"de": "\n[check_test_coverage] Behebung: fehlende Testdatei(en) vor dem Commit hinzufügen.",
"en": "\n[check_test_coverage] Fix: add the missing test file(s) before committing.",
"pl": "\n[check_test_coverage] Fix: add the missing test file(s) before committing.",
"ru": "\n[check_test_coverage] Fix: add the missing test file(s) before committing.",
"zh": "\n[check_test_coverage] Fix: add the missing test file(s) before committing."
"pl": "\n[check_test_coverage] Poprawka: dodaj brakujące pliki testowe przed commitem.",
"ru": "\n[check_test_coverage] Исправление: добавьте недостающие тестовые файл(ы) перед коммитом.",
"zh": "\n[check_test_coverage] 修复:提交前添加缺失的测试文件。"
},
"\n[dry-run] Changelog:\n{changelog}": {
"bg": "\n[dry-run] Changelog:\n{changelog}",
"de": "\n[dry-run] Changelog:\n{changelog}",
"bg": "\n[dry-run] Списък на промените:\n{changelog}",
"de": "\n[dry-run] Änderungsprotokoll:\n{changelog}",
"en": "\n[dry-run] Changelog:\n{changelog}",
"pl": "\n[dry-run] Changelog:\n{changelog}",
"ru": "\n[dry-run] Changelog:\n{changelog}",
"zh": "\n[dry-run] Changelog:\n{changelog}"
"pl": "\n[dry-run] Dziennik zmian:\n{changelog}",
"ru": "\n[dry-run] Журнал изменений:\n{changelog}",
"zh": "\n[dry-run] 变更日志:\n{changelog}"
},
"\n{label} files changed ({count}):": {
"bg": "\n{label} files changed ({count}):",
"de": "\n{label} files changed ({count}):",
"bg": "\nПроменени файлове — {label} ({count}):",
"de": "\n{label} geänderte Dateien ({count}):",
"en": "\n{label} files changed ({count}):",
"pl": "\n{label} plików zmienionych ({count}):",
"ru": "\n{label} files changed ({count}):",
"zh": "\n{label} files changed ({count}):"
"ru": "\nИзменённые файлы — {label} ({count}):",
"zh": "\n{label} 个已更改文件({count}):"
},
"\n{separator}": {
"bg": "\n{separator}",
@@ -256,36 +248,36 @@
"zh": "\n{separator}"
},
"\n{tag} files ({count}):": {
"bg": "\n{tag} files ({count}):",
"de": "\n{tag} files ({count}):",
"bg": "\n{tag} файла ({count}):",
"de": "\n{tag} Dateien ({count}):",
"en": "\n{tag} files ({count}):",
"pl": "\nPliki {tag} ({count}):",
"ru": "\n{tag} files ({count}):",
"zh": "\n{tag} files ({count}):"
"ru": "\n{tag} файлов ({count}):",
"zh": "\n{tag} 个文件({count}):"
},
" Could not fetch logs: {error}": {
"bg": " Could not fetch logs: {error}",
"de": " Could not fetch logs: {error}",
"bg": " Неуспешно извличане на логове: {error}",
"de": " Logs konnten nicht abgerufen werden: {error}",
"en": " Could not fetch logs: {error}",
"pl": " Could not fetch logs: {error}",
"ru": " Could not fetch logs: {error}",
"zh": " Could not fetch logs: {error}"
"pl": " Nie udało się pobrać logów: {error}",
"ru": " Не удалось получить логи: {error}",
"zh": " 无法获取日志:{error}"
},
" pytest stderr (last 300 chars): {stderr}": {
"bg": " pytest stderr (last 300 chars): {stderr}",
"de": " pytest stderr (last 300 chars): {stderr}",
"bg": " pytest stderr (последни 300 символа): {stderr}",
"de": " pytest stderr (letzte 300 Zeichen): {stderr}",
"en": " pytest stderr (last 300 chars): {stderr}",
"pl": " pytest stderr (last 300 chars): {stderr}",
"ru": " pytest stderr (last 300 chars): {stderr}",
"zh": " pytest stderr (last 300 chars): {stderr}"
"pl": " pytest stderr (ostatnie 300 znaków): {stderr}",
"ru": " pytest stderr (последние 300 символов): {stderr}",
"zh": " pytest stderr(最后 300 个字符):{stderr}"
},
" pytest stdout (last 300 chars): {stdout}": {
"bg": " pytest stdout (last 300 chars): {stdout}",
"de": " pytest stdout (last 300 chars): {stdout}",
"bg": " pytest stdout (последни 300 символа): {stdout}",
"de": " pytest stdout (letzte 300 Zeichen): {stdout}",
"en": " pytest stdout (last 300 chars): {stdout}",
"pl": " pytest stdout (last 300 chars): {stdout}",
"ru": " pytest stdout (last 300 chars): {stdout}",
"zh": " pytest stdout (last 300 chars): {stdout}"
"pl": " pytest stdout (ostatnie 300 znaków): {stdout}",
"ru": " pytest stdout (последние 300 символов): {stdout}",
"zh": " pytest stdout(最后 300 个字符):{stdout}"
},
" stderr: {stderr}": {
"bg": " stderr: {stderr}",
@@ -328,12 +320,12 @@
"zh": " - 阻止被拒绝的审查: 是"
},
" - 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)",
"bg": " - Директни push-ове: БЛОКИРАНИ (изисква се PR; разрешени потребители могат да push-ват)",
"de": " - Direkte Pushes: BLOCKIERT (PR erforderlich, freigegebene Benutzer dürfen pushen)",
"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)",
"zh": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)"
"ru": " - Прямые push: ЗАБЛОКИРОВАНЫ (требуется PR; разрешённые пользователи могут push)",
"zh": " - 直接推送:已阻止(需要 PR,白名单用户可推送)"
},
" - Dismiss stale approvals: yes": {
"bg": " - Анулиране на остарели одобрения: да",
@@ -360,12 +352,12 @@
"zh": " - 必需状态检查: {checks}"
},
" - {count} standard labels verified": {
"bg": " - {count} standard labels verified",
"de": " - {count} standard labels verified",
"bg": " - {count} стандартни етикета проверени",
"de": " - {count} Standard-Labels geprüft",
"en": " - {count} standard labels verified",
"pl": " - {count} standard labels verified",
"ru": " - {count} standard labels verified",
"zh": " - {count} standard labels verified"
"pl": " - {count} standardowych etykiet zweryfikowanych",
"ru": " - {count} стандартных меток проверено",
"zh": " - 已验证 {count} 个标准标签"
},
" -> {dir}": {
"bg": " -> {dir}",
@@ -384,52 +376,52 @@
"zh": ""
},
" Auto-fixed trailing whitespace in {n} files": {
"bg": " Auto-fixed trailing whitespace in {n} files",
"de": " Auto-fixed trailing whitespace in {n} files",
"bg": " Автоматично коригирани крайни интервали в {n} файла",
"de": " Abschließende Leerzeichen in {n} Dateien automatisch korrigiert",
"en": " Auto-fixed trailing whitespace in {n} files",
"pl": " Auto-fixed trailing whitespace in {n} files",
"ru": " Auto-fixed trailing whitespace in {n} files",
"zh": " Auto-fixed trailing whitespace in {n} files"
"pl": " Automatycznie poprawiono końcowe białe znaki w {n} plikach",
"ru": " Автоматически исправлены конечные пробелы в {n} файлах",
"zh": " 已自动修复 {n} 个文件中的行尾空白"
},
" Collecting code quality...": {
"bg": " Collecting code quality...",
"de": " Collecting code quality...",
"bg": " Събиране на качество на кода...",
"de": " Codequalität wird erfasst...",
"en": " Collecting code quality...",
"pl": " Collecting code quality...",
"ru": " Collecting code quality...",
"zh": " Collecting code quality..."
"pl": " Zbieranie jakości kodu...",
"ru": " Сбор данных о качестве кода...",
"zh": " 正在收集代码质量..."
},
" Collecting coverage and tests...": {
"bg": " Collecting coverage and tests...",
"de": " Collecting coverage and tests...",
"bg": " Събиране на покритие и тестове...",
"de": " Coverage und Tests werden erfasst...",
"en": " Collecting coverage and tests...",
"pl": " Collecting coverage and tests...",
"ru": " Collecting coverage and tests...",
"zh": " Collecting coverage and tests..."
"pl": " Zbieranie pokrycia i testów...",
"ru": " Сбор покрытия и тестов...",
"zh": " 正在收集覆盖率和测试..."
},
" Collecting doc coverage...": {
"bg": " Collecting doc coverage...",
"de": " Collecting doc coverage...",
"bg": " Събиране на покритие на документацията...",
"de": " Dokumentationsabdeckung wird erfasst...",
"en": " Collecting doc coverage...",
"pl": " Collecting doc coverage...",
"ru": " Collecting doc coverage...",
"zh": " Collecting doc coverage..."
"pl": " Zbieranie pokrycia dokumentacji...",
"ru": " Сбор покрытия документации...",
"zh": " 正在收集文档覆盖率..."
},
" Collecting version...": {
"bg": " Collecting version...",
"de": " Collecting version...",
"bg": " Събиране на версия...",
"de": " Version wird erfasst...",
"en": " Collecting version...",
"pl": " Collecting version...",
"ru": " Collecting version...",
"zh": " Collecting version..."
"pl": " Zbieranie wersji...",
"ru": " Сбор версии...",
"zh": " 正在收集版本..."
},
" Deleted: {version}": {
"bg": " Deleted: {version}",
"de": " Deleted: {version}",
"bg": " Изтрито: {version}",
"de": " Gelöscht: {version}",
"en": " Deleted: {version}",
"pl": " Deleted: {version}",
"ru": " Deleted: {version}",
"zh": " Deleted: {version}"
"pl": " Usunięto: {version}",
"ru": " Удалено: {version}",
"zh": " 已删除:{version}"
},
" FAIL: {title} — page not found in wiki!": {
"bg": "",
@@ -440,12 +432,20 @@
"zh": ""
},
" FAILED to delete: {version}": {
"bg": " FAILED to delete: {version}",
"de": " FAILED to delete: {version}",
"bg": " НЕУСПЕШНО изтриване: {version}",
"de": " Löschen FEHLGESCHLAGEN: {version}",
"en": " FAILED to delete: {version}",
"pl": " FAILED to delete: {version}",
"ru": " FAILED to delete: {version}",
"zh": " FAILED to delete: {version}"
"pl": " NIE UDAŁO się usunąć: {version}",
"ru": " НЕ УДАЛОСЬ удалить: {version}",
"zh": " 删除失败:{version}"
},
" Fix the PR title with:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n Or manually set the PR title to: '{expected}'": {
"bg": " Коригирайте заглавието на PR с:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n Или ръчно задайте заглавие на PR: '{expected}'",
"de": " Korrigieren Sie den PR-Titel mit:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n Oder setzen Sie den PR-Titel manuell auf: '{expected}'",
"en": " Fix the PR title with:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n Or manually set the PR title to: '{expected}'",
"pl": " Popraw tytuł PR poleceniem:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n Lub ręcznie ustaw tytuł PR na: '{expected}'",
"ru": " Исправьте заголовок PR командой:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n Или вручную установите заголовок PR: '{expected}'",
"zh": " 使用以下命令修复 PR 标题:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n 或手动将 PR 标题设置为:'{expected}'"
},
" Fixed {fixes} version ref(s) in {file}": {
"bg": "",
@@ -456,36 +456,44 @@
"zh": ""
},
" Generated: {path}": {
"bg": " Generated: {path}",
"de": " Generated: {path}",
"bg": " Генерирано: {path}",
"de": " Generiert: {path}",
"en": " Generated: {path}",
"pl": " Generated: {path}",
"ru": " Generated: {path}",
"zh": " Generated: {path}"
"pl": " Wygenerowano: {path}",
"ru": " Сгенерировано: {path}",
"zh": " 已生成:{path}"
},
" HTTP 500 from registry, retrying in {wait:.0f}s (attempt {attempt}/5)...": {
"bg": " HTTP 500 от регистъра, повторен опит след {wait:.0f}с (опит {attempt}/5)...",
"de": " HTTP 500 vom Registry, Wiederholung in {wait:.0f}s (Versuch {attempt}/5)...",
"en": "HTTP 500 from registry, retrying in {wait:.0f}s (attempt {attempt}/5)...",
"pl": " HTTP 500 z rejestru, ponawianie za {wait:.0f}s (próba {attempt}/5)...",
"ru": " HTTP 500 от реестра, повтор через {wait:.0f}с (попытка {attempt}/5)...",
"zh": " 注册表返回 HTTP 500{wait:.0f}秒后重试(第{attempt}/5次尝试)..."
},
" MISSING: {cmd}": {
"bg": " MISSING: {cmd}",
"de": " MISSING: {cmd}",
"bg": " ЛИПСВА: {cmd}",
"de": " FEHLT: {cmd}",
"en": " MISSING: {cmd}",
"pl": " MISSING: {cmd}",
"ru": " MISSING: {cmd}",
"zh": " MISSING: {cmd}"
"pl": " BRAKUJE: {cmd}",
"ru": " ОТСУТСТВУЕТ: {cmd}",
"zh": " 缺失:{cmd}"
},
" MISSING: {module}": {
"bg": " MISSING: {module}",
"de": " MISSING: {module}",
"bg": " ЛИПСВА: {module}",
"de": " FEHLT: {module}",
"en": " MISSING: {module}",
"pl": " BRAK: {module}",
"ru": " MISSING: {module}",
"zh": " MISSING: {module}"
"ru": " ОТСУТСТВУЕТ: {module}",
"zh": " 缺失:{module}"
},
" MISSING: {script}": {
"bg": " MISSING: {script}",
"de": " MISSING: {script}",
"bg": " ЛИПСВА: {script}",
"de": " FEHLT: {script}",
"en": " MISSING: {script}",
"pl": " BRAK: {script}",
"ru": " MISSING: {script}",
"zh": " MISSING: {script}"
"ru": " ОТСУТСТВУЕТ: {script}",
"zh": " 缺失:{script}"
},
" OK: {cmd}": {
"bg": " OK: {cmd}",
@@ -493,7 +501,7 @@
"en": " OK: {cmd}",
"pl": " OK: {cmd}",
"ru": " OK: {cmd}",
"zh": " OK: {cmd}"
"zh": " 正常:{cmd}"
},
" OK: {module}": {
"bg": " OK: {module}",
@@ -501,7 +509,7 @@
"en": " OK: {module}",
"pl": " OK: {module}",
"ru": " OK: {module}",
"zh": " OK: {module}"
"zh": " 正常:{module}"
},
" OK: {script}": {
"bg": " OK: {script}",
@@ -509,7 +517,7 @@
"en": " OK: {script}",
"pl": " OK: {script}",
"ru": " OK: {script}",
"zh": " OK: {script}"
"zh": " 正常:{script}"
},
" OK: {title}": {
"bg": "",
@@ -520,12 +528,12 @@
"zh": ""
},
" Package: {pkg}": {
"bg": " Package: {pkg}",
"de": " Package: {pkg}",
"bg": " Пакет: {pkg}",
"de": " Paket: {pkg}",
"en": " Package: {pkg}",
"pl": " Package: {pkg}",
"ru": " Package: {pkg}",
"zh": " Package: {pkg}"
"pl": " Pakiet: {pkg}",
"ru": " Пакет: {pkg}",
"zh": " 包:{pkg}"
},
" Pruned: {file} (not in mapping)": {
"bg": "",
@@ -536,20 +544,20 @@
"zh": ""
},
" Quality checks: {checks}": {
"bg": " Quality checks: {checks}",
"de": " Quality checks: {checks}",
"bg": " Проверки на качеството: {checks}",
"de": " Qualitätsprüfungen: {checks}",
"en": " Quality checks: {checks}",
"pl": " Quality checks: {checks}",
"ru": " Quality checks: {checks}",
"zh": " Quality checks: {checks}"
"pl": " Kontrole jakości: {checks}",
"ru": " Проверки качества: {checks}",
"zh": " 质量检查:{checks}"
},
" Repo root: {root}": {
"bg": " Repo root: {root}",
"de": " Repo root: {root}",
"bg": " Корен на репозитория: {root}",
"de": " Repo-Wurzel: {root}",
"en": " Repo root: {root}",
"pl": " Repo root: {root}",
"ru": " Repo root: {root}",
"zh": " Repo root: {root}"
"pl": " Katalog główny repo: {root}",
"ru": " Корень репозитория: {root}",
"zh": " 仓库根目录:{root}"
},
" Run 'make install-checkmake' to install the Makefile linter.": {
"bg": " Изпълнете 'make install-checkmake' за инсталиране на Makefile линтера.",
@@ -568,12 +576,12 @@
"zh": ""
},
" Test paths: {testpaths}": {
"bg": " Test paths: {testpaths}",
"de": " Test paths: {testpaths}",
"bg": " Тестови пътища: {testpaths}",
"de": " Testpfade: {testpaths}",
"en": " Test paths: {testpaths}",
"pl": " Test paths: {testpaths}",
"ru": " Test paths: {testpaths}",
"zh": " Test paths: {testpaths}"
"pl": " Ścieżki testów: {testpaths}",
"ru": " Пути тестов: {testpaths}",
"zh": " 测试路径:{testpaths}"
},
" WARN: Mapped file {file} is empty, skipping": {
"bg": "",
@@ -592,84 +600,84 @@
"zh": ""
},
" WARNING: Could not extract coverage from pytest output (rc={rc})": {
"bg": " WARNING: Could not extract coverage from pytest output (rc={rc})",
"de": " WARNING: Could not extract coverage from pytest output (rc={rc})",
"bg": " ПРЕДУПРЕЖДЕНИЕ: Не може да се извлече покритие от pytest изхода (rc={rc})",
"de": " WARNUNG: Coverage konnte nicht aus pytest-Ausgabe extrahiert werden (rc={rc})",
"en": " WARNING: Could not extract coverage from pytest output (rc={rc})",
"pl": " WARNING: Could not extract coverage from pytest output (rc={rc})",
"ru": " WARNING: Could not extract coverage from pytest output (rc={rc})",
"zh": " WARNING: Could not extract coverage from pytest output (rc={rc})"
"pl": " OSTRZEŻENIE: Nie można wyodrębnić pokrycia z wyjścia pytest (rc={rc})",
"ru": " ПРЕДУПРЕЖДЕНИЕ: Не удалось извлечь покрытие из вывода pytest (rc={rc})",
"zh": " 警告:无法从 pytest 输出中提取覆盖率 (rc={rc})"
},
" WARNING: Could not extract doc coverage (rc={rc})": {
"bg": " WARNING: Could not extract doc coverage (rc={rc})",
"de": " WARNING: Could not extract doc coverage (rc={rc})",
"bg": " ПРЕДУПРЕЖДЕНИЕ: Не може да се извлече покритие на документацията (rc={rc})",
"de": " WARNUNG: Dokumentationsabdeckung konnte nicht extrahiert werden (rc={rc})",
"en": " WARNING: Could not extract doc coverage (rc={rc})",
"pl": " WARNING: Could not extract doc coverage (rc={rc})",
"ru": " WARNING: Could not extract doc coverage (rc={rc})",
"zh": " WARNING: Could not extract doc coverage (rc={rc})"
"pl": " OSTRZEŻENIE: Nie można wyodrębnić pokrycia dokumentacji (rc={rc})",
"ru": " ПРЕДУПРЕЖДЕНИЕ: Не удалось извлечь покрытие документации (rc={rc})",
"zh": " 警告:无法提取文档覆盖率 (rc={rc})"
},
" WARNING: Could not extract test count from pytest output (rc={rc})": {
"bg": " WARNING: Could not extract test count from pytest output (rc={rc})",
"de": " WARNING: Could not extract test count from pytest output (rc={rc})",
"bg": " ПРЕДУПРЕЖДЕНИЕ: Не може да се извлече брой тестове от pytest изхода (rc={rc})",
"de": " WARNUNG: Testanzahl konnte nicht aus pytest-Ausgabe extrahiert werden (rc={rc})",
"en": " WARNING: Could not extract test count from pytest output (rc={rc})",
"pl": " WARNING: Could not extract test count from pytest output (rc={rc})",
"ru": " WARNING: Could not extract test count from pytest output (rc={rc})",
"zh": " WARNING: Could not extract test count from pytest output (rc={rc})"
"pl": " OSTRZEŻENIE: Nie można wyodrębnić liczby testów z wyjścia pytest (rc={rc})",
"ru": " ПРЕДУПРЕЖДЕНИЕ: Не удалось извлечь число тестов из вывода pytest (rc={rc})",
"zh": " 警告:无法从 pytest 输出中提取测试数量 (rc={rc})"
},
" WARNING: No Python package found under src/ — version badge will show 'unknown'": {
"bg": " WARNING: No Python package found under src/ — version badge will show 'unknown'",
"de": " WARNING: No Python package found under src/version badge will show 'unknown'",
"bg": " ПРЕДУПРЕЖДЕНИЕ: Не е намерен Python пакет под src/ — значкът за версия ще показва 'unknown'",
"de": " WARNUNG: Kein Python-Paket unter src/ gefundenVersions-Badge zeigt 'unknown'",
"en": " WARNING: No Python package found under src/ — version badge will show 'unknown'",
"pl": " WARNING: No Python package found under src/ — version badge will show 'unknown'",
"ru": " WARNING: No Python package found under src/ — version badge will show 'unknown'",
"zh": " WARNING: No Python package found under src/ — version badge will show 'unknown'"
"pl": " OSTRZEŻENIE: Nie znaleziono pakietu Python pod src/ — odznaka wersji pokaże 'unknown'",
"ru": " ПРЕДУПРЕЖДЕНИЕ: Python-пакет не найден в src/ — значок версии покажет 'unknown'",
"zh": " 警告:src/ 下未找到 Python 包——版本徽章将显示 'unknown'"
},
" WARNING: No __version__ found in {init_file} — version badge will show 'unknown'": {
"bg": " WARNING: No __version__ found in {init_file} — version badge will show 'unknown'",
"de": " WARNING: No __version__ found in {init_file} — version badge will show 'unknown'",
"bg": " ПРЕДУПРЕЖДЕНИЕ: Не е намерен __version__ в {init_file} — значкът за версия ще показва 'unknown'",
"de": " WARNUNG: Kein __version__ in {init_file} gefundenVersions-Badge zeigt 'unknown'",
"en": " WARNING: No __version__ found in {init_file} — version badge will show 'unknown'",
"pl": " WARNING: No __version__ found in {init_file} — version badge will show 'unknown'",
"ru": " WARNING: No __version__ found in {init_file} — version badge will show 'unknown'",
"zh": " WARNING: No __version__ found in {init_file} — version badge will show 'unknown'"
"pl": " OSTRZEŻENIE: Nie znaleziono __version__ w {init_file} — odznaka wersji pokaże 'unknown'",
"ru": " ПРЕДУПРЕЖДЕНИЕ: __version__ не найден в {init_file} — значок версии покажет 'unknown'",
"zh": " 警告:{init_file} 中未找到 __version__——版本徽章将显示 'unknown'"
},
" WARNING: No coverage target detected (no src/ package, no --cov in pyproject.toml)": {
"bg": " WARNING: No coverage target detected (no src/ package, no --cov in pyproject.toml)",
"de": " WARNING: No coverage target detected (no src/ package, no --cov in pyproject.toml)",
"bg": " ПРЕДУПРЕЖДЕНИЕ: Не е открита цел за покритие (няма src/ пакет, няма --cov в pyproject.toml)",
"de": " WARNUNG: Kein Coverage-Ziel erkannt (kein src/-Paket, kein --cov in pyproject.toml)",
"en": " WARNING: No coverage target detected (no src/ package, no --cov in pyproject.toml)",
"pl": " WARNING: No coverage target detected (no src/ package, no --cov in pyproject.toml)",
"ru": " WARNING: No coverage target detected (no src/ package, no --cov in pyproject.toml)",
"zh": " WARNING: No coverage target detected (no src/ package, no --cov in pyproject.toml)"
"pl": " OSTRZEŻENIE: Nie wykryto celu pokrycia (brak pakietu src/, brak --cov w pyproject.toml)",
"ru": " ПРЕДУПРЕЖДЕНИЕ: Цель покрытия не обнаружена (нет пакета src/, нет --cov в pyproject.toml)",
"zh": " 警告:未检测到覆盖率目标(无 src/ 包,pyproject.toml 中无 --cov"
},
" WARNING: {init_file} not found — version badge will show 'unknown'": {
"bg": " WARNING: {init_file} not found — version badge will show 'unknown'",
"de": " WARNING: {init_file} not found — version badge will show 'unknown'",
"bg": " ПРЕДУПРЕЖДЕНИЕ: {init_file} не е намерен — значкът за версия ще показва 'unknown'",
"de": " WARNUNG: {init_file} nicht gefundenVersions-Badge zeigt 'unknown'",
"en": " WARNING: {init_file} not found — version badge will show 'unknown'",
"pl": " WARNING: {init_file} not found — version badge will show 'unknown'",
"ru": " WARNING: {init_file} not found — version badge will show 'unknown'",
"zh": " WARNING: {init_file} not found — version badge will show 'unknown'"
"pl": " OSTRZEŻENIE: Nie znaleziono {init_file} — odznaka wersji pokaże 'unknown'",
"ru": " ПРЕДУПРЕЖДЕНИЕ: {init_file} не найден — значок версии покажет 'unknown'",
"zh": " 警告:未找到 {init_file}——版本徽章将显示 'unknown'"
},
" WARNING: {name} failed (rc={rc})": {
"bg": " WARNING: {name} failed (rc={rc})",
"de": " WARNING: {name} failed (rc={rc})",
"bg": " ПРЕДУПРЕЖДЕНИЕ: {name} се провали (rc={rc})",
"de": " WARNUNG: {name} fehlgeschlagen (rc={rc})",
"en": " WARNING: {name} failed (rc={rc})",
"pl": " WARNING: {name} failed (rc={rc})",
"ru": " WARNING: {name} failed (rc={rc})",
"zh": " WARNING: {name} failed (rc={rc})"
"pl": " OSTRZEŻENIE: {name} nie powiodło się (rc={rc})",
"ru": " ПРЕДУПРЕЖДЕНИЕ: {name} завершился с ошибкой (rc={rc})",
"zh": " 警告:{name} 失败 (rc={rc})"
},
" WARNING: {name} not installed — skipping (counted as pass)": {
"bg": " WARNING: {name} not installed — skipping (counted as pass)",
"de": " WARNING: {name} not installed — skipping (counted as pass)",
"bg": " ПРЕДУПРЕЖДЕНИЕ: {name} не е инсталиран — пропуска се (отчита се като успешно)",
"de": " WARNUNG: {name} nicht installiert — übersprungen (als bestanden gezählt)",
"en": " WARNING: {name} not installed — skipping (counted as pass)",
"pl": " WARNING: {name} not installed — skipping (counted as pass)",
"ru": " WARNING: {name} not installed — skipping (counted as pass)",
"zh": " WARNING: {name} not installed — skipping (counted as pass)"
"pl": " OSTRZEŻENIE: {name} nie jest zainstalowane — pomijanie (liczone jako zaliczone)",
"ru": " ПРЕДУПРЕЖДЕНИЕ: {name} не установлен — пропускается (засчитывается как успех)",
"zh": " 警告:{name} 未安装——跳过(计为通过)"
},
" [dry-run] Would delete: {version}": {
"bg": " [dry-run] Would delete: {version}",
"de": " [dry-run] Would delete: {version}",
"bg": " [dry-run] Ще бъде изтрито: {version}",
"de": " [dry-run] Würde löschen: {version}",
"en": " [dry-run] Would delete: {version}",
"pl": " [dry-run] Would delete: {version}",
"ru": " [dry-run] Would delete: {version}",
"zh": " [dry-run] Would delete: {version}"
"pl": " [dry-run] Usunięto by: {version}",
"ru": " [dry-run] Было бы удалено: {version}",
"zh": " [dry-run] 将删除:{version}"
},
" {name}: {label}={message} ({color})": {
"bg": " {name}: {label}={message} ({color})",
@@ -696,12 +704,12 @@
"zh": ""
},
" {n} stale docs found (warnings only)": {
"bg": " {n} stale docs found (warnings only)",
"de": " {n} stale docs found (warnings only)",
"bg": " Намерени {n} остарели документа (само предупреждения)",
"de": " {n} veraltete Dokumente gefunden (nur Warnungen)",
"en": " {n} stale docs found (warnings only)",
"pl": " {n} stale docs found (warnings only)",
"ru": " {n} stale docs found (warnings only)",
"zh": " {n} stale docs found (warnings only)"
"pl": " Znaleziono {n} nieaktualnych dokumentów (tylko ostrzeżenia)",
"ru": " Найдено {n} устаревших документов (только предупреждения)",
"zh": " 发现 {n} 个过时文档(仅警告)"
},
" {tool}: found at {path}": {
"bg": " {tool}: намерен на {path}",
@@ -712,76 +720,100 @@
"zh": " {tool}: 在 {path} 找到"
},
" {version} (created: {created})": {
"bg": " {version} (created: {created})",
"de": " {version} (created: {created})",
"bg": " {version} (създадено: {created})",
"de": " {version} (erstellt: {created})",
"en": " {version} (created: {created})",
"pl": " {version} (created: {created})",
"ru": " {version} (created: {created})",
"zh": " {version} (created: {created})"
},
"--checklist-categories must list at least 8 of 13 categories. Got {count}.": {
"bg": "--checklist-categories must list at least 8 of 13 categories. Got {count}.",
"de": "--checklist-categories must list at least 8 of 13 categories. Got {count}.",
"en": "--checklist-categories must list at least 8 of 13 categories. Got {count}.",
"pl": "--checklist-categories must list at least 8 of 13 categories. Got {count}.",
"ru": "--checklist-categories must list at least 8 of 13 categories. Got {count}.",
"zh": "--checklist-categories must list at least 8 of 13 categories. Got {count}."
},
"--checklist-confirmed is required for APPROVE events.": {
"bg": "--checklist-confirmed is required for APPROVE events.",
"de": "--checklist-confirmed is required for APPROVE events.",
"en": "--checklist-confirmed is required for APPROVE events.",
"pl": "--checklist-confirmed is required for APPROVE events.",
"ru": "--checklist-confirmed is required for APPROVE events.",
"zh": "--checklist-confirmed is required for APPROVE events."
"pl": " {version} (utworzono: {created})",
"ru": " {version} (создано: {created})",
"zh": " {version}(创建于:{created}"
},
"--push requires --registry": {
"bg": "--push requires --registry",
"de": "--push requires --registry",
"bg": "--push изисква --registry",
"de": "--push erfordert --registry",
"en": "--push requires --registry",
"pl": "--push requires --registry",
"ru": "--push requires --registry",
"zh": "--push requires --registry"
"pl": "--push wymaga --registry",
"ru": "--push требует --registry",
"zh": "--push 需要 --registry"
},
"--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.",
"bg": "--skip-build: пропуска се изграждане на пакета и публикуване в PyPI.",
"de": "--skip-build: Paket-Build und PyPI-Veröffentlichung werden übersprungen.",
"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.",
"zh": "--skip-build: skipping package build and PyPI publish."
"ru": "--skip-build: сборка пакета и публикация в PyPI пропускаются.",
"zh": "--skip-build:跳过包构建和 PyPI 发布。"
},
"=== Release Alignment Verification ===\n": {
"bg": "=== Release Alignment Verification ===\n",
"de": "=== Release Alignment Verification ===\n",
"bg": "=== Проверка на съответствието на версиите ===\n",
"de": "=== Release-Abgleich-Verifizierung ===\n",
"en": "=== Release Alignment Verification ===\n",
"pl": "=== Weryfikacja zgodności wydań ===\n",
"ru": "=== Release Alignment Verification ===\n",
"zh": "=== Release Alignment Verification ===\n"
"ru": "=== Проверка соответствия релизов ===\n",
"zh": "=== 发布一致性验证 ===\n"
},
"API poll warning: {exc}": {
"bg": "API poll warning: {exc}",
"de": "API poll warning: {exc}",
"bg": "Предупреждение при API запитване: {exc}",
"de": "Warnung bei API-Abfrage: {exc}",
"en": "API poll warning: {exc}",
"pl": "Ostrzeżenie sondowania API: {exc}",
"ru": "API poll warning: {exc}",
"zh": "API poll warning: {exc}"
"ru": "Предупреждение при опросе API: {exc}",
"zh": "API 轮询警告:{exc}"
},
"Acceptance Criteria has {count} unchecked item(s). All AC items must be checked (- [x]) before merge.": {
"bg": "Acceptance Criteria има {count} неотметнати елемента. Всички AC елементи трябва да са отметнати (- [x]) преди merge.",
"de": "Acceptance Criteria enthält {count} nicht abgehakte Elemente. Alle AC-Elemente müssen vor dem Merge abgehakt sein (- [x]).",
"en": "Acceptance Criteria has {count} unchecked item(s). All AC items must be checked (- [x]) before merge.",
"pl": "Acceptance Criteria ma {count} niezaznaczonych elementów. Wszystkie elementy AC muszą być zaznaczone (- [x]) przed merge.",
"ru": "Acceptance Criteria содержит {count} неотмеченных элементов. Все элементы AC должны быть отмечены (- [x]) перед merge.",
"zh": "验收标准有 {count} 个未勾选项目。所有 AC 项目必须在合并前勾选(- [x])。"
},
"Acceptance Criteria section has no checklist items. Add at least one '- [ ] item'.": {
"bg": "Секцията Acceptance Criteria няма елементи от checklist. Добавете поне един '- [ ] item'.",
"de": "Der Abschnitt Acceptance Criteria enthält keine Checklisten-Elemente. Mindestens ein '- [ ] item' hinzufügen.",
"en": "Acceptance Criteria section has no checklist items. Add at least one '- [ ] item'.",
"pl": "Sekcja Acceptance Criteria nie zawiera elementów checklisty. Dodaj co najmniej jeden '- [ ] item'.",
"ru": "Раздел Acceptance Criteria не содержит элементов чек-листа. Добавьте хотя бы один '- [ ] item'.",
"zh": "验收标准部分没有清单项目。至少添加一个 '- [ ] item'。"
},
"Action to perform": {
"bg": "Действие за изпълнение",
"de": "Auszuführende Aktion",
"en": "Action to perform",
"pl": "Akcja do wykonania",
"ru": "Выполняемое действие",
"zh": "要执行的操作"
},
"Add @patch(\"subprocess.run\") or patch the calling function to fix this.": {
"bg": "Добавете @patch(\"subprocess.run\") или patch-нете извикващата функция, за да коригирате това.",
"de": "Fügen Sie @patch(\"subprocess.run\") hinzu oder patchen Sie die aufrufende Funktion.",
"en": "Add @patch(\"subprocess.run\") or patch the calling function to fix this.",
"pl": "Dodaj @patch(\"subprocess.run\") lub załataj funkcję wywołującą, aby to naprawić.",
"ru": "Добавьте @patch(\"subprocess.run\") или исправьте вызывающую функцию.",
"zh": "添加 @patch(\"subprocess.run\") 或修补调用函数以修复此问题。"
},
"Added label '{label}' to PR #{pr}.": {
"bg": "Added label '{label}' to PR #{pr}.",
"de": "Added label '{label}' to PR #{pr}.",
"bg": "Добавен етикет '{label}' към PR #{pr}.",
"de": "Label '{label}' zu PR #{pr} hinzugefügt.",
"en": "Added label '{label}' to PR #{pr}.",
"pl": "Added label '{label}' to PR #{pr}.",
"ru": "Added label '{label}' to PR #{pr}.",
"zh": "Added label '{label}' to PR #{pr}."
"pl": "Dodano etykietę '{label}' do PR #{pr}.",
"ru": "Добавлена метка '{label}' к PR #{pr}.",
"zh": "已向 PR #{pr} 添加标签 '{label}'。"
},
"Additional directory to scan (default: scripts, tests). Can be repeated.": {
"bg": "Additional directory to scan (default: scripts, tests). Can be repeated.",
"de": "Additional directory to scan (default: scripts, tests). Can be repeated.",
"bg": "Допълнителна директория за сканиране (по подразбиране: scripts, tests). Може да се повтаря.",
"de": "Zusätzliches zu scannendes Verzeichnis (Standard: scripts, tests). Wiederholbar.",
"en": "Additional directory to scan (default: scripts, tests). Can be repeated.",
"pl": "Additional directory to scan (default: scripts, tests). Can be repeated.",
"ru": "Additional directory to scan (default: scripts, tests). Can be repeated.",
"zh": "Additional directory to scan (default: scripts, tests). Can be repeated."
"pl": "Dodatkowy katalog do skanowania (domyślnie: scripts, tests). Można powtarzać.",
"ru": "Дополнительная директория для сканирования (по умолчанию: scripts, tests). Можно повторять.",
"zh": "要扫描的附加目录(默认:scriptstests)。可重复使用。"
},
"Additional excluded patterns (in addition to defaults)": {
"bg": "Допълнителни изключени шаблони (в допълнение към подразбираните)",
"de": "Zusätzliche ausgeschlossene Muster (zusätzlich zu den Standardwerten)",
"en": "Additional excluded patterns (in addition to defaults)",
"pl": "Dodatkowe wykluczone wzorce (oprócz domyślnych)",
"ru": "Дополнительные исключённые шаблоны (в дополнение к стандартным)",
"zh": "附加排除模式(除默认模式外)"
},
"Allow empty tag (PR mode where SHA is concrete).": {
"bg": "Позволи празен таг (PR режим, където SHA е конкретен).",
@@ -791,6 +823,14 @@
"ru": "Разрешить пустой тег (режим PR, где SHA конкретен).",
"zh": "允许空标签(SHA 为具体值的 PR 模式)。"
},
"Allow missing spec (warn only, don't fail)": {
"bg": "Позволи липсващ spec (само предупреждение, без грешка)",
"de": "Fehlende Spec erlauben (nur warnen, nicht fehlschlagen)",
"en": "Allow missing spec (warn only, don't fail)",
"pl": "Zezwól na brakujący spec (tylko ostrzeżenie, bez błędu)",
"ru": "Разрешить отсутствующий spec (только предупреждение, без ошибки)",
"zh": "允许缺少规范(仅警告,不失败)"
},
"Another runner failed. Stopping this runner early.": {
"bg": "Друг runner се провали. Спиране на този runner по-рано.",
"de": "Ein anderer Runner ist fehlgeschlagen. Dieser Runner wird vorzeitig gestoppt.",
@@ -800,68 +840,76 @@
"zh": "另一个 runner 失败。提前停止此 runner。"
},
"Assigned {count} files to runner {runner_index}": {
"bg": "Assigned {count} files to runner {runner_index}",
"de": "Assigned {count} files to runner {runner_index}",
"bg": "Разпределени {count} файла към runner {runner_index}",
"de": "{count} Dateien an Runner {runner_index} zugewiesen",
"en": "Assigned {count} files to runner {runner_index}",
"pl": "Assigned {count} files to runner {runner_index}",
"ru": "Assigned {count} files to runner {runner_index}",
"zh": "Assigned {count} files to runner {runner_index}"
"pl": "Przypisano {count} plików do runnera {runner_index}",
"ru": "Назначено {count} файлов раннеру {runner_index}",
"zh": "已将 {count} 个文件分配给 runner {runner_index}"
},
"Assigned {count} items to runner {runner_index}: {encoded}": {
"bg": "Assigned {count} items to runner {runner_index}: {encoded}",
"de": "Assigned {count} items to runner {runner_index}: {encoded}",
"bg": "Разпределени {count} елемента към runner {runner_index}: {encoded}",
"de": "{count} Elemente an Runner {runner_index} zugewiesen: {encoded}",
"en": "Assigned {count} items to runner {runner_index}: {encoded}",
"pl": "Assigned {count} items to runner {runner_index}: {encoded}",
"ru": "Assigned {count} items to runner {runner_index}: {encoded}",
"zh": "Assigned {count} items to runner {runner_index}: {encoded}"
"pl": "Przypisano {count} elementów do runnera {runner_index}: {encoded}",
"ru": "Назначено {count} элементов раннеру {runner_index}: {encoded}",
"zh": "已将 {count} 个项目分配给 runner {runner_index}{encoded}"
},
"Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.": {
"bg": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"de": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"bg": "Автоматичният rebase се провали с HTTP {status}: {message}\nНаправете rebase ръчно:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nСлед това добавете отново етикета ready-to-merge.",
"de": "Auto-Rebase mit HTTP {status} fehlgeschlagen: {message}\nManuell rebasen:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nDann das ready-to-merge-Label erneut hinzufügen.",
"en": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"pl": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"ru": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"zh": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label."
"pl": "Automatyczny rebase nie powiódł się z HTTP {status}: {message}\nWykonaj rebase ręcznie:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nNastępnie ponownie dodaj etykietę ready-to-merge.",
"ru": "Автоматический rebase завершился с HTTP {status}: {message}\nВыполните rebase вручную:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nЗатем снова добавьте метку ready-to-merge.",
"zh": "自动 rebase 失败,HTTP {status}{message}\n请手动 rebase\n git fetch origin master && git rebase origin/master && git push --force-with-lease\n然后重新添加 ready-to-merge 标签。"
},
"Automated CI commit (badge) — skipping post-merge jobs.": {
"bg": "Automated CI commit (badge) — skipping post-merge jobs.",
"de": "Automated CI commit (badge) — skipping post-merge jobs.",
"bg": "Автоматизиран CI комит (значка) — пропускат се post-merge задачите.",
"de": "Automatisierter CI-Commit (Badge) — Post-Merge-Jobs werden übersprungen.",
"en": "Automated CI commit (badge) — skipping post-merge jobs.",
"pl": "Automated CI commit (badge) — skipping post-merge jobs.",
"ru": "Automated CI commit (badge) — skipping post-merge jobs.",
"zh": "Automated CI commit (badge) — skipping post-merge jobs."
"pl": "Zautomatyzowany commit CI (odznaka) — pomijanie zadań post-merge.",
"ru": "Автоматический CI-коммит (значок) — post-merge задачи пропускаются.",
"zh": "自动 CI 提交(徽章)——跳过后续合并任务。"
},
"Badge push attempt {attempt}/{retries} failed — retrying: {error}": {
"bg": "Badge push attempt {attempt}/{retries} failed — retrying: {error}",
"de": "Badge push attempt {attempt}/{retries} failed — retrying: {error}",
"bg": "Опит {attempt}/{retries} за push на значки се провали — повторен опит: {error}",
"de": "Badge-Push-Versuch {attempt}/{retries} fehlgeschlagen — erneuter Versuch: {error}",
"en": "Badge push attempt {attempt}/{retries} failed — retrying: {error}",
"pl": "Badge push attempt {attempt}/{retries} failed — retrying: {error}",
"ru": "Badge push attempt {attempt}/{retries} failed — retrying: {error}",
"zh": "Badge push attempt {attempt}/{retries} failed — retrying: {error}"
"pl": "Próba {attempt}/{retries} push odznak nie powiodła się — ponawianie: {error}",
"ru": "Попытка {attempt}/{retries} push значков не удалась — повтор: {error}",
"zh": "徽章推送尝试 {attempt}/{retries} 失败——正在重试:{error}"
},
"Badge push failed after {retries} attempts: {error}": {
"bg": "Badge push failed after {retries} attempts: {error}",
"de": "Badge push failed after {retries} attempts: {error}",
"bg": "Push на значки се провали след {retries} опита: {error}",
"de": "Badge-Push nach {retries} Versuchen fehlgeschlagen: {error}",
"en": "Badge push failed after {retries} attempts: {error}",
"pl": "Badge push failed after {retries} attempts: {error}",
"ru": "Badge push failed after {retries} attempts: {error}",
"zh": "Badge push failed after {retries} attempts: {error}"
"pl": "Push odznak nie powiódł się po {retries} próbach: {error}",
"ru": "Push значков не удался после {retries} попыток: {error}",
"zh": "徽章推送在 {retries} 次尝试后失败:{error}"
},
"Badges commit SHA: {sha}": {
"bg": "Badges commit SHA: {sha}",
"de": "Badges commit SHA: {sha}",
"bg": "SHA на комита със значки: {sha}",
"de": "SHA des Badge-Commits: {sha}",
"en": "Badges commit SHA: {sha}",
"pl": "Badges commit SHA: {sha}",
"ru": "Badges commit SHA: {sha}",
"zh": "Badges commit SHA: {sha}"
"pl": "SHA commita z odznakami: {sha}",
"ru": "SHA коммита значков: {sha}",
"zh": "徽章提交 SHA{sha}"
},
"Badges pushed to badges branch": {
"bg": "Badges pushed to badges branch",
"de": "Badges pushed to badges branch",
"bg": "Значките са push-нати към клона badges",
"de": "Badges zum badges-Branch gepusht",
"en": "Badges pushed to badges branch",
"pl": "Badges pushed to badges branch",
"ru": "Badges pushed to badges branch",
"zh": "Badges pushed to badges branch"
"pl": "Odznaki wypchnięte do gałęzi badges",
"ru": "Значки отправлены в ветку badges",
"zh": "徽章已推送到 badges 分支"
},
"Base ref for diff": {
"bg": "Базов ref за diff",
"de": "Basis-Ref für Diff",
"en": "Base ref for diff",
"pl": "Bazowy ref dla diff",
"ru": "Базовый ref для diff",
"zh": "用于 diff 的基准 ref"
},
"Branch '{branch}' does not contain a task ID.\n Expected format: {prefix}-N-short-description": {
"bg": "Клон '{branch}' не съдържа ID на задача.\n Очакван формат: {prefix}-N-кратко-описание",
@@ -880,108 +928,148 @@
"zh": "分支 '{branch}' 不包含任务 ID。\n 预期格式: {prefix}-N-简短描述\n 示例: {prefix}-42-add-feature\n 修复: 重命名分支或先创建 Vikunja 任务:\n python -m devx.tools.create_task --title \"任务标题\""
},
"Branch is already up-to-date with origin/master.": {
"bg": "Branch is already up-to-date with origin/master.",
"de": "Branch is already up-to-date with origin/master.",
"bg": "Клонът вече е актуален спрямо origin/master.",
"de": "Branch ist bereits aktuell mit origin/master.",
"en": "Branch is already up-to-date with origin/master.",
"pl": "Branch is already up-to-date with origin/master.",
"ru": "Branch is already up-to-date with origin/master.",
"zh": "Branch is already up-to-date with origin/master."
"pl": "Gałąź jest już aktualna względem origin/master.",
"ru": "Ветка уже актуальна относительно origin/master.",
"zh": "分支已与 origin/master 同步。"
},
"Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.": {
"bg": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"de": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"bg": "Клонът изостава от master. Автоматичен rebase чрез Gitea API...\nНов CI run ще стартира автоматично след rebase.\nСледващият опит за auto-merge ще слее този PR.",
"de": "Branch liegt hinter master. Auto-Rebase via Gitea API...\nEin neuer CI-Lauf startet nach dem Rebase automatisch.\nDer nächste Auto-Merge-Versuch mergt diesen PR.",
"en": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"pl": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"ru": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"zh": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR."
"pl": "Gałąź jest za master. Automatyczny rebase przez Gitea API...\nNowy przebieg CI rozpocznie się automatycznie po rebase.\nNastępna próba auto-merge połączy ten PR.",
"ru": "Ветка отстаёт от master. Автоматический rebase через Gitea API...\nНовый CI-запуск начнётся автоматически после rebase.\nСледующая попытка auto-merge сольёт этот PR.",
"zh": "分支落后于 master。正在通过 Gitea API 自动 rebase...\nrebase 后将自动开始新的 CI 运行。\n下一次自动合并尝试将合并此 PR"
},
"Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master": {
"bg": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
"de": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
"bg": "Клонът изостава от origin/master. Първо rebase: git fetch origin master && git rebase origin/master",
"de": "Branch liegt hinter origin/master. Zuerst rebasen: git fetch origin master && git rebase origin/master",
"en": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
"pl": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
"ru": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
"zh": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master"
"pl": "Gałąź jest za origin/master. Najpierw wykonaj rebase: git fetch origin master && git rebase origin/master",
"ru": "Ветка отстаёт от origin/master. Сначала rebase: git fetch origin master && git rebase origin/master",
"zh": "分支落后于 origin/master。请先 rebasegit fetch origin master && git rebase origin/master"
},
"Branch is {count} commit(s) behind master. Rebasing...": {
"bg": "Branch is {count} commit(s) behind master. Rebasing...",
"de": "Branch is {count} commit(s) behind master. Rebasing...",
"bg": "Клонът изостава с {count} комит(а) от master. Rebase...",
"de": "Branch ist {count} Commit(s) hinter master. Rebase läuft...",
"en": "Branch is {count} commit(s) behind master. Rebasing...",
"pl": "Branch is {count} commit(s) behind master. Rebasing...",
"ru": "Branch is {count} commit(s) behind master. Rebasing...",
"zh": "Branch is {count} commit(s) behind master. Rebasing..."
"pl": "Gałąź jest o {count} commit(ów) za master. Rebase...",
"ru": "Ветка отстаёт на {count} коммит(ов) от master. Rebase...",
"zh": "分支落后 master {count} 个提交。正在 rebase..."
},
"Branch name (auto-fetched from PR if not given)": {
"bg": "Име на клон (извлича се автоматично от PR, ако не е зададено)",
"de": "Branch-Name (wird aus PR abgerufen, falls nicht angegeben)",
"en": "Branch name (auto-fetched from PR if not given)",
"pl": "Nazwa gałęzi (pobierana automatycznie z PR, jeśli nie podano)",
"ru": "Имя ветки (извлекается из PR, если не указано)",
"zh": "分支名称(未提供时从 PR 自动获取)"
},
"Branch name (e.g., DEVX-256-fix-foo)": {
"bg": "Branch name (e.g., DEVX-256-fix-foo)",
"de": "Branch name (e.g., DEVX-256-fix-foo)",
"bg": "Име на клон (напр. DEVX-256-fix-foo)",
"de": "Branch-Name (z. B. DEVX-256-fix-foo)",
"en": "Branch name (e.g., DEVX-256-fix-foo)",
"pl": "Branch name (e.g., DEVX-256-fix-foo)",
"ru": "Branch name (e.g., DEVX-256-fix-foo)",
"zh": "Branch name (e.g., DEVX-256-fix-foo)"
"pl": "Nazwa gałęzi (np. DEVX-256-fix-foo)",
"ru": "Имя ветки (напр. DEVX-256-fix-foo)",
"zh": "分支名称(例如 DEVX-256-fix-foo"
},
"Branch name (e.g., OBL-INFRA-531-fix-foo)": {
"bg": "Име на клон (напр. OBL-INFRA-531-fix-foo)",
"de": "Branch-Name (z. B. OBL-INFRA-531-fix-foo)",
"en": "Branch name (e.g., OBL-INFRA-531-fix-foo)",
"pl": "Nazwa gałęzi (np. OBL-INFRA-531-fix-foo)",
"ru": "Имя ветки (напр. OBL-INFRA-531-fix-foo)",
"zh": "分支名称(例如 OBL-INFRA-531-fix-foo"
},
"Branch name must contain a task ID.": {
"bg": "Branch name must contain a task ID.",
"de": "Branch name must contain a task ID.",
"bg": "Името на клона трябва да съдържа task ID.",
"de": "Der Branch-Name muss eine Task-ID enthalten.",
"en": "Branch name must contain a task ID.",
"pl": "Branch name must contain a task ID.",
"ru": "Branch name must contain a task ID.",
"zh": "Branch name must contain a task ID."
"pl": "Nazwa gałęzi musi zawierać ID zadania.",
"ru": "Имя ветки должно содержать ID задачи.",
"zh": "分支名称必须包含任务 ID"
},
"Build failed for {name}": {
"bg": "Build failed for {name}",
"de": "Build failed for {name}",
"bg": "Изграждането на {name} се провали",
"de": "Build für {name} fehlgeschlagen",
"en": "Build failed for {name}",
"pl": "Build failed for {name}",
"ru": "Build failed for {name}",
"zh": "Build failed for {name}"
"pl": "Budowanie {name} nie powiodło się",
"ru": "Сборка {name} не удалась",
"zh": "{name} 构建失败"
},
"Bumping version: {current} -> v{new_version}": {
"bg": "Bumping version: {current} -> v{new_version}",
"de": "Bumping version: {current} -> v{new_version}",
"bg": "Увеличаване на версията: {current} -> v{new_version}",
"de": "Version wird erhöht: {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}",
"zh": "Bumping version: {current} -> v{new_version}"
"ru": "Повышение версии: {current} -> v{new_version}",
"zh": "升级版本:{current} -> v{new_version}"
},
"CI checks did not complete within timeout.": {
"bg": "CI checks did not complete within timeout.",
"de": "CI checks did not complete within timeout.",
"bg": "CI проверките не завършиха в рамките на таймаута.",
"de": "CI-Checks wurden nicht innerhalb des Timeouts abgeschlossen.",
"en": "CI checks did not complete within timeout.",
"pl": "CI checks did not complete within timeout.",
"ru": "CI checks did not complete within timeout.",
"zh": "CI checks did not complete within timeout."
"pl": "Kontrole CI nie zakończyły się w ramach limitu czasu.",
"ru": "CI-проверки не завершились в течение таймаута.",
"zh": "CI 检查未在超时时间内完成。"
},
"CI checks failed.": {
"bg": "CI checks failed.",
"de": "CI checks failed.",
"bg": "CI проверките се провалиха.",
"de": "CI-Checks fehlgeschlagen.",
"en": "CI checks failed.",
"pl": "CI checks failed.",
"ru": "CI checks failed.",
"zh": "CI checks failed."
"pl": "Kontrole CI nie powiodły się.",
"ru": "CI-проверки завершились с ошибкой.",
"zh": "CI 检查失败。"
},
"CI run ID (for set-failed/set-passed)": {
"bg": "ID на CI run (за set-failed/set-passed)",
"de": "CI-Run-ID (für set-failed/set-passed)",
"en": "CI run ID (for set-failed/set-passed)",
"pl": "ID przebiegu CI (dla set-failed/set-passed)",
"ru": "ID CI-запуска (для set-failed/set-passed)",
"zh": "CI 运行 ID(用于 set-failed/set-passed"
},
"CI run ID that triggered the publish": {
"bg": "ID на CI run, който задейства публикуването",
"de": "CI-Run-ID, die die Veröffentlichung ausgelöst hat",
"en": "CI run ID that triggered the publish",
"pl": "ID przebiegu CI, który wyzwolił publikację",
"ru": "ID CI-запуска, инициировавшего публикацию",
"zh": "触发发布的 CI 运行 ID"
},
"CI_GITEA_API_TOKEN not set: {error}": {
"bg": "CI_GITEA_API_TOKEN не е зададен: {error}",
"de": "CI_GITEA_API_TOKEN nicht gesetzt: {error}",
"en": "CI_GITEA_API_TOKEN not set: {error}",
"pl": "CI_GITEA_API_TOKEN nie jest ustawiony: {error}",
"ru": "CI_GITEA_API_TOKEN не задан: {error}",
"zh": "未设置 CI_GITEA_API_TOKEN{error}"
},
"CI_GITEA_TOKEN environment variable required": {
"bg": "CI_GITEA_TOKEN environment variable required",
"de": "CI_GITEA_TOKEN environment variable required",
"bg": "Изисква се променлива на средата CI_GITEA_TOKEN",
"de": "Umgebungsvariable CI_GITEA_TOKEN erforderlich",
"en": "CI_GITEA_TOKEN environment variable required",
"pl": "CI_GITEA_TOKEN environment variable required",
"ru": "CI_GITEA_TOKEN environment variable required",
"zh": "CI_GITEA_TOKEN environment variable required"
"pl": "Wymagana zmienna środowiskowa CI_GITEA_TOKEN",
"ru": "Требуется переменная окружения CI_GITEA_TOKEN",
"zh": "需要环境变量 CI_GITEA_TOKEN"
},
"CI_GITEA_TOKEN is not set.": {
"bg": "CI_GITEA_TOKEN is not set.",
"de": "CI_GITEA_TOKEN is not set.",
"bg": "CI_GITEA_TOKEN не е зададен.",
"de": "CI_GITEA_TOKEN ist nicht gesetzt.",
"en": "CI_GITEA_TOKEN is not set.",
"pl": "CI_GITEA_TOKEN is not set.",
"ru": "CI_GITEA_TOKEN is not set.",
"zh": "CI_GITEA_TOKEN is not set."
"pl": "CI_GITEA_TOKEN nie jest ustawiony.",
"ru": "CI_GITEA_TOKEN не задан.",
"zh": "未设置 CI_GITEA_TOKEN"
},
"CI_GITEA_TOKEN is not set. Add it to .env or export it.": {
"bg": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"de": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"bg": "CI_GITEA_TOKEN не е зададен. Добавете го в .env или го експортирайте.",
"de": "CI_GITEA_TOKEN ist nicht gesetzt. Zu .env hinzufügen oder exportieren.",
"en": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"pl": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"ru": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"zh": "CI_GITEA_TOKEN is not set. Add it to .env or export it."
"pl": "CI_GITEA_TOKEN nie jest ustawiony. Dodaj go do .env lub wyeksportuj.",
"ru": "CI_GITEA_TOKEN не задан. Добавьте его в .env или экспортируйте.",
"zh": "未设置 CI_GITEA_TOKEN。请添加到 .env 或导出。"
},
"CI_GITEA_TOKEN is not set. Required to create a PR.": {
"bg": "CI_GITEA_TOKEN не е зададен. Необходим за създаване на PR.",
@@ -992,12 +1080,12 @@
"zh": "CI_GITEA_TOKEN 未设置。创建 PR 所需。"
},
"CI_GITEA_TOKEN not set — skipping login configuration.": {
"bg": "CI_GITEA_TOKEN not set — skipping login configuration.",
"de": "CI_GITEA_TOKEN not set — skipping login configuration.",
"bg": "CI_GITEA_TOKEN не е зададен — пропуска се конфигурацията за вход.",
"de": "CI_GITEA_TOKEN nicht gesetztLogin-Konfiguration wird übersprungen.",
"en": "CI_GITEA_TOKEN not set — skipping login configuration.",
"pl": "CI_GITEA_TOKEN not set — skipping login configuration.",
"ru": "CI_GITEA_TOKEN not set — skipping login configuration.",
"zh": "CI_GITEA_TOKEN not set — skipping login configuration."
"pl": "CI_GITEA_TOKEN nie jest ustawiony — pomijanie konfiguracji logowania.",
"ru": "CI_GITEA_TOKEN не задан — настройка входа пропускается.",
"zh": "未设置 CI_GITEA_TOKEN——跳过登录配置。"
},
"Cannot read __version__ from src/{pkg}/__init__.py — skipping.": {
"bg": "",
@@ -1008,20 +1096,20 @@
"zh": ""
},
"Cannot rebase: not on a branch (detached HEAD).": {
"bg": "Cannot rebase: not on a branch (detached HEAD).",
"de": "Cannot rebase: not on a branch (detached HEAD).",
"bg": "Не може rebase: не сте на клон (detached HEAD).",
"de": "Rebase nicht möglich: nicht auf einem Branch (detached HEAD).",
"en": "Cannot rebase: not on a branch (detached HEAD).",
"pl": "Cannot rebase: not on a branch (detached HEAD).",
"ru": "Cannot rebase: not on a branch (detached HEAD).",
"zh": "Cannot rebase: not on a branch (detached HEAD)."
"pl": "Nie można wykonać rebase: nie na gałęzi (detached HEAD).",
"ru": "Невозможно выполнить rebase: не на ветке (detached HEAD).",
"zh": "无法 rebase:不在分支上(detached HEAD)。"
},
"Checking CLI command documentation...": {
"bg": "Checking CLI command documentation...",
"de": "Checking CLI command documentation...",
"bg": "Проверка на документацията за CLI команди...",
"de": "Prüfe CLI-Befehlsdokumentation...",
"en": "Checking CLI command documentation...",
"pl": "Sprawdzanie dokumentacji poleceń CLI...",
"ru": "Checking CLI command documentation...",
"zh": "Checking CLI command documentation..."
"ru": "Проверка документации CLI-команд...",
"zh": "正在检查 CLI 命令文档..."
},
"Checking code block languages...": {
"bg": "",
@@ -1032,28 +1120,28 @@
"zh": ""
},
"Checking docs structure...": {
"bg": "Checking docs structure...",
"de": "Checking docs structure...",
"bg": "Проверка на структурата на документацията...",
"de": "Prüfe Dokumentationsstruktur...",
"en": "Checking docs structure...",
"pl": "Checking docs structure...",
"ru": "Checking docs structure...",
"zh": "Checking docs structure..."
"pl": "Sprawdzanie struktury dokumentacji...",
"ru": "Проверка структуры документации...",
"zh": "正在检查文档结构..."
},
"Checking duplicate headings...": {
"bg": "Checking duplicate headings...",
"de": "Checking duplicate headings...",
"bg": "Проверка за дублирани заглавия...",
"de": "Prüfe auf doppelte Überschriften...",
"en": "Checking duplicate headings...",
"pl": "Checking duplicate headings...",
"ru": "Checking duplicate headings...",
"zh": "Checking duplicate headings..."
"pl": "Sprawdzanie zduplikowanych nagłówków...",
"ru": "Проверка дублирующихся заголовков...",
"zh": "正在检查重复标题..."
},
"Checking for TODO/FIXME markers...": {
"bg": "Checking for TODO/FIXME markers...",
"de": "Checking for TODO/FIXME markers...",
"bg": "Проверка за TODO/FIXME маркери...",
"de": "Prüfe auf TODO/FIXME-Marker...",
"en": "Checking for TODO/FIXME markers...",
"pl": "Checking for TODO/FIXME markers...",
"ru": "Checking for TODO/FIXME markers...",
"zh": "Checking for TODO/FIXME markers..."
"pl": "Sprawdzanie znaczników TODO/FIXME...",
"ru": "Проверка меток TODO/FIXME...",
"zh": "正在检查 TODO/FIXME 标记..."
},
"Checking for orphan docs...": {
"bg": "",
@@ -1064,28 +1152,28 @@
"zh": ""
},
"Checking for stale docs...": {
"bg": "Checking for stale docs...",
"de": "Checking for stale docs...",
"bg": "Проверка за остарели документи...",
"de": "Prüfe auf veraltete Dokumente...",
"en": "Checking for stale docs...",
"pl": "Checking for stale docs...",
"ru": "Checking for stale docs...",
"zh": "Checking for stale docs..."
"pl": "Sprawdzanie nieaktualnych dokumentów...",
"ru": "Проверка устаревших документов...",
"zh": "正在检查过时文档..."
},
"Checking heading hierarchy...": {
"bg": "Checking heading hierarchy...",
"de": "Checking heading hierarchy...",
"bg": "Проверка на йерархията на заглавията...",
"de": "Prüfe Überschriftenhierarchie...",
"en": "Checking heading hierarchy...",
"pl": "Checking heading hierarchy...",
"ru": "Checking heading hierarchy...",
"zh": "Checking heading hierarchy..."
"pl": "Sprawdzanie hierarchii nagłówków...",
"ru": "Проверка иерархии заголовков...",
"zh": "正在检查标题层级..."
},
"Checking internal links...": {
"bg": "Checking internal links...",
"de": "Checking internal links...",
"bg": "Проверка на вътрешни връзки...",
"de": "Prüfe interne Links...",
"en": "Checking internal links...",
"pl": "Checking internal links...",
"ru": "Checking internal links...",
"zh": "Checking internal links..."
"pl": "Sprawdzanie linków wewnętrznych...",
"ru": "Проверка внутренних ссылок...",
"zh": "正在检查内部链接..."
},
"Checking line length...": {
"bg": "",
@@ -1104,12 +1192,12 @@
"zh": ""
},
"Checking required files...": {
"bg": "Checking required files...",
"de": "Checking required files...",
"bg": "Проверка на задължителните файлове...",
"de": "Prüfe erforderliche Dateien...",
"en": "Checking required files...",
"pl": "Checking required files...",
"ru": "Checking required files...",
"zh": "Checking required files..."
"pl": "Sprawdzanie wymaganych plików...",
"ru": "Проверка обязательных файлов...",
"zh": "正在检查必需文件..."
},
"Checking single H1 per file...": {
"bg": "",
@@ -1120,20 +1208,20 @@
"zh": ""
},
"Checking status for PR #{pr_number}...": {
"bg": "Checking status for PR #{pr_number}...",
"de": "Checking status for PR #{pr_number}...",
"bg": "Проверка на статуса на PR #{pr_number}...",
"de": "Prüfe Status für PR #{pr_number}...",
"en": "Checking status for PR #{pr_number}...",
"pl": "Checking status for PR #{pr_number}...",
"ru": "Checking status for PR #{pr_number}...",
"zh": "Checking status for PR #{pr_number}..."
"pl": "Sprawdzanie statusu PR #{pr_number}...",
"ru": "Проверка статуса PR #{pr_number}...",
"zh": "正在检查 PR #{pr_number} 的状态..."
},
"Checking trailing whitespace...": {
"bg": "Checking trailing whitespace...",
"de": "Checking trailing whitespace...",
"bg": "Проверка за крайни интервали...",
"de": "Prüfe auf abschließende Leerzeichen...",
"en": "Checking trailing whitespace...",
"pl": "Checking trailing whitespace...",
"ru": "Checking trailing whitespace...",
"zh": "Checking trailing whitespace..."
"pl": "Sprawdzanie końcowych białych znaków...",
"ru": "Проверка конечных пробелов...",
"zh": "正在检查行尾空白..."
},
"Checking version references for {pkg} (current: v{version})": {
"bg": "",
@@ -1143,6 +1231,14 @@
"ru": "",
"zh": ""
},
"CliRunner.invoke({target}) in test '{test}' reaches unpatched dangerous functions: {funcs}. Add @patch for each or patch the calling function.": {
"bg": "CliRunner.invoke({target}) в тест '{test}' достига непачнати опасни функции: {funcs}. Добавете @patch за всяка или patch-нете извикващата функция.",
"de": "CliRunner.invoke({target}) in Test '{test}' erreicht ungepatchte gefährliche Funktionen: {funcs}. @patch für jede hinzufügen oder die aufrufende Funktion patchen.",
"en": "CliRunner.invoke({target}) in test '{test}' reaches unpatched dangerous functions: {funcs}. Add @patch for each or patch the calling function.",
"pl": "CliRunner.invoke({target}) w teście '{test}' sięga niezałatanych niebezpiecznych funkcji: {funcs}. Dodaj @patch dla każdej lub załataj funkcję wywołującą.",
"ru": "CliRunner.invoke({target}) в тесте '{test}' достигает незапатченных опасных функций: {funcs}. Добавьте @patch для каждой или запатчите вызывающую функцию.",
"zh": "测试 '{test}' 中的 CliRunner.invoke({target}) 触达未修补的危险函数:{funcs}。请为每个函数添加 @patch 或修补调用函数。"
},
"Cloned existing wiki.": {
"bg": "",
"de": "",
@@ -1160,28 +1256,28 @@
"zh": ""
},
"Command failed ({cmd}): {stderr}": {
"bg": "Command failed ({cmd}): {stderr}",
"de": "Command failed ({cmd}): {stderr}",
"bg": "Командата се провали ({cmd}): {stderr}",
"de": "Befehl fehlgeschlagen ({cmd}): {stderr}",
"en": "Command failed ({cmd}): {stderr}",
"pl": "Polecenie nie powiodło się ({cmd}): {stderr}",
"ru": "Command failed ({cmd}): {stderr}",
"zh": "Command failed ({cmd}): {stderr}"
"ru": "Команда завершилась с ошибкой ({cmd}): {stderr}",
"zh": "命令失败({cmd}):{stderr}"
},
"Commit message: {msg}": {
"bg": "Commit message: {msg}",
"de": "Commit message: {msg}",
"bg": "Съобщение на комит: {msg}",
"de": "Commit-Nachricht: {msg}",
"en": "Commit message: {msg}",
"pl": "Commit message: {msg}",
"ru": "Commit message: {msg}",
"zh": "Commit message: {msg}"
"pl": "Treść commita: {msg}",
"ru": "Сообщение коммита: {msg}",
"zh": "提交信息:{msg}"
},
"Commit: {sha}": {
"bg": "Commit: {sha}",
"bg": "Комит: {sha}",
"de": "Commit: {sha}",
"en": "Commit: {sha}",
"pl": "Commit: {sha}",
"ru": "Commit: {sha}",
"zh": "Commit: {sha}"
"ru": "Коммит: {sha}",
"zh": "提交:{sha}"
},
"Committing and pushing...": {
"bg": "",
@@ -1192,12 +1288,12 @@
"zh": ""
},
"Comparing {base}..{head} ({count} files changed)": {
"bg": "Comparing {base}..{head} ({count} files changed)",
"de": "Comparing {base}..{head} ({count} files changed)",
"bg": "Сравняване на {base}..{head} ({count} променени файла)",
"de": "Vergleiche {base}..{head} ({count} geänderte Dateien)",
"en": "Comparing {base}..{head} ({count} files changed)",
"pl": "Porównywanie {base}..{head} ({count} zmienionych plików)",
"ru": "Comparing {base}..{head} ({count} files changed)",
"zh": "Comparing {base}..{head} ({count} files changed)"
"ru": "Сравнение {base}..{head} ({count} изменённых файлов)",
"zh": "正在比较 {base}..{head}{count} 个文件已更改)"
},
"Configuration OK: [tool.devx] present, devx versions consistent.": {
"bg": "Конфигурацията е OK: [tool.devx] присъства, версиите на devx са консистентни.",
@@ -1208,12 +1304,12 @@
"zh": "配置正常: [tool.devx] 已存在, devx 版本一致。"
},
"Configuration validation failed.": {
"bg": "Configuration validation failed.",
"de": "Configuration validation failed.",
"bg": "Валидацията на конфигурацията се провали.",
"de": "Konfigurationsvalidierung fehlgeschlagen.",
"en": "Configuration validation failed.",
"pl": "Configuration validation failed.",
"ru": "Configuration validation failed.",
"zh": "Configuration validation failed."
"pl": "Walidacja konfiguracji nie powiodła się.",
"ru": "Проверка конфигурации не удалась.",
"zh": "配置验证失败。"
},
"Configuring branch protection for {branch}...": {
"bg": "Конфигуриране на защита на клона {branch}...",
@@ -1232,20 +1328,20 @@
"zh": "正在配置仓库设置..."
},
"Configuring tea login '{name}' for {url}...": {
"bg": "Configuring tea login '{name}' for {url}...",
"de": "Configuring tea login '{name}' for {url}...",
"bg": "Конфигуриране на tea вход '{name}' за {url}...",
"de": "Konfiguriere tea-Login '{name}' für {url}...",
"en": "Configuring tea login '{name}' for {url}...",
"pl": "Configuring tea login '{name}' for {url}...",
"ru": "Configuring tea login '{name}' for {url}...",
"zh": "Configuring tea login '{name}' for {url}..."
"pl": "Konfigurowanie logowania tea '{name}' dla {url}...",
"ru": "Настройка входа tea '{name}' для {url}...",
"zh": "正在为 {url} 配置 tea 登录 '{name}'..."
},
"Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.": {
"bg": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"de": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"bg": "Не може да се определи номерът на PR. Използвайте --pr, за да го зададете изрично,\nили изпълнете командата от клон с отворен PR.",
"de": "PR-Nummer konnte nicht ermittelt werden. Mit --pr explizit angeben,\noder den Befehl von einem Branch mit offenem PR ausführen.",
"en": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"pl": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"ru": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"zh": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR."
"pl": "Nie można wykryć numeru PR. Użyj --pr, aby go podać jawnie,\nlub uruchom polecenie z gałęzi z otwartym PR.",
"ru": "Не удалось определить номер PR. Укажите его явно через --pr,\nили выполните команду из ветки с открытым PR.",
"zh": "无法检测 PR 编号。请使用 --pr 明确指定,\n或在有开放 PR 的分支上运行此命令。"
},
"Could not detect current branch: {error}": {
"bg": "Не може да се определи текущия клон: {error}",
@@ -1255,37 +1351,45 @@
"ru": "Не удалось определить текущую ветку: {error}",
"zh": "无法检测当前分支: {error}"
},
"Could not determine branch name from PR #{pr}": {
"bg": "Не може да се определи името на клона от PR #{pr}",
"de": "Branch-Name konnte aus PR #{pr} nicht ermittelt werden",
"en": "Could not determine branch name from PR #{pr}",
"pl": "Nie można określić nazwy gałęzi z PR #{pr}",
"ru": "Не удалось определить имя ветки из PR #{pr}",
"zh": "无法从 PR #{pr} 确定分支名称"
},
"Could not determine head SHA for PR #{pr_number}.": {
"bg": "Could not determine head SHA for PR #{pr_number}.",
"de": "Could not determine head SHA for PR #{pr_number}.",
"bg": "Не може да се определи head SHA за PR #{pr_number}.",
"de": "Head-SHA für PR #{pr_number} konnte nicht ermittelt werden.",
"en": "Could not determine head SHA for PR #{pr_number}.",
"pl": "Could not determine head SHA for PR #{pr_number}.",
"ru": "Could not determine head SHA for PR #{pr_number}.",
"zh": "Could not determine head SHA for PR #{pr_number}."
"pl": "Nie można określić head SHA dla PR #{pr_number}.",
"ru": "Не удалось определить head SHA для PR #{pr_number}.",
"zh": "无法确定 PR #{pr_number} 的 head SHA。"
},
"Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.": {
"bg": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"de": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"bg": "Не може да се определи репозитория. Задайте променливите DEVX_REPO_OWNER и DEVX_REPO_NAME\nили GITHUB_REPOSITORY.",
"de": "Repository konnte nicht ermittelt werden. Setzen Sie DEVX_REPO_OWNER und DEVX_REPO_NAME\noder die Umgebungsvariable GITHUB_REPOSITORY.",
"en": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"pl": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"ru": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"zh": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables."
"pl": "Nie można określić repozytorium. Ustaw zmienne DEVX_REPO_OWNER i DEVX_REPO_NAME\nlub GITHUB_REPOSITORY.",
"ru": "Не удалось определить репозиторий. Задайте переменные DEVX_REPO_OWNER и DEVX_REPO_NAME\nили GITHUB_REPOSITORY.",
"zh": "无法确定仓库。请设置环境变量 DEVX_REPO_OWNER DEVX_REPO_NAME\n GITHUB_REPOSITORY"
},
"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.",
"bg": "Не може да се извлече conventional commit съобщение от PR комитите.",
"de": "Konnte keine Conventional-Commit-Nachricht aus den PR-Commits extrahieren.",
"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.",
"zh": "Could not extract conventional commit message from PR commits."
"ru": "Не удалось извлечь conventional commit сообщение из коммитов PR.",
"zh": "无法从 PR 提交中提取 conventional commit 信息。"
},
"Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).": {
"bg": "Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).",
"de": "Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).",
"bg": "Не може да се извлече заглавието на PR от Gitea (CI_GITEA_TOKEN не е зададен или PR не е намерен).",
"de": "PR-Titel konnte nicht von Gitea abgerufen werden (CI_GITEA_TOKEN nicht gesetzt oder PR nicht gefunden).",
"en": "Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).",
"pl": "Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).",
"ru": "Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).",
"zh": "Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found)."
"pl": "Nie można pobrać tytułu PR z Gitea (CI_GITEA_TOKEN nie ustawiony lub PR nie znaleziony).",
"ru": "Не удалось получить заголовок PR из Gitea (CI_GITEA_TOKEN не задан или PR не найден).",
"zh": "无法从 Gitea 获取 PR 标题(CI_GITEA_TOKEN 未设置或 PR 未找到)。"
},
"Could not find Vikunja task {task_id} in project {project_id}.": {
"bg": "Не е намерена Vikunja задача {task_id} в проект {project_id}.",
@@ -1296,28 +1400,36 @@
"zh": "在项目 {project_id} 中找不到 Vikunja 任务 {task_id}。"
},
"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.",
"bg": "Не е намерена Vikunja задача {task_id} в проект {project_id}. Всеки PR трябва да има съответна Vikunja задача.",
"de": "Vikunja-Task {task_id} in Projekt {project_id} nicht gefunden. Jeder PR muss einen entsprechenden Vikunja-Task haben.",
"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.",
"zh": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task."
"ru": "Задача Vikunja {task_id} в проекте {project_id} не найдена. Каждый PR должен иметь соответствующую задачу Vikunja.",
"zh": "在项目 {project_id} 中未找到 Vikunja 任务 {task_id}。每个 PR 必须有对应的 Vikunja 任务。"
},
"Could not find __version__ in {file}": {
"bg": "Could not find __version__ in {file}",
"de": "Could not find __version__ in {file}",
"bg": "Не е намерен __version__ в {file}",
"de": "__version__ in {file} nicht gefunden",
"en": "Could not find __version__ in {file}",
"pl": "Nie znaleziono __version__ w {file}",
"ru": "Could not find __version__ in {file}",
"zh": "Could not find __version__ in {file}"
"ru": "__version__ не найден в {file}",
"zh": "在 {file} 中未找到 __version__"
},
"Could not find pinned version for {pkg}": {
"bg": "Не е намерена фиксирана версия за {pkg}",
"de": "Keine gepinnte Version für {pkg} gefunden",
"en": "Could not find pinned version for {pkg}",
"pl": "Nie znaleziono przypiętej wersji dla {pkg}",
"ru": "Не найдена закреплённая версия для {pkg}",
"zh": "未找到 {pkg} 的固定版本"
},
"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.",
"bg": "Не може да се извлече време за изпълнение на теста от изхода.",
"de": "Testausführungszeit konnte aus der Ausgabe nicht gelesen werden.",
"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.",
"zh": "Could not parse test execution time from output."
"ru": "Не удалось извлечь время выполнения теста из вывода.",
"zh": "无法从输出中解析测试执行时间。"
},
"Created PR #{index}: {title}\n {url}": {
"bg": "Създаден PR #{index}: {title}\n {url}",
@@ -1336,28 +1448,44 @@
"zh": "已创建 Vikunja 任务: {identifier} (id={task_id})"
},
"Created issue #{issue_id}: {title}": {
"bg": "Created issue #{issue_id}: {title}",
"de": "Created issue #{issue_id}: {title}",
"bg": "Създадено issue #{issue_id}: {title}",
"de": "Issue #{issue_id} erstellt: {title}",
"en": "Created issue #{issue_id}: {title}",
"pl": "Utworzono zgłoszenie #{issue_id}: {title}",
"ru": "Created issue #{issue_id}: {title}",
"zh": "Created issue #{issue_id}: {title}"
"ru": "Создано issue #{issue_id}: {title}",
"zh": "已创建 issue #{issue_id}{title}"
},
"Created release commit.": {
"bg": "Created release commit.",
"de": "Created release commit.",
"bg": "Създаден е release комит.",
"de": "Release-Commit erstellt.",
"en": "Created release commit.",
"pl": "Utworzono commit wydania.",
"ru": "Created release commit.",
"zh": "Created release commit."
"ru": "Создан релизный коммит.",
"zh": "已创建发布提交。"
},
"Dependencies must have documentation comments.": {
"bg": "Dependencies must have documentation comments.",
"de": "Dependencies must have documentation comments.",
"bg": "Зависимостите трябва да имат документиращи коментари.",
"de": "Abhängigkeiten müssen Dokumentationskommentare haben.",
"en": "Dependencies must have documentation comments.",
"pl": "Dependencies must have documentation comments.",
"ru": "Dependencies must have documentation comments.",
"zh": "Dependencies must have documentation comments."
"pl": "Zależności muszą mieć komentarze dokumentacyjne.",
"ru": "Зависимости должны иметь документирующие комментарии.",
"zh": "依赖项必须有文档注释。"
},
"Directory containing Ansible roles": {
"bg": "Директория, съдържаща Ansible роли",
"de": "Verzeichnis mit Ansible-Rollen",
"en": "Directory containing Ansible roles",
"pl": "Katalog zawierający role Ansible",
"ru": "Директория, содержащая роли Ansible",
"zh": "包含 Ansible 角色的目录"
},
"Directory containing spec files": {
"bg": "Директория, съдържаща spec файлове",
"de": "Verzeichnis mit Spec-Dateien",
"en": "Directory containing spec files",
"pl": "Katalog zawierający pliki spec",
"ru": "Директория, содержащая spec-файлы",
"zh": "包含规范文件的目录"
},
"Directory to scan (default: tests/integration). Can be repeated.": {
"bg": "Директория за сканиране (по подразбиране: tests/integration). Може да се повтаря.",
@@ -1376,7 +1504,7 @@
"zh": "Docker 守护进程已在运行"
},
"Docker daemon failed to start": {
"bg": "Docker daemon failed to start",
"bg": "Docker демонът не успя да стартира",
"de": "Docker-Daemon konnte nicht gestartet werden",
"en": "Docker daemon failed to start",
"pl": "Nie udało się uruchomić demona Docker",
@@ -1384,7 +1512,7 @@
"zh": "Docker 守护进程启动失败"
},
"Docker daemon started": {
"bg": "Docker daemon started",
"bg": "Docker демонът стартира",
"de": "Docker-Daemon gestartet",
"en": "Docker daemon started",
"pl": "Demon Docker uruchomiony",
@@ -1392,20 +1520,20 @@
"zh": "Docker 守护进程已启动"
},
"Dockerfile not found: {path}": {
"bg": "Dockerfile not found: {path}",
"de": "Dockerfile not found: {path}",
"bg": "Dockerfile не е намерен: {path}",
"de": "Dockerfile nicht gefunden: {path}",
"en": "Dockerfile not found: {path}",
"pl": "Dockerfile not found: {path}",
"ru": "Dockerfile not found: {path}",
"zh": "Dockerfile not found: {path}"
"pl": "Nie znaleziono Dockerfile: {path}",
"ru": "Dockerfile не найден: {path}",
"zh": "未找到 Dockerfile{path}"
},
"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.",
"bg": "Режим dry-run: на клон '{branch}' (не master). Някои проверки може да се държат различно.",
"de": "Dry-Run-Modus: auf Branch '{branch}' (nicht master). Einige Prüfungen können sich anders verhalten.",
"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.",
"zh": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently."
"ru": "Режим dry-run: в ветке '{branch}' (не master). Некоторые проверки могут вести себя иначе.",
"zh": "Dry-run 模式:在分支 '{branch}' 上(非 master)。某些检查可能表现不同。"
},
"ERROR: CI_GITEA_TOKEN is not set.": {
"bg": "ГРЕШКА: CI_GITEA_TOKEN не е зададен.",
@@ -1424,12 +1552,12 @@
"zh": "错误:未指定仓库名称。请使用 --repo 或设置 DEVX_REPO_NAME。"
},
"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:",
"bg": "ГРЕШКА: Проверката за консистентност на таговете се провали. Съществуващите тагове са несъответстващи:",
"de": "FEHLER: Tag-Konsistenzprüfung fehlgeschlagen. Bestehende Tags sind falsch zugeordnet:",
"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:",
"zh": "ERROR: Tag consistency check failed. Existing tags are misaligned:"
"ru": "ОШИБКА: Проверка согласованности тегов не удалась. Существующие теги несогласованы:",
"zh": "错误:标签一致性检查失败。现有标签不匹配:"
},
"ERROR: VIKUNJA_TOKEN is not set.": {
"bg": "ГРЕШКА: VIKUNJA_TOKEN не е зададен.",
@@ -1440,12 +1568,12 @@
"zh": "错误:未设置 VIKUNJA_TOKEN。"
},
"ERROR: mapping.json not found at {path}": {
"bg": "ERROR: mapping.json not found at {path}",
"de": "ERROR: mapping.json not found at {path}",
"bg": "ГРЕШКА: mapping.json не е намерен в {path}",
"de": "FEHLER: mapping.json nicht gefunden unter {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}",
"zh": "ERROR: mapping.json not found at {path}"
"ru": "ОШИБКА: mapping.json не найден по пути {path}",
"zh": "错误:在 {path} 未找到 mapping.json"
},
"Each item must be a string or an object with 'id', got {type}": {
"bg": "Всеки елемент трябва да е низ или обект с 'id', получено {type}",
@@ -1456,12 +1584,12 @@
"zh": "每个元素必须是字符串或带有 'id' 的对象,得到 {type}"
},
"Ensuring standard labels...": {
"bg": "Ensuring standard labels...",
"de": "Ensuring standard labels...",
"bg": "Осигуряване на стандартни етикети...",
"de": "Standard-Labels werden sichergestellt...",
"en": "Ensuring standard labels...",
"pl": "Ensuring standard labels...",
"ru": "Ensuring standard labels...",
"zh": "Ensuring standard labels..."
"pl": "Zapewnianie standardowych etykiet...",
"ru": "Обеспечение стандартных меток...",
"zh": "正在确保标准标签..."
},
"FAIL: Could not clone wiki for verification.": {
"bg": "",
@@ -1472,60 +1600,76 @@
"zh": ""
},
"FAIL: {n} documentation issues found:": {
"bg": "FAIL: {n} documentation issues found:",
"de": "FAIL: {n} documentation issues found:",
"bg": "ГРЕШКА: Намерени {n} проблема в документацията:",
"de": "FEHLER: {n} Dokumentationsprobleme gefunden:",
"en": "FAIL: {n} documentation issues found:",
"pl": "FAIL: {n} documentation issues found:",
"ru": "FAIL: {n} documentation issues found:",
"zh": "FAIL: {n} documentation issues found:"
"pl": "BŁĄD: Znaleziono {n} problemów z dokumentacją:",
"ru": "ОШИБКА: Найдено {n} проблем в документации:",
"zh": "失败:发现 {n} 个文档问题:"
},
"FAILED: {count} undocumented dependency/ies": {
"bg": "FAILED: {count} undocumented dependency/ies",
"de": "FAILED: {count} undocumented dependency/ies",
"bg": "НЕУСПЕШНО: {count} недокументирани зависимости",
"de": "FEHLGESCHLAGEN: {count} undokumentierte Abhängigkeit(en)",
"en": "FAILED: {count} undocumented dependency/ies",
"pl": "FAILED: {count} undocumented dependency/ies",
"ru": "FAILED: {count} undocumented dependency/ies",
"zh": "FAILED: {count} undocumented dependency/ies"
"pl": "NIEUDANE: {count} nieudokumentowanych zależności",
"ru": "ПРОВАЛЕНО: {count} недокументированных зависимостей",
"zh": "失败:{count} 个未记录的依赖项"
},
"Failed images: {names}": {
"bg": "Failed images: {names}",
"de": "Failed images: {names}",
"bg": "Неуспешни изображения: {names}",
"de": "Fehlgeschlagene Images: {names}",
"en": "Failed images: {names}",
"pl": "Failed images: {names}",
"ru": "Failed images: {names}",
"zh": "Failed images: {names}"
"pl": "Nieudane obrazy: {names}",
"ru": "Неудавшиеся образы: {names}",
"zh": "失败的镜像:{names}"
},
"Failed to create branch: {error}": {
"bg": "Неуспешно създаване на клон: {error}",
"de": "Branch konnte nicht erstellt werden: {error}",
"en": "Failed to create branch: {error}",
"pl": "Nie udało się utworzyć gałęzi: {error}",
"ru": "Не удалось создать ветку: {error}",
"zh": "创建分支失败:{error}"
},
"Failed to create issue via tea: {error}": {
"bg": "Failed to create issue via tea: {error}",
"de": "Failed to create issue via tea: {error}",
"bg": "Неуспешно създаване на issue чрез tea: {error}",
"de": "Issue konnte nicht via tea erstellt werden: {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}",
"zh": "Failed to create issue via tea: {error}"
"ru": "Не удалось создать issue через tea: {error}",
"zh": "通过 tea 创建 issue 失败:{error}"
},
"Failed to delete {count} image version(s)": {
"bg": "Failed to delete {count} image version(s)",
"de": "Failed to delete {count} image version(s)",
"bg": "Неуспешно изтриване на {count} версии на изображения",
"de": "{count} Image-Version(en) konnten nicht gelöscht werden",
"en": "Failed to delete {count} image version(s)",
"pl": "Failed to delete {count} image version(s)",
"ru": "Failed to delete {count} image version(s)",
"zh": "Failed to delete {count} image version(s)"
"pl": "Nie udało się usunąć {count} wersji obrazów",
"ru": "Не удалось удалить {count} версий образов",
"zh": "删除 {count} 个镜像版本失败"
},
"Failed to fetch PR #{pr}: {error}": {
"bg": "Неуспешно извличане на PR #{pr}: {error}",
"de": "PR #{pr} konnte nicht abgerufen werden: {error}",
"en": "Failed to fetch PR #{pr}: {error}",
"pl": "Nie udało się pobrać PR #{pr}: {error}",
"ru": "Не удалось получить PR #{pr}: {error}",
"zh": "获取 PR #{pr} 失败:{error}"
},
"Failed to list versions for {name}: {error}": {
"bg": "Failed to list versions for {name}: {error}",
"de": "Failed to list versions for {name}: {error}",
"bg": "Неуспешно изброяване на версиите за {name}: {error}",
"de": "Versionen für {name} konnten nicht aufgelistet werden: {error}",
"en": "Failed to list versions for {name}: {error}",
"pl": "Failed to list versions for {name}: {error}",
"ru": "Failed to list versions for {name}: {error}",
"zh": "Failed to list versions for {name}: {error}"
"pl": "Nie udało się wylistować wersji dla {name}: {error}",
"ru": "Не удалось получить список версий для {name}: {error}",
"zh": "列出 {name} 的版本失败:{error}"
},
"Failed to push release commit after 3 attempts. Manual intervention required.": {
"bg": "Failed to push release commit after 3 attempts. Manual intervention required.",
"de": "Failed to push release commit after 3 attempts. Manual intervention required.",
"bg": "Неуспешен push на release комита след 3 опита. Изисква се ръчна намеса.",
"de": "Release-Commit konnte nach 3 Versuchen nicht gepusht werden. Manuelles Eingreifen erforderlich.",
"en": "Failed to push release commit after 3 attempts. Manual intervention required.",
"pl": "Failed to push release commit after 3 attempts. Manual intervention required.",
"ru": "Failed to push release commit after 3 attempts. Manual intervention required.",
"zh": "Failed to push release commit after 3 attempts. Manual intervention required."
"pl": "Nie udało się wypchnąć commita release po 3 próbach. Wymagana ręczna interwencja.",
"ru": "Не удалось отправить релизный коммит после 3 попыток. Требуется ручное вмешательство.",
"zh": "3 次尝试后仍无法推送发布提交。需要人工干预。"
},
"Failed to start ssh-agent: {error}": {
"bg": "Неуспешно стартиране на ssh-agent: {error}",
@@ -1535,61 +1679,93 @@
"ru": "Не удалось запустить ssh-agent: {error}",
"zh": "启动 ssh-agent 失败: {error}"
},
"Failed to update PR #{pr}: {error}": {
"bg": "Неуспешно обновяване на PR #{pr}: {error}",
"de": "PR #{pr} konnte nicht aktualisiert werden: {error}",
"en": "Failed to update PR #{pr}: {error}",
"pl": "Nie udało się zaktualizować PR #{pr}: {error}",
"ru": "Не удалось обновить PR #{pr}: {error}",
"zh": "更新 PR #{pr} 失败:{error}"
},
"Failed to update {file}": {
"bg": "Неуспешно обновяване на {file}",
"de": "{file} konnte nicht aktualisiert werden",
"en": "Failed to update {file}",
"pl": "Nie udało się zaktualizować {file}",
"ru": "Не удалось обновить {file}",
"zh": "更新 {file} 失败"
},
"Fetch failed: {error}": {
"bg": "Fetch failed: {error}",
"de": "Fetch failed: {error}",
"bg": "Извличането се провали: {error}",
"de": "Abruf fehlgeschlagen: {error}",
"en": "Fetch failed: {error}",
"pl": "Fetch failed: {error}",
"ru": "Fetch failed: {error}",
"zh": "Fetch failed: {error}"
"pl": "Pobieranie nie powiodło się: {error}",
"ru": "Получение не удалось: {error}",
"zh": "获取失败:{error}"
},
"Fetching logs for PR #{pr_number}...": {
"bg": "Fetching logs for PR #{pr_number}...",
"de": "Fetching logs for PR #{pr_number}...",
"bg": "Извличане на логове за PR #{pr_number}...",
"de": "Rufe Logs für PR #{pr_number} ab...",
"en": "Fetching logs for PR #{pr_number}...",
"pl": "Fetching logs for PR #{pr_number}...",
"ru": "Fetching logs for PR #{pr_number}...",
"zh": "Fetching logs for PR #{pr_number}..."
"pl": "Pobieranie logów dla PR #{pr_number}...",
"ru": "Получение логов для PR #{pr_number}...",
"zh": "正在获取 PR #{pr_number} 的日志..."
},
"Fetching origin/master...": {
"bg": "Fetching origin/master...",
"de": "Fetching origin/master...",
"bg": "Извличане на origin/master...",
"de": "Rufe origin/master ab...",
"en": "Fetching origin/master...",
"pl": "Fetching origin/master...",
"ru": "Fetching origin/master...",
"zh": "Fetching origin/master..."
"pl": "Pobieranie origin/master...",
"ru": "Получение origin/master...",
"zh": "正在获取 origin/master..."
},
"Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.": {
"bg": "Корекция: добавете @patch декоратори или with patch() контекстни мениджъри за subprocess/time.sleep извиквания, или patch-нете извикващата функция.",
"de": "Behebung: @patch-Dekoratoren oder with patch()-Kontextmanager für subprocess/time.sleep-Aufrufe hinzufügen, oder die aufrufende Funktion patchen.",
"en": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.",
"pl": "Poprawka: dodaj dekoratory @patch lub menedżery kontekstu with patch() dla wywołań subprocess/time.sleep, albo załataj funkcję wywołującą.",
"ru": "Исправление: добавьте декораторы @patch или контекстные менеджеры with patch() для вызовов subprocess/time.sleep, либо запатчите вызывающую функцию.",
"zh": "修复:为 subprocess/time.sleep 调用添加 @patch 装饰器或 with patch() 上下文管理器,或修补调用函数。"
},
"Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.\n": {
"bg": "Корекция: добавете @patch декоратори или with patch() контекстни мениджъри за subprocess/time.sleep извиквания, или patch-нете извикващата функция.\n",
"de": "Behebung: @patch-Dekoratoren oder with patch()-Kontextmanager für subprocess/time.sleep-Aufrufe hinzufügen, oder die aufrufende Funktion patchen.\n",
"en": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.\n",
"pl": "Poprawka: dodaj dekoratory @patch lub menedżery kontekstu with patch() dla wywołań subprocess/time.sleep, albo załataj funkcję wywołującą.\n",
"ru": "Исправление: добавьте декораторы @patch или контекстные менеджеры with patch() для вызовов subprocess/time.sleep, либо запатчите вызывающую функцию.\n",
"zh": "修复:为 subprocess/time.sleep 调用添加 @patch 装饰器或 with patch() 上下文管理器,或修补调用函数。\n"
},
"Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.": {
"bg": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"de": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"bg": "Force-push се провали:\n{error}\nОтдалеченото репозитори може да съдържа неочаквани комити. Извличане и нов опит.",
"de": "Force-Push fehlgeschlagen:\n{error}\nDas Remote kann unerwartete Commits enthalten. Fetchen und erneut versuchen.",
"en": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"pl": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"ru": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"zh": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again."
"pl": "Force-push nie powiódł się:\n{error}\nZdalne repozytorium może mieć nieoczekiwane commity. Pobierz i spróbuj ponownie.",
"ru": "Force-push не удался:\n{error}\nУдалённый репозиторий может содержать неожиданные коммиты. Выполните fetch и повторите.",
"zh": "强制推送失败:\n{error}\n远程可能有意外提交。请先 fetch 后重试。"
},
"Force-pushing...": {
"bg": "Force-pushing...",
"de": "Force-pushing...",
"bg": "Force-push...",
"de": "Force-Push läuft...",
"en": "Force-pushing...",
"pl": "Force-pushing...",
"ru": "Force-pushing...",
"zh": "Force-pushing..."
"pl": "Wymuszone wypychanie...",
"ru": "Force-push...",
"zh": "正在强制推送..."
},
"Found {count} mutable global(s) — use factory functions or pytest fixtures.": {
"bg": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
"de": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
"bg": "Намерени {count} променливи глобални — използвайте фабрични функции или pytest fixtures.",
"de": "{count} mutable Global(s) gefunden — Factory-Funktionen oder pytest-Fixtures verwenden.",
"en": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
"pl": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
"ru": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
"zh": "Found {count} mutable global(s) — use factory functions or pytest fixtures."
"pl": "Znaleziono {count} mutowalnych globali — użyj funkcji fabrykujących lub fixture'ów pytest.",
"ru": "Найдено {count} изменяемых глобальных — используйте фабричные функции или pytest-фикстуры.",
"zh": "发现 {count} 个可变全局变量——请使用工厂函数或 pytest fixtures"
},
"Found {count} stale documentation reference(s)": {
"bg": "Found {count} stale documentation reference(s)",
"de": "Found {count} stale documentation reference(s)",
"bg": "Намерени {count} остарели препратки в документацията",
"de": "{count} veraltete Dokumentationsreferenz(en) gefunden",
"en": "Found {count} stale documentation reference(s)",
"pl": "Found {count} stale documentation reference(s)",
"ru": "Found {count} stale documentation reference(s)",
"zh": "Found {count} stale documentation reference(s)"
"pl": "Znaleziono {count} nieaktualnych odwołań w dokumentacji",
"ru": "Найдено {count} устаревших ссылок в документации",
"zh": "发现 {count} 个过时的文档引用"
},
"Found {count} unsafe identity check(s) in integration tests.": {
"bg": "Намерени са {count} небрежни проверки за идентичност в интеграционните тестове.",
@@ -1600,44 +1776,44 @@
"zh": "在集成测试中发现 {count} 个不安全的身份检查。"
},
"Found {count} version(s):": {
"bg": "Found {count} version(s):",
"de": "Found {count} version(s):",
"bg": "Намерени {count} версии:",
"de": "{count} Version(en) gefunden:",
"en": "Found {count} version(s):",
"pl": "Found {count} version(s):",
"ru": "Found {count} version(s):",
"zh": "Found {count} version(s):"
"pl": "Znaleziono {count} wersji:",
"ru": "Найдено {count} версий:",
"zh": "找到 {count} 个版本:"
},
"GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation.": {
"bg": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"de": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"bg": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID не са зададени; изпълнение без отмяна между раннъри.",
"de": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID nicht gesetzt; läuft ohne Runner-übergreifende Abbrüche.",
"en": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"pl": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID nie ustawione; uruchamianie bez anulowania między runnerami.",
"ru": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
"zh": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation."
"ru": "GITEA_URL/CI_GITEA_TOKEN/RUN_ID не заданы; работа без отмены между раннерами.",
"zh": "未设置 GITEA_URL/CI_GITEA_TOKEN/RUN_ID;运行时无法进行跨 runner 取消。"
},
"Generated {count} badge files": {
"bg": "Generated {count} badge files",
"de": "Generated {count} badge files",
"bg": "Генерирани {count} файла със значки",
"de": "{count} Badge-Dateien generiert",
"en": "Generated {count} badge files",
"pl": "Generated {count} badge files",
"ru": "Generated {count} badge files",
"zh": "Generated {count} badge files"
"pl": "Wygenerowano {count} plików odznak",
"ru": "Сгенерировано {count} файлов значков",
"zh": "已生成 {count} 个徽章文件"
},
"Generated {file} with prefix '{prefix}'.": {
"bg": "Generated {file} with prefix '{prefix}'.",
"de": "Generated {file} with prefix '{prefix}'.",
"bg": "Генериран {file} с префикс '{prefix}'.",
"de": "{file} mit Präfix '{prefix}' generiert.",
"en": "Generated {file} with prefix '{prefix}'.",
"pl": "Wygenerowano {file} z prefiksem '{prefix}'.",
"ru": "Generated {file} with prefix '{prefix}'.",
"zh": "Generated {file} with prefix '{prefix}'."
"ru": "Сгенерирован {file} с префиксом '{prefix}'.",
"zh": "已生成带前缀 '{prefix}' 的 {file}。"
},
"Generating badges in {out}...": {
"bg": "Generating badges in {out}...",
"de": "Generating badges in {out}...",
"bg": "Генериране на значки в {out}...",
"de": "Generiere Badges in {out}...",
"en": "Generating badges in {out}...",
"pl": "Generating badges in {out}...",
"ru": "Generating badges in {out}...",
"zh": "Generating badges in {out}..."
"pl": "Generowanie odznak w {out}...",
"ru": "Генерация значков в {out}...",
"zh": "正在 {out} 中生成徽章..."
},
"Git tag or ref that was deployed": {
"bg": "Git таг или референция, която беше разгърната",
@@ -1656,12 +1832,12 @@
"zh": "要部署的 Git 标签(例如 v0.28.1)。"
},
"Gitea API token not set. Set one of: {names}": {
"bg": "Gitea API token not set. Set one of: {names}",
"de": "Gitea API token not set. Set one of: {names}",
"bg": "Gitea API токен не е зададен. Задайте един от: {names}",
"de": "Gitea-API-Token nicht gesetzt. Setzen Sie einen von: {names}",
"en": "Gitea API token not set. Set one of: {names}",
"pl": "Gitea API token not set. Set one of: {names}",
"ru": "Gitea API token not set. Set one of: {names}",
"zh": "Gitea API token not set. Set one of: {names}"
"pl": "Token API Gitea nie jest ustawiony. Ustaw jeden z: {names}",
"ru": "Токен Gitea API не задан. Установите один из: {names}",
"zh": "未设置 Gitea API 令牌。请设置以下之一:{names}"
},
"Gitea PyPI registry: {tag} already published — continuing.": {
"bg": "Gitea PyPI registry: {tag} вече е публикуван — продължава.",
@@ -1680,36 +1856,36 @@
"zh": "Gitea release {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.",
"de": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
"bg": "HEAD е release комит ('{msg}'), но тагът {tag} липсва. Възстановяване чрез създаване на таг.",
"de": "HEAD ist ein Release-Commit ('{msg}'), aber Tag {tag} fehlt. Wiederherstellung durch Tag-Erstellung.",
"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.",
"zh": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag."
"ru": "HEAD является релизным коммитом ('{msg}'), но тег {tag} отсутствует. Восстановление созданием тега.",
"zh": "HEAD 是发布提交('{msg}'),但缺少标签 {tag}。正在通过创建标签恢复。"
},
"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.",
"bg": "HEAD е release комит за v{version}, но тагът {tag} сочи към различен комит ({tag_commit} срещу HEAD {head_commit}). Това показва несъответствие таг/комит.",
"de": "HEAD ist ein Release-Commit für v{version}, aber Tag {tag} zeigt auf einen anderen Commit ({tag_commit} vs. HEAD {head_commit}). Dies deutet auf eine Tag/Commit-Fehlzuordnung hin.",
"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.",
"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."
"ru": "HEAD является релизным коммитом для v{version}, но тег {tag} указывает на другой коммит ({tag_commit} против HEAD {head_commit}). Это указывает на несоответствие тег/коммит.",
"zh": "HEAD 是 v{version} 的发布提交,但标签 {tag} 指向不同的提交({tag_commit} HEAD {head_commit})。这表明标签/提交不匹配。"
},
"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.",
"bg": "HEAD вече е release комит ('{msg}') и тагът {tag} сочи към HEAD. Пропуска се.",
"de": "HEAD ist bereits ein Release-Commit ('{msg}') und Tag {tag} zeigt auf HEAD. Wird übersprungen.",
"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.",
"zh": "HEAD is already a release commit ('{msg}') and tag {tag} points to HEAD. Skipping."
"ru": "HEAD уже является релизным коммитом ('{msg}') и тег {tag} указывает на HEAD. Пропускается.",
"zh": "HEAD 已是发布提交('{msg}')且标签 {tag} 指向 HEAD。跳过。"
},
"HEAD is not a release commit for {tag} — skipping publish.": {
"bg": "HEAD is not a release commit for {tag} — skipping publish.",
"de": "HEAD is not a release commit for {tag} — skipping publish.",
"bg": "HEAD не е release комит за {tag} — публикуването се пропуска.",
"de": "HEAD ist kein Release-Commit für {tag} — Veröffentlichung wird übersprungen.",
"en": "HEAD is not a release commit for {tag} — skipping publish.",
"pl": "HEAD nie jest commitem wydania dla {tag} — pomijanie publikacji.",
"ru": "HEAD is not a release commit for {tag} — skipping publish.",
"zh": "HEAD is not a release commit for {tag} — skipping publish."
"ru": "HEAD не является релизным коммитом для {tag} — публикация пропускается.",
"zh": "HEAD 不是 {tag} 的发布提交——跳过发布。"
},
"HTTP error: {status} — {message}": {
"bg": "HTTP грешка: {status} — {message}",
@@ -1727,6 +1903,22 @@
"ru": "HTTP {status} Запрещено — у вашего токена нет прав администратора.\nУбедитесь, что токен принадлежит владельцу репозитория или администратору организации.\nЛибо настройте защиту ветки вручную в разделе Настройки → Ветки.",
"zh": "HTTP {status} 禁止访问 — 您的令牌缺少管理员权限。\n请确保令牌属于仓库所有者或组织管理员。\n或者,您可以在 设置 → 分支 中手动配置分支保护。"
},
"Head ref for diff": {
"bg": "Head ref за diff",
"de": "Head-Ref für Diff",
"en": "Head ref for diff",
"pl": "Head ref dla diff",
"ru": "Head ref для diff",
"zh": "用于 diff 的 head ref"
},
"Heavy import '{mod}' (~{ms:.0f}ms) at module level — this slows test collection for all tests. Move inside test functions or use lazy import.": {
"bg": "Тежък импорт '{mod}' (~{ms:.0f}ms) на ниво модул — това забавя събирането на всички тестове. Преместете в тестови функции или използвайте lazy import.",
"de": "Schwerer Import '{mod}' (~{ms:.0f}ms) auf Modulebene — verlangsamt die Testerfassung für alle Tests. In Testfunktionen verschieben oder Lazy-Import verwenden.",
"en": "Heavy import '{mod}' (~{ms:.0f}ms) at module level — this slows test collection for all tests. Move inside test functions or use lazy import.",
"pl": "Ciężki import '{mod}' (~{ms:.0f}ms) na poziomie modułu — spowalnia zbieranie wszystkich testów. Przenieś do funkcji testowych lub użyj leniwego importu.",
"ru": "Тяжёлый импорт '{mod}' (~{ms:.0f}ms) на уровне модуля — замедляет сбор всех тестов. Переместите внутрь тестовых функций или используйте ленивый импорт.",
"zh": "模块级重导入 '{mod}'~{ms:.0f}ms)——减慢所有测试的收集速度。请移入测试函数内或使用惰性导入。"
},
"Host Docker not available, starting local dockerd...": {
"bg": "Хост Docker не е наличен, стартиране на локален dockerd...",
"de": "Host-Docker nicht verfügbar, lokaler dockerd wird gestartet...",
@@ -1736,28 +1928,28 @@
"zh": "主机 Docker 不可用,正在启动本地 dockerd..."
},
"Image 'tags' must be a list": {
"bg": "Image 'tags' must be a list",
"de": "Image 'tags' must be a list",
"bg": "Полето 'tags' на изображението трябва да е списък",
"de": "Image-'tags' muss eine Liste sein",
"en": "Image 'tags' must be a list",
"pl": "Image 'tags' must be a list",
"ru": "Image 'tags' must be a list",
"zh": "Image 'tags' must be a list"
"pl": "'tags' obrazu musi b listą",
"ru": "Поле 'tags' образа должно быть списком",
"zh": "镜像的 'tags' 必须是列表"
},
"Image manifest entry missing 'dockerfile'": {
"bg": "Image manifest entry missing 'dockerfile'",
"de": "Image manifest entry missing 'dockerfile'",
"bg": "Записът в манифеста на изображението няма 'dockerfile'",
"de": "Image-Manifest-Eintrag ohne 'dockerfile'",
"en": "Image manifest entry missing 'dockerfile'",
"pl": "Image manifest entry missing 'dockerfile'",
"ru": "Image manifest entry missing 'dockerfile'",
"zh": "Image manifest entry missing 'dockerfile'"
"pl": "Wpis manifestu obrazu nie zawiera 'dockerfile'",
"ru": "Запись манифеста образа не содержит 'dockerfile'",
"zh": "镜像清单条目缺少 'dockerfile'"
},
"Image manifest entry missing 'name'": {
"bg": "Image manifest entry missing 'name'",
"de": "Image manifest entry missing 'name'",
"bg": "Записът в манифеста на изображението няма 'name'",
"de": "Image-Manifest-Eintrag ohne 'name'",
"en": "Image manifest entry missing 'name'",
"pl": "Image manifest entry missing 'name'",
"ru": "Image manifest entry missing 'name'",
"zh": "Image manifest entry missing 'name'"
"pl": "Wpis manifestu obrazu nie zawiera 'name'",
"ru": "Запись манифеста образа не содержит 'name'",
"zh": "镜像清单条目缺少 'name'"
},
"Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}": {
"bg": "Инфраструктурен commit (без идентификатор на задача DEVX-N), пропускаме обновяването на Vikunja: {msg}",
@@ -1768,36 +1960,44 @@
"zh": "基础设施提交(无 DEVX-N 任务 ID),跳过 Vikunja 更新: {msg}"
},
"Integration tests cancelled — another runner failed.": {
"bg": "Integration tests cancelled — another runner failed.",
"de": "Integration tests cancelled — another runner failed.",
"bg": "Интеграционните тестове са отменени — друг runner се провали.",
"de": "Integrationstests abgebrochen — ein anderer Runner ist fehlgeschlagen.",
"en": "Integration tests cancelled — another runner failed.",
"pl": "Testy integracyjne anulowane — inny runner zakończył się niepowodzeniem.",
"ru": "Integration tests cancelled — another runner failed.",
"zh": "Integration tests cancelled — another runner failed."
"ru": "Интеграционные тесты отменены — другой раннер завершился с ошибкой.",
"zh": "集成测试已取消——另一个 runner 失败。"
},
"Integration tests failed with exit code {code}": {
"bg": "Integration tests failed with exit code {code}",
"de": "Integration tests failed with exit code {code}",
"bg": "Интеграционните тестове се провалиха с изходен код {code}",
"de": "Integrationstests mit Exit-Code {code} fehlgeschlagen",
"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}",
"zh": "Integration tests failed with exit code {code}"
"ru": "Интеграционные тесты завершились с кодом {code}",
"zh": "集成测试失败,退出码 {code}"
},
"Integration tests passed.": {
"bg": "Integration tests passed.",
"de": "Integration tests passed.",
"bg": "Интеграционните тестове преминаха.",
"de": "Integrationstests bestanden.",
"en": "Integration tests passed.",
"pl": "Testy integracyjne zakończone pomyślnie.",
"ru": "Integration tests passed.",
"zh": "Integration tests passed."
"ru": "Интеграционные тесты пройдены.",
"zh": "集成测试通过。"
},
"Invalid checklist category: {cat}. Must be numbers.": {
"bg": "Invalid checklist category: {cat}. Must be numbers.",
"de": "Invalid checklist category: {cat}. Must be numbers.",
"en": "Invalid checklist category: {cat}. Must be numbers.",
"pl": "Invalid checklist category: {cat}. Must be numbers.",
"ru": "Invalid checklist category: {cat}. Must be numbers.",
"zh": "Invalid checklist category: {cat}. Must be numbers."
"Invalid repo format: {repo}": {
"bg": "Невалиден формат на репозитория: {repo}",
"de": "Ungültiges Repo-Format: {repo}",
"en": "Invalid repo format: {repo}",
"pl": "Nieprawidłowy format repo: {repo}",
"ru": "Неверный формат репозитория: {repo}",
"zh": "无效的仓库格式:{repo}"
},
"Invalid repo format: {repo}. Expected owner/name.": {
"bg": "Невалиден формат на репозитория: {repo}. Очаква се owner/name.",
"de": "Ungültiges Repo-Format: {repo}. Erwartet: owner/name.",
"en": "Invalid repo format: {repo}. Expected owner/name.",
"pl": "Nieprawidłowy format repo: {repo}. Oczekiwano owner/name.",
"ru": "Неверный формат репозитория: {repo}. Ожидается owner/name.",
"zh": "无效的仓库格式:{repo}。应为 owner/name。"
},
"Items input must be a JSON array, got {type}": {
"bg": "Входните данни трябва да са JSON масив, получено {type}",
@@ -1808,44 +2008,44 @@
"zh": "输入必须是 JSON 数组,得到 {type}"
},
"Label '{label}' already on PR #{pr}.": {
"bg": "Label '{label}' already on PR #{pr}.",
"de": "Label '{label}' already on PR #{pr}.",
"bg": "Етикетът '{label}' вече е на PR #{pr}.",
"de": "Label '{label}' bereits auf PR #{pr}.",
"en": "Label '{label}' already on PR #{pr}.",
"pl": "Label '{label}' already on PR #{pr}.",
"ru": "Label '{label}' already on PR #{pr}.",
"zh": "Label '{label}' already on PR #{pr}."
"pl": "Etykieta '{label}' już jest na PR #{pr}.",
"ru": "Метка '{label}' уже есть на PR #{pr}.",
"zh": "标签 '{label}' 已在 PR #{pr} 上。"
},
"Latest run: #{run_id} (status: {status})": {
"bg": "Latest run: #{run_id} (status: {status})",
"de": "Latest run: #{run_id} (status: {status})",
"bg": "Последен run: #{run_id} (статус: {status})",
"de": "Letzter Lauf: #{run_id} (Status: {status})",
"en": "Latest run: #{run_id} (status: {status})",
"pl": "Latest run: #{run_id} (status: {status})",
"ru": "Latest run: #{run_id} (status: {status})",
"zh": "Latest run: #{run_id} (status: {status})"
"pl": "Ostatni przebieg: #{run_id} (status: {status})",
"ru": "Последний запуск: #{run_id} (статус: {status})",
"zh": "最近运行:#{run_id}(状态:{status}"
},
"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}",
"bg": "Lint се провали — отказ за версия. Първо коригирайте lint грешките.\n{stderr}",
"de": "Lint fehlgeschlagen — Release wird verweigert. Zuerst Lint-Fehler beheben.\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}",
"zh": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}"
"ru": "Lint не пройден — отказ в релизе. Сначала исправьте ошибки lint.\n{stderr}",
"zh": "Lint 失败——拒绝发布。请先修复 lint 错误。\n{stderr}"
},
"Lint passed.": {
"bg": "Lint passed.",
"de": "Lint passed.",
"bg": "Lint премина.",
"de": "Lint bestanden.",
"en": "Lint passed.",
"pl": "Lint zakończony pomyślnie.",
"ru": "Lint passed.",
"zh": "Lint passed."
"ru": "Lint пройден.",
"zh": "Lint 通过。"
},
"Linting documentation in {root}...": {
"bg": "Linting documentation in {root}...",
"de": "Linting documentation in {root}...",
"bg": "Lint на документацията в {root}...",
"de": "Linting der Dokumentation in {root}...",
"en": "Linting documentation in {root}...",
"pl": "Linting documentation in {root}...",
"ru": "Linting documentation in {root}...",
"zh": "Linting documentation in {root}..."
"pl": "Lintowanie dokumentacji w {root}...",
"ru": "Проверка документации в {root}...",
"zh": "正在检查 {root} 中的文档..."
},
"Login to {registry} failed: {error}": {
"bg": "Влизането в {registry} не успя: {error}",
@@ -1864,20 +2064,36 @@
"zh": "测试 '{test}' 中有 {count} 次迭代的循环 — 考虑使用基于属性的测试 (hypothesis) 或减少到 <= {max} 次迭代。"
},
"Manifest file not found: {path}": {
"bg": "Manifest file not found: {path}",
"de": "Manifest file not found: {path}",
"bg": "Файлът на манифеста не е намерен: {path}",
"de": "Manifestdatei nicht gefunden: {path}",
"en": "Manifest file not found: {path}",
"pl": "Manifest file not found: {path}",
"ru": "Manifest file not found: {path}",
"zh": "Manifest file not found: {path}"
"pl": "Nie znaleziono pliku manifestu: {path}",
"ru": "Файл манифеста не найден: {path}",
"zh": "未找到清单文件:{path}"
},
"Manifest must be a JSON list": {
"bg": "Manifest must be a JSON list",
"de": "Manifest must be a JSON list",
"bg": "Манифестът трябва да е JSON списък",
"de": "Manifest muss eine JSON-Liste sein",
"en": "Manifest must be a JSON list",
"pl": "Manifest must be a JSON list",
"ru": "Manifest must be a JSON list",
"zh": "Manifest must be a JSON list"
"pl": "Manifest musi być listą JSON",
"ru": "Манифест должен быть JSON-списком",
"zh": "清单必须是 JSON 列表"
},
"Max files changed (excluded files not counted)": {
"bg": "Максимум променени файлове (изключените файлове не се броят)",
"de": "Max. geänderte Dateien (ausgeschlossene Dateien nicht gezählt)",
"en": "Max files changed (excluded files not counted)",
"pl": "Maks. zmienionych plików (wykluczone pliki nie są liczone)",
"ru": "Макс. изменённых файлов (исключённые файлы не учитываются)",
"zh": "最大更改文件数(排除的文件不计入)"
},
"Max lines changed (excluded files not counted)": {
"bg": "Максимум променени редове (изключените файлове не се броят)",
"de": "Max. geänderte Zeilen (ausgeschlossene Dateien nicht gezählt)",
"en": "Max lines changed (excluded files not counted)",
"pl": "Maks. zmienionych linii (wykluczone pliki nie są liczone)",
"ru": "Макс. изменённых строк (исключённые файлы не учитываются)",
"zh": "最大更改行数(排除的文件不计入)"
},
"Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.": {
"bg": "Сливането неуспешно с HTTP {status}: {message}\nПроверете дали PR е готов и имате права за сливане.",
@@ -1887,13 +2103,21 @@
"ru": "Слияние не удалось: HTTP {status}: {message}\nПроверьте, что PR готов и у вас есть права на слияние.",
"zh": "合并失败: HTTP {status}: {message}\n请检查 PR 是否准备就绪且您具有合并权限。"
},
"Missing required section: {section}": {
"bg": "Липсва задължителна секция: {section}",
"de": "Erforderlicher Abschnitt fehlt: {section}",
"en": "Missing required section: {section}",
"pl": "Brak wymaganej sekcji: {section}",
"ru": "Отсутствует обязательный раздел: {section}",
"zh": "缺少必需部分:{section}"
},
"Missing tests for changed files.": {
"bg": "Missing tests for changed files.",
"de": "Missing tests for changed files.",
"bg": "Липсват тестове за променените файлове.",
"de": "Tests für geänderte Dateien fehlen.",
"en": "Missing tests for changed files.",
"pl": "Missing tests for changed files.",
"ru": "Missing tests for changed files.",
"zh": "Missing tests for changed files."
"pl": "Brak testów dla zmienionych plików.",
"ru": "Отсутствуют тесты для изменённых файлов.",
"zh": "缺少已更改文件的测试。"
},
"Module {mod} has no main() function": {
"bg": "Модул {mod} няма функция main()",
@@ -1911,6 +2135,14 @@
"ru": "Директория molecule не найдена: {path}",
"zh": "未找到 molecule 目录: {path}"
},
"New version to pin": {
"bg": "Нова версия за фиксиране",
"de": "Neue zu pinnende Version",
"en": "New version to pin",
"pl": "Nowa wersja do przypięcia",
"ru": "Новая версия для закрепления",
"zh": "要固定的新版本"
},
"Next steps:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-short-description\n 3. Implement changes, commit with conventional commit format\n 4. git push -u origin HEAD\n 5. make create-pr (creates PR with title: {identifier}: {title})": {
"bg": "Следващи стъпки:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-кратко-описание\n 3. Имплементирайте промените, commit с conventional commit формат\n 4. git push -u origin HEAD\n 5. make create-pr (създава PR с заглавие: {identifier}: {title})",
"de": "Nächste Schritte:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-kurz-beschreibung\n 3. Änderungen implementieren, mit Conventional-Commit-Format committen\n 4. git push -u origin HEAD\n 5. make create-pr (erstellt PR mit Titel: {identifier}: {title})",
@@ -1936,12 +2168,12 @@
"zh": "不错!PR #{pr_number} 已 squash 合并,标题: {merge_title}"
},
"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.",
"bg": "Чудесно! Версия v{version} е тагната и push-ната. Workflow-ът за публикуване ще се задейства.",
"de": "Release v{version} getaggt und gepusht. Der Publish-Workflow wird ausgelöst.",
"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.",
"zh": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered."
"ru": "Релиз v{version} помечен и отправлен. Workflow публикации будет запущен.",
"zh": "发布 v{version} 已打标签并推送。发布工作流将被触发。"
},
"Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.": {
"bg": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) е обновена и маркирана като готова.",
@@ -1951,13 +2183,21 @@
"ru": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) обновлена и отмечена как выполненная.",
"zh": "不错!Vikunja 任务 {task_id} (ID {vikunja_id}) 已更新并标记为完成。"
},
"Nightly gate failed — staging deploy blocked.": {
"bg": "Nightly проверката се провали — staging деплой е блокиран.",
"de": "Nightly-Gate fehlgeschlagen — Staging-Deploy blockiert.",
"en": "Nightly gate failed — staging deploy blocked.",
"pl": "Brama nightly nie powiodła się — wdrożenie staging zablokowane.",
"ru": "Nightly-проверка не пройдена — деплой на staging заблокирован.",
"zh": "Nightly 门禁失败——staging 部署已阻止。"
},
"No CI checks found for commit {sha}.": {
"bg": "No CI checks found for commit {sha}.",
"de": "No CI checks found for commit {sha}.",
"bg": "Не са намерени CI проверки за комит {sha}.",
"de": "Keine CI-Checks für Commit {sha} gefunden.",
"en": "No CI checks found for commit {sha}.",
"pl": "No CI checks found for commit {sha}.",
"ru": "No CI checks found for commit {sha}.",
"zh": "No CI checks found for commit {sha}."
"pl": "Nie znaleziono kontroli CI dla commita {sha}.",
"ru": "CI-проверки для коммита {sha} не найдены.",
"zh": "未找到提交 {sha} 的 CI 检查。"
},
"No Python package found under src/ — skipping version check.": {
"bg": "",
@@ -1967,21 +2207,29 @@
"ru": "",
"zh": ""
},
"No REQ-ID lines found. Each requirement must be labeled (e.g., 'REQ-1: <description>').": {
"bg": "Не са намерени REQ-ID редове. Всяко изискване трябва да е означено (напр. 'REQ-1: <описание>').",
"de": "Keine REQ-ID-Zeilen gefunden. Jede Anforderung muss gekennzeichnet sein (z. B. 'REQ-1: <Beschreibung>').",
"en": "No REQ-ID lines found. Each requirement must be labeled (e.g., 'REQ-1: <description>').",
"pl": "Nie znaleziono wierszy REQ-ID. Każde wymaganie musi być oznaczone (np. 'REQ-1: <opis>').",
"ru": "Строки REQ-ID не найдены. Каждое требование должно быть помечено (напр. 'REQ-1: <описание>').",
"zh": "未找到 REQ-ID 行。每个需求必须标记(例如 'REQ-1: <描述>')。"
},
"No badge SVG files generated": {
"bg": "No badge SVG files generated",
"de": "No badge SVG files generated",
"bg": "Не са генерирани SVG файлове със значки",
"de": "Keine Badge-SVG-Dateien generiert",
"en": "No badge SVG files generated",
"pl": "No badge SVG files generated",
"ru": "No badge SVG files generated",
"zh": "No badge SVG files generated"
"pl": "Nie wygenerowano plików SVG odznak",
"ru": "SVG-файлы значков не сгенерированы",
"zh": "未生成徽章 SVG 文件"
},
"No badge URLs found to update — README already up to date": {
"bg": "No badge URLs found to update — README already up to date",
"de": "No badge URLs found to update — README already up to date",
"bg": "Не са намерени URL на значки за обновяване — README вече е актуално",
"de": "Keine Badge-URLs zum Aktualisieren gefunden — README bereits aktuell",
"en": "No badge URLs found to update — README already up to date",
"pl": "No badge URLs found to update — README already up to date",
"ru": "No badge URLs found to update — README already up to date",
"zh": "No badge URLs found to update — README already up to date"
"pl": "Nie znaleziono URL-i odznak do aktualizacji — README już aktualne",
"ru": "URL значков для обновления не найдены — README уже актуален",
"zh": "未找到需要更新的徽章 URL——README 已是最新"
},
"No badge changes — skipping commit": {
"bg": "",
@@ -1992,12 +2240,12 @@
"zh": ""
},
"No changes between {base} and {head}.": {
"bg": "No changes between {base} and {head}.",
"de": "No changes between {base} and {head}.",
"bg": "Няма промени между {base} и {head}.",
"de": "Keine Änderungen zwischen {base} und {head}.",
"en": "No changes between {base} and {head}.",
"pl": "Brak zmian między {base} i {head}.",
"ru": "No changes between {base} and {head}.",
"zh": "No changes between {base} and {head}."
"ru": "Нет изменений между {base} и {head}.",
"zh": "{base} {head} 之间没有更改。"
},
"No changes to sync — wiki is up to date.": {
"bg": "",
@@ -2008,36 +2256,36 @@
"zh": ""
},
"No failed jobs.": {
"bg": "No failed jobs.",
"de": "No failed jobs.",
"bg": "Няма неуспешни задачи.",
"de": "Keine fehlgeschlagenen Jobs.",
"en": "No failed jobs.",
"pl": "No failed jobs.",
"ru": "No failed jobs.",
"zh": "No failed jobs."
"pl": "Brak nieudanych zadań.",
"ru": "Нет неудавшихся задач.",
"zh": "没有失败的任务。"
},
"No job matching '{job}' found.": {
"bg": "No job matching '{job}' found.",
"de": "No job matching '{job}' found.",
"bg": "Не е намерена задача, съответстваща на '{job}'.",
"de": "Kein Job gefunden, der '{job}' entspricht.",
"en": "No job matching '{job}' found.",
"pl": "No job matching '{job}' found.",
"ru": "No job matching '{job}' found.",
"zh": "No job matching '{job}' found."
"pl": "Nie znaleziono zadania pasującego do '{job}'.",
"ru": "Задача, соответствующая '{job}', не найдена.",
"zh": "未找到匹配 '{job}' 的任务。"
},
"No jobs found for run #{run_id}.": {
"bg": "No jobs found for run #{run_id}.",
"de": "No jobs found for run #{run_id}.",
"bg": "Не са намерени задачи за run #{run_id}.",
"de": "Keine Jobs für Lauf #{run_id} gefunden.",
"en": "No jobs found for run #{run_id}.",
"pl": "No jobs found for run #{run_id}.",
"ru": "No jobs found for run #{run_id}.",
"zh": "No jobs found for run #{run_id}."
"pl": "Nie znaleziono zadań dla przebiegu #{run_id}.",
"ru": "Задачи для запуска #{run_id} не найдены.",
"zh": "未找到运行 #{run_id} 的任务。"
},
"No open PR found for branch '{branch}'.": {
"bg": "No open PR found for branch '{branch}'.",
"de": "No open PR found for branch '{branch}'.",
"bg": "Не е намерен отворен PR за клон '{branch}'.",
"de": "Kein offener PR für Branch '{branch}' gefunden.",
"en": "No open PR found for branch '{branch}'.",
"pl": "No open PR found for branch '{branch}'.",
"ru": "No open PR found for branch '{branch}'.",
"zh": "No open PR found for branch '{branch}'."
"pl": "Nie znaleziono otwartego PR dla gałęzi '{branch}'.",
"ru": "Открытый PR для ветки '{branch}' не найден.",
"zh": "未找到分支 '{branch}' 的开放 PR。"
},
"No push needed (no changes or push failed).": {
"bg": "",
@@ -2047,133 +2295,133 @@
"ru": "",
"zh": ""
},
"No spec file found for task {task_id} in {dir}/. Expected: {dir}/{task_id}.md": {
"bg": "Не е намерен spec файл за задача {task_id} в {dir}/. Очаква се: {dir}/{task_id}.md",
"de": "Keine Spec-Datei für Task {task_id} in {dir}/ gefunden. Erwartet: {dir}/{task_id}.md",
"en": "No spec file found for task {task_id} in {dir}/. Expected: {dir}/{task_id}.md",
"pl": "Nie znaleziono pliku spec dla zadania {task_id} w {dir}/. Oczekiwano: {dir}/{task_id}.md",
"ru": "Spec-файл для задачи {task_id} в {dir}/ не найден. Ожидается: {dir}/{task_id}.md",
"zh": "在 {dir}/ 中未找到任务 {task_id} 的规范文件。应为:{dir}/{task_id}.md"
},
"No staged changes — version and changelog already up to date.": {
"bg": "No staged changes — version and changelog already up to date.",
"de": "No staged changesversion and changelog already up to date.",
"bg": "Няма staged промени — версията и changelog вече са актуални.",
"de": "Keine gestagten ÄnderungenVersion und Changelog bereits aktuell.",
"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.",
"zh": "No staged changes — version and changelog already up to date."
"ru": "Нет staged-изменений — версия и changelog уже актуальны.",
"zh": "没有暂存的更改——版本和 changelog 已是最新。"
},
"No tag found — skipping publish.": {
"bg": "No tag found — skipping publish.",
"de": "No tag found — skipping publish.",
"bg": "Не е намерен таг — публикуването се пропуска.",
"de": "Kein Tag gefundenVeröffentlichung wird übersprungen.",
"en": "No tag found — skipping publish.",
"pl": "Nie znaleziono tagu — pomijanie publikacji.",
"ru": "No tag found — skipping publish.",
"zh": "No tag found — skipping publish."
"ru": "Тег не найден — публикация пропускается.",
"zh": "未找到标签——跳过发布。"
},
"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.",
"bg": "Не са намерени тагове — всички промени се третират като видими за потребителя.",
"de": "Keine Tags gefunden — alle Änderungen werden als nutzersichtbar behandelt.",
"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.",
"zh": "No tags found — treating all changes as user-facing."
"ru": "Теги не найдены — все изменения считаются пользовательскими.",
"zh": "未找到标签——所有更改视为面向用户。"
},
"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.",
"bg": "Не е намерен task ID ({prefix}-N) в съобщението на комита: {msg}. Всеки неинфраструктурен комит трябва да има task ID.",
"de": "Keine Task-ID ({prefix}-N) in Commit-Nachricht gefunden: {msg}. Jeder Nicht-Infrastruktur-Commit muss eine Task-ID haben.",
"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.",
"zh": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID."
"ru": "В сообщении коммита не найден ID задачи ({prefix}-N): {msg}. Каждый неинфраструктурный коммит должен иметь ID задачи.",
"zh": "提交信息中未找到任务 ID{prefix}-N):{msg}。每个非基础设施提交必须有任务 ID"
},
"No task ID found in branch '{branch}'. Expected format: {prefix}-N-description.": {
"bg": "Не е намерен task ID в клон '{branch}'. Очакван формат: {prefix}-N-description.",
"de": "Keine Task-ID in Branch '{branch}' gefunden. Erwartetes Format: {prefix}-N-description.",
"en": "No task ID found in branch '{branch}'. Expected format: {prefix}-N-description.",
"pl": "Nie znaleziono ID zadania w gałęzi '{branch}'. Oczekiwany format: {prefix}-N-description.",
"ru": "ID задачи не найден в ветке '{branch}'. Ожидаемый формат: {prefix}-N-description.",
"zh": "分支 '{branch}' 中未找到任务 ID。预期格式:{prefix}-N-description。"
},
"No task ID found in branch name '{branch}'. Expected format: <PREFIX>-N-description.": {
"bg": "Не е намерен task ID в името на клона '{branch}'. Очакван формат: <PREFIX>-N-description.",
"de": "Keine Task-ID im Branch-Namen '{branch}' gefunden. Erwartetes Format: <PREFIX>-N-description.",
"en": "No task ID found in branch name '{branch}'. Expected format: <PREFIX>-N-description.",
"pl": "Nie znaleziono ID zadania w nazwie gałęzi '{branch}'. Oczekiwany format: <PREFIX>-N-description.",
"ru": "ID задачи не найден в имени ветки '{branch}'. Ожидаемый формат: <PREFIX>-N-description.",
"zh": "分支名称 '{branch}' 中未找到任务 ID。预期格式:<PREFIX>-N-description。"
},
"No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.": {
"bg": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
"de": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
"bg": "Не е намерен task ID в името на клона '{branch}'. Очакван формат: {prefix}-N-description.",
"de": "Keine Task-ID im Branch-Namen '{branch}' gefunden. Erwartetes Format: {prefix}-N-description.",
"en": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
"pl": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
"ru": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
"zh": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description."
"pl": "Nie znaleziono ID zadania w nazwie gałęzi '{branch}'. Oczekiwany format: {prefix}-N-description.",
"ru": "ID задачи не найден в имени ветки '{branch}'. Ожидаемый формат: {prefix}-N-description.",
"zh": "分支名称 '{branch}' 中未找到任务 ID。预期格式:{prefix}-N-description"
},
"No unreleased changes found. Nothing to release.": {
"bg": "No unreleased changes found. Nothing to release.",
"de": "No unreleased changes found. Nothing to release.",
"bg": "Не са намерени непубликувани промени. Няма какво да се издаде.",
"de": "Keine unveröffentlichten Änderungen gefunden. Nichts zu veröffentlichen.",
"en": "No unreleased changes found. Nothing to release.",
"pl": "Nie znaleziono nieopublikowanych zmian. Nic do wydania.",
"ru": "No unreleased changes found. Nothing to release.",
"zh": "No unreleased changes found. Nothing to release."
"ru": "Не найдено невыпущенных изменений. Нечего выпускать.",
"zh": "未找到未发布的更改。没有可发布的内容。"
},
"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.",
"bg": "Няма видими за потребителя промени от {tag} — променени са само workflow/инфраструктурни файлове. Изданието се пропуска.",
"de": "Keine nutzersichtbaren Änderungen seit {tag} — nur Workflow-/Infrastrukturdateien geändert. Release wird übersprungen.",
"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.",
"zh": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release."
"ru": "Нет пользовательских изменений с {tag} — изменены только workflow/инфраструктурные файлы. Релиз пропускается.",
"zh": "自 {tag} 以来没有面向用户的更改——仅更改了工作流/基础设施文件。跳过发布。"
},
"No versions found.": {
"bg": "No versions found.",
"de": "No versions found.",
"bg": "Не са намерени версии.",
"de": "Keine Versionen gefunden.",
"en": "No versions found.",
"pl": "No versions found.",
"ru": "No versions found.",
"zh": "No versions found."
"pl": "Nie znaleziono wersji.",
"ru": "Версии не найдены.",
"zh": "未找到版本。"
},
"No workflow runs found for SHA {sha}.": {
"bg": "No workflow runs found for SHA {sha}.",
"de": "No workflow runs found for SHA {sha}.",
"bg": "Не са намерени workflow runs за SHA {sha}.",
"de": "Keine Workflow-Läufe für SHA {sha} gefunden.",
"en": "No workflow runs found for SHA {sha}.",
"pl": "No workflow runs found for SHA {sha}.",
"ru": "No workflow runs found for SHA {sha}.",
"zh": "No workflow runs found for SHA {sha}."
},
"Note: CI token also cannot approve. Posting COMMENT instead.": {
"bg": "Забележка: CI тоукънът също не може да одобри. Публикуване на COMMENT вместо това.",
"de": "Hinweis: CI-Token kann ebenfalls nicht genehmigen. COMMENT wird stattdessen gesendet.",
"en": "Note: CI token also cannot approve. Posting COMMENT instead.",
"pl": "Uwaga: Token CI również nie może zatwierdzić. Publikowanie COMMENT zamiast tego.",
"ru": "Примечание: CI токен также не может одобрить. Публикация COMMENT вместо этого.",
"zh": "注意:CI 令牌也无法批准。改为发布 COMMENT。"
},
"Note: Self-approval not allowed with reviewer token. Retrying with CI token.": {
"bg": "Забележка: Само-одобрението не е разрешено с тоукън на рецензента. Повторен опит с CI тоукън.",
"de": "Hinweis: Selbstgenehmigung mit Reviewer-Token nicht erlaubt. Wiederholung mit CI-Token.",
"en": "Note: Self-approval not allowed with reviewer token. Retrying with CI token.",
"pl": "Uwaga: Samo-zatwierdzenie niedozwolone tokenem recenzenta. Ponawianie tokenem CI.",
"ru": "Примечание: Самоодобрение токеном ревьюера не разрешено. Повторная попытка с CI токеном.",
"zh": "注意:不允许使用审阅者令牌进行自我批准。正在使用 CI 令牌重试。"
},
"Note: Self-approval not allowed. Posting COMMENT instead.": {
"bg": "Забележка: Само-одобрението не е разрешено. Публикуване на COMMENT вместо това.",
"de": "Hinweis: Selbstgenehmigung nicht erlaubt. COMMENT wird stattdessen gesendet.",
"en": "Note: Self-approval not allowed. Posting COMMENT instead.",
"pl": "Uwaga: Samo-zatwierdzenie niedozwolone. Publikowanie COMMENT zamiast tego.",
"ru": "Примечание: Самоодобрение не разрешено. Публикация COMMENT вместо этого.",
"zh": "注意:不允许自我批准。改为发布 COMMENT。"
"pl": "Nie znaleziono przebiegów workflow dla SHA {sha}.",
"ru": "Workflow-запуски для SHA {sha} не найдены.",
"zh": "未找到 SHA {sha} 的工作流运行。"
},
"Nothing to push.": {
"bg": "Nothing to push.",
"de": "Nothing to push.",
"bg": "Няма какво да се push-не.",
"de": "Nichts zu pushen.",
"en": "Nothing to push.",
"pl": "Nothing to push.",
"ru": "Nothing to push.",
"zh": "Nothing to push."
"pl": "Nic do wypchnięcia.",
"ru": "Нечего отправлять.",
"zh": "没有可推送的内容。"
},
"Only check staged files (for pre-commit)": {
"bg": "Only check staged files (for pre-commit)",
"de": "Only check staged files (for pre-commit)",
"bg": "Проверява само staged файлове (за pre-commit)",
"de": "Nur gestagte Dateien prüfen (für Pre-Commit)",
"en": "Only check staged files (for pre-commit)",
"pl": "Only check staged files (for pre-commit)",
"ru": "Only check staged files (for pre-commit)",
"zh": "Only check staged files (for pre-commit)"
"pl": "Sprawdza tylko pliki staged (dla pre-commit)",
"ru": "Проверять только staged-файлы (для pre-commit)",
"zh": "仅检查暂存文件(用于 pre-commit"
},
"Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE": {
"bg": "Опа! Съобщението за 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",
"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",
"zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE"
"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, deps, revert, BREAKING CHANGE": {
"bg": "Опа! Съобщението за commit трябва да следва конвенционален формат.\n Очаква се: <type>: <description>\n Получено: {subject}\n Разрешени типове: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, 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, deps, 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, deps, 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, deps, revert, BREAKING CHANGE",
"ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE",
"zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, deps, revert, BREAKING CHANGE"
},
"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.",
"bg": "Не включвайте task ID ({prefix}-N) в комитите на feature клони.\n Task ID се добавя автоматично при merge чрез CI.",
"de": "Task-ID ({prefix}-N) nicht in Feature-Branch-Commits aufnehmen.\n Die Task-ID wird beim Merge automatisch via CI hinzugefügt.",
"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.",
"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."
"ru": "Не включайте ID задачи ({prefix}-N) в коммиты feature-веток.\n ID задачи будет добавлен автоматически при merge через CI.",
"zh": "请勿在功能分支提交中包含任务 ID{prefix}-N)。\n 任务 ID 将在合并时由 CI 自动添加。"
},
"Oops! Gitea PyPI registry publish failed:\n{stderr}": {
"bg": "Опа! Публикуването в Gitea PyPI registry неуспешно:\n{stderr}",
@@ -2184,20 +2432,20 @@
"zh": "哎呀!Gitea PyPI registry 发布失败:\n{stderr}"
},
"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}",
"bg": "Комитът на master клона трябва да следва conventional формата след task ID.\n Очаква се: {prefix}-N: <type>: <description>\n Получено: {subject}",
"de": "Master-Branch-Commits müssen nach der Task-ID dem Conventional-Format folgen.\n Erwartet: {prefix}-N: <type>: <description>\n Erhalten: {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}",
"zh": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: {prefix}-N: <type>: <description>\n Got: {subject}"
"ru": "Коммит ветки master должен следовать conventional-формату после ID задачи.\n Ожидается: {prefix}-N: <type>: <description>\n Получено: {subject}",
"zh": "master 分支提交必须在任务 ID 后遵循 conventional 格式。\n 预期:{prefix}-N: <type>: <description>\n 实际:{subject}"
},
"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}",
"bg": "Комитите на master клона трябва да започват с task ID.\n Очаква се: {prefix}-N: <conventional commit message>\n Получено: {subject}",
"de": "Master-Branch-Commits müssen mit einer Task-ID beginnen.\n Erwartet: {prefix}-N: <conventional commit message>\n Erhalten: {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}",
"zh": "Oops! Master branch commits must start with a task ID.\n Expected: {prefix}-N: <conventional commit message>\n Got: {subject}"
"ru": "Коммиты ветки master должны начинаться с ID задачи.\n Ожидается: {prefix}-N: <conventional commit message>\n Получено: {subject}",
"zh": "master 分支提交必须以任务 ID 开头。\n 预期:{prefix}-N: <conventional commit message>\n 实际:{subject}"
},
"Oops! No task ID found in branch name '{branch}'. Branch names must include the task ID prefix (e.g., DEVX-31-fix-bug).": {
"bg": "Ой! Не е намерен ID на задача в името на клона '{branch}'. Имената на клонове трябва да включват префикса за ID на задача (напр. DEVX-31-fix-bug).",
@@ -2208,20 +2456,20 @@
"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}": {
"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}",
"bg": "Заглавието на PR трябва да следва формата '{prefix}-N: <заглавие на задачата>'.\n Очаква се: {task_id}: <заглавие на задачата>\n Получено: {pr_title}",
"de": "Der PR-Titel muss dem Format '{prefix}-N: <Aufgabentitel>' folgen.\n Erwartet: {task_id}: <Aufgabentitel>\n Erhalten: {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}",
"zh": "Oops! PR title must follow format '{prefix}-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}"
"ru": "Заголовок PR должен соответствовать формату '{prefix}-N: <название задачи>'.\n Ожидается: {task_id}: <название задачи>\n Получено: {pr_title}",
"zh": "PR 标题必须遵循格式 '{prefix}-N: <任务标题>'\n 预期:{task_id}: <任务标题>\n 实际:{pr_title}"
},
"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}",
"bg": "Несъответствие на task ID в заглавието на PR.\n Task ID на клона: {task_id}\n Заглавие на PR: {pr_title}",
"de": "Task-ID des PR-Titels stimmt nicht überein.\n Branch-Task-ID: {task_id}\n PR-Titel: {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}",
"zh": "Oops! PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {pr_title}"
"ru": "Несоответствие ID задачи в заголовке PR.\n ID задачи ветки: {task_id}\n Заголовок PR: {pr_title}",
"zh": "PR 标题任务 ID 不匹配。\n 分支任务 ID{task_id}\n PR 标题: {pr_title}"
},
"Oops! Package build failed:\n{stderr}": {
"bg": "Опа! Сборката на пакета неуспешна:\n{stderr}",
@@ -2240,20 +2488,20 @@
"zh": "哎呀!PyPI 发布失败:\n{stderr}"
},
"PASS: All documentation checks passed!": {
"bg": "PASS: All documentation checks passed!",
"de": "PASS: All documentation checks passed!",
"bg": "УСПЕХ: Всички проверки на документацията преминаха!",
"de": "ERFOLG: Alle Dokumentationsprüfungen bestanden!",
"en": "PASS: All documentation checks passed!",
"pl": "PASS: All documentation checks passed!",
"ru": "PASS: All documentation checks passed!",
"zh": "PASS: All documentation checks passed!"
"pl": "SUKCES: Wszystkie kontrole dokumentacji przeszły!",
"ru": "УСПЕШНО: Все проверки документации пройдены!",
"zh": "通过:所有文档检查均已通过!"
},
"PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.": {
"bg": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"de": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"bg": "PR #{pr} е rebase-нат успешно. Нов CI run ще стартира автоматично.\nАко auto-merge е включен (етикет ready-to-merge), следващият CI run\nще опита да слее този PR.",
"de": "PR #{pr} erfolgreich rebased. Ein neuer CI-Lauf startet automatisch.\nWenn Auto-Merge aktiviert ist (ready-to-merge-Label), versucht der nächste\nCI-Lauf, diesen PR zu mergen.",
"en": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"pl": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"ru": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"zh": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR."
"pl": "PR #{pr} rebased pomyślnie. Nowy przebieg CI rozpocznie się automatycznie.\nJeśli auto-merge jest włączony (etykieta ready-to-merge), następny przebieg CI\nspróbuje połączyć ten PR.",
"ru": "PR #{pr} успешно rebased. Новый CI-запуск начнётся автоматически.\nЕсли auto-merge включён (метка ready-to-merge), следующий CI-запуск\nпопытается слить этот PR.",
"zh": "PR #{pr} rebase 成功。新的 CI 运行将自动开始。\n如果启用了自动合并(ready-to-merge 标签),下一次 CI 运行\n将尝试合并此 PR"
},
"PR already exists: #{index} — {url}": {
"bg": "PR вече съществува: #{index} — {url}",
@@ -2263,61 +2511,117 @@
"ru": "PR уже существует: #{index} — {url}",
"zh": "PR 已存在: #{index} — {url}"
},
"PR has 'refactoring' label — size check bypassed.": {
"bg": "PR има етикет 'refactoring' — проверката за размер се заобикаля.",
"de": "PR hat 'refactoring'-Label — Größenprüfung umgangen.",
"en": "PR has 'refactoring' label — size check bypassed.",
"pl": "PR ma etykietę 'refactoring' — kontrola rozmiaru pominięta.",
"ru": "PR имеет метку 'refactoring' — проверка размера обойдена.",
"zh": "PR 带有 'refactoring' 标签——大小检查已绕过。"
},
"PR has {file_count} files changed (max {max_files}). Excluded: {excluded_count} files.": {
"bg": "PR има {file_count} променени файла (макс. {max_files}). Изключени: {excluded_count} файла.",
"de": "PR hat {file_count} geänderte Dateien (max. {max_files}). Ausgeschlossen: {excluded_count} Dateien.",
"en": "PR has {file_count} files changed (max {max_files}). Excluded: {excluded_count} files.",
"pl": "PR ma {file_count} zmienionych plików (maks. {max_files}). Wykluczone: {excluded_count} plików.",
"ru": "PR содержит {file_count} изменённых файлов (макс. {max_files}). Исключено: {excluded_count} файлов.",
"zh": "PR 有 {file_count} 个文件更改(上限 {max_files})。已排除:{excluded_count} 个文件。"
},
"PR has {line_count} lines changed (max {max_lines}). Excluded: {excluded_count} files.": {
"bg": "PR има {line_count} променени реда (макс. {max_lines}). Изключени: {excluded_count} файла.",
"de": "PR hat {line_count} geänderte Zeilen (max. {max_lines}). Ausgeschlossen: {excluded_count} Dateien.",
"en": "PR has {line_count} lines changed (max {max_lines}). Excluded: {excluded_count} files.",
"pl": "PR ma {line_count} zmienionych linii (maks. {max_lines}). Wykluczone: {excluded_count} plików.",
"ru": "PR содержит {line_count} изменённых строк (макс. {max_lines}). Исключено: {excluded_count} файлов.",
"zh": "PR 有 {line_count} 行更改(上限 {max_lines})。已排除:{excluded_count} 个文件。"
},
"PR number (to fetch title from Gitea)": {
"bg": "PR number (to fetch title from Gitea)",
"de": "PR number (to fetch title from Gitea)",
"bg": "Номер на PR (за извличане на заглавие от Gitea)",
"de": "PR-Nummer (zum Abrufen des Titels von Gitea)",
"en": "PR number (to fetch title from Gitea)",
"pl": "PR number (to fetch title from Gitea)",
"ru": "PR number (to fetch title from Gitea)",
"zh": "PR number (to fetch title from Gitea)"
"pl": "Numer PR (do pobrania tytułu z Gitea)",
"ru": "Номер PR (для получения заголовка из Gitea)",
"zh": "PR 编号(用于从 Gitea 获取标题)"
},
"PR number for label check": {
"bg": "Номер на PR за проверка на етикети",
"de": "PR-Nummer für die Label-Prüfung",
"en": "PR number for label check",
"pl": "Numer PR do kontroli etykiet",
"ru": "Номер PR для проверки меток",
"zh": "用于标签检查的 PR 编号"
},
"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}",
"bg": "Номерът на PR трябва да е цяло число, получено: {pr_number}",
"de": "PR-Nummer muss eine Ganzzahl sein, erhalten: {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}",
"zh": "PR number must be an integer, got: {pr_number}"
"ru": "Номер PR должен быть целым числом, получено: {pr_number}",
"zh": "PR 编号必须是整数,实际得到:{pr_number}"
},
"PR number to fix": {
"bg": "Номер на PR за коригиране",
"de": "Zu korrigierende PR-Nummer",
"en": "PR number to fix",
"pl": "Numer PR do poprawy",
"ru": "Номер PR для исправления",
"zh": "要修复的 PR 编号"
},
"PR size OK: {file_count} files, {line_count} lines (max {max_files} files, {max_lines} lines).": {
"bg": "Размерът на PR е ОК: {file_count} файла, {line_count} реда (макс. {max_files} файла, {max_lines} реда).",
"de": "PR-Größe OK: {file_count} Dateien, {line_count} Zeilen (max. {max_files} Dateien, {max_lines} Zeilen).",
"en": "PR size OK: {file_count} files, {line_count} lines (max {max_files} files, {max_lines} lines).",
"pl": "Rozmiar PR OK: {file_count} plików, {line_count} linii (maks. {max_files} plików, {max_lines} linii).",
"ru": "Размер PR в норме: {file_count} файлов, {line_count} строк (макс. {max_files} файлов, {max_lines} строк).",
"zh": "PR 大小正常:{file_count} 个文件,{line_count} 行(上限 {max_files} 个文件,{max_lines} 行)。"
},
"PR size check failed.": {
"bg": "Проверката на размера на PR се провали.",
"de": "PR-Größenprüfung fehlgeschlagen.",
"en": "PR size check failed.",
"pl": "Kontrola rozmiaru PR nie powiodła się.",
"ru": "Проверка размера PR не пройдена.",
"zh": "PR 大小检查失败。"
},
"PR title (auto-fetched if --pr-number given)": {
"bg": "PR title (auto-fetched if --pr-number given)",
"de": "PR title (auto-fetched if --pr-number given)",
"bg": "Заглавие на PR (извлича се автоматично, ако е зададен --pr-number)",
"de": "PR-Titel (wird automatisch abgerufen, wenn --pr-number angegeben)",
"en": "PR title (auto-fetched if --pr-number given)",
"pl": "PR title (auto-fetched if --pr-number given)",
"ru": "PR title (auto-fetched if --pr-number given)",
"zh": "PR title (auto-fetched if --pr-number given)"
"pl": "Tytuł PR (pobierany automatycznie, gdy podano --pr-number)",
"ru": "Заголовок PR (извлекается автоматически при указании --pr-number)",
"zh": "PR 标题(提供 --pr-number 时自动获取)"
},
"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}",
"bg": "Заглавието на PR не съвпада със заглавието на Vikunja задачата.\n Очаква се: {expected}\n Получено: {pr_title}",
"de": "PR-Titel stimmt nicht mit Vikunja-Task-Titel überein.\n Erwartet: {expected}\n Erhalten: {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}",
"zh": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}"
"ru": "Заголовок PR не совпадает с названием задачи Vikunja.\n Ожидается: {expected}\n Получено: {pr_title}",
"zh": "PR 标题与 Vikunja 任务标题不匹配。\n 预期:{expected}\n 实际:{pr_title}"
},
"PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}": {
"bg": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
"de": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
"bg": "Заглавието на PR не съвпада със заглавието на Vikunja задачата.\n Очаква се: {expected}\n Получено: {title}",
"de": "PR-Titel stimmt nicht mit Vikunja-Task-Titel überein.\n Erwartet: {expected}\n Erhalten: {title}",
"en": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
"pl": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
"ru": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
"zh": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}"
"pl": "Tytuł PR nie zgadza się z tytułem zadania Vikunja.\n Oczekiwano: {expected}\n Otrzymano: {title}",
"ru": "Заголовок PR не совпадает с названием задачи Vikunja.\n Ожидается: {expected}\n Получено: {title}",
"zh": "PR 标题与 Vikunja 任务标题不匹配。\n 预期:{expected}\n 实际:{title}"
},
"PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}": {
"bg": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
"de": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
"bg": "Заглавието на PR трябва да следва формата '{prefix}-N: <заглавие на задачата>'.\n Получено: {title}",
"de": "Der PR-Titel muss dem Format '{prefix}-N: <Aufgabentitel>' folgen.\n Erhalten: {title}",
"en": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
"pl": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
"ru": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
"zh": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}"
"pl": "Tytuł PR musi być w formacie '{prefix}-N: <tytuł zadania>'.\n Otrzymano: {title}",
"ru": "Заголовок PR должен соответствовать формату '{prefix}-N: <название задачи>'.\n Получено: {title}",
"zh": "PR 标题必须遵循格式 '{prefix}-N: <任务标题>'\n 实际:{title}"
},
"PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}": {
"bg": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
"de": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
"bg": "Несъответствие на task ID в заглавието на PR.\n Task ID на клона: {task_id}\n Заглавие на PR: {title}",
"de": "Task-ID des PR-Titels stimmt nicht überein.\n Branch-Task-ID: {task_id}\n PR-Titel: {title}",
"en": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
"pl": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
"ru": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
"zh": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}"
"pl": "Niezgodność ID zadania w tytule PR.\n ID zadania gałęzi: {task_id}\n Tytuł PR: {title}",
"ru": "Несоответствие ID задачи в заголовке PR.\n ID задачи ветки: {task_id}\n Заголовок PR: {title}",
"zh": "PR 标题任务 ID 不匹配。\n 分支任务 ID{task_id}\n PR 标题: {title}"
},
"PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.": {
"bg": "PYPI_TOKEN не е зададен и няма конфигуриран URL на registry — пропускаме публикуването в PyPI. Без притеснения, просто ще създадем Gitea release.",
@@ -2327,21 +2631,29 @@
"ru": "PYPI_TOKEN не задан и URL registry не настроен — пропускаем публикацию в PyPI. Не беспокойтесь, мы просто создадим Gitea release.",
"zh": "未设置 PYPI_TOKEN 且未配置 registry URL — 跳过 PyPI 发布。别担心,我们直接创建 Gitea release。"
},
"Package name to bump (e.g., grm, sso-bridge)": {
"bg": "Име на пакет за увеличаване (напр. grm, sso-bridge)",
"de": "Zu erhöhender Paketname (z. B. grm, sso-bridge)",
"en": "Package name to bump (e.g., grm, sso-bridge)",
"pl": "Nazwa pakietu do podbicia (np. grm, sso-bridge)",
"ru": "Имя пакета для повышения (напр. grm, sso-bridge)",
"zh": "要升级的包名(例如 grm、sso-bridge"
},
"Package owner not specified. Use --owner or set [tool.devx] repo_owner.": {
"bg": "Package owner not specified. Use --owner or set [tool.devx] repo_owner.",
"de": "Package owner not specified. Use --owner or set [tool.devx] repo_owner.",
"bg": "Собственикът на пакета не е зададен. Използвайте --owner или задайте [tool.devx] repo_owner.",
"de": "Paket-Eigentümer nicht angegeben. Verwenden Sie --owner oder setzen Sie [tool.devx] repo_owner.",
"en": "Package owner not specified. Use --owner or set [tool.devx] repo_owner.",
"pl": "Package owner not specified. Use --owner or set [tool.devx] repo_owner.",
"ru": "Package owner not specified. Use --owner or set [tool.devx] repo_owner.",
"zh": "Package owner not specified. Use --owner or set [tool.devx] repo_owner."
"pl": "Nie określono właściciela pakietu. Użyj --owner lub ustaw [tool.devx] repo_owner.",
"ru": "Владелец пакета не указан. Используйте --owner или задайте [tool.devx] repo_owner.",
"zh": "未指定包所有者。使用 --owner 或设置 [tool.devx] repo_owner"
},
"Package: {owner}/{name}": {
"bg": "Package: {owner}/{name}",
"de": "Package: {owner}/{name}",
"bg": "Пакет: {owner}/{name}",
"de": "Paket: {owner}/{name}",
"en": "Package: {owner}/{name}",
"pl": "Package: {owner}/{name}",
"ru": "Package: {owner}/{name}",
"zh": "Package: {owner}/{name}"
"pl": "Pakiet: {owner}/{name}",
"ru": "Пакет: {owner}/{name}",
"zh": "包:{owner}/{name}"
},
"Parsed owner={owner}, repo={repo} from DEVX_REPO_NAME": {
"bg": "Разбор на owner={owner}, repo={repo} от DEVX_REPO_NAME",
@@ -2352,28 +2664,28 @@
"zh": "从 DEVX_REPO_NAME 解析 owner={owner}, repo={repo}"
},
"Path to pyproject.toml (default: pyproject.toml in CWD).": {
"bg": "Path to pyproject.toml (default: pyproject.toml in CWD).",
"de": "Path to pyproject.toml (default: pyproject.toml in CWD).",
"bg": "Път до pyproject.toml (по подразбиране: pyproject.toml в CWD).",
"de": "Pfad zu pyproject.toml (Standard: pyproject.toml im CWD).",
"en": "Path to pyproject.toml (default: pyproject.toml in CWD).",
"pl": "Path to pyproject.toml (default: pyproject.toml in CWD).",
"ru": "Path to pyproject.toml (default: pyproject.toml in CWD).",
"zh": "Path to pyproject.toml (default: pyproject.toml in CWD)."
"pl": "Ścieżka do pyproject.toml (domyślnie: pyproject.toml w CWD).",
"ru": "Путь к pyproject.toml (по умолчанию: pyproject.toml в CWD).",
"zh": "pyproject.toml 的路径(默认:CWD 中的 pyproject.toml)。"
},
"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.",
"bg": "Проверката за скорост на тест СЕ ПРОВАЛИ: {count} тест(а) надвишават лимита от {limit}s.",
"de": "Pro-Test-Geschwindigkeitsprüfung FEHLGESCHLAGEN: {count} Test(s) überschreiten das {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.",
"zh": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit."
"ru": "Проверка скорости тестов ПРОВАЛЕНА: {count} тест(ов) превышают лимит {limit}s.",
"zh": "单测试速度检查失败:{count} 个测试超过 {limit}s 限制。"
},
"Pre-merge validation failed.": {
"bg": "Pre-merge validation failed.",
"de": "Pre-merge validation failed.",
"bg": "Предmerge валидацията се провали.",
"de": "Pre-Merge-Validierung fehlgeschlagen.",
"en": "Pre-merge validation failed.",
"pl": "Pre-merge validation failed.",
"ru": "Pre-merge validation failed.",
"zh": "Pre-merge validation failed."
"pl": "Walidacja przed merge nie powiodła się.",
"ru": "Проверка перед слиянием не пройдена.",
"zh": "合并前验证失败。"
},
"Pre-push check passed: task {task_id} exists.": {
"bg": "Pre-push проверката премина: задача {task_id} съществува.",
@@ -2384,28 +2696,28 @@
"zh": "Pre-push 检查通过: 任务 {task_id} 存在。"
},
"Print warnings but always exit 0": {
"bg": "Print warnings but always exit 0",
"de": "Print warnings but always exit 0",
"bg": "Печатай предупреждения, но винаги излизай с код 0",
"de": "Warnungen ausgeben, aber immer mit 0 beenden",
"en": "Print warnings but always exit 0",
"pl": "Print warnings but always exit 0",
"ru": "Print warnings but always exit 0",
"zh": "Print warnings but always exit 0"
"pl": "Wypisuj ostrzeżenia, ale zawsze kończ kodem 0",
"ru": "Выводить предупреждения, но всегда завершать с кодом 0",
"zh": "打印警告但始终以 0 退出"
},
"Provide --manifest or both --dockerfile and --name": {
"bg": "Provide --manifest or both --dockerfile and --name",
"de": "Provide --manifest or both --dockerfile and --name",
"bg": "Задайте --manifest или и --dockerfile, и --name",
"de": "--manifest oder sowohl --dockerfile als auch --name angeben",
"en": "Provide --manifest or both --dockerfile and --name",
"pl": "Provide --manifest or both --dockerfile and --name",
"ru": "Provide --manifest or both --dockerfile and --name",
"zh": "Provide --manifest or both --dockerfile and --name"
"pl": "Podaj --manifest lub zarówno --dockerfile, jak i --name",
"ru": "Укажите --manifest или оба --dockerfile и --name",
"zh": "提供 --manifest 或同时提供 --dockerfile --name"
},
"Provide a commit message file or use --git.": {
"bg": "Provide a commit message file or use --git.",
"de": "Provide a commit message file or use --git.",
"bg": "Предоставете файл със съобщение на комит или използвайте --git.",
"de": "Commit-Nachrichtendatei bereitstellen oder --git verwenden.",
"en": "Provide a commit message file or use --git.",
"pl": "Podaj plik komunikatu commitu lub użyj --git.",
"ru": "Provide a commit message file or use --git.",
"zh": "Provide a commit message file or use --git."
"ru": "Укажите файл с сообщением коммита или используйте --git.",
"zh": "提供提交信息文件或使用 --git"
},
"Published to Gitea PyPI registry.": {
"bg": "Публикувано в Gitea PyPI registry.",
@@ -2424,28 +2736,28 @@
"zh": "已发布到 PyPI。"
},
"Publishing release {tag}...": {
"bg": "Publishing release {tag}...",
"de": "Publishing release {tag}...",
"bg": "Публикуване на версия {tag}...",
"de": "Veröffentliche Release {tag}...",
"en": "Publishing release {tag}...",
"pl": "Publikowanie wydania {tag}...",
"ru": "Publishing release {tag}...",
"zh": "Publishing release {tag}..."
"ru": "Публикация релиза {tag}...",
"zh": "正在发布 {tag}..."
},
"Push attempt {n}/3 failed: {err}": {
"bg": "Push attempt {n}/3 failed: {err}",
"de": "Push attempt {n}/3 failed: {err}",
"bg": "Опит {n}/3 за push се провали: {err}",
"de": "Push-Versuch {n}/3 fehlgeschlagen: {err}",
"en": "Push attempt {n}/3 failed: {err}",
"pl": "Push attempt {n}/3 failed: {err}",
"ru": "Push attempt {n}/3 failed: {err}",
"zh": "Push attempt {n}/3 failed: {err}"
"pl": "Próba {n}/3 push nie powiodła się: {err}",
"ru": "Попытка {n}/3 push не удалась: {err}",
"zh": "推送尝试 {n}/3 失败:{err}"
},
"Push failed for {tag}: {error}": {
"bg": "Push failed for {tag}: {error}",
"de": "Push failed for {tag}: {error}",
"bg": "Push за {tag} се провали: {error}",
"de": "Push für {tag} fehlgeschlagen: {error}",
"en": "Push failed for {tag}: {error}",
"pl": "Push failed for {tag}: {error}",
"ru": "Push failed for {tag}: {error}",
"zh": "Push failed for {tag}: {error}"
"pl": "Push dla {tag} nie powiódł się: {error}",
"ru": "Push для {tag} не удался: {error}",
"zh": "推送 {tag} 失败:{error}"
},
"Push failed: {error}": {
"bg": "",
@@ -2456,28 +2768,28 @@
"zh": ""
},
"Pushed README update with badge SHA {sha}": {
"bg": "Pushed README update with badge SHA {sha}",
"de": "Pushed README update with badge SHA {sha}",
"bg": "Push-ната е README актуализация със SHA на значката {sha}",
"de": "README-Update mit Badge-SHA {sha} gepusht",
"en": "Pushed README update with badge SHA {sha}",
"pl": "Pushed README update with badge SHA {sha}",
"ru": "Pushed README update with badge SHA {sha}",
"zh": "Pushed README update with badge SHA {sha}"
"pl": "Wypchnięto aktualizację README z SHA odznaki {sha}",
"ru": "Отправлено обновление README с SHA значка {sha}",
"zh": "已推送带徽章 SHA {sha} 的 README 更新"
},
"Pushed release commit to master.": {
"bg": "Pushed release commit to master.",
"de": "Pushed release commit to master.",
"bg": "Release комитът е push-нат към master.",
"de": "Release-Commit zu master gepusht.",
"en": "Pushed release commit to master.",
"pl": "Wypchnięto commit wydania do master.",
"ru": "Pushed release commit to master.",
"zh": "Pushed release commit to master."
"ru": "Релизный коммит отправлен в master.",
"zh": "发布提交已推送到 master"
},
"Pushed {branch} to origin.": {
"bg": "Pushed {branch} to origin.",
"de": "Pushed {branch} to origin.",
"bg": "Клонът {branch} е push-нат към origin.",
"de": "{branch} zu origin gepusht.",
"en": "Pushed {branch} to origin.",
"pl": "Pushed {branch} to origin.",
"ru": "Pushed {branch} to origin.",
"zh": "Pushed {branch} to origin."
"pl": "Wypchnięto {branch} do origin.",
"ru": "Ветка {branch} отправлена в origin.",
"zh": "已将 {branch} 推送到 origin"
},
"PyPI publish failed (non-fatal — continuing to Gitea release):\n{error}": {
"bg": "Публикуването в PyPI неуспешно (некритично — продължава към Gitea release):\n{error}",
@@ -2488,116 +2800,132 @@
"zh": "PyPI 发布失败(非致命 — 继续创建 Gitea release):\n{error}"
},
"REPO argument is required (or set GITHUB_REPOSITORY env var).": {
"bg": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
"de": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
"bg": "Аргументът REPO е задължителен (или задайте променливата GITHUB_REPOSITORY).",
"de": "REPO-Argument ist erforderlich (oder GITHUB_REPOSITORY-Umgebungsvariable setzen).",
"en": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
"pl": "Argument REPO jest wymagany (lub ustaw zmienną GITHUB_REPOSITORY).",
"ru": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
"zh": "REPO argument is required (or set GITHUB_REPOSITORY env var)."
"ru": "Аргумент REPO обязателен (или задайте переменную окружения GITHUB_REPOSITORY).",
"zh": "REPO 参数是必需的(或设置 GITHUB_REPOSITORY 环境变量)。"
},
"Real subprocess call(s) detected in test '{test}' without @patch:": {
"bg": "Открити реални subprocess извиквания в тест '{test}' без @patch:",
"de": "Echte subprocess-Aufrufe in Test '{test}' ohne @patch erkannt:",
"en": "Real subprocess call(s) detected in test '{test}' without @patch:",
"pl": "Wykryto prawdziwe wywołania subprocess w teście '{test}' bez @patch:",
"ru": "Обнаружены реальные вызовы subprocess в тесте '{test}' без @patch:",
"zh": "在测试 '{test}' 中检测到未经 @patch 的真实 subprocess 调用:"
},
"Rebase attempt {n}/3 failed: {err}": {
"bg": "Rebase attempt {n}/3 failed: {err}",
"de": "Rebase attempt {n}/3 failed: {err}",
"bg": "Опит {n}/3 за rebase се провали: {err}",
"de": "Rebase-Versuch {n}/3 fehlgeschlagen: {err}",
"en": "Rebase attempt {n}/3 failed: {err}",
"pl": "Rebase attempt {n}/3 failed: {err}",
"ru": "Rebase attempt {n}/3 failed: {err}",
"zh": "Rebase attempt {n}/3 failed: {err}"
"pl": "Próba {n}/3 rebase nie powiodła się: {err}",
"ru": "Попытка {n}/3 rebase не удалась: {err}",
"zh": "Rebase 尝试 {n}/3 失败:{err}"
},
"Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue": {
"bg": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"de": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"bg": "Rebase се провали (конфликти или друга грешка):\n{error}\nРазрешете конфликтите и изпълнете: git rebase --continue",
"de": "Rebase fehlgeschlagen (Konflikte oder anderer Fehler):\n{error}\nKonflikte lösen und ausführen: git rebase --continue",
"en": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"pl": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"ru": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"zh": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue"
"pl": "Rebase nie powiódł się (konflikty lub inny błąd):\n{error}\nRozwiąż konflikty i uruchom: git rebase --continue",
"ru": "Rebase не удался (конфликты или другая ошибка):\n{error}\nРазрешите конфликты и выполните: git rebase --continue",
"zh": "Rebase 失败(冲突或其他错误):\n{error}\n解决冲突并运行:git rebase --continue"
},
"Rebase failed with HTTP {status}: {message}": {
"bg": "Rebase failed with HTTP {status}: {message}",
"de": "Rebase failed with HTTP {status}: {message}",
"bg": "Rebase се провали с HTTP {status}: {message}",
"de": "Rebase mit HTTP {status} fehlgeschlagen: {message}",
"en": "Rebase failed with HTTP {status}: {message}",
"pl": "Rebase failed with HTTP {status}: {message}",
"ru": "Rebase failed with HTTP {status}: {message}",
"zh": "Rebase failed with HTTP {status}: {message}"
"pl": "Rebase nie powiódł się z HTTP {status}: {message}",
"ru": "Rebase завершился с HTTP {status}: {message}",
"zh": "Rebase 失败,HTTP {status}{message}"
},
"Rebase successful.": {
"bg": "Rebase successful.",
"de": "Rebase successful.",
"bg": "Rebase успешен.",
"de": "Rebase erfolgreich.",
"en": "Rebase successful.",
"pl": "Rebase successful.",
"ru": "Rebase successful.",
"zh": "Rebase successful."
"pl": "Rebase powiódł się.",
"ru": "Rebase успешен.",
"zh": "Rebase 成功。"
},
"Rebasing PR #{pr} via Gitea API...": {
"bg": "Rebasing PR #{pr} via Gitea API...",
"de": "Rebasing PR #{pr} via Gitea API...",
"bg": "Rebase на PR #{pr} чрез Gitea API...",
"de": "Rebase von PR #{pr} via Gitea API...",
"en": "Rebasing PR #{pr} via Gitea API...",
"pl": "Rebasing PR #{pr} via Gitea API...",
"ru": "Rebasing PR #{pr} via Gitea API...",
"zh": "Rebasing PR #{pr} via Gitea API..."
"pl": "Rebase PR #{pr} przez Gitea API...",
"ru": "Rebase PR #{pr} через Gitea API...",
"zh": "正在通过 Gitea API 对 PR #{pr} 执行 rebase..."
},
"Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars": {
"bg": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars",
"de": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars",
"bg": "Изискват се идентификационни данни за регистъра: задайте променливите CI_GITEA_TOKEN и CI_GITEA_USERNAME",
"de": "Registry-Anmeldedaten erforderlich: Umgebungsvariablen CI_GITEA_TOKEN und CI_GITEA_USERNAME setzen",
"en": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars",
"pl": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars",
"ru": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars",
"zh": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars"
"pl": "Wymagane dane uwierzytelniające rejestru: ustaw zmienne CI_GITEA_TOKEN i CI_GITEA_USERNAME",
"ru": "Требуются учётные данные реестра: задайте переменные окружения CI_GITEA_TOKEN и CI_GITEA_USERNAME",
"zh": "需要注册表凭据:设置环境变量 CI_GITEA_TOKEN CI_GITEA_USERNAME"
},
"Registry login failed": {
"bg": "Registry login failed",
"de": "Registry login failed",
"bg": "Входът в регистъра се провали",
"de": "Registry-Login fehlgeschlagen",
"en": "Registry login failed",
"pl": "Registry login failed",
"ru": "Registry login failed",
"zh": "Registry login failed"
"pl": "Logowanie do rejestru nie powiodło się",
"ru": "Вход в реестр не удался",
"zh": "注册表登录失败"
},
"Registry login failed: {error}": {
"bg": "Registry login failed: {error}",
"de": "Registry login failed: {error}",
"bg": "Входът в регистъра се провали: {error}",
"de": "Registry-Login fehlgeschlagen: {error}",
"en": "Registry login failed: {error}",
"pl": "Registry login failed: {error}",
"ru": "Registry login failed: {error}",
"zh": "Registry login failed: {error}"
"pl": "Logowanie do rejestru nie powiodło się: {error}",
"ru": "Вход в реестр не удался: {error}",
"zh": "注册表登录失败:{error}"
},
"Regular merge commit — running all post-merge jobs.": {
"bg": "Regular merge commit — running all post-merge jobs.",
"de": "Regular merge commit — running all post-merge jobs.",
"bg": "Обикновен merge комит — изпълняват се всички post-merge задачи.",
"de": "Regulärer Merge-Commit — alle Post-Merge-Jobs werden ausgeführt.",
"en": "Regular merge commit — running all post-merge jobs.",
"pl": "Regular merge commit — running all post-merge jobs.",
"ru": "Regular merge commit — running all post-merge jobs.",
"zh": "Regular merge commit — running all post-merge jobs."
"pl": "Zwykły commit merge uruchamianie wszystkich zadań post-merge.",
"ru": "Обычный merge-коммит — выполняются все post-merge задачи.",
"zh": "常规合并提交——运行所有合并后任务。"
},
"Release commit — skipping all post-merge jobs.": {
"bg": "Release commit — skipping all post-merge jobs.",
"de": "Release commit — skipping all post-merge jobs.",
"bg": "Release комит — всички post-merge задачи се пропускат.",
"de": "Release-Commit — alle Post-Merge-Jobs werden übersprungen.",
"en": "Release commit — skipping all post-merge jobs.",
"pl": "Release commit — skipping all post-merge jobs.",
"ru": "Release commit — skipping all post-merge jobs.",
"zh": "Release commit — skipping all post-merge jobs."
"pl": "Commit release — pomijanie wszystkich zadań post-merge.",
"ru": "Релизный коммит — все post-merge задачи пропускаются.",
"zh": "发布提交——跳过所有合并后任务。"
},
"Release creation failed: {error}": {
"bg": "Release creation failed: {error}",
"de": "Release creation failed: {error}",
"bg": "Създаването на версия се провали: {error}",
"de": "Release-Erstellung fehlgeschlagen: {error}",
"en": "Release creation failed: {error}",
"pl": "Tworzenie wydania nie powiodło się: {error}",
"ru": "Release creation failed: {error}",
"zh": "Release creation failed: {error}"
"ru": "Создание релиза не удалось: {error}",
"zh": "创建发布失败:{error}"
},
"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}'.",
"bg": "Release трябва да се изпълнява на master, в момента сте на '{branch}'.",
"de": "Release muss auf master ausgeführt werden, aktuell auf '{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}'.",
"zh": "Release must be run on master, currently on '{branch}'."
"ru": "Релиз должен выполняться на master, сейчас на '{branch}'.",
"zh": "发布必须在 master 上运行,当前在 '{branch}'"
},
"Repo (owner/name) for label check": {
"bg": "Репозитори (owner/name) за проверка на етикети",
"de": "Repo (owner/name) für die Label-Prüfung",
"en": "Repo (owner/name) for label check",
"pl": "Repo (owner/name) do kontroli etykiet",
"ru": "Репозиторий (owner/name) для проверки меток",
"zh": "用于标签检查的仓库(owner/name"
},
"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}",
"bg": "Репозиторият трябва да е във формат 'owner/name', получено: {repo}",
"de": "Repo muss im Format 'owner/name' sein, erhalten: {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}",
"zh": "Repo must be in 'owner/name' format, got: {repo}"
"ru": "Репозиторий должен быть в формате 'owner/name', получено: {repo}",
"zh": "仓库必须为 'owner/name' 格式,实际为:{repo}"
},
"Repository configuration complete.": {
"bg": "Конфигурирането на хранилището е завършено.",
@@ -2608,20 +2936,20 @@
"zh": "仓库配置完成。"
},
"Repository in owner/name format": {
"bg": "Repository in owner/name format",
"de": "Repository in owner/name format",
"bg": "Репозитория във формат owner/name",
"de": "Repository im Format owner/name",
"en": "Repository in owner/name format",
"pl": "Repository in owner/name format",
"ru": "Repository in owner/name format",
"zh": "Repository in owner/name format"
"pl": "Repozytorium w formacie owner/name",
"ru": "Репозиторий в формате owner/name",
"zh": "owner/name 格式的仓库"
},
"Repository name not set. Use DEVX_REPO_NAME, [tool.devx] repo_name, or GITHUB_REPOSITORY env var.": {
"bg": "Repository name not set. Use DEVX_REPO_NAME, [tool.devx] repo_name, or GITHUB_REPOSITORY env var.",
"de": "Repository name not set. Use DEVX_REPO_NAME, [tool.devx] repo_name, or GITHUB_REPOSITORY env var.",
"bg": "Името на репозитория не е зададено. Използвайте DEVX_REPO_NAME, [tool.devx] repo_name или променливата GITHUB_REPOSITORY.",
"de": "Repository-Name nicht gesetzt. Verwenden Sie DEVX_REPO_NAME, [tool.devx] repo_name oder die Umgebungsvariable GITHUB_REPOSITORY.",
"en": "Repository name not set. Use DEVX_REPO_NAME, [tool.devx] repo_name, or GITHUB_REPOSITORY env var.",
"pl": "Repository name not set. Use DEVX_REPO_NAME, [tool.devx] repo_name, or GITHUB_REPOSITORY env var.",
"ru": "Repository name not set. Use DEVX_REPO_NAME, [tool.devx] repo_name, or GITHUB_REPOSITORY env var.",
"zh": "Repository name not set. Use DEVX_REPO_NAME, [tool.devx] repo_name, or GITHUB_REPOSITORY env var."
"pl": "Nazwa repozytorium nie jest ustawiona. Użyj DEVX_REPO_NAME, [tool.devx] repo_name lub zmiennej GITHUB_REPOSITORY.",
"ru": "Имя репозитория не задано. Используйте DEVX_REPO_NAME, [tool.devx] repo_name или переменную окружения GITHUB_REPOSITORY.",
"zh": "未设置仓库名称。使用 DEVX_REPO_NAME[tool.devx] repo_name GITHUB_REPOSITORY 环境变量。"
},
"Repository owner not set. Use --owner or DEVX_REPO_OWNER env var.": {
"bg": "Собственикът на хранилището не е зададен. Използвайте --owner или DEVX_REPO_OWNER env var.",
@@ -2639,29 +2967,21 @@
"ru": "Отсутствуют обязательные инструменты.",
"zh": "缺少必需的工具。"
},
"Review body must be at least 50 characters.": {
"bg": "Review body must be at least 50 characters.",
"de": "Review body must be at least 50 characters.",
"en": "Review body must be at least 50 characters.",
"pl": "Review body must be at least 50 characters.",
"ru": "Review body must be at least 50 characters.",
"zh": "Review body must be at least 50 characters."
},
"Roles directory not found: {path}": {
"bg": "Roles directory not found: {path}",
"de": "Roles directory not found: {path}",
"bg": "Директорията с роли не е намерена: {path}",
"de": "Rollenverzeichnis nicht gefunden: {path}",
"en": "Roles directory not found: {path}",
"pl": "Katalog ról nie znaleziony: {path}",
"ru": "Roles directory not found: {path}",
"zh": "Roles directory not found: {path}"
"ru": "Директория ролей не найдена: {path}",
"zh": "未找到角色目录:{path}"
},
"Runner count: {count}": {
"bg": "Runner count: {count}",
"de": "Runner count: {count}",
"bg": "Брой раннъри: {count}",
"de": "Runner-Anzahl: {count}",
"en": "Runner count: {count}",
"pl": "Runner count: {count}",
"ru": "Runner count: {count}",
"zh": "Runner count: {count}"
"pl": "Liczba runnerów: {count}",
"ru": "Количество раннеров: {count}",
"zh": "Runner 数量:{count}"
},
"Runner index {index} out of range (0..{max})": {
"bg": "Индексът на runner {index} е извън диапазона (0..{max})",
@@ -2672,52 +2992,52 @@
"zh": "Runner 索引 {index} 超出范围 (0..{max})"
},
"Runner index {runner_index} is out of range (must be >= 1)": {
"bg": "Runner index {runner_index} is out of range (must be >= 1)",
"de": "Runner index {runner_index} is out of range (must be >= 1)",
"bg": "Индексът на runner {runner_index} е извън обхват (трябва да е >= 1)",
"de": "Runner-Index {runner_index} außerhalb des Bereichs (muss >= 1 sein)",
"en": "Runner index {runner_index} is out of range (must be >= 1)",
"pl": "Runner index {runner_index} is out of range (must be >= 1)",
"ru": "Runner index {runner_index} is out of range (must be >= 1)",
"zh": "Runner index {runner_index} is out of range (must be >= 1)"
"pl": "Indeks runnera {runner_index} poza zakresem (musi b >= 1)",
"ru": "Индекс раннера {runner_index} вне диапазона (должен быть >= 1)",
"zh": "Runner 索引 {runner_index} 超出范围(必须 >= 1"
},
"Runner indices: {indices}": {
"bg": "Runner indices: {indices}",
"de": "Runner indices: {indices}",
"bg": "Индекси на раннъри: {indices}",
"de": "Runner-Indizes: {indices}",
"en": "Runner indices: {indices}",
"pl": "Runner indices: {indices}",
"ru": "Runner indices: {indices}",
"zh": "Runner indices: {indices}"
"pl": "Indeksy runnerów: {indices}",
"ru": "Индексы раннеров: {indices}",
"zh": "Runner 索引:{indices}"
},
"Runner {i}: {labels}": {
"bg": "Runner {i}: {labels}",
"bg": "Раннер {i}: {labels}",
"de": "Runner {i}: {labels}",
"en": "Runner {i}: {labels}",
"pl": "Runner {i}: {labels}",
"ru": "Runner {i}: {labels}",
"zh": "Runner {i}: {labels}"
"ru": "Раннер {i}: {labels}",
"zh": "Runner {i}{labels}"
},
"Running lint checks...": {
"bg": "Running lint checks...",
"de": "Running lint checks...",
"bg": "Изпълнение на lint проверки...",
"de": "Lint-Checks laufen...",
"en": "Running lint checks...",
"pl": "Uruchamianie kontroli lint...",
"ru": "Running lint checks...",
"zh": "Running lint checks..."
"ru": "Выполнение проверок lint...",
"zh": "正在运行 lint 检查..."
},
"Running tests...": {
"bg": "Running tests...",
"de": "Running tests...",
"bg": "Изпълнение на тестове...",
"de": "Tests laufen...",
"en": "Running tests...",
"pl": "Uruchamianie testów...",
"ru": "Running tests...",
"zh": "Running tests..."
"ru": "Выполнение тестов...",
"zh": "正在运行测试..."
},
"Running: {cmd}": {
"bg": "Running: {cmd}",
"de": "Running: {cmd}",
"bg": "Изпълнение: {cmd}",
"de": "Ausführen: {cmd}",
"en": "Running: {cmd}",
"pl": "Running: {cmd}",
"ru": "Running: {cmd}",
"zh": "Running: {cmd}"
"pl": "Uruchamianie: {cmd}",
"ru": "Выполнение: {cmd}",
"zh": "运行中:{cmd}"
},
"SSH key set up successfully": {
"bg": "SSH ключът е настроен успешно",
@@ -2743,45 +3063,85 @@
"ru": "SSH_PRIVATE_KEY не задан — пропуск настройки SSH-ключа",
"zh": "SSH_PRIVATE_KEY 未设置 — 跳过 SSH 密钥设置"
},
"Show what would be done without creating PR": {
"bg": "Покажи какво би било направено без създаване на PR",
"de": "Zeigen, was getan würde, ohne PR zu erstellen",
"en": "Show what would be done without creating PR",
"pl": "Pokaż, co zostałoby zrobione bez tworzenia PR",
"ru": "Показать, что было бы сделано без создания PR",
"zh": "显示将要执行的操作而不创建 PR"
},
"Show what would change without updating": {
"bg": "Покажи какво би се променило без обновяване",
"de": "Zeigen, was sich ändern würde, ohne zu aktualisieren",
"en": "Show what would change without updating",
"pl": "Pokaż, co by się zmieniło bez aktualizacji",
"ru": "Показать, что изменилось бы без обновления",
"zh": "显示将要更改的内容而不更新"
},
"Single platform to test against": {
"bg": "Единна платформа за тестване",
"de": "Einzelne Plattform zum Testen",
"en": "Single platform to test against",
"pl": "Pojedyncza platforma do testowania",
"ru": "Единая платформа для тестирования",
"zh": "用于测试的单一平台"
},
"Skip Vikunja title match check": {
"bg": "Skip Vikunja title match check",
"de": "Skip Vikunja title match check",
"bg": "Пропусни проверката за съвпадение на заглавието с Vikunja",
"de": "Vikunja-Titelübereinstimmungsprüfung überspringen",
"en": "Skip Vikunja title match check",
"pl": "Skip Vikunja title match check",
"ru": "Skip Vikunja title match check",
"zh": "Skip Vikunja title match check"
"pl": "Pomiń kontrolę zgodności tytułu z Vikunja",
"ru": "Пропустить проверку совпадения заголовка с Vikunja",
"zh": "跳过 Vikunja 标题匹配检查"
},
"Skip branch-behind-master check": {
"bg": "Skip branch-behind-master check",
"de": "Skip branch-behind-master check",
"bg": "Пропусни проверката дали клонът изостава от master",
"de": "Prüfung „Branch hinter master“ überspringen",
"en": "Skip branch-behind-master check",
"pl": "Skip branch-behind-master check",
"ru": "Skip branch-behind-master check",
"zh": "Skip branch-behind-master check"
"pl": "Pomiń kontrolę czy gałąź jest za master",
"ru": "Пропустить проверку отставания ветки от master",
"zh": "跳过分支落后于 master 的检查"
},
"Skipping commit push — no staged changes.": {
"bg": "Skipping commit push — no staged changes.",
"de": "Skipping commit push — no staged changes.",
"bg": "Пропуска се push на комита — няма staged промени.",
"de": "Commit-Push wird übersprungen — keine gestagten Änderungen.",
"en": "Skipping commit push — no staged changes.",
"pl": "Pomijanie wypchnięcia commit — brak zmian w staging.",
"ru": "Skipping commit push — no staged changes.",
"zh": "Skipping commit push — no staged changes."
"ru": "Push коммита пропускается — нет staged-изменений.",
"zh": "跳过提交推送——没有暂存的更改。"
},
"Skipping — runner index {runner_index} > max runners {max_runners}": {
"bg": "Skipping — runner index {runner_index} > max runners {max_runners}",
"de": "Skippingrunner index {runner_index} > max runners {max_runners}",
"bg": "Пропуска се — индекс на runner {runner_index} > максимум раннъри {max_runners}",
"de": "ÜbersprungenRunner-Index {runner_index} > max. Runner {max_runners}",
"en": "Skipping — runner index {runner_index} > max runners {max_runners}",
"pl": "Skipping — runner index {runner_index} > max runners {max_runners}",
"ru": "Skipping — runner index {runner_index} > max runners {max_runners}",
"zh": "Skipping — runner index {runner_index} > max runners {max_runners}"
"pl": "Pomijanie — indeks runnera {runner_index} > maks. runnerów {max_runners}",
"ru": "Пропускается — индекс раннера {runner_index} > макс. раннеров {max_runners}",
"zh": "跳过——runner 索引 {runner_index} > 最大 runner {max_runners}"
},
"Source repo that published (owner/name)": {
"bg": "Изходно репозитори, което е публикувало (owner/name)",
"de": "Quell-Repo, das veröffentlicht hat (owner/name)",
"en": "Source repo that published (owner/name)",
"pl": "Repozytorium źródłowe, które opublikowało (owner/name)",
"ru": "Исходный репозиторий, выполнивший публикацию (owner/name)",
"zh": "已发布的源仓库(owner/name"
},
"Spec validation failed.": {
"bg": "Валидацията на spec се провали.",
"de": "Spec-Validierung fehlgeschlagen.",
"en": "Spec validation failed.",
"pl": "Walidacja spec nie powiodła się.",
"ru": "Проверка spec не пройдена.",
"zh": "规范验证失败。"
},
"Synced to latest origin/{branch}": {
"bg": "Synced to latest origin/{branch}",
"de": "Synced to latest origin/{branch}",
"bg": "Синхронизирано към последния origin/{branch}",
"de": "Mit neuestem origin/{branch} synchronisiert",
"en": "Synced to latest origin/{branch}",
"pl": "Synced to latest origin/{branch}",
"ru": "Synced to latest origin/{branch}",
"zh": "Synced to latest origin/{branch}"
"pl": "Zsynchronizowano z najnowszym origin/{branch}",
"ru": "Синхронизировано с последним origin/{branch}",
"zh": "已同步到最新的 origin/{branch}"
},
"Syncing files...": {
"bg": "",
@@ -2800,60 +3160,84 @@
"zh": ""
},
"Tag consistency check failed.": {
"bg": "Tag consistency check failed.",
"de": "Tag consistency check failed.",
"bg": "Проверката за консистентност на таговете се провали.",
"de": "Tag-Konsistenzprüfung fehlgeschlagen.",
"en": "Tag consistency check failed.",
"pl": "Kontrola zgodności tagów nie powiodła się.",
"ru": "Tag consistency check failed.",
"zh": "Tag consistency check failed."
"ru": "Проверка согласованности тегов не пройдена.",
"zh": "标签一致性检查失败。"
},
"Tag is required (or use --from-tag).": {
"bg": "Tag is required (or use --from-tag).",
"de": "Tag is required (or use --from-tag).",
"bg": "Тагът е задължителен (или използвайте --from-tag).",
"de": "Tag ist erforderlich (oder --from-tag verwenden).",
"en": "Tag is required (or use --from-tag).",
"pl": "Tag jest wymagany (lub użyj --from-tag).",
"ru": "Tag is required (or use --from-tag).",
"zh": "Tag is required (or use --from-tag)."
"ru": "Тег обязателен (или используйте --from-tag).",
"zh": "标签是必需的(或使用 --from-tag)。"
},
"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.",
"bg": "Тагът v{version} вече съществува. Workflow-ът за публикуване вече трябва да е задействан.",
"de": "Tag v{version} existierte bereits. Der Publish-Workflow sollte bereits ausgelöst worden sein.",
"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.",
"zh": "Tag v{version} already existed. Publish workflow should already have been triggered."
"ru": "Тег v{version} уже существует. Workflow публикации уже должен был быть запущен.",
"zh": "标签 v{version} 已存在。发布工作流应已被触发。"
},
"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.",
"bg": "Тагът {tag} вече съществува и сочи към HEAD. Създаването се пропуска.",
"de": "Tag {tag} existiert bereits und zeigt auf HEAD. Erstellung wird übersprungen.",
"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.",
"zh": "Tag {tag} already exists and points to HEAD. Skipping creation."
"ru": "Тег {tag} уже существует и указывает на HEAD. Создание пропускается.",
"zh": "标签 {tag} 已存在且指向 HEAD。跳过创建。"
},
"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.",
"bg": "Тагът {tag} вече съществува, но сочи към {tag_commit} (очаква се HEAD {head_commit}). Това показва несъответствие таг/комит. Изпълнете 'python3 -m devx.ci.release --verify' за подробности.",
"de": "Tag {tag} existiert bereits, zeigt aber auf {tag_commit} (erwartet HEAD {head_commit}). Dies deutet auf eine Tag/Commit-Fehlzuordnung hin. Führen Sie 'python3 -m devx.ci.release --verify' für Details aus.",
"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.",
"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."
"ru": "Тег {tag} уже существует, но указывает на {tag_commit} (ожидается HEAD {head_commit}). Это указывает на несоответствие тег/коммит. Выполните 'python3 -m devx.ci.release --verify' для подробностей.",
"zh": "标签 {tag} 已存在但指向 {tag_commit}(预期为 HEAD {head_commit})。这表明标签/提交不匹配。运行 'python3 -m devx.ci.release --verify' 了解详情。"
},
"Target repo (owner/name) to create PR in": {
"bg": "Целево репозитори (owner/name) за създаване на PR",
"de": "Ziel-Repo (owner/name) zum Erstellen des PR",
"en": "Target repo (owner/name) to create PR in",
"pl": "Docelowe repo (owner/name) do utworzenia PR",
"ru": "Целевой репозиторий (owner/name) для создания PR",
"zh": "用于创建 PR 的目标仓库(owner/name"
},
"Task ID: {task_id}": {
"bg": "Task ID: {task_id}",
"de": "Task ID: {task_id}",
"bg": "ID на задача: {task_id}",
"de": "Task-ID: {task_id}",
"en": "Task ID: {task_id}",
"pl": "ID zadania: {task_id}",
"ru": "Task ID: {task_id}",
"zh": "Task ID: {task_id}"
"ru": "ID задачи: {task_id}",
"zh": "任务 ID{task_id}"
},
"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.",
"bg": "Тестът '{name}' отне {elapsed:.2f}s (лимит: {limit}s). Оптимизирайте: използвайте по-леки fixtures, намалете I/O или mock-нете външни извиквания.",
"de": "Test '{name}' dauerte {elapsed:.2f}s (Limit: {limit}s). Optimieren: leichtere Fixtures verwenden, I/O reduzieren oder externe Aufrufe mocken.",
"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.",
"zh": "Test '{name}' took {elapsed:.2f}s (limit: {limit}s). Optimise: use lighter fixtures, reduce I/O, or mock external calls."
"ru": "Тест '{name}' занял {elapsed:.2f}s (лимит: {limit}s). Оптимизируйте: используйте более лёгкие фикстуры, уменьшите I/O или замокайте внешние вызовы.",
"zh": "测试 '{name}' 耗时 {elapsed:.2f}s(限制:{limit}s)。优化:使用更轻的 fixtures、减少 I/O 或 mock 外部调用。"
},
"Test isolation check FAILED: {count} violation(s) in {files} file(s).": {
"bg": "Проверката за изолация на тестовете СЕ ПРОВАЛИ: {count} нарушение(я) във {files} файл(а).",
"de": "Testisolierungsprüfung FEHLGESCHLAGEN: {count} Verstoß/Verstöße in {files} Datei(en).",
"en": "Test isolation check FAILED: {count} violation(s) in {files} file(s).",
"pl": "Kontrola izolacji testów NIEUDANA: {count} naruszeń w {files} plikach.",
"ru": "Проверка изоляции тестов ПРОВАЛЕНА: {count} нарушение(й) в {files} файл(ах).",
"zh": "测试隔离检查失败:{files} 个文件中存在 {count} 处违规。"
},
"Test isolation check passed with {count} advisory warning(s) in {files} file(s).": {
"bg": "Проверката за изолация на тестовете премина с {count} предупредителни бележки във {files} файл(а).",
"de": "Testisolierungsprüfung mit {count} Hinweiswarnung(en) in {files} Datei(en) bestanden.",
"en": "Test isolation check passed with {count} advisory warning(s) in {files} file(s).",
"pl": "Kontrola izolacji testów przeszła z {count} ostrzeżeniami doradczymi w {files} plikach.",
"ru": "Проверка изоляции тестов пройдена с {count} предупреждением(ями) в {files} файл(ах).",
"zh": "测试隔离检查通过,{files} 个文件中有 {count} 条建议性警告。"
},
"Test isolation check passed: {count} test files analyzed, no violations found.": {
"bg": "Проверката за изолация на тестове премина: анализирани са {count} тестови файла, няма нарушения.",
@@ -2864,60 +3248,76 @@
"zh": "测试隔离检查通过:已分析 {count} 个测试文件,未发现违规。"
},
"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}",
"bg": "Тестовете се провалиха — отказ за версия. Първо коригирайте неуспешните тестове.\n{stderr}",
"de": "Tests fehlgeschlagen — Release wird verweigert. Zuerst Testfehler beheben.\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}",
"zh": "Tests failed — refusing to release. Fix test failures first.\n{stderr}"
"ru": "Тесты не пройдены — отказ в релизе. Сначала исправьте ошибки тестов.\n{stderr}",
"zh": "测试失败——拒绝发布。请先修复测试失败。\n{stderr}"
},
"Tests passed.": {
"bg": "Tests passed.",
"de": "Tests passed.",
"bg": "Тестовете преминаха.",
"de": "Tests bestanden.",
"en": "Tests passed.",
"pl": "Testy zakończone pomyślnie.",
"ru": "Tests passed.",
"zh": "Tests passed."
"ru": "Тесты пройдены.",
"zh": "测试通过。"
},
"Timeout reached after {timeout}s.": {
"bg": "Timeout reached after {timeout}s.",
"de": "Timeout reached after {timeout}s.",
"bg": "Достигнат таймаут след {timeout}s.",
"de": "Timeout nach {timeout}s erreicht.",
"en": "Timeout reached after {timeout}s.",
"pl": "Timeout reached after {timeout}s.",
"ru": "Timeout reached after {timeout}s.",
"zh": "Timeout reached after {timeout}s."
"pl": "Limit czasu osiągnięty po {timeout}s.",
"ru": "Таймаут достигнут после {timeout}s.",
"zh": "{timeout} 秒后达到超时。"
},
"Transitive-subprocess advisories (runtime audit is authoritative):": {
"bg": "Съветващи бележки за транзитивни subprocess (runtime одитът е решаващ):",
"de": "Transitive-Subprocess-Hinweise (Runtime-Audit ist maßgeblich):",
"en": "Transitive-subprocess advisories (runtime audit is authoritative):",
"pl": "Ostrzeżenia dotyczące subprocessów przechodnich (audyt runtime jest rozstrzygający):",
"ru": "Предупреждения о транзитивных subprocess (авторитетен runtime-аудит):",
"zh": "传递性 subprocess 建议(以运行时审计为准):"
},
"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).",
"bg": "Модулните тестове преминаха за {duration:.2f}s (под лимита {max}s, всички тестове под лимита {single}s на тест).",
"de": "Unit-Tests in {duration:.2f}s bestanden (unter {max}s-Limit, alle Tests unter {single}s Pro-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).",
"zh": "Unit tests passed in {duration:.2f}s (under {max}s limit, all tests under {single}s per-test limit)."
"ru": "Модульные тесты пройдены за {duration:.2f}s (ниже лимита {max}s, все тесты ниже лимита {single}s на тест).",
"zh": "单元测试在 {duration:.2f}s 内通过(低于 {max}s 限制,所有测试均低于 {single}s 单测试限制)。"
},
"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.",
"bg": "Модулните тестове са твърде бавни: {duration:.2f}s (максимум: {max}s).\n Корекция: изпълнете 'make pytest-cov' за профилиране, след това оптимизирайте бавните тестове.\n Съвет: избягвайте ненужни импорти, използвайте по-леки mocks или кеширайте fixtures.",
"de": "Unit-Tests zu langsam: {duration:.2f}s (max. erlaubt: {max}s).\n Behebung: 'make pytest-cov' zum Profilieren ausführen, dann langsame Tests optimieren.\n Hinweis: unnötige Imports vermeiden, leichtere Mocks verwenden oder Fixtures cachen.",
"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.",
"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."
"ru": "Модульные тесты слишком медленные: {duration:.2f}s (макс.: {max}s).\n Исправление: выполните 'make pytest-cov' для профилирования, затем оптимизируйте медленные тесты.\n Совет: избегайте ненужных импортов, используйте более лёгкие моки или кешируйте фикстуры.",
"zh": "单元测试过慢:{duration:.2f}s(最大允许:{max}s)。\n 修复:运行 'make pytest-cov' 进行性能分析,然后优化慢测试。\n 提示:避免不必要的导入,使用更轻的 mock 或缓存 fixtures"
},
"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}",
"bg": "Непозната категория проверка '{check}'. Налични: all, user-facing{tags}",
"de": "Unbekannte Check-Kategorie '{check}'. Verfügbar: 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}",
"zh": "Unknown check category '{check}'. Available: all, user-facing{tags}"
"ru": "Неизвестная категория проверки '{check}'. Доступны: all, user-facing{tags}",
"zh": "未知检查类别 '{check}'。可用:alluser-facing{tags}"
},
"Unknown nightly status — staging deploy blocked.": {
"bg": "Непознат nightly статус — staging деплой е блокиран.",
"de": "Unbekannter Nightly-Status — Staging-Deploy blockiert.",
"en": "Unknown nightly status — staging deploy blocked.",
"pl": "Nieznany status nightly — wdrożenie staging zablokowane.",
"ru": "Неизвестный статус nightly — деплой на staging заблокирован.",
"zh": "未知的 nightly 状态——staging 部署已阻止。"
},
"Updated badge URLs in {filename}": {
"bg": "Updated badge URLs in {filename}",
"de": "Updated badge URLs in {filename}",
"bg": "Обновени URL на значки в {filename}",
"de": "Badge-URLs in {filename} aktualisiert",
"en": "Updated badge URLs in {filename}",
"pl": "Updated badge URLs in {filename}",
"ru": "Updated badge URLs in {filename}",
"zh": "Updated badge URLs in {filename}"
"pl": "Zaktualizowano URL-e odznak w {filename}",
"ru": "Обновлены URL значков в {filename}",
"zh": "已更新 {filename} 中的徽章 URL"
},
"Updated documentation version references to v{version}": {
"bg": "",
@@ -2928,20 +3328,20 @@
"zh": ""
},
"Updated version in {init}": {
"bg": "Updated version in {init}",
"de": "Updated version in {init}",
"bg": "Обновена версия в {init}",
"de": "Version in {init} aktualisiert",
"en": "Updated version in {init}",
"pl": "Zaktualizowano wersję w {init}",
"ru": "Updated version in {init}",
"zh": "Updated version in {init}"
"ru": "Версия обновлена в {init}",
"zh": "已更新 {init} 中的版本"
},
"Updated {changelog_file}": {
"bg": "Updated {changelog_file}",
"de": "Updated {changelog_file}",
"bg": "Обновен {changelog_file}",
"de": "{changelog_file} aktualisiert",
"en": "Updated {changelog_file}",
"pl": "Zaktualizowano {changelog_file}",
"ru": "Updated {changelog_file}",
"zh": "Updated {changelog_file}"
"ru": "Обновлён {changelog_file}",
"zh": "已更新 {changelog_file}"
},
"Use string comparison or _is_truthy()/_is_falsy() helpers instead. Add '{marker}' to suppress individual lines.": {
"bg": "Използвайте сравнение на низове или _is_truthy()/_is_falsy() помощници. Добавете '{marker}' за потискане на отделни редове.",
@@ -2968,20 +3368,20 @@
"zh": "VIKUNJA_TOKEN 未设置。在 .env 或环境中设置它。"
},
"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.",
"bg": "VIKUNJA_TOKEN не е зададен. Изисква се в CI за валидиране на заглавията на PR.",
"de": "VIKUNJA_TOKEN ist nicht gesetzt. In CI zur Validierung von PR-Titeln erforderlich.",
"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.",
"zh": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles."
"ru": "VIKUNJA_TOKEN не задан. Требуется в CI для проверки заголовков PR.",
"zh": "未设置 VIKUNJA_TOKEN。CI 中验证 PR 标题时需要。"
},
"Version file: {file}": {
"bg": "Version file: {file}",
"de": "Version file: {file}",
"bg": "Файл с версия: {file}",
"de": "Versionsdatei: {file}",
"en": "Version file: {file}",
"pl": "Plik wersji: {file}",
"ru": "Version file: {file}",
"zh": "Version file: {file}"
"ru": "Файл версии: {file}",
"zh": "版本文件:{file}"
},
"Version stays at v{version} — no version bump from git-cliff. Commits since last tag don't warrant a new release. Skipping.": {
"bg": "",
@@ -2992,12 +3392,12 @@
"zh": ""
},
"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.",
"bg": "Грешка в Vikunja API (HTTP {status}): {message}. Задача {task_id} НЕ е обновена. Merge-ът успя, но Vikunja задачата изисква ръчно обновяване.",
"de": "Vikunja-API-Fehler (HTTP {status}): {message}. Task {task_id} wurde NICHT aktualisiert. Der Merge war erfolgreich, aber der Vikunja-Task muss manuell aktualisiert werden.",
"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.",
"zh": "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 (HTTP {status}): {message}. Задача {task_id} НЕ была обновлена. Слияние прошло успешно, но задачу Vikunja нужно обновить вручную.",
"zh": "Vikunja API 错误(HTTP {status}):{message}。任务 {task_id} 未更新。合并成功,但 Vikunja 任务需要手动更新。"
},
"Vikunja task title '{title}' starts with '{prefix}:'. The task title should NOT include the '{prefix}' prefix — it is automatically added to the PR title. Update the Vikunja task title to remove the prefix.": {
"bg": "Заглавието на задачата във Vikunja '{title}' започва с '{prefix}:'. Заглавието на задачата НЕ трябва да съдържа префикса '{prefix}' — той се добавя автоматично към заглавието на PR. Актуализирайте заглавието на задачата във Vikunja, за да премахнете префикса.",
@@ -3048,12 +3448,12 @@
"zh": "警告: 无法解析 Python 版本 '{version}'。"
},
"WARNING: --skip-tests passed — skipping test verification.": {
"bg": "WARNING: --skip-tests passed — skipping test verification.",
"de": "WARNING: --skip-tests passed — skipping test verification.",
"bg": "ПРЕДУПРЕЖДЕНИЕ: зададен е --skip-tests — проверката на тестовете се пропуска.",
"de": "WARNUNG: --skip-tests übergeben — Testverifizierung wird übersprungen.",
"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.",
"zh": "WARNING: --skip-tests passed — skipping test verification."
"ru": "ПРЕДУПРЕЖДЕНИЕ: передан --skip-tests — проверка тестов пропускается.",
"zh": "警告:已传入 --skip-tests——跳过测试验证。"
},
"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}). Изтрийте .taskid от хранилището — името на клона е единственият източник на истината.",
@@ -3096,68 +3496,68 @@
"zh": ""
},
"Waiting for CI checks to complete (timeout: {timeout}s)...": {
"bg": "Waiting for CI checks to complete (timeout: {timeout}s)...",
"de": "Waiting for CI checks to complete (timeout: {timeout}s)...",
"bg": "Изчакване CI проверките да завършат (таймаут: {timeout}s)...",
"de": "Warte auf Abschluss der CI-Checks (Timeout: {timeout}s)...",
"en": "Waiting for CI checks to complete (timeout: {timeout}s)...",
"pl": "Waiting for CI checks to complete (timeout: {timeout}s)...",
"ru": "Waiting for CI checks to complete (timeout: {timeout}s)...",
"zh": "Waiting for CI checks to complete (timeout: {timeout}s)..."
"pl": "Oczekiwanie na zakończenie kontroli CI (limit: {timeout}s)...",
"ru": "Ожидание завершения CI-проверок (таймаут: {timeout}s)...",
"zh": "等待 CI 检查完成(超时:{timeout}s..."
},
"Warning: could not fetch tags from origin.": {
"bg": "Warning: could not fetch tags from origin.",
"de": "Warning: could not fetch tags from origin.",
"bg": "Предупреждение: не могат да се извлекат таговете от origin.",
"de": "Warnung: Tags konnten nicht von origin abgerufen werden.",
"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.",
"zh": "Warning: could not fetch tags from origin."
"ru": "Предупреждение: не удалось получить теги из origin.",
"zh": "警告:无法从 origin 获取标签。"
},
"Warning: instance-level runners query failed: {error}": {
"bg": "Warning: instance-level runners query failed: {error}",
"de": "Warning: instance-level runners query failed: {error}",
"bg": "Предупреждение: заявката за раннъри на ниво инстанция се провали: {error}",
"de": "Warnung: Abfrage der Runner auf Instanzebene fehlgeschlagen: {error}",
"en": "Warning: instance-level runners query failed: {error}",
"pl": "Warning: instance-level runners query failed: {error}",
"ru": "Warning: instance-level runners query failed: {error}",
"zh": "Warning: instance-level runners query failed: {error}"
"pl": "Ostrzeżenie: zapytanie o runnery na poziomie instancji nie powiodło się: {error}",
"ru": "Предупреждение: запрос раннеров на уровне инстанса не удался: {error}",
"zh": "警告:实例级 runner 查询失败:{error}"
},
"Warning: instance-level runners query returned HTTP {status}": {
"bg": "Warning: instance-level runners query returned HTTP {status}",
"de": "Warning: instance-level runners query returned HTTP {status}",
"bg": "Предупреждение: заявката за раннъри на ниво инстанция върна HTTP {status}",
"de": "Warnung: Abfrage der Runner auf Instanzebene gab HTTP {status} zurück",
"en": "Warning: instance-level runners query returned HTTP {status}",
"pl": "Warning: instance-level runners query returned HTTP {status}",
"ru": "Warning: instance-level runners query returned HTTP {status}",
"zh": "Warning: instance-level runners query returned HTTP {status}"
"pl": "Ostrzeżenie: zapytanie o runnery na poziomie instancji zwróciło HTTP {status}",
"ru": "Предупреждение: запрос раннеров на уровне инстанса вернул HTTP {status}",
"zh": "警告:实例级 runner 查询返回 HTTP {status}"
},
"Warning: org-level runners query failed: {error}": {
"bg": "Warning: org-level runners query failed: {error}",
"de": "Warning: org-level runners query failed: {error}",
"bg": "Предупреждение: заявката за раннъри на ниво организация се провали: {error}",
"de": "Warnung: Abfrage der Runner auf Organisationsebene fehlgeschlagen: {error}",
"en": "Warning: org-level runners query failed: {error}",
"pl": "Warning: org-level runners query failed: {error}",
"ru": "Warning: org-level runners query failed: {error}",
"zh": "Warning: org-level runners query failed: {error}"
"pl": "Ostrzeżenie: zapytanie o runnery na poziomie organizacji nie powiodło się: {error}",
"ru": "Предупреждение: запрос раннеров на уровне организации не удался: {error}",
"zh": "警告:组织级 runner 查询失败:{error}"
},
"Warning: org-level runners query returned HTTP {status}": {
"bg": "Warning: org-level runners query returned HTTP {status}",
"de": "Warning: org-level runners query returned HTTP {status}",
"bg": "Предупреждение: заявката за раннъри на ниво организация върна HTTP {status}",
"de": "Warnung: Abfrage der Runner auf Organisationsebene gab HTTP {status} zurück",
"en": "Warning: org-level runners query returned HTTP {status}",
"pl": "Warning: org-level runners query returned HTTP {status}",
"ru": "Warning: org-level runners query returned HTTP {status}",
"zh": "Warning: org-level runners query returned HTTP {status}"
"pl": "Ostrzeżenie: zapytanie o runnery na poziomie organizacji zwróciło HTTP {status}",
"ru": "Предупреждение: запрос раннеров на уровне организации вернул HTTP {status}",
"zh": "警告:组织级 runner 查询返回 HTTP {status}"
},
"Warning: repo-level runners query failed: {error}": {
"bg": "Warning: repo-level runners query failed: {error}",
"de": "Warning: repo-level runners query failed: {error}",
"bg": "Предупреждение: заявката за раннъри на ниво репозитори се провали: {error}",
"de": "Warnung: Abfrage der Runner auf Repo-Ebene fehlgeschlagen: {error}",
"en": "Warning: repo-level runners query failed: {error}",
"pl": "Warning: repo-level runners query failed: {error}",
"ru": "Warning: repo-level runners query failed: {error}",
"zh": "Warning: repo-level runners query failed: {error}"
"pl": "Ostrzeżenie: zapytanie o runnery na poziomie repozytorium nie powiodło się: {error}",
"ru": "Предупреждение: запрос раннеров на уровне репозитория не удался: {error}",
"zh": "警告:仓库级 runner 查询失败:{error}"
},
"Warning: repo-level runners query returned HTTP {status}": {
"bg": "Warning: repo-level runners query returned HTTP {status}",
"de": "Warning: repo-level runners query returned HTTP {status}",
"bg": "Предупреждение: заявката за раннъри на ниво репозитори върна HTTP {status}",
"de": "Warnung: Abfrage der Runner auf Repo-Ebene gab HTTP {status} zurück",
"en": "Warning: repo-level runners query returned HTTP {status}",
"pl": "Warning: repo-level runners query returned HTTP {status}",
"ru": "Warning: repo-level runners query returned HTTP {status}",
"zh": "Warning: repo-level runners query returned HTTP {status}"
"pl": "Ostrzeżenie: zapytanie o runnery na poziomie repozytorium zwróciło HTTP {status}",
"ru": "Предупреждение: запрос раннеров на уровне репозитория вернул HTTP {status}",
"zh": "警告:仓库级 runner 查询返回 HTTP {status}"
},
"Wiki repo not found or empty — initializing fresh.": {
"bg": "",
@@ -3199,13 +3599,21 @@
"ru": "Записать deploy-ref в файл $GITHUB_OUTPUT.",
"zh": "将 deploy-ref 写入 $GITHUB_OUTPUT 文件。"
},
"Write results to $GITHUB_OUTPUT": {
"bg": "Записва резултатите в $GITHUB_OUTPUT",
"de": "Ergebnisse nach $GITHUB_OUTPUT schreiben",
"en": "Write results to $GITHUB_OUTPUT",
"pl": "Zapisuje wyniki do $GITHUB_OUTPUT",
"ru": "Записывать результаты в $GITHUB_OUTPUT",
"zh": "将结果写入 $GITHUB_OUTPUT"
},
"Wrote tag {tag} to GITHUB_OUTPUT.": {
"bg": "Wrote tag {tag} to GITHUB_OUTPUT.",
"de": "Wrote tag {tag} to GITHUB_OUTPUT.",
"bg": "Тагът {tag} е записан в GITHUB_OUTPUT.",
"de": "Tag {tag} nach GITHUB_OUTPUT geschrieben.",
"en": "Wrote tag {tag} to GITHUB_OUTPUT.",
"pl": "Wrote tag {tag} to GITHUB_OUTPUT.",
"ru": "Wrote tag {tag} to GITHUB_OUTPUT.",
"zh": "Wrote tag {tag} to GITHUB_OUTPUT."
"pl": "Zapisano tag {tag} do GITHUB_OUTPUT.",
"ru": "Тег {tag} записан в GITHUB_OUTPUT.",
"zh": "已将标签 {tag} 写入 GITHUB_OUTPUT"
},
"[check-api-identity-checks] Passed: no unsafe identity checks found": {
"bg": "[check-api-identity-checks] Мина: не са намерени небрежни проверки за идентичност",
@@ -3216,12 +3624,12 @@
"zh": "[check-api-identity-checks] 通过:未发现不安全的身份检查"
},
"[check-dep-docs] Passed: all dependencies are documented": {
"bg": "[check-dep-docs] Passed: all dependencies are documented",
"de": "[check-dep-docs] Passed: all dependencies are documented",
"bg": "[check-dep-docs] Успешно: всички зависимости са документирани",
"de": "[check-dep-docs] Bestanden: alle Abhängigkeiten sind dokumentiert",
"en": "[check-dep-docs] Passed: all dependencies are documented",
"pl": "[check-dep-docs] Passed: all dependencies are documented",
"ru": "[check-dep-docs] Passed: all dependencies are documented",
"zh": "[check-dep-docs] Passed: all dependencies are documented"
"pl": "[check-dep-docs] Zaliczone: wszystkie zależności są udokumentowane",
"ru": "[check-dep-docs] Пройдено: все зависимости задокументированы",
"zh": "[check-dep-docs] 通过:所有依赖项均已记录"
},
"[check-deps] All core tools present.": {
"bg": "[check-deps] Всички основни инструменти са налични.",
@@ -3248,28 +3656,76 @@
"zh": "[check-deps] 虚拟环境 .venv 已就绪 (Python {version})。"
},
"[check-mutable-globals] Passed: no mutable path globals found": {
"bg": "[check-mutable-globals] Passed: no mutable path globals found",
"de": "[check-mutable-globals] Passed: no mutable path globals found",
"bg": "[check-mutable-globals] Успешно: не са намерени променливи пътеки глобали",
"de": "[check-mutable-globals] Bestanden: keine mutablen Pfad-Globals gefunden",
"en": "[check-mutable-globals] Passed: no mutable path globals found",
"pl": "[check-mutable-globals] Passed: no mutable path globals found",
"ru": "[check-mutable-globals] Passed: no mutable path globals found",
"zh": "[check-mutable-globals] Passed: no mutable path globals found"
"pl": "[check-mutable-globals] Zaliczone: nie znaleziono mutowalnych globali ścieżek",
"ru": "[check-mutable-globals] Пройдено: изменяемых глобальных путей не найдено",
"zh": "[check-mutable-globals] 通过:未发现可变路径全局变量"
},
"[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)": {
"bg": "[check-test-speed] Открита е CI среда — лимитите се скалират с {factor}x (общо: {orig}s → {eff}s, на тест: {orig_s}s → {eff_s}s)",
"de": "[check-test-speed] CI-Umgebung erkannt — Limits werden um Faktor {factor}x skaliert (gesamt: {orig}s → {eff}s, pro Test: {orig_s}s → {eff_s}s)",
"en": "[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)",
"pl": "[check-test-speed] Wykryto środowisko CI — limity skalowane {factor}x (razem: {orig}s → {eff}s, na test: {orig_s}s → {eff_s}s)",
"ru": "[check-test-speed] Обнаружена среда CI — лимиты масштабируются в {factor}x (всего: {orig}s → {eff}s, на тест: {orig_s}s → {eff_s}s)",
"zh": "[check-test-speed] 检测到 CI 环境——限制按 {factor}x 缩放(总计:{orig}s → {eff}s,单测试:{orig_s}s → {eff_s}s"
},
"[check_agent_docs] Passed: scanned {count} file(s), no stale references": {
"bg": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
"de": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
"bg": "[check_agent_docs] Успешно: сканирани {count} файл(а), няма остарели препратки",
"de": "[check_agent_docs] Bestanden: {count} Datei(en) gescannt, keine veralteten Referenzen",
"en": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
"pl": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
"ru": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
"zh": "[check_agent_docs] Passed: scanned {count} file(s), no stale references"
"pl": "[check_agent_docs] Zaliczone: przeskanowano {count} plików, brak nieaktualnych odwołań",
"ru": "[check_agent_docs] Пройдено: проверено {count} файл(ов), устаревших ссылок нет",
"zh": "[check_agent_docs] 通过:已扫描 {count} 个文件,无过时引用"
},
"[check_test_coverage] No changed files to check.": {
"bg": "[check_test_coverage] No changed files to check.",
"de": "[check_test_coverage] No changed files to check.",
"bg": "[check_test_coverage] Няма променени файлове за проверка.",
"de": "[check_test_coverage] Keine geänderten Dateien zu prüfen.",
"en": "[check_test_coverage] No changed files to check.",
"pl": "[check_test_coverage] No changed files to check.",
"ru": "[check_test_coverage] No changed files to check.",
"zh": "[check_test_coverage] No changed files to check."
"pl": "[check_test_coverage] Brak zmienionych plików do sprawdzenia.",
"ru": "[check_test_coverage] Нет изменённых файлов для проверки.",
"zh": "[check_test_coverage] 没有需要检查的已更改文件。"
},
"[dep-pr] Bumping {pkg} from {old} to {new} in {file}": {
"bg": "[dep-pr] Увеличаване на {pkg} от {old} на {new} в {file}",
"de": "[dep-pr] Erhöhe {pkg} von {old} auf {new} in {file}",
"en": "[dep-pr] Bumping {pkg} from {old} to {new} in {file}",
"pl": "[dep-pr] Podbijanie {pkg} z {old} do {new} w {file}",
"ru": "[dep-pr] Повышение {pkg} с {old} до {new} в {file}",
"zh": "[dep-pr] 将 {file} 中的 {pkg} 从 {old} 升级到 {new}"
},
"[dep-pr] Could not find pinned version for {pkg} in infra repo.": {
"bg": "[dep-pr] Не е намерена фиксирана версия за {pkg} в infra репозиторито.",
"de": "[dep-pr] Keine gepinnte Version für {pkg} im Infra-Repo gefunden.",
"en": "[dep-pr] Could not find pinned version for {pkg} in infra repo.",
"pl": "[dep-pr] Nie znaleziono przypiętej wersji dla {pkg} w repo infra.",
"ru": "[dep-pr] Не найдена закреплённая версия для {pkg} в репозитории infra.",
"zh": "[dep-pr] 在 infra 仓库中未找到 {pkg} 的固定版本。"
},
"[dep-pr] Created PR #{number}: {title}": {
"bg": "[dep-pr] Създаден PR #{number}: {title}",
"de": "[dep-pr] PR #{number} erstellt: {title}",
"en": "[dep-pr] Created PR #{number}: {title}",
"pl": "[dep-pr] Utworzono PR #{number}: {title}",
"ru": "[dep-pr] Создан PR #{number}: {title}",
"zh": "[dep-pr] 已创建 PR #{number}{title}"
},
"[dep-pr] PR already exists: #{number}": {
"bg": "[dep-pr] PR вече съществува: #{number}",
"de": "[dep-pr] PR existiert bereits: #{number}",
"en": "[dep-pr] PR already exists: #{number}",
"pl": "[dep-pr] PR już istnieje: #{number}",
"ru": "[dep-pr] PR уже существует: #{number}",
"zh": "[dep-pr] PR 已存在:#{number}"
},
"[dep-pr] {pkg} already at {version} — no PR needed.": {
"bg": "[dep-pr] {pkg} вече е на {version} — не е нужен PR.",
"de": "[dep-pr] {pkg} bereits auf {version} — kein PR nötig.",
"en": "[dep-pr] {pkg} already at {version} — no PR needed.",
"pl": "[dep-pr] {pkg} już jest na {version} — PR niepotrzebny.",
"ru": "[dep-pr] {pkg} уже на версии {version} — PR не нужен.",
"zh": "[dep-pr] {pkg} 已处于 {version}——无需 PR。"
},
"[docker-login] Logged in to {registry}.": {
"bg": "[docker-login] Влязъл в {registry}.",
@@ -3312,36 +3768,36 @@
"zh": ""
},
"[dry-run] Would commit: release: v{version} [skip ci]": {
"bg": "[dry-run] Would commit: release: v{version} [skip ci]",
"de": "[dry-run] Would commit: release: v{version} [skip ci]",
"bg": "[dry-run] Ще се комитне: release: v{version} [skip ci]",
"de": "[dry-run] Würde committen: release: v{version} [skip ci]",
"en": "[dry-run] Would commit: release: v{version} [skip ci]",
"pl": "[dry-run] Utworzono by commit: release: v{version} [skip ci]",
"ru": "[dry-run] Would commit: release: v{version} [skip ci]",
"zh": "[dry-run] Would commit: release: v{version} [skip ci]"
"ru": "[dry-run] Было бы закоммичено: release: v{version} [skip ci]",
"zh": "[dry-run] 将提交:release: v{version} [skip ci]"
},
"[dry-run] Would create tag: v{version}": {
"bg": "[dry-run] Would create tag: v{version}",
"de": "[dry-run] Would create tag: v{version}",
"bg": "[dry-run] Ще се създаде таг: v{version}",
"de": "[dry-run] Würde Tag erstellen: 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}",
"zh": "[dry-run] Would create tag: v{version}"
"ru": "[dry-run] Был бы создан тег: v{version}",
"zh": "[dry-run] 将创建标签:v{version}"
},
"[dry-run] Would create tag: {tag}": {
"bg": "[dry-run] Would create tag: {tag}",
"de": "[dry-run] Would create tag: {tag}",
"bg": "[dry-run] Ще се създаде таг: {tag}",
"de": "[dry-run] Würde Tag erstellen: {tag}",
"en": "[dry-run] Would create tag: {tag}",
"pl": "[dry-run] Utworzono by tag: {tag}",
"ru": "[dry-run] Would create tag: {tag}",
"zh": "[dry-run] Would create tag: {tag}"
"ru": "[dry-run] Был бы создан тег: {tag}",
"zh": "[dry-run] 将创建标签:{tag}"
},
"[dry-run] Would push commit to master": {
"bg": "[dry-run] Would push commit to master",
"de": "[dry-run] Would push commit to master",
"bg": "[dry-run] Ще се push-не комит към master",
"de": "[dry-run] Würde Commit zu master pushen",
"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",
"zh": "[dry-run] Would push commit to master"
"ru": "[dry-run] Коммит был бы отправлен в master",
"zh": "[dry-run] 将推送提交到 master"
},
"[dry-run] Would update doc version references via check_doc_versions --fix": {
"bg": "",
@@ -3352,20 +3808,68 @@
"zh": ""
},
"[dry-run] Would update {changelog_file}": {
"bg": "[dry-run] Would update {changelog_file}",
"de": "[dry-run] Would update {changelog_file}",
"bg": "[dry-run] Ще се обнови {changelog_file}",
"de": "[dry-run] Würde {changelog_file} aktualisieren",
"en": "[dry-run] Would update {changelog_file}",
"pl": "[dry-run] Zaktualizowano by {changelog_file}",
"ru": "[dry-run] Would update {changelog_file}",
"zh": "[dry-run] Would update {changelog_file}"
"ru": "[dry-run] Был бы обновлён {changelog_file}",
"zh": "[dry-run] 将更新 {changelog_file}"
},
"[dry-run] Would update {init}": {
"bg": "[dry-run] Would update {init}",
"de": "[dry-run] Would update {init}",
"bg": "[dry-run] Ще се обнови {init}",
"de": "[dry-run] Würde {init} aktualisieren",
"en": "[dry-run] Would update {init}",
"pl": "[dry-run] Zaktualizowano by {init}",
"ru": "[dry-run] Would update {init}",
"zh": "[dry-run] Would update {init}"
"ru": "[dry-run] Был бы обновлён {init}",
"zh": "[dry-run] 将更新 {init}"
},
"[fast-molecule] Changed roles: {roles}": {
"bg": "[fast-molecule] Променени роли: {roles}",
"de": "[fast-molecule] Geänderte Rollen: {roles}",
"en": "[fast-molecule] Changed roles: {roles}",
"pl": "[fast-molecule] Zmienione role: {roles}",
"ru": "[fast-molecule] Изменённые роли: {roles}",
"zh": "[fast-molecule] 已更改的角色:{roles}"
},
"[nightly-gate] Nightly FAILED{run}. Staging deploys are blocked until nightly passes.": {
"bg": "[nightly-gate] Nightly СЕ ПРОВАЛИ{run}. Staging деплой е блокиран, докато nightly не премине.",
"de": "[nightly-gate] Nightly FEHLGESCHLAGEN{run}. Staging-Deploys sind blockiert, bis Nightly besteht.",
"en": "[nightly-gate] Nightly FAILED{run}. Staging deploys are blocked until nightly passes.",
"pl": "[nightly-gate] Nightly NIEUDANY{run}. Wdrożenia staging są zablokowane, dopóki nightly nie przejdzie.",
"ru": "[nightly-gate] Nightly ПРОВАЛЕН{run}. Деплои на staging заблокированы, пока nightly не пройдёт.",
"zh": "[nightly-gate] Nightly 失败{run}。在 nightly 通过之前,staging 部署被阻止。"
},
"[nightly-gate] Set NIGHTLY_STATUS=failed{run}": {
"bg": "[nightly-gate] Зададено NIGHTLY_STATUS=failed{run}",
"de": "[nightly-gate] NIGHTLY_STATUS=failed{run} gesetzt",
"en": "[nightly-gate] Set NIGHTLY_STATUS=failed{run}",
"pl": "[nightly-gate] Ustawiono NIGHTLY_STATUS=failed{run}",
"ru": "[nightly-gate] Установлено NIGHTLY_STATUS=failed{run}",
"zh": "[nightly-gate] 已设置 NIGHTLY_STATUS=failed{run}"
},
"[nightly-gate] Set NIGHTLY_STATUS=passed{run}": {
"bg": "[nightly-gate] Зададено NIGHTLY_STATUS=passed{run}",
"de": "[nightly-gate] NIGHTLY_STATUS=passed{run} gesetzt",
"en": "[nightly-gate] Set NIGHTLY_STATUS=passed{run}",
"pl": "[nightly-gate] Ustawiono NIGHTLY_STATUS=passed{run}",
"ru": "[nightly-gate] Установлено NIGHTLY_STATUS=passed{run}",
"zh": "[nightly-gate] 已设置 NIGHTLY_STATUS=passed{run}"
},
"[nightly-gate] Unknown nightly status: {status} — blocking deploy (fail closed).": {
"bg": "[nightly-gate] Непознат nightly статус: {status} — деплой се блокира (fail closed).",
"de": "[nightly-gate] Unbekannter Nightly-Status: {status} — Deploy wird blockiert (fail closed).",
"en": "[nightly-gate] Unknown nightly status: {status} — blocking deploy (fail closed).",
"pl": "[nightly-gate] Nieznany status nightly: {status} — wdrożenie blokowane (fail closed).",
"ru": "[nightly-gate] Неизвестный статус nightly: {status} — деплой блокируется (fail closed).",
"zh": "[nightly-gate] 未知的 nightly 状态:{status}——部署被阻止(fail closed)。"
},
"[spec-check] Spec validated: {path}": {
"bg": "[spec-check] Spec валидиран: {path}",
"de": "[spec-check] Spec validiert: {path}",
"en": "[spec-check] Spec validated: {path}",
"pl": "[spec-check] Spec zweryfikowany: {path}",
"ru": "[spec-check] Spec проверен: {path}",
"zh": "[spec-check] 规范已验证:{path}"
},
"[tofu-init] Done.": {
"bg": "[tofu-init] Готово.",
@@ -3448,36 +3952,52 @@
"zh": "失败"
},
"git command failed ({cmd}): {stderr}": {
"bg": "git command failed ({cmd}): {stderr}",
"de": "git command failed ({cmd}): {stderr}",
"bg": "git командата се провали ({cmd}): {stderr}",
"de": "git-Befehl fehlgeschlagen ({cmd}): {stderr}",
"en": "git command failed ({cmd}): {stderr}",
"pl": "polecenie git nie powiodło się ({cmd}): {stderr}",
"ru": "git command failed ({cmd}): {stderr}",
"zh": "git command failed ({cmd}): {stderr}"
"ru": "git-команда завершилась с ошибкой ({cmd}): {stderr}",
"zh": "git 命令失败({cmd}):{stderr}"
},
"git diff --numstat failed: {stderr}": {
"bg": "git diff --numstat се провали: {stderr}",
"de": "git diff --numstat fehlgeschlagen: {stderr}",
"en": "git diff --numstat failed: {stderr}",
"pl": "git diff --numstat nie powiódł się: {stderr}",
"ru": "git diff --numstat завершился с ошибкой: {stderr}",
"zh": "git diff --numstat 失败:{stderr}"
},
"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.",
"bg": "git-cliff генерира празен changelog за v{version}. Проверете cliff.toml и историята на комитите.",
"de": "git-cliff hat ein leeres Changelog für v{version} generiert. Prüfen Sie cliff.toml und die Commit-Historie.",
"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.",
"zh": "git-cliff generated empty changelog for v{version}. Check cliff.toml and commit history."
"ru": "git-cliff сгенерировал пустой changelog для v{version}. Проверьте cliff.toml и историю коммитов.",
"zh": "git-cliff 为 v{version} 生成了空的 changelog。请检查 cliff.toml 和提交历史。"
},
"git-cliff returned empty version.": {
"bg": "git-cliff returned empty version.",
"de": "git-cliff returned empty version.",
"bg": "git-cliff върна празна версия.",
"de": "git-cliff gab eine leere Version zurück.",
"en": "git-cliff returned empty version.",
"pl": "git-cliff zwrócił pustą wersję.",
"ru": "git-cliff returned empty version.",
"zh": "git-cliff returned empty version."
"ru": "git-cliff вернул пустую версию.",
"zh": "git-cliff 返回了空版本。"
},
"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).",
"bg": "git-cliff върна невалиден формат на версия: {version}. Очаква се semver (напр. 0.4.1).",
"de": "git-cliff gab ein ungültiges Versionsformat zurück: {version}. Erwartet: Semver (z. B. 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).",
"zh": "git-cliff returned invalid version format: {version}. Expected semver (e.g., 0.4.1)."
"ru": "git-cliff вернул неверный формат версии: {version}. Ожидается semver (напр. 0.4.1).",
"zh": "git-cliff 返回了无效的版本格式:{version}。应为 semver(例如 0.4.1)。"
},
"importlib.reload({mod}) called {n} time(s) in test '{test}' — odd count leaves module in modified state. Add a final reload to restore defaults or wrap in try/finally.": {
"bg": "importlib.reload({mod}) е извикан {n} път(и) в тест '{test}' — нечетен брой оставя модула в променено състояние. Добавете финален reload за възстановяване на подразбираните или обвийте в try/finally.",
"de": "importlib.reload({mod}) wurde {n} Mal in Test '{test}' aufgerufen — ungerade Anzahl lässt Modul in verändertem Zustand. Finalen Reload hinzufügen oder in try/finally einhüllen.",
"en": "importlib.reload({mod}) called {n} time(s) in test '{test}' — odd count leaves module in modified state. Add a final reload to restore defaults or wrap in try/finally.",
"pl": "importlib.reload({mod}) wywołano {n} raz(y) w teście '{test}' — nieparzysta liczba pozostawia moduł w zmienionym stanie. Dodaj końcowy reload, aby przywrócić domyślne, lub owiń w try/finally.",
"ru": "importlib.reload({mod}) вызван {n} раз(а) в тесте '{test}' — нечётное количество оставляет модуль в изменённом состоянии. Добавьте финальный reload для восстановления или оберните в try/finally.",
"zh": "在测试 '{test}' 中调用了 {n} 次 importlib.reload({mod})——奇数次会使模块保持修改状态。请添加最后的 reload 恢复默认或用 try/finally 包裹。"
},
"in_progress": {
"bg": "в процес",
@@ -3504,20 +4024,20 @@
"zh": "indices={indices}"
},
"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}",
"bg": "Ключовете и стойностите на mapping.json трябва да са низове, получено {k}={v}",
"de": "mapping.json-Schlüssel und -Werte müssen Strings sein, erhalten {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}",
"zh": "mapping.json keys and values must be strings, got {k}={v}"
"ru": "Ключи и значения mapping.json должны быть строками, получено {k}={v}",
"zh": "mapping.json 的键和值必须是字符串,实际得到 {k}={v}"
},
"mapping.json must be a dict of file-path -> page-title, got {type}": {
"bg": "mapping.json must be a dict of file-path -> page-title, got {type}",
"de": "mapping.json must be a dict of file-path -> page-title, got {type}",
"bg": "mapping.json трябва да е dict от file-path -> page-title, получено {type}",
"de": "mapping.json muss ein Dict von file-path -> page-title sein, erhalten {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}",
"zh": "mapping.json must be a dict of file-path -> page-title, got {type}"
"ru": "mapping.json должен быть dict вида file-path -> page-title, получено {type}",
"zh": "mapping.json 必须是 file-path -> page-title 的字典,实际得到 {type}"
},
"pending": {
"bg": "в очакване",
@@ -3536,20 +4056,20 @@
"zh": "在当前目录中未找到 pyproject.toml。"
},
"tea login '{name}' already configured.": {
"bg": "tea login '{name}' already configured.",
"de": "tea login '{name}' already configured.",
"bg": "tea входът '{name}' вече е конфигуриран.",
"de": "tea-Login '{name}' bereits konfiguriert.",
"en": "tea login '{name}' already configured.",
"pl": "tea login '{name}' already configured.",
"ru": "tea login '{name}' already configured.",
"zh": "tea login '{name}' already configured."
"pl": "Logowanie tea '{name}' już skonfigurowane.",
"ru": "Вход tea '{name}' уже настроен.",
"zh": "tea 登录 '{name}' 已配置。"
},
"tea not installed — skipping login configuration.": {
"bg": "tea not installed — skipping login configuration.",
"de": "tea not installed — skipping login configuration.",
"bg": "tea не е инсталиран — пропуска се конфигурацията за вход.",
"de": "tea nicht installiert — Login-Konfiguration wird übersprungen.",
"en": "tea not installed — skipping login configuration.",
"pl": "tea not installed — skipping login configuration.",
"ru": "tea not installed — skipping login configuration.",
"zh": "tea not installed — skipping login configuration."
"pl": "tea nie jest zainstalowany — pomijanie konfiguracji logowania.",
"ru": "tea не установлен — настройка входа пропускается.",
"zh": "未安装 tea——跳过登录配置。"
},
"time.sleep called in test '{test}' without @patch — this causes real wall-clock delays. Add @patch(\"<module>.time.sleep\").": {
"bg": "time.sleep извикано в тест '{test}' без @patch — това причинява реални забавяния. Добавете @patch(\"<module>.time.sleep\").",
@@ -3600,12 +4120,12 @@
"zh": "{env} 未设置。请在 .env 文件中设置。"
},
"{file} already exists. Use --force to overwrite.": {
"bg": "{file} already exists. Use --force to overwrite.",
"de": "{file} already exists. Use --force to overwrite.",
"bg": "{file} вече съществува. Използвайте --force за презаписване.",
"de": "{file} existiert bereits. Mit --force überschreiben.",
"en": "{file} already exists. Use --force to overwrite.",
"pl": "{file} już istnieje. Użyj --force, aby nadpisać.",
"ru": "{file} already exists. Use --force to overwrite.",
"zh": "{file} already exists. Use --force to overwrite."
"ru": "{file} уже существует. Используйте --force для перезаписи.",
"zh": "{file} 已存在。使用 --force 覆盖。"
},
"{func} called in test '{test}' without @patch — this function {desc}. Add @patch(\"<module>.{func}\").": {
"bg": "{func} извикано в тест '{test}' без @patch — тази функция {desc}. Добавете @patch(\"<module>.{func}\").",
@@ -3630,173 +4150,5 @@
"pl": "{separator}",
"ru": "{separator}",
"zh": "{separator}"
},
"\nTest isolation check FAILED: {count} violation(s) in {files} file(s).\n": {
"bg": "\nTest isolation check FAILED: {count} violation(s) in {files} file(s).\n",
"de": "\nTest isolation check FAILED: {count} violation(s) in {files} file(s).\n",
"en": "\nTest isolation check FAILED: {count} violation(s) in {files} file(s).\n",
"pl": "\nTest isolation check FAILED: {count} violation(s) in {files} file(s).\n",
"ru": "\nTest isolation check FAILED: {count} violation(s) in {files} file(s).\n",
"zh": "\nTest isolation check FAILED: {count} violation(s) in {files} file(s).\n"
},
" Fix the PR title with:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n Or manually set the PR title to: '{expected}'": {
"bg": " Fix the PR title with:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n Or manually set the PR title to: '{expected}'",
"de": " Fix the PR title with:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n Or manually set the PR title to: '{expected}'",
"en": " Fix the PR title with:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n Or manually set the PR title to: '{expected}'",
"pl": " Fix the PR title with:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n Or manually set the PR title to: '{expected}'",
"ru": " Fix the PR title with:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n Or manually set the PR title to: '{expected}'",
"zh": " Fix the PR title with:\n python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n Or manually set the PR title to: '{expected}'"
},
"Add @patch(\"subprocess.run\") or patch the calling function to fix this.": {
"bg": "Add @patch(\"subprocess.run\") or patch the calling function to fix this.",
"de": "Add @patch(\"subprocess.run\") or patch the calling function to fix this.",
"en": "Add @patch(\"subprocess.run\") or patch the calling function to fix this.",
"pl": "Add @patch(\"subprocess.run\") or patch the calling function to fix this.",
"ru": "Add @patch(\"subprocess.run\") or patch the calling function to fix this.",
"zh": "Add @patch(\"subprocess.run\") or patch the calling function to fix this."
},
"Branch name (auto-fetched from PR if not given)": {
"bg": "Branch name (auto-fetched from PR if not given)",
"de": "Branch name (auto-fetched from PR if not given)",
"en": "Branch name (auto-fetched from PR if not given)",
"pl": "Branch name (auto-fetched from PR if not given)",
"ru": "Branch name (auto-fetched from PR if not given)",
"zh": "Branch name (auto-fetched from PR if not given)"
},
"CI_GITEA_API_TOKEN not set: {error}": {
"bg": "CI_GITEA_API_TOKEN not set: {error}",
"de": "CI_GITEA_API_TOKEN not set: {error}",
"en": "CI_GITEA_API_TOKEN not set: {error}",
"pl": "CI_GITEA_API_TOKEN not set: {error}",
"ru": "CI_GITEA_API_TOKEN not set: {error}",
"zh": "CI_GITEA_API_TOKEN not set: {error}"
},
"CliRunner.invoke({target}) in test '{test}' reaches unpatched dangerous functions: {funcs}. Add @patch for each or patch the calling function.": {
"bg": "CliRunner.invoke({target}) in test '{test}' reaches unpatched dangerous functions: {funcs}. Add @patch for each or patch the calling function.",
"de": "CliRunner.invoke({target}) in test '{test}' reaches unpatched dangerous functions: {funcs}. Add @patch for each or patch the calling function.",
"en": "CliRunner.invoke({target}) in test '{test}' reaches unpatched dangerous functions: {funcs}. Add @patch for each or patch the calling function.",
"pl": "CliRunner.invoke({target}) in test '{test}' reaches unpatched dangerous functions: {funcs}. Add @patch for each or patch the calling function.",
"ru": "CliRunner.invoke({target}) in test '{test}' reaches unpatched dangerous functions: {funcs}. Add @patch for each or patch the calling function.",
"zh": "CliRunner.invoke({target}) in test '{test}' reaches unpatched dangerous functions: {funcs}. Add @patch for each or patch the calling function."
},
"Could not determine branch name from PR #{pr}": {
"bg": "Could not determine branch name from PR #{pr}",
"de": "Could not determine branch name from PR #{pr}",
"en": "Could not determine branch name from PR #{pr}",
"pl": "Could not determine branch name from PR #{pr}",
"ru": "Could not determine branch name from PR #{pr}",
"zh": "Could not determine branch name from PR #{pr}"
},
"Failed to fetch PR #{pr}: {error}": {
"bg": "Failed to fetch PR #{pr}: {error}",
"de": "Failed to fetch PR #{pr}: {error}",
"en": "Failed to fetch PR #{pr}: {error}",
"pl": "Failed to fetch PR #{pr}: {error}",
"ru": "Failed to fetch PR #{pr}: {error}",
"zh": "Failed to fetch PR #{pr}: {error}"
},
"Failed to update PR #{pr}: {error}": {
"bg": "Failed to update PR #{pr}: {error}",
"de": "Failed to update PR #{pr}: {error}",
"en": "Failed to update PR #{pr}: {error}",
"pl": "Failed to update PR #{pr}: {error}",
"ru": "Failed to update PR #{pr}: {error}",
"zh": "Failed to update PR #{pr}: {error}"
},
"Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.": {
"bg": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.",
"de": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.",
"en": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.",
"pl": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.",
"ru": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.",
"zh": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function."
},
"Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.\n": {
"bg": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.\n",
"de": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.\n",
"en": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.\n",
"pl": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.\n",
"ru": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.\n",
"zh": "Fix: add @patch decorators or with patch() context managers for subprocess/time.sleep calls, or patch the calling function.\n"
},
"Heavy import '{mod}' (~{ms:.0f}ms) at module level — this slows test collection for all tests. Move inside test functions or use lazy import.": {
"bg": "Heavy import '{mod}' (~{ms:.0f}ms) at module level — this slows test collection for all tests. Move inside test functions or use lazy import.",
"de": "Heavy import '{mod}' (~{ms:.0f}ms) at module level — this slows test collection for all tests. Move inside test functions or use lazy import.",
"en": "Heavy import '{mod}' (~{ms:.0f}ms) at module level — this slows test collection for all tests. Move inside test functions or use lazy import.",
"pl": "Heavy import '{mod}' (~{ms:.0f}ms) at module level — this slows test collection for all tests. Move inside test functions or use lazy import.",
"ru": "Heavy import '{mod}' (~{ms:.0f}ms) at module level — this slows test collection for all tests. Move inside test functions or use lazy import.",
"zh": "Heavy import '{mod}' (~{ms:.0f}ms) at module level — this slows test collection for all tests. Move inside test functions or use lazy import."
},
"No task ID found in branch '{branch}'. Expected format: {prefix}-N-description.": {
"bg": "No task ID found in branch '{branch}'. Expected format: {prefix}-N-description.",
"de": "No task ID found in branch '{branch}'. Expected format: {prefix}-N-description.",
"en": "No task ID found in branch '{branch}'. Expected format: {prefix}-N-description.",
"pl": "No task ID found in branch '{branch}'. Expected format: {prefix}-N-description.",
"ru": "No task ID found in branch '{branch}'. Expected format: {prefix}-N-description.",
"zh": "No task ID found in branch '{branch}'. Expected format: {prefix}-N-description."
},
"PR number to fix": {
"bg": "PR number to fix",
"de": "PR number to fix",
"en": "PR number to fix",
"pl": "PR number to fix",
"ru": "PR number to fix",
"zh": "PR number to fix"
},
"Real subprocess call(s) detected in test '{test}' without @patch:": {
"bg": "Real subprocess call(s) detected in test '{test}' without @patch:",
"de": "Real subprocess call(s) detected in test '{test}' without @patch:",
"en": "Real subprocess call(s) detected in test '{test}' without @patch:",
"pl": "Real subprocess call(s) detected in test '{test}' without @patch:",
"ru": "Real subprocess call(s) detected in test '{test}' without @patch:",
"zh": "Real subprocess call(s) detected in test '{test}' without @patch:"
},
"Show what would change without updating": {
"bg": "Show what would change without updating",
"de": "Show what would change without updating",
"en": "Show what would change without updating",
"pl": "Show what would change without updating",
"ru": "Show what would change without updating",
"zh": "Show what would change without updating"
},
"Test isolation check FAILED: {count} violation(s) in {files} file(s).": {
"bg": "Test isolation check FAILED: {count} violation(s) in {files} file(s).",
"de": "Test isolation check FAILED: {count} violation(s) in {files} file(s).",
"en": "Test isolation check FAILED: {count} violation(s) in {files} file(s).",
"pl": "Test isolation check FAILED: {count} violation(s) in {files} file(s).",
"ru": "Test isolation check FAILED: {count} violation(s) in {files} file(s).",
"zh": "Test isolation check FAILED: {count} violation(s) in {files} file(s)."
},
"Test isolation check passed with {count} advisory warning(s) in {files} file(s).": {
"bg": "Test isolation check passed with {count} advisory warning(s) in {files} file(s).",
"de": "Test isolation check passed with {count} advisory warning(s) in {files} file(s).",
"en": "Test isolation check passed with {count} advisory warning(s) in {files} file(s).",
"pl": "Test isolation check passed with {count} advisory warning(s) in {files} file(s).",
"ru": "Test isolation check passed with {count} advisory warning(s) in {files} file(s).",
"zh": "Test isolation check passed with {count} advisory warning(s) in {files} file(s)."
},
"Transitive-subprocess advisories (runtime audit is authoritative):": {
"bg": "Transitive-subprocess advisories (runtime audit is authoritative):",
"de": "Transitive-subprocess advisories (runtime audit is authoritative):",
"en": "Transitive-subprocess advisories (runtime audit is authoritative):",
"pl": "Transitive-subprocess advisories (runtime audit is authoritative):",
"ru": "Transitive-subprocess advisories (runtime audit is authoritative):",
"zh": "Transitive-subprocess advisories (runtime audit is authoritative):"
},
"importlib.reload({mod}) called {n} time(s) in test '{test}' — odd count leaves module in modified state. Add a final reload to restore defaults or wrap in try/finally.": {
"bg": "importlib.reload({mod}) called {n} time(s) in test '{test}' — odd count leaves module in modified state. Add a final reload to restore defaults or wrap in try/finally.",
"de": "importlib.reload({mod}) called {n} time(s) in test '{test}' — odd count leaves module in modified state. Add a final reload to restore defaults or wrap in try/finally.",
"en": "importlib.reload({mod}) called {n} time(s) in test '{test}' — odd count leaves module in modified state. Add a final reload to restore defaults or wrap in try/finally.",
"pl": "importlib.reload({mod}) called {n} time(s) in test '{test}' — odd count leaves module in modified state. Add a final reload to restore defaults or wrap in try/finally.",
"ru": "importlib.reload({mod}) called {n} time(s) in test '{test}' — odd count leaves module in modified state. Add a final reload to restore defaults or wrap in try/finally.",
"zh": "importlib.reload({mod}) called {n} time(s) in test '{test}' — odd count leaves module in modified state. Add a final reload to restore defaults or wrap in try/finally."
},
"[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)": {
"en": "[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)",
"bg": "[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)",
"de": "[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)",
"pl": "[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)",
"ru": "[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)",
"zh": "[check-test-speed] CI environment detected — scaling limits by {factor}x (total: {orig}s → {eff}s, per-test: {orig_s}s → {eff_s}s)"
}
}
+6
View File
@@ -923,6 +923,12 @@ class TestGiteaClientActions:
)
def test_get_repo_variable_returns_value(self) -> None:
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
client._session.request = MagicMock(return_value=_mock_response({"data": "v0.28.1"}))
result = client.get_repo_variable("PRODUCTION_DEPLOY_TAG")
assert result == "v0.28.1"
def test_get_repo_variable_falls_back_to_value(self) -> None:
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
client._session.request = MagicMock(return_value=_mock_response({"value": "v0.28.1"}))
result = client.get_repo_variable("PRODUCTION_DEPLOY_TAG")
+225 -8
View File
@@ -13,7 +13,9 @@ from click.testing import CliRunner
import devx.tools.build_image as build_image
from devx.tools.build_image import (
ImageSpec,
PushHTTP500Error,
build_full_tag,
delete_remote_manifest,
load_manifest,
push_image,
registry_login,
@@ -196,7 +198,7 @@ class TestBuildImage:
class TestPushImage:
def test_success(self) -> None:
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
mock_result = MagicMock(returncode=0, stderr="", stdout="")
mock_result = MagicMock(returncode=0, stdout="")
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run:
assert push_image(spec, "git.example.com") is True
assert mock_run.call_count == 2
@@ -204,8 +206,8 @@ class TestPushImage:
def test_partial_failure(self) -> None:
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
results = [
MagicMock(returncode=0, stderr="", stdout=""),
MagicMock(returncode=1, stderr="push failed", stdout=""),
MagicMock(returncode=0, stdout=""),
MagicMock(returncode=1, stdout="push failed"),
]
with patch("devx.tools.build_image.subprocess.run", side_effect=results):
assert push_image(spec, "git.example.com") is False
@@ -216,6 +218,212 @@ class TestPushImage:
assert push_image(spec, "git.example.com", dry_run=True) is True
mock_run.assert_not_called()
def test_no_delete_on_success_with_creds(self) -> None:
"""Push-first: no delete needed when push succeeds."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
mock_result = MagicMock(returncode=0, stdout="")
with (
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
):
assert push_image(spec, "git.example.com", username="user", token="tok") is True
mock_del.assert_not_called()
def test_no_delete_without_creds(self) -> None:
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
mock_result = MagicMock(returncode=0, stdout="")
with (
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
):
assert push_image(spec, "git.example.com") is True
mock_del.assert_not_called()
def test_delete_and_retry_on_already_exists(self) -> None:
"""Gitea #31964: push fails with 'already exists', delete + retry."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
results = [
MagicMock(returncode=1, stdout="package version already exists"),
MagicMock(returncode=0, stdout=""),
]
with (
patch("devx.tools.build_image.subprocess.run", side_effect=results),
patch("devx.tools.build_image.delete_remote_manifest", return_value=True) as mock_del,
):
assert push_image(spec, "git.example.com", username="user", token="tok") is True
mock_del.assert_called_once_with(
"git.example.com",
"ci-base",
"latest",
"user",
"tok",
dry_run=False,
)
def test_no_delete_on_non_already_exists_failure(self) -> None:
"""Push fails for other reasons (non-500) — old manifest preserved."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
mock_result = MagicMock(returncode=1, stdout="denied: requested access to the resource is denied")
with (
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
):
assert push_image(spec, "git.example.com", username="user", token="tok") is False
mock_del.assert_not_called()
def test_retry_also_fails(self) -> None:
"""Gitea #31964 retry also fails — both pushes fail."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
results = [
MagicMock(returncode=1, stdout="package version already exists"),
MagicMock(returncode=1, stdout="push failed again"),
]
with (
patch("devx.tools.build_image.subprocess.run", side_effect=results),
patch("devx.tools.build_image.delete_remote_manifest", return_value=True),
):
assert push_image(spec, "git.example.com", username="user", token="tok") is False
def test_http_500_retries_then_succeeds(self) -> None:
"""HTTP 500 from registry race condition — retry succeeds."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
results = [
MagicMock(returncode=1, stdout="received unexpected HTTP status: 500 Internal Server Error"),
MagicMock(returncode=0, stdout=""),
]
with (
patch("devx.tools.build_image.subprocess.run", side_effect=results),
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
patch("time.sleep"),
):
assert push_image(spec, "git.example.com", username="user", token="tok") is True
mock_del.assert_not_called()
def test_http_500_retries_all_fail(self) -> None:
"""HTTP 500 retries exhausted — push fails, no delete attempted."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
mock_result = MagicMock(returncode=1, stdout="received unexpected HTTP status: 500 Internal Server Error")
with (
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
patch("time.sleep"),
):
assert push_image(spec, "git.example.com", username="user", token="tok") is False
mock_del.assert_not_called()
def test_run_push_raises_on_500(self) -> None:
"""_run_push raises PushHTTP500Error when stdout contains 500."""
from devx.tools.build_image import _run_push
mock_result = MagicMock(returncode=1, stdout="received unexpected HTTP status: 500 Internal Server Error")
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
with pytest.raises(PushHTTP500Error, match="500"):
_run_push(["docker", "push", "img:latest"])
def test_run_push_no_raise_on_non_500(self) -> None:
"""_run_push returns result when stdout has no 500."""
from devx.tools.build_image import _run_push
mock_result = MagicMock(returncode=1, stdout="denied: access denied")
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
result = _run_push(["docker", "push", "img:latest"])
assert result.returncode == 1
def test_run_push_no_raise_on_success(self) -> None:
"""_run_push returns result on success."""
from devx.tools.build_image import _run_push
mock_result = MagicMock(returncode=0, stdout="")
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
result = _run_push(["docker", "push", "img:latest"])
assert result.returncode == 0
class TestDeleteRemoteManifest:
def test_dry_run(self) -> None:
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t", dry_run=True) is True
def test_tag_not_found(self) -> None:
import urllib.error
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
mock_urlopen.side_effect = urllib.error.HTTPError("url", 404, "Not Found", {}, None)
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
def test_delete_success(self) -> None:
mock_head_resp = MagicMock()
mock_head_resp.__enter__.return_value.headers.get.return_value = "sha256:abc123"
mock_del_resp = MagicMock()
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
mock_urlopen.side_effect = [mock_head_resp, mock_del_resp]
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
def test_delete_404_treated_as_success(self) -> None:
import urllib.error
mock_head_resp = MagicMock()
mock_head_resp.__enter__.return_value.headers.get.return_value = "sha256:abc123"
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
mock_urlopen.side_effect = [
mock_head_resp,
urllib.error.HTTPError("url", 404, "Not Found", {}, None),
]
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
def test_head_error_does_not_block(self) -> None:
import urllib.error
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
mock_urlopen.side_effect = urllib.error.HTTPError("url", 500, "Server Error", {}, None)
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
def test_url_error_does_not_block(self) -> None:
import urllib.error
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
mock_urlopen.side_effect = urllib.error.URLError("network down")
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
def test_no_digest_does_not_block(self) -> None:
mock_head_resp = MagicMock()
mock_head_resp.__enter__.return_value.headers.get.return_value = None
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
mock_urlopen.return_value = mock_head_resp
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
def test_head_405_passes_through(self) -> None:
import urllib.error
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
mock_urlopen.side_effect = urllib.error.HTTPError("url", 405, "Method Not Allowed", {}, None)
# 405 falls through with pass, digest never set, returns True
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
assert mock_urlopen.call_count == 1
def test_delete_500_does_not_block(self) -> None:
import urllib.error
mock_head_resp = MagicMock()
mock_head_resp.__enter__.return_value.headers.get.return_value = "sha256:abc123"
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
mock_urlopen.side_effect = [
mock_head_resp,
urllib.error.HTTPError("url", 500, "Server Error", {}, None),
]
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
def test_delete_url_error_does_not_block(self) -> None:
import urllib.error
mock_head_resp = MagicMock()
mock_head_resp.__enter__.return_value.headers.get.return_value = "sha256:abc123"
with patch("devx.tools.build_image.urllib.request.urlopen") as mock_urlopen:
mock_urlopen.side_effect = [
mock_head_resp,
urllib.error.URLError("network down"),
]
assert delete_remote_manifest("git.example.com", "ci-base", "latest", "u", "t") is True
class TestSortVersions:
def test_sort_by_created_at_desc(self) -> None:
@@ -588,11 +796,20 @@ class TestCLIBuildImage:
"devx.tools.build_image.subprocess.run",
side_effect=[login_result, build_result, push_result],
):
result = runner.invoke(
build_image.main,
["--dockerfile", str(dockerfile), "--name", "ci-base", "--push", "--registry", "git.example.com"],
)
assert result.exit_code != 0
with patch("devx.tools.build_image.delete_remote_manifest", return_value=True):
result = runner.invoke(
build_image.main,
[
"--dockerfile",
str(dockerfile),
"--name",
"ci-base",
"--push",
"--registry",
"git.example.com",
],
)
assert result.exit_code != 0
class TestCLICleanImages:
+175
View File
@@ -0,0 +1,175 @@
"""Unit tests for devx.ci.check_pr_size."""
from unittest.mock import MagicMock, patch
from click.testing import CliRunner
from devx.ci.check_pr_size import (
check_size,
cli,
get_diff_stats,
has_refactoring_label,
is_excluded,
)
class TestIsExcluded:
def test_excludes_changelog(self) -> None:
assert is_excluded("CHANGELOG.md", ["CHANGELOG.md"])
def test_excludes_svg_glob(self) -> None:
assert is_excluded("docs/badges/coverage.svg", ["*.svg"])
def test_does_not_exclude_source(self) -> None:
assert not is_excluded("src/devx/ci/check_pr_size.py", ["CHANGELOG.md", "*.svg"])
def test_excludes_readme(self) -> None:
assert is_excluded("README.md", ["README.md"])
def test_excludes_plans_glob(self) -> None:
assert is_excluded("docs/plans/sso-bridge-full-extraction.md", ["docs/plans/*"])
def test_does_not_exclude_specs(self) -> None:
assert not is_excluded("docs/specs/DEVX-165.md", ["docs/plans/*"])
class TestCheckSize:
def test_under_limits_passes(self) -> None:
stats = [("src/main.py", 100, 50), ("tests/test_main.py", 80, 20)]
ok, detail = check_size(stats, max_lines=500, max_files=10, excluded_patterns=[])
assert ok is True
assert "250" in detail # 100+50+80+20
def test_over_lines_fails(self) -> None:
stats = [("src/main.py", 300, 300)]
ok, detail = check_size(stats, max_lines=500, max_files=10, excluded_patterns=[])
assert ok is False
assert "600" in detail
def test_over_files_fails(self) -> None:
stats = [(f"src/file{i}.py", 10, 5) for i in range(15)]
ok, detail = check_size(stats, max_lines=500, max_files=10, excluded_patterns=[])
assert ok is False
assert "15" in detail
def test_excluded_files_not_counted(self) -> None:
stats = [("CHANGELOG.md", 500, 500), ("src/main.py", 10, 5)]
ok, detail = check_size(stats, max_lines=500, max_files=10, excluded_patterns=["CHANGELOG.md"])
assert ok is True
assert "15" in detail # only 10+5
def test_empty_stats_passes(self) -> None:
ok, detail = check_size([], max_lines=500, max_files=10, excluded_patterns=[])
assert ok is True
class TestGetDiffStats:
@patch("devx.ci.check_pr_size.subprocess.run")
def test_parses_numstat_output(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(
returncode=0,
stdout="10\t5\tsrc/main.py\n20\t10\ttests/test_main.py\n",
stderr="",
)
stats = get_diff_stats("origin/master", "HEAD")
assert len(stats) == 2
assert stats[0] == ("src/main.py", 10, 5)
assert stats[1] == ("tests/test_main.py", 20, 10)
@patch("devx.ci.check_pr_size.subprocess.run")
def test_handles_binary_files(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(
returncode=0,
stdout="-\t-\timage.png\n",
stderr="",
)
stats = get_diff_stats("origin/master", "HEAD")
assert len(stats) == 1
assert stats[0] == ("image.png", 0, 0)
@patch("devx.ci.check_pr_size.subprocess.run")
def test_empty_output(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
stats = get_diff_stats("origin/master", "HEAD")
assert stats == []
@patch("devx.ci.check_pr_size.subprocess.run")
def test_git_diff_failure_raises(self, mock_run: MagicMock) -> None:
import pytest
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="fatal: bad ref")
with pytest.raises(Exception, match="git diff|bad ref"):
get_diff_stats("origin/master", "HEAD")
@patch("devx.ci.check_pr_size.subprocess.run")
def test_malformed_line_skipped(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(
returncode=0,
stdout="not_a_valid_line\n10\t5\tsrc/main.py\n",
stderr="",
)
stats = get_diff_stats("origin/master", "HEAD")
assert len(stats) == 1
assert stats[0] == ("src/main.py", 10, 5)
class TestCli:
@patch("devx.ci.check_pr_size.subprocess.run")
def test_passes_when_small(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(
returncode=0,
stdout="10\t5\tsrc/main.py\n",
stderr="",
)
runner = CliRunner()
result = runner.invoke(cli, ["--base", "origin/master", "--head", "HEAD"])
assert result.exit_code == 0
assert "OK" in result.output
@patch("devx.ci.check_pr_size.subprocess.run")
def test_fails_when_too_large(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(
returncode=0,
stdout="300\t300\tsrc/main.py\n",
stderr="",
)
runner = CliRunner()
result = runner.invoke(cli, ["--base", "origin/master", "--head", "HEAD", "--max-lines", "500"])
assert result.exit_code != 0
assert "600" in result.output
@patch("devx.ci.check_pr_size.subprocess.run")
@patch("devx.ci.check_pr_size.has_refactoring_label", return_value=True)
def test_bypasses_with_refactoring_label(self, mock_label: MagicMock, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(
returncode=0,
stdout="300\t300\tsrc/main.py\n",
stderr="",
)
runner = CliRunner()
result = runner.invoke(
cli,
["--base", "origin/master", "--head", "HEAD", "--repo", "owner/repo", "--pr-number", "42"],
)
assert result.exit_code == 0
assert "bypassed" in result.output.lower()
class TestHasRefactoringLabel:
@patch("devx.ci.check_pr_size.GiteaClient")
@patch("devx.ci.check_pr_size.get_ci_token", return_value="fake-token")
def test_returns_true_when_label_present(self, mock_token: MagicMock, mock_client_cls: MagicMock) -> None:
mock_client = mock_client_cls.return_value
mock_client.get_pr.return_value = {"labels": [{"name": "refactoring"}, {"name": "bug"}]}
assert has_refactoring_label("owner/repo", 42) is True
@patch("devx.ci.check_pr_size.GiteaClient")
@patch("devx.ci.check_pr_size.get_ci_token", return_value="fake-token")
def test_returns_false_when_label_absent(self, mock_token: MagicMock, mock_client_cls: MagicMock) -> None:
mock_client = mock_client_cls.return_value
mock_client.get_pr.return_value = {"labels": [{"name": "bug"}]}
assert has_refactoring_label("owner/repo", 42) is False
@patch("devx.ci.check_pr_size.get_ci_token", side_effect=Exception("no token"))
def test_returns_false_on_error(self, mock_token: MagicMock) -> None:
assert has_refactoring_label("owner/repo", 42) is False
-7
View File
@@ -107,13 +107,6 @@ class TestCiCommands:
assert result.exit_code == 0
mock_run.assert_called_once_with("devx.ci.post_merge", ["DEVX-1"])
@patch("devx.cli._run_module")
def test_ci_pr_review(self, mock_run: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(cli, ["ci", "pr-review", "42"])
assert result.exit_code == 0
mock_run.assert_called_once_with("devx.ci.pr_review", ["42"])
@patch("devx.cli._run_module")
def test_ci_publish(self, mock_run: MagicMock) -> None:
runner = CliRunner()
+1
View File
@@ -38,6 +38,7 @@ class TestConfigConstants:
def test_conventional_re(self) -> None:
assert CONVENTIONAL_RE.match("feat: add feature")
assert CONVENTIONAL_RE.match("fix(scope): bug fix")
assert CONVENTIONAL_RE.match("deps: bump devx from v0.50.2 to v0.51.0")
assert not CONVENTIONAL_RE.match("random message")
assert not CONVENTIONAL_RE.match("feat:")
assert not CONVENTIONAL_RE.match("BREAKING CHANGE: something")
+182
View File
@@ -0,0 +1,182 @@
"""Unit tests for devx.ci.create_dependency_pr."""
from pathlib import Path
from unittest.mock import MagicMock, patch
import click
from click.testing import CliRunner
from devx.ci.create_dependency_pr import (
cli,
create_vikunja_task,
find_existing_pr,
find_pinned_version,
update_pinned_version,
)
class TestFindPinnedVersion:
def test_finds_pip_git_pin(self, tmp_path: Path) -> None:
content = "grm @ git+https://git.example.com/repo.git@v0.5.1"
path = tmp_path / "pyproject.toml"
path.write_text(content)
version = find_pinned_version("grm", str(path))
assert version == "0.5.1"
def test_finds_pyproject_pin(self, tmp_path: Path) -> None:
content = 'grm = "0.5.1"'
path = tmp_path / "pyproject.toml"
path.write_text(content)
version = find_pinned_version("grm", str(path))
assert version == "0.5.1"
def test_finds_ansible_var_pin(self, tmp_path: Path) -> None:
content = 'grm_version: "0.5.1"'
path = tmp_path / "images.yml"
path.write_text(content)
version = find_pinned_version("grm", str(path))
assert version == "0.5.1"
def test_finds_image_version_pin(self, tmp_path: Path) -> None:
content = 'sso_bridge_image_version: "1.2.3"'
path = tmp_path / "images.yml"
path.write_text(content)
version = find_pinned_version("sso_bridge", str(path))
assert version == "1.2.3"
def test_returns_none_when_not_found(self, tmp_path: Path) -> None:
path = tmp_path / "pyproject.toml"
path.write_text('other = "1.0.0"')
assert find_pinned_version("grm", str(path)) is None
def test_returns_none_when_file_missing(self, tmp_path: Path) -> None:
assert find_pinned_version("grm", str(tmp_path / "nonexistent.toml")) is None
class TestUpdatePinnedVersion:
def test_updates_pip_git_pin(self, tmp_path: Path) -> None:
content = "grm @ git+https://git.example.com/repo.git@v0.5.1"
path = tmp_path / "pyproject.toml"
path.write_text(content)
changed = update_pinned_version(str(path), "grm", "0.5.1", "0.5.2")
assert changed is True
assert "0.5.2" in path.read_text()
assert "0.5.1" not in path.read_text()
def test_updates_pyproject_pin(self, tmp_path: Path) -> None:
content = 'grm = "0.5.1"'
path = tmp_path / "pyproject.toml"
path.write_text(content)
changed = update_pinned_version(str(path), "grm", "0.5.1", "0.5.2")
assert changed is True
assert 'grm = "0.5.2"' in path.read_text()
def test_no_change_when_version_not_found(self, tmp_path: Path) -> None:
content = 'other = "1.0.0"'
path = tmp_path / "pyproject.toml"
path.write_text(content)
changed = update_pinned_version(str(path), "grm", "0.5.1", "0.5.2")
assert changed is False
def test_no_change_when_file_missing(self, tmp_path: Path) -> None:
changed = update_pinned_version(str(tmp_path / "nonexistent"), "grm", "0.5.1", "0.5.2")
assert changed is False
class TestFindExistingPr:
@patch("devx.tools.create_pr.GiteaClient")
def test_returns_pr_when_found(self, mock_client_cls: MagicMock) -> None:
mock_client = mock_client_cls.return_value
mock_client.list_prs.return_value = [
{"head": {"ref": "deps/grm-0.5.2"}, "number": 42},
{"head": {"ref": "other-branch"}, "number": 43},
]
result = find_existing_pr(mock_client, "deps/grm-0.5.2")
assert result is not None
assert result["number"] == 42
@patch("devx.tools.create_pr.GiteaClient")
def test_returns_none_when_not_found(self, mock_client_cls: MagicMock) -> None:
mock_client = mock_client_cls.return_value
mock_client.list_prs.return_value = []
result = find_existing_pr(mock_client, "deps/grm-0.5.2")
assert result is None
class TestCli:
@patch("devx.ci.create_dependency_pr.find_pinned_version")
@patch("devx.ci.create_dependency_pr.get_ci_token")
def test_same_version_no_pr(self, mock_token: MagicMock, mock_find: MagicMock) -> None:
mock_token.return_value = "fake-token"
mock_find.return_value = "0.5.2"
runner = CliRunner()
result = runner.invoke(
cli,
[
"--package",
"grm",
"--new-version",
"0.5.2",
"--source-repo",
"oblachno/grm",
],
)
assert result.exit_code == 0
assert "no pr needed" in result.output.lower()
@patch("devx.ci.create_dependency_pr.find_pinned_version")
@patch("devx.ci.create_dependency_pr.get_ci_token")
def test_dry_run(self, mock_token: MagicMock, mock_find: MagicMock) -> None:
mock_token.return_value = "fake-token"
mock_find.return_value = "0.5.1"
runner = CliRunner()
result = runner.invoke(
cli,
[
"--package",
"grm",
"--new-version",
"0.5.2",
"--source-repo",
"oblachno/grm",
"--dry-run",
],
)
assert result.exit_code == 0
assert "DRY RUN" in result.output
@patch("devx.ci.create_dependency_pr.find_pinned_version")
@patch("devx.ci.create_dependency_pr.get_ci_token")
def test_version_not_found_fails(self, mock_token: MagicMock, mock_find: MagicMock) -> None:
mock_token.return_value = "fake-token"
mock_find.return_value = None
runner = CliRunner()
result = runner.invoke(
cli,
[
"--package",
"nonexistent",
"--new-version",
"1.0.0",
"--source-repo",
"oblachno/test",
],
)
assert result.exit_code != 0
class TestCreateVikunjaTask:
def test_returns_none_when_no_token(self) -> None:
with patch("devx.ci.create_dependency_pr.get_vikunja_token", side_effect=click.ClickException("no token")):
result = create_vikunja_task("Test", "desc")
assert result is None
def test_returns_identifier_on_success(self) -> None:
with (
patch("devx.ci.create_dependency_pr.get_vikunja_token", return_value="fake-token"),
patch("devx.api_clients.VikunjaClient") as mock_client_cls,
):
mock_client = mock_client_cls.return_value
mock_client.create_task.return_value = {"identifier": "OBL-INFRA-999"}
result = create_vikunja_task("Test", "desc")
assert result == "OBL-INFRA-999"
+82
View File
@@ -0,0 +1,82 @@
"""Unit tests for devx.ci.fast_molecule."""
from pathlib import Path
from unittest.mock import MagicMock, patch
from click.testing import CliRunner
from devx.ci.fast_molecule import (
build_molecule_commands,
cli,
get_molecule_scenarios,
)
class TestGetMoleculeScenarios:
def test_finds_scenarios(self, tmp_path: Path) -> None:
roles_dir = tmp_path / "ansible" / "roles" / "myrole" / "molecule"
roles_dir.mkdir(parents=True)
(roles_dir / "default").mkdir()
(roles_dir / "default" / "molecule.yml").write_text("name: default")
(roles_dir / "full").mkdir()
(roles_dir / "full" / "molecule.yml").write_text("name: full")
(roles_dir / "no_scenario").mkdir() # No molecule.yml
scenarios = get_molecule_scenarios("myrole", str(tmp_path / "ansible" / "roles"))
assert sorted(scenarios) == ["default", "full"]
def test_returns_empty_when_no_molecule_dir(self, tmp_path: Path) -> None:
scenarios = get_molecule_scenarios("nonexistent", str(tmp_path / "ansible" / "roles"))
assert scenarios == []
class TestBuildMoleculeCommands:
def test_builds_commands_for_roles(self, tmp_path: Path) -> None:
roles_dir = tmp_path / "ansible" / "roles"
for role in ["role_a", "role_b"]:
mol_dir = roles_dir / role / "molecule" / "default"
mol_dir.mkdir(parents=True)
(mol_dir / "molecule.yml").write_text("name: default")
commands = build_molecule_commands({"role_a", "role_b"}, str(roles_dir))
assert len(commands) == 2
assert all("molecule test -s default" in c for c in commands)
assert all("--destroy=never" in c for c in commands)
assert all("ubuntu-2604" in c for c in commands)
def test_empty_when_no_scenarios(self, tmp_path: Path) -> None:
commands = build_molecule_commands({"nonexistent"}, str(tmp_path / "ansible" / "roles"))
assert commands == []
def test_empty_when_no_roles(self) -> None:
assert build_molecule_commands(set()) == []
class TestCli:
@patch("devx.ci.fast_molecule.get_changed_files")
def test_no_changes(self, mock_get: MagicMock) -> None:
mock_get.return_value = []
runner = CliRunner()
result = runner.invoke(cli, ["--base", "origin/master", "--head", "HEAD"])
assert result.exit_code == 0
assert "No files changed" in result.output
@patch("devx.ci.fast_molecule.detect_changed_roles")
@patch("devx.ci.fast_molecule.get_changed_files")
def test_no_ansible_changes(self, mock_get: MagicMock, mock_detect: MagicMock) -> None:
mock_get.return_value = ["src/main.py", "README.md"]
mock_detect.return_value = set()
runner = CliRunner()
result = runner.invoke(cli, ["--base", "origin/master", "--head", "HEAD"])
assert result.exit_code == 0
assert "No Ansible roles changed" in result.output
@patch("devx.ci.fast_molecule.detect_changed_roles")
@patch("devx.ci.fast_molecule.get_changed_files")
def test_detects_changed_roles(self, mock_get: MagicMock, mock_detect: MagicMock) -> None:
mock_get.return_value = ["ansible/roles/sso_bridge/tasks/main.yml"]
mock_detect.return_value = {"sso_bridge"}
runner = CliRunner()
result = runner.invoke(cli, ["--base", "origin/master", "--head", "HEAD"])
assert result.exit_code == 0
assert "sso_bridge" in result.output
+117
View File
@@ -0,0 +1,117 @@
"""Unit tests for devx.ci.nightly_gate."""
from unittest.mock import MagicMock, patch
from click.testing import CliRunner
from devx.ci.nightly_gate import cli, get_nightly_status, set_nightly_status
class TestGetNightlyStatus:
@patch("devx.ci.nightly_gate.GiteaClient")
def test_returns_value_when_set(self, mock_client_cls: MagicMock) -> None:
mock_client = mock_client_cls.return_value
mock_client.get_repo_variable.return_value = "passed:12345"
result = get_nightly_status(mock_client)
assert result == "passed:12345"
@patch("devx.ci.nightly_gate.GiteaClient")
def test_returns_empty_when_not_set(self, mock_client_cls: MagicMock) -> None:
mock_client = mock_client_cls.return_value
mock_client.get_repo_variable.return_value = None
result = get_nightly_status(mock_client)
assert result == ""
class TestSetNightlyStatus:
@patch("devx.ci.nightly_gate.GiteaClient")
def test_sets_passed(self, mock_client_cls: MagicMock) -> None:
mock_client = mock_client_cls.return_value
set_nightly_status(mock_client, "passed:12345")
mock_client.set_repo_variable.assert_called_once_with("NIGHTLY_STATUS", "passed:12345")
@patch("devx.ci.nightly_gate.GiteaClient")
def test_sets_failed(self, mock_client_cls: MagicMock) -> None:
mock_client = mock_client_cls.return_value
set_nightly_status(mock_client, "failed:99999")
mock_client.set_repo_variable.assert_called_once_with("NIGHTLY_STATUS", "failed:99999")
class TestCli:
@patch("devx.ci.nightly_gate.GiteaClient")
@patch("devx.ci.nightly_gate.get_ci_token")
def test_check_bootstrap_allows_deploy(self, mock_token: MagicMock, mock_client_cls: MagicMock) -> None:
mock_token.return_value = "fake-token"
mock_client = mock_client_cls.return_value
mock_client.get_repo_variable.return_value = None
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--action", "check"])
assert result.exit_code == 0
assert "bootstrap" in result.output.lower()
@patch("devx.ci.nightly_gate.GiteaClient")
@patch("devx.ci.nightly_gate.get_ci_token")
def test_check_passed_allows_deploy(self, mock_token: MagicMock, mock_client_cls: MagicMock) -> None:
mock_token.return_value = "fake-token"
mock_client = mock_client_cls.return_value
mock_client.get_repo_variable.return_value = "passed:12345"
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--action", "check"])
assert result.exit_code == 0
assert "passed" in result.output.lower()
@patch("devx.ci.nightly_gate.GiteaClient")
@patch("devx.ci.nightly_gate.get_ci_token")
def test_check_failed_blocks_deploy(self, mock_token: MagicMock, mock_client_cls: MagicMock) -> None:
mock_token.return_value = "fake-token"
mock_client = mock_client_cls.return_value
mock_client.get_repo_variable.return_value = "failed:99999"
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--action", "check"])
assert result.exit_code != 0
assert "blocked" in result.output.lower()
@patch("devx.ci.nightly_gate.GiteaClient")
@patch("devx.ci.nightly_gate.get_ci_token")
def test_check_unknown_status_blocks_deploy(self, mock_token: MagicMock, mock_client_cls: MagicMock) -> None:
mock_token.return_value = "fake-token"
mock_client = mock_client_cls.return_value
mock_client.get_repo_variable.return_value = "garbage-value"
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--action", "check"])
assert result.exit_code != 0
assert "fail closed" in result.output.lower()
@patch("devx.ci.nightly_gate.GiteaClient")
@patch("devx.ci.nightly_gate.get_ci_token")
def test_set_passed(self, mock_token: MagicMock, mock_client_cls: MagicMock) -> None:
mock_token.return_value = "fake-token"
mock_client = mock_client_cls.return_value
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--action", "set-passed", "--run-id", "12345"])
assert result.exit_code == 0
mock_client.set_repo_variable.assert_called_once_with("NIGHTLY_STATUS", "passed:12345")
@patch("devx.ci.nightly_gate.GiteaClient")
@patch("devx.ci.nightly_gate.get_ci_token")
def test_set_failed(self, mock_token: MagicMock, mock_client_cls: MagicMock) -> None:
mock_token.return_value = "fake-token"
mock_client = mock_client_cls.return_value
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--action", "set-failed", "--run-id", "99999"])
assert result.exit_code == 0
mock_client.set_repo_variable.assert_called_once_with("NIGHTLY_STATUS", "failed:99999")
@patch("devx.ci.nightly_gate.get_ci_token")
def test_fails_without_token(self, mock_token: MagicMock) -> None:
import click as click_mod
mock_token.side_effect = click_mod.ClickException("No token")
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "oblachno/infra", "--action", "check"])
assert result.exit_code != 0
def test_fails_with_invalid_repo(self) -> None:
runner = CliRunner()
result = runner.invoke(cli, ["--repo", "invalid", "--action", "check"])
assert result.exit_code != 0
-1022
View File
@@ -1,1022 +0,0 @@
"""Unit tests for scripts/ci/pr_review.py."""
from unittest.mock import MagicMock, patch
import pytest
from click.testing import CliRunner
from devx.ci.pr_review import (
ReviewResult,
build_review_body,
check_architecture_compliance,
check_best_practices,
check_commit_conventions,
check_documentation,
check_function_length,
check_i18n,
check_resource_management,
check_security,
check_test_coverage,
is_python_file,
is_workflow_only,
main,
post_review,
run_review,
)
from devx.exceptions import APIError
class TestIsPythonFile:
def test_python_file_in_src(self) -> None:
assert is_python_file("src/devx/cli.py") is True
def test_python_file_in_scripts(self) -> None:
assert is_python_file("scripts/ci/release.py") is True
def test_test_file_excluded(self) -> None:
assert is_python_file("tests/unit/test_cli.py") is False
def test_non_python_file(self) -> None:
assert is_python_file("README.md") is False
def test_yaml_file(self) -> None:
assert is_python_file(".gitea/workflows/ci.yml") is False
class TestIsWorkflowOnly:
def test_yaml_is_workflow(self) -> None:
assert is_workflow_only(".gitea/workflows/ci.yml") is True
def test_md_is_workflow(self) -> None:
assert is_workflow_only("README.md") is True
def test_python_is_not_workflow(self) -> None:
assert is_workflow_only("src/devx/cli.py") is False
def test_ansible_is_workflow(self) -> None:
assert is_workflow_only("ansible/tasks/main.yml") is True
class TestReviewResult:
def test_empty_result_has_no_issues(self) -> None:
result = ReviewResult()
assert result.has_issues is False
def test_add_issue_makes_has_issues_true(self) -> None:
result = ReviewResult()
result.add_issue("src/foo.py", 10, "bad code")
assert result.has_issues is True
assert len(result.issues) == 1
assert result.issues[0]["path"] == "src/foo.py"
assert result.issues[0]["new_position"] == 10
def test_add_summary(self) -> None:
result = ReviewResult()
result.add_summary("all good")
assert "all good" in result.summary
class TestCheckArchitectureCompliance:
def test_subprocess_in_cli_triggers_issue(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/cli.py",
"patch": "@@ -10,3 +10,4 @@\n def foo():\n pass\n+ subprocess.run(['ls'])\n",
}
]
check_architecture_compliance(files, result)
assert result.has_issues
assert "subprocess" in result.issues[0]["body"].lower()
def test_subprocess_in_other_file_ok(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/executor.py",
"patch": "@@ -10,3 +10,4 @@\n def foo():\n pass\n+ subprocess.run(['ls'])\n",
}
]
check_architecture_compliance(files, result)
assert not result.has_issues
def test_no_changes_adds_ok_summary(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": ""}]
check_architecture_compliance(files, result)
assert any("Architecture compliance: OK" in s for s in result.summary)
def test_non_python_file_skipped(self) -> None:
result = ReviewResult()
files = [{"filename": "README.md", "patch": "@@ -1,1 +1,2 @@\n+subprocess.run(['ls'])\n"}]
check_architecture_compliance(files, result)
assert not result.has_issues
def test_empty_patch_skipped(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": ""}]
check_architecture_compliance(files, result)
assert not result.has_issues
def test_os_system_in_cli_triggers_issue(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/cli.py",
"patch": "@@ -10,3 +10,4 @@\n def foo():\n pass\n+ os.system('ls')\n",
}
]
check_architecture_compliance(files, result)
assert result.has_issues
assert "os.system" in result.issues[0]["body"]
def test_malformed_hunk_header_no_line_number(self) -> None:
"""A @@ header without a +N line number is handled gracefully."""
result = ReviewResult()
files = [
{
"filename": "src/devx/cli.py",
"patch": "@@ -1,2 @@\n+ subprocess.run(['ls'])\n",
}
]
check_architecture_compliance(files, result)
assert result.has_issues
class TestCheckBestPractices:
def test_print_triggers_warning(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/cli.py",
"patch": "@@ -10,3 +10,4 @@\n def foo():\n pass\n+ print('hello')\n",
}
]
check_best_practices(files, result)
assert result.has_issues
assert "print()" in result.issues[0]["body"]
def test_bare_except_triggers_warning(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/runner_manager.py",
"patch": "@@ -10,3 +10,4 @@\n def foo():\n pass\n+ except:\n pass\n",
}
]
check_best_practices(files, result)
assert result.has_issues
assert "bare except" in result.issues[0]["body"]
def test_todo_triggers_warning(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/cli.py",
"patch": "@@ -10,3 +10,4 @@\n def foo():\n pass\n+ # TODO: fix this\n",
}
]
check_best_practices(files, result)
assert result.has_issues
assert "TODO" in result.issues[0]["body"]
def test_clean_code_no_issues(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/cli.py",
"patch": "@@ -10,3 +10,4 @@\n def foo():\n pass\n+ click.echo('hello')\n",
}
]
check_best_practices(files, result)
assert not result.has_issues
def test_empty_patch_skipped(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": ""}]
check_best_practices(files, result)
assert not result.has_issues
def test_non_python_file_skipped(self) -> None:
result = ReviewResult()
files = [{"filename": "README.md", "patch": "@@ -1,1 +1,2 @@\n+print('hello')\n"}]
check_best_practices(files, result)
assert not result.has_issues
def test_malformed_hunk_header_no_line_number(self) -> None:
"""A @@ header without a +N line number is handled gracefully."""
result = ReviewResult()
files = [
{
"filename": "src/devx/cli.py",
"patch": "@@ -1,2 @@\n+ print('hello')\n",
}
]
check_best_practices(files, result)
assert result.has_issues
assert "print()" in result.issues[0]["body"]
class TestCheckSecurity:
def test_hardcoded_secret_triggers_error(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/config.py",
"patch": "@@ -10,3 +10,4 @@\n def foo():\n pass\n+ token = 'abc123secrettoken456'\n",
}
]
check_security(files, result)
assert result.has_issues
assert "secret" in result.issues[0]["body"].lower()
def test_example_token_not_flagged(self) -> None:
result = ReviewResult()
files = [
{
"filename": ".env.example",
"patch": "@@ -1,1 +1,2 @@\n+token = your-example-token\n",
}
]
check_security(files, result)
assert not result.has_issues
def test_shell_true_triggers_warning(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/executor.py",
"patch": "@@ -10,3 +10,4 @@\n def foo():\n pass\n+ subprocess.run('ls', shell=True)\n",
}
]
check_best_practices(files, result)
assert result.has_issues
assert "shell=True" in result.issues[0]["body"]
def test_empty_patch_skipped(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/config.py", "patch": ""}]
check_security(files, result)
assert not result.has_issues
def test_non_python_file_skipped(self) -> None:
result = ReviewResult()
files = [{"filename": "docs/config.md", "patch": "@@ -1,1 +1,2 @@\n+token = 'abc123secrettoken456'\n"}]
check_security(files, result)
assert not result.has_issues
def test_malformed_hunk_header_no_line_number(self) -> None:
"""A @@ header without a +N line number is handled gracefully."""
result = ReviewResult()
files = [
{
"filename": "src/devx/config.py",
"patch": "@@ -1,2 @@\n+ token = 'abc123secrettoken456'\n",
}
]
check_security(files, result)
assert result.has_issues
assert "secret" in result.issues[0]["body"].lower()
class TestCheckI18n:
def test_raw_string_in_echo_triggers_warning(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": '@@ -1,1 +1,1 @@\n+click.echo("Hello world")\n'}]
check_i18n(files, result)
assert result.has_issues
assert any("i18n" in i["body"] for i in result.issues)
def test_translated_string_no_warning(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": '@@ -1,1 +1,1 @@\n+click.echo(_("Hello world"))\n'}]
check_i18n(files, result)
assert not result.has_issues
def test_fstring_in_echo_triggers_warning(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": '@@ -1,1 +1,1 @@\n+click.echo(f"Hello {name}")\n'}]
check_i18n(files, result)
assert result.has_issues
def test_raw_exception_triggers_warning(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/cli.py",
"patch": '@@ -1,1 +1,1 @@\n+raise click.ClickException("Something went wrong")\n',
}
]
check_i18n(files, result)
assert result.has_issues
def test_non_src_file_skipped(self) -> None:
result = ReviewResult()
files = [{"filename": "scripts/ci/test.py", "patch": '@@ -1,1 +1,1 @@\n+click.echo("Hello world")\n'}]
check_i18n(files, result)
assert not result.has_issues
def test_comment_skipped(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": '@@ -1,1 +1,1 @@\n+# click.echo("Hello world")\n'}]
check_i18n(files, result)
assert not result.has_issues
def test_empty_patch_skipped(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": ""}]
check_i18n(files, result)
assert not result.has_issues
def test_clean_code_adds_ok_summary(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": '@@ -1,1 +1,1 @@\n+click.echo(_("Hello world"))\n'}]
check_i18n(files, result)
assert any("i18n: OK" in s for s in result.summary)
def test_malformed_hunk_header_no_line_number(self) -> None:
"""A @@ header without a +N line number is handled gracefully."""
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": '@@ -1,2 @@\n+click.echo("Hello world")\n'}]
check_i18n(files, result)
assert result.has_issues
assert any("i18n" in i["body"] for i in result.issues)
class TestCheckResourceManagement:
def test_open_without_with_triggers_warning(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": '@@ -1,1 +1,1 @@\n+f = open("file.txt")\n'}]
check_resource_management(files, result)
assert result.has_issues
assert any("resource" in i["body"].lower() for i in result.issues)
def test_open_with_with_no_warning(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/cli.py",
"patch": '@@ -1,1 +1,1 @@\n+with open("file.txt") as f:\n+ pass\n',
}
]
check_resource_management(files, result)
assert not result.has_issues
def test_popen_without_cleanup_triggers_warning(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/executor.py",
"patch": '@@ -1,1 +1,1 @@\n+proc = subprocess.Popen(["cmd"])\n',
}
]
check_resource_management(files, result)
assert result.has_issues
def test_popen_with_communicate_no_warning(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/executor.py",
"patch": '@@ -1,1 +1,1 @@\n+out, err = subprocess.Popen(["cmd"], stdout=PIPE).communicate()\n',
}
]
check_resource_management(files, result)
assert not result.has_issues
def test_comment_skipped(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": '@@ -1,1 +1,1 @@\n+# f = open("file.txt")\n'}]
check_resource_management(files, result)
assert not result.has_issues
def test_empty_patch_skipped(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": ""}]
check_resource_management(files, result)
assert not result.has_issues
def test_non_python_file_skipped(self) -> None:
result = ReviewResult()
files = [{"filename": "docs/config.md", "patch": '@@ -1,1 +1,1 @@\n+f = open("file.txt")\n'}]
check_resource_management(files, result)
assert not result.has_issues
def test_clean_code_adds_ok_summary(self) -> None:
result = ReviewResult()
files = [
{
"filename": "src/devx/cli.py",
"patch": '@@ -1,1 +1,1 @@\n+with open("file.txt") as f:\n+ data = f.read()\n',
}
]
check_resource_management(files, result)
assert any("Resource management: OK" in s for s in result.summary)
def test_malformed_hunk_header_no_line_number(self) -> None:
"""A @@ header without a +N line number is handled gracefully."""
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": '@@ -1,2 @@\n+f = open("file.txt")\n'}]
check_resource_management(files, result)
assert result.has_issues
assert any("resource" in i["body"].lower() for i in result.issues)
class TestCheckFunctionLength:
def test_long_function_triggers_warning(self) -> None:
result = ReviewResult()
# Create a patch with a function that adds > 50 lines
added_lines = "\n".join(f"+ x = {i}" for i in range(55))
patch = f"@@ -10,3 +10,59 @@\n+def foo():\n+ pass\n{added_lines}\n"
files = [{"filename": "src/devx/cli.py", "patch": patch}]
check_function_length(files, result)
assert result.has_issues
assert "foo" in result.issues[0]["body"]
def test_short_function_no_warning(self) -> None:
result = ReviewResult()
patch = "@@ -10,3 +10,8 @@\n def foo():\n pass\n+ x = 1\n+ y = 2\n+ z = 3\n"
files = [{"filename": "src/devx/cli.py", "patch": patch}]
check_function_length(files, result)
assert not result.has_issues
def test_empty_patch_skipped(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": ""}]
check_function_length(files, result)
assert not result.has_issues
def test_non_python_file_skipped(self) -> None:
result = ReviewResult()
added_lines = "\n".join(f"+ x = {i}" for i in range(55))
patch = f"@@ -10,3 +10,59 @@\n+def foo():\n+ pass\n{added_lines}\n"
files = [{"filename": "README.md", "patch": patch}]
check_function_length(files, result)
assert not result.has_issues
def test_multiple_functions_resets_count(self) -> None:
"""Two short functions back-to-back should not trigger the length warning."""
result = ReviewResult()
patch = "@@ -10,3 +10,15 @@\n+def foo():\n+ x = 1\n+def bar():\n+ y = 2\n"
files = [{"filename": "src/devx/cli.py", "patch": patch}]
check_function_length(files, result)
assert not result.has_issues
def test_long_function_followed_by_new_hunk(self) -> None:
"""Long function followed by @@ header triggers the warning at hunk boundary."""
result = ReviewResult()
added_lines = "\n".join(f"+ x = {i}" for i in range(55))
patch = (
f"@@ -10,3 +10,59 @@\n+def foo():\n+ pass\n{added_lines}\n@@ -100,3 +100,5 @@\n+def bar():\n+ pass\n"
)
files = [{"filename": "src/devx/cli.py", "patch": patch}]
check_function_length(files, result)
assert result.has_issues
assert "foo" in result.issues[0]["body"]
def test_long_function_followed_by_new_def(self) -> None:
"""Long function followed by another def triggers the warning at def boundary."""
result = ReviewResult()
added_lines = "\n".join(f"+ x = {i}" for i in range(55))
patch = f"@@ -10,3 +10,60 @@\n+def foo():\n+ pass\n{added_lines}\n+def bar():\n+ pass\n"
files = [{"filename": "src/devx/cli.py", "patch": patch}]
check_function_length(files, result)
assert result.has_issues
assert "foo" in result.issues[0]["body"]
def test_malformed_hunk_header_no_line_number(self) -> None:
"""A @@ header without a +N line number is handled gracefully."""
result = ReviewResult()
files = [{"filename": "src/devx/cli.py", "patch": "@@ -1,2 @@\n+def foo():\n+ pass\n"}]
check_function_length(files, result)
assert not result.has_issues
class TestCheckDocumentation:
def test_src_changes_without_docs_warns(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py"}]
check_documentation(files, result)
assert any("WARNING" in s for s in result.summary)
def test_src_changes_with_docs_ok(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py"}, {"filename": "docs/user/cli-commands.md"}]
check_documentation(files, result)
assert any("Documentation: OK" in s for s in result.summary)
def test_ansible_changes_without_docs_warns(self) -> None:
result = ReviewResult()
files = [{"filename": "ansible/roles/gitea-runner/tasks/main.yml"}]
check_documentation(files, result)
assert any("WARNING" in s for s in result.summary)
def test_only_doc_changes_ok(self) -> None:
result = ReviewResult()
files = [{"filename": "README.md"}]
check_documentation(files, result)
assert any("Documentation: OK" in s for s in result.summary)
def test_tofu_changes_without_docs_warns(self) -> None:
result = ReviewResult()
files = [{"filename": "tofu/modules/hetzner-vm/main.tf"}]
check_documentation(files, result)
assert any("WARNING" in s for s in result.summary)
def test_workflow_changes_info(self) -> None:
result = ReviewResult()
files = [{"filename": ".gitea/workflows/ci.yml"}]
check_documentation(files, result)
assert any("INFO" in s for s in result.summary)
def test_todo_in_doc_patch_warns(self) -> None:
result = ReviewResult()
files = [{"filename": "docs/guide.md", "patch": "+TODO: fix this later\n+Some content\n"}]
check_documentation(files, result)
assert any("TODO" in s for s in result.summary)
def test_todo_in_readme_patch_warns(self) -> None:
result = ReviewResult()
files = [{"filename": "README.md", "patch": "+FIXME: broken\n"}]
check_documentation(files, result)
assert any("FIXME" in s for s in result.summary)
def test_no_todo_in_doc_patch_ok(self) -> None:
result = ReviewResult()
files = [{"filename": "docs/guide.md", "patch": "+Some content\n"}]
check_documentation(files, result)
assert not any("TODO" in s for s in result.summary)
class TestCheckTestCoverage:
def test_src_changes_without_tests_warns(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py"}]
check_test_coverage(files, result)
assert any("WARNING" in s for s in result.summary)
def test_src_changes_with_tests_ok(self) -> None:
result = ReviewResult()
files = [{"filename": "src/devx/cli.py"}, {"filename": "tests/unit/test_cli.py"}]
check_test_coverage(files, result)
assert any("Tests: OK" in s for s in result.summary)
def test_only_test_changes_ok(self) -> None:
result = ReviewResult()
files = [{"filename": "tests/unit/test_cli.py"}]
check_test_coverage(files, result)
assert any("Tests: OK" in s for s in result.summary)
class TestBuildReviewBody:
def test_body_contains_summary(self) -> None:
result = ReviewResult()
result.add_summary("- Architecture compliance: OK")
body = build_review_body(result)
assert "Architecture compliance: OK" in body
assert "Automated PR Review" in body
def test_body_contains_issues(self) -> None:
result = ReviewResult()
result.add_issue("src/foo.py", 10, "bad code")
body = build_review_body(result)
assert "1 issue(s) found" in body
assert "src/foo.py:10" in body
assert "bad code" in body
def test_body_contains_no_issues_message(self) -> None:
result = ReviewResult()
body = build_review_body(result)
assert "No issues found" in body
def test_body_contains_auto_merge_note(self) -> None:
"""Review body must mention auto-merge."""
result = ReviewResult()
body = build_review_body(result)
assert "Auto-merge" in body
class TestRunReview:
@patch("devx.ci.pr_review.GiteaClient")
def test_run_review_with_no_files(self, mock_client_class: MagicMock) -> None:
mock_client = mock_client_class.return_value
mock_client.get_pr_files.return_value = []
result = run_review(mock_client, "42")
assert "No files changed" in result.summary[0]
@patch("devx.ci.pr_review.GiteaClient")
def test_run_review_finds_issues(self, mock_client_class: MagicMock) -> None:
mock_client = mock_client_class.return_value
mock_client.get_pr_files.return_value = [
{
"filename": "src/devx/cli.py",
"patch": "@@ -10,3 +10,4 @@\n def foo():\n pass\n+ print('hello')\n",
}
]
mock_client.get_pr_commits.return_value = [{"commit": {"message": "fix: resolve print issue"}}]
result = run_review(mock_client, "42")
assert result.has_issues
def test_run_review_handles_api_error(self) -> None:
client = MagicMock()
client.get_pr_files.side_effect = APIError(404, "Not found")
result = run_review(client, "42")
assert any("ERROR" in s for s in result.summary)
class TestCheckCommitConventions:
def test_conventional_commit_found(self) -> None:
"""Should report OK when at least one commit is conventional."""
client = MagicMock()
client.get_pr_commits.return_value = [
{"commit": {"message": "fix: resolve bug\n\nDetails"}},
{"commit": {"message": "wip: testing"}},
]
result = ReviewResult()
check_commit_conventions(client, "42", result)
assert any("OK" in s for s in result.summary)
def test_no_conventional_commit(self) -> None:
"""Should warn when no commits are conventional."""
client = MagicMock()
client.get_pr_commits.return_value = [
{"commit": {"message": "updated stuff"}},
{"commit": {"message": "wip: testing"}},
]
result = ReviewResult()
check_commit_conventions(client, "42", result)
assert any("WARNING" in s for s in result.summary)
def test_merge_commits_excluded(self) -> None:
"""Merge commits should be excluded from the check."""
client = MagicMock()
client.get_pr_commits.return_value = [
{"commit": {"message": "Merge branch 'feature' into master"}},
{"commit": {"message": "fix: resolve bug"}},
]
result = ReviewResult()
check_commit_conventions(client, "42", result)
assert any("OK" in s for s in result.summary)
def test_all_merges_and_reverts(self) -> None:
"""Should report OK when all commits are merges/reverts."""
client = MagicMock()
client.get_pr_commits.return_value = [
{"commit": {"message": "Merge branch 'feature' into master"}},
{"commit": {"message": "Revert: bad commit"}},
]
result = ReviewResult()
check_commit_conventions(client, "42", result)
assert any("merges/reverts" in s for s in result.summary)
def test_no_commits(self) -> None:
"""Should report OK when there are no commits."""
client = MagicMock()
client.get_pr_commits.return_value = []
result = ReviewResult()
check_commit_conventions(client, "42", result)
assert any("no commits" in s for s in result.summary)
def test_api_error(self) -> None:
"""Should report ERROR when API call fails."""
client = MagicMock()
client.get_pr_commits.side_effect = APIError(500, "server error")
result = ReviewResult()
check_commit_conventions(client, "42", result)
assert any("ERROR" in s for s in result.summary)
class TestPostReview:
def test_post_review_with_issues(self) -> None:
client = MagicMock()
result = ReviewResult()
result.add_issue("src/foo.py", 10, "bad code")
post_review(client, "42", result)
client.create_review.assert_called_once()
call_args = client.create_review.call_args
assert call_args[1]["event"] == "REQUEST_CHANGES"
assert call_args[1]["comments"] == result.issues
def test_post_review_without_issues_uses_comment_not_approve(self) -> None:
"""Automated review posts COMMENT, not APPROVE (self-approval not allowed)."""
client = MagicMock()
result = ReviewResult()
post_review(client, "42", result)
client.create_review.assert_called_once()
call_args = client.create_review.call_args
assert call_args[1]["event"] == "COMMENT"
assert call_args[1]["comments"] == []
class TestMain:
@patch("devx.ci.pr_review.run_review")
@patch("devx.ci.pr_review.GiteaClient")
def test_dry_run_does_not_post(self, mock_client_class: MagicMock, mock_run: MagicMock) -> None:
mock_run.return_value = ReviewResult()
runner = CliRunner()
result = runner.invoke(main, ["42", "my-org/my-repo", "--dry-run"], env={"CI_GITEA_TOKEN": "fake"})
assert result.exit_code == 0
assert "[dry-run]" in result.output
mock_client_class.return_value.create_review.assert_not_called()
@patch("devx.ci.pr_review.run_review")
@patch("devx.ci.pr_review.GiteaClient")
def test_post_review_on_success(self, mock_client_class: MagicMock, mock_run: MagicMock) -> None:
mock_run.return_value = ReviewResult()
mock_client_class.return_value.create_review.return_value = {"id": 123}
runner = CliRunner()
result = runner.invoke(main, ["42", "my-org/my-repo"], env={"CI_GITEA_TOKEN": "fake"})
assert result.exit_code == 0
assert "Review #123" in result.output
mock_client_class.return_value.create_review.assert_called_once()
@patch("devx.ci.pr_review.run_review")
@patch("devx.ci.pr_review.GiteaClient")
def test_self_approval_falls_back_to_comment(self, mock_client_class: MagicMock, mock_run: MagicMock) -> None:
"""If REQUEST_CHANGES fails with 422 (self-approval), fall back to COMMENT."""
mock_run.return_value = ReviewResult()
client = mock_client_class.return_value
client.create_review.side_effect = [
APIError(422, "approve your own pull is not allowed"),
{"id": 124},
]
runner = CliRunner()
result = runner.invoke(main, ["42", "my-org/my-repo"], env={"CI_GITEA_TOKEN": "fake"})
assert result.exit_code == 0
assert "Review #124" in result.output
assert client.create_review.call_count == 2
@patch("devx.ci.pr_review.run_review")
@patch("devx.ci.pr_review.GiteaClient")
def test_other_api_error_re_raises(self, mock_client_class: MagicMock, mock_run: MagicMock) -> None:
"""Non-approval API errors should re-raise, not fall back."""
mock_run.return_value = ReviewResult()
client = mock_client_class.return_value
client.create_review.side_effect = APIError(500, "Internal server error")
runner = CliRunner()
result = runner.invoke(main, ["42", "my-org/my-repo"], env={"CI_GITEA_TOKEN": "fake"})
assert result.exit_code != 0
@patch.dict("os.environ", {"CI_GITEA_API_TOKEN": "", "CI_GITEA_TOKEN": ""})
def test_no_token_raises(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["42", "my-org/my-repo"], env={"CI_GITEA_API_TOKEN": "", "CI_GITEA_TOKEN": ""})
assert result.exit_code != 0
assert "CI_GITEA_TOKEN" in result.output
class TestManualReview:
@patch("devx.ci.pr_review.GiteaClient")
def test_approve_success(self, mock_client_class: MagicMock) -> None:
mock_client_class.return_value.create_review.return_value = {"id": 200}
runner = CliRunner()
result = runner.invoke(
main,
[
"42",
"oblachno-oss/devx",
"--event",
"APPROVE",
"--body",
"All 13 REVIEW_CHECKLIST.md categories verified. Architecture: clean. Security: no issues.",
"--checklist-confirmed",
"--checklist-categories",
"1,2,3,4,5,6,7,8,9,10,11,12,13",
],
env={"CI_GITEA_TOKEN": "fake"},
)
assert result.exit_code == 0
assert "Review #200" in result.output
@patch("devx.ci.pr_review.GiteaClient")
def test_approve_without_checklist_confirmed_fails(self, mock_client_class: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(
main,
[
"42",
"oblachno-oss/devx",
"--event",
"APPROVE",
"--body",
"x" * 60,
"--checklist-categories",
"1,2,3,4,5,6,7,8",
],
env={"CI_GITEA_TOKEN": "fake"},
)
assert result.exit_code != 0
assert "checklist-confirmed" in result.output
@patch("devx.ci.pr_review.GiteaClient")
def test_approve_with_too_few_categories_fails(self, mock_client_class: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(
main,
[
"42",
"oblachno-oss/devx",
"--event",
"APPROVE",
"--body",
"x" * 60,
"--checklist-confirmed",
"--checklist-categories",
"1,2,3",
],
env={"CI_GITEA_TOKEN": "fake"},
)
assert result.exit_code != 0
assert "at least 8" in result.output
@patch("devx.ci.pr_review.GiteaClient")
def test_approve_with_short_body_fails(self, mock_client_class: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(
main,
[
"42",
"oblachno-oss/devx",
"--event",
"APPROVE",
"--body",
"LGTM",
"--checklist-confirmed",
"--checklist-categories",
"1,2,3,4,5,6,7,8",
],
env={"CI_GITEA_TOKEN": "fake"},
)
assert result.exit_code != 0
assert "50 characters" in result.output
@patch("devx.ci.pr_review.GiteaClient")
def test_approve_with_invalid_category_fails(self, mock_client_class: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(
main,
[
"42",
"oblachno-oss/devx",
"--event",
"APPROVE",
"--body",
"x" * 60,
"--checklist-confirmed",
"--checklist-categories",
"1,2,abc,4",
],
env={"CI_GITEA_TOKEN": "fake"},
)
assert result.exit_code != 0
assert "Invalid" in result.output
@patch("devx.ci.pr_review.GiteaClient")
def test_request_changes_success(self, mock_client_class: MagicMock) -> None:
mock_client_class.return_value.create_review.return_value = {"id": 201}
runner = CliRunner()
result = runner.invoke(
main,
[
"42",
"oblachno-oss/devx",
"--event",
"REQUEST_CHANGES",
"--body",
"Please fix the architecture issues in the CLI module before merging.",
],
env={"CI_GITEA_TOKEN": "fake"},
)
assert result.exit_code == 0
assert "Review #201" in result.output
@patch("devx.ci.pr_review.GiteaClient")
def test_manual_review_dry_run(self, mock_client_class: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(
main,
["42", "oblachno-oss/devx", "--event", "COMMENT", "--body", "x" * 60, "--dry-run"],
env={"CI_GITEA_TOKEN": "fake"},
)
assert result.exit_code == 0
assert "[dry-run]" in result.output
mock_client_class.return_value.create_review.assert_not_called()
@patch("devx.ci.pr_review.GiteaClient")
def test_manual_review_self_approval_fallback_to_comment(
self, mock_client_class: MagicMock, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Self-approval with no CI token available → fall back to COMMENT."""
monkeypatch.delenv("CI_GITEA_API_TOKEN", raising=False)
monkeypatch.delenv("CI_GITEA_TOKEN", raising=False)
client = mock_client_class.return_value
client.create_review.side_effect = [
APIError(422, "approve your own pull is not allowed"),
{"id": 202},
]
runner = CliRunner()
result = runner.invoke(
main,
[
"42",
"oblachno-oss/devx",
"--event",
"APPROVE",
"--body",
"x" * 60,
"--checklist-confirmed",
"--checklist-categories",
"1,2,3,4,5,6,7,8",
],
env={"REVIEWER_GITEA_API_TOKEN": "fake-reviewer"},
)
assert result.exit_code == 0
assert "Review #202" in result.output
# Without CI_GITEA_API_TOKEN, the fallback is COMMENT
assert "Self-approval not allowed. Posting COMMENT instead." in result.output
assert client.create_review.call_count == 2
assert client.create_review.call_args_list[1].kwargs.get("event") == "COMMENT"
@patch("devx.ci.pr_review.GiteaClient")
def test_manual_review_self_approval_falls_back_to_ci_token(self, mock_client_class: MagicMock) -> None:
"""Self-approval with CI token available → retry APPROVE with CI token (different user)."""
client = mock_client_class.return_value
client.create_review.side_effect = [
APIError(422, "approve your own pull is not allowed"),
{"id": 303},
]
runner = CliRunner()
result = runner.invoke(
main,
[
"42",
"oblachno-oss/devx",
"--event",
"APPROVE",
"--body",
"x" * 60,
"--checklist-confirmed",
"--checklist-categories",
"1,2,3,4,5,6,7,8",
],
env={"REVIEWER_GITEA_API_TOKEN": "fake-reviewer", "CI_GITEA_API_TOKEN": "fake-ci"},
)
assert result.exit_code == 0
assert "Review #303" in result.output
assert "Retrying with CI token" in result.output
# Second call should still be APPROVE (CI token retry)
assert client.create_review.call_count == 2
assert client.create_review.call_args_list[1].kwargs.get("event") == "APPROVE"
@patch("devx.ci.pr_review.GiteaClient")
def test_manual_review_ci_token_also_fails_falls_back_to_comment(self, mock_client_class: MagicMock) -> None:
"""Self-approval + CI token retry also fails → fall back to COMMENT."""
client = mock_client_class.return_value
client.create_review.side_effect = [
APIError(422, "approve your own pull is not allowed"),
APIError(422, "approve your own pull is not allowed"),
{"id": 404},
]
runner = CliRunner()
result = runner.invoke(
main,
[
"42",
"oblachno-oss/devx",
"--event",
"APPROVE",
"--body",
"x" * 60,
"--checklist-confirmed",
"--checklist-categories",
"1,2,3,4,5,6,7,8",
],
env={"REVIEWER_GITEA_API_TOKEN": "fake-reviewer", "CI_GITEA_API_TOKEN": "fake-ci"},
)
assert result.exit_code == 0
assert "Review #404" in result.output
assert "CI token also cannot approve" in result.output
# Third call should be COMMENT (final fallback)
assert client.create_review.call_count == 3
assert client.create_review.call_args_list[2].kwargs.get("event") == "COMMENT"
@patch("devx.ci.pr_review.GiteaClient")
def test_manual_review_other_error_re_raises(self, mock_client_class: MagicMock) -> None:
client = mock_client_class.return_value
client.create_review.side_effect = APIError(500, "Internal server error")
runner = CliRunner()
result = runner.invoke(
main,
["42", "oblachno-oss/devx", "--event", "COMMENT", "--body", "x" * 60],
env={"CI_GITEA_TOKEN": "fake"},
)
assert result.exit_code != 0
def test_main_module_block() -> None:
import devx.ci.pr_review as pr
with patch.object(pr, "main") as mock_main:
with patch.object(pr, "__name__", "__main__"):
pr.main([])
mock_main.assert_called_once_with([])
+357
View File
@@ -0,0 +1,357 @@
"""Structural tests for spec-driven development workflows and skills in devx.
These tests parse devx's own workflow YAML files and assert that the
spec-driven development steps, jobs, and env vars are present and
correctly wired. They also validate that the skills exist in devx's
own .devin/skills/ directory with required sections.
devx must NOT be aware of other repos (infra, grm, sso-bridge). Those
repos consume devx; devx does not test them.
"""
from __future__ import annotations
from pathlib import Path
import pytest
import yaml
# devx repo root
# __file__ = .../devx/tests/unit/test_spec_driven_workflows.py
# parents[2] = .../devx
_DEVX = Path(__file__).resolve().parents[2]
def _load_workflow(filename: str) -> dict:
"""Load a devx workflow YAML file and return parsed dict."""
path = _DEVX / ".gitea" / "workflows" / filename
if not path.exists():
pytest.skip(f"Workflow {filename} not found in devx")
with open(path, encoding="utf-8") as f:
return yaml.safe_load(f)
def _read_skill(skill_name: str) -> str:
"""Read a skill file from devx's .devin/skills/ directory."""
skill_path = _DEVX / ".devin" / "skills" / skill_name / "SKILL.md"
if not skill_path.exists():
pytest.fail(f"SKILL.md not found for {skill_name} in devx")
return skill_path.read_text(encoding="utf-8")
def _get_step_names(job: dict) -> list[str]:
"""Extract step names from a job dict."""
names = []
for step in job.get("steps", []):
if "name" in step:
names.append(step["name"])
return names
def _find_step(job: dict, name_part: str) -> dict | None:
"""Find a step by partial name match."""
for step in job.get("steps", []):
if "name" in step and name_part.lower() in step["name"].lower():
return step
return None
def _get_run_commands(step: dict) -> str:
"""Get the run command from a step."""
return step.get("run", "")
# ============================================================================
# devx ci.yml — spec validation + PR size
# ============================================================================
class TestDevxCiWorkflow:
@pytest.fixture
def workflow(self) -> dict:
return _load_workflow("ci.yml")
def test_validate_job_exists(self, workflow: dict) -> None:
assert "validate" in workflow["jobs"]
def test_has_spec_validation_step(self, workflow: dict) -> None:
steps = _get_step_names(workflow["jobs"]["validate"])
assert any("Validate spec file" in s for s in steps), "validate job must have 'Validate spec file' step"
def test_has_pr_size_check_step(self, workflow: dict) -> None:
steps = _get_step_names(workflow["jobs"]["validate"])
assert any("Check PR size" in s for s in steps), "validate job must have 'Check PR size' step"
def test_spec_validation_uses_correct_module(self, workflow: dict) -> None:
step = _find_step(workflow["jobs"]["validate"], "Validate spec file")
assert step is not None
cmd = _get_run_commands(step)
assert "devx.ci.validate_spec" in cmd
assert "--github-output" in cmd
def test_pr_size_uses_correct_module(self, workflow: dict) -> None:
step = _find_step(workflow["jobs"]["validate"], "Check PR size")
assert step is not None
cmd = _get_run_commands(step)
assert "devx.ci.check_pr_size" in cmd
assert "--github-output" in cmd
def test_spec_validation_sets_task_prefix(self, workflow: dict) -> None:
step = _find_step(workflow["jobs"]["validate"], "Validate spec file")
assert step is not None
env = step.get("env", {})
assert env.get("DEVX_TASK_PREFIX") == "DEVX"
def test_has_auto_merge_job(self, workflow: dict) -> None:
assert "auto-merge" in workflow["jobs"], "ci.yml must have 'auto-merge' job"
# ============================================================================
# devx post-merge.yml — release + publish
# ============================================================================
class TestDevxPostMergeWorkflow:
@pytest.fixture
def workflow(self) -> dict:
return _load_workflow("post-merge.yml")
def test_post_merge_workflow_exists(self, workflow: dict) -> None:
assert workflow is not None
def test_has_release_and_maintain_job(self, workflow: dict) -> None:
assert "release-and-maintain" in workflow["jobs"], "post-merge must have 'release-and-maintain' job"
def test_has_publish_step(self, workflow: dict) -> None:
job = workflow["jobs"].get("release-and-maintain", {})
steps = _get_step_names(job)
assert any("publish" in s.lower() for s in steps), "post-merge must have a publish step"
# ============================================================================
# Skill files — spec-driven-development SKILL.md in devx
# ============================================================================
class TestSpecDrivenDevelopmentSkill:
REQUIRED_SECTIONS = [
"## Overview",
"## Workflow",
"## Spec Template",
"## CI Validation",
"## Acceptance Criteria",
]
def test_skill_exists_in_repo(self) -> None:
skill_path = _DEVX / ".devin" / "skills" / "spec-driven-development" / "SKILL.md"
assert skill_path.exists(), "SKILL.md not found in devx"
def test_skill_has_required_sections(self) -> None:
content = _read_skill("spec-driven-development")
for section in self.REQUIRED_SECTIONS:
assert section in content, f"SKILL.md missing section: {section}"
def test_skill_mentions_req_ids(self) -> None:
content = _read_skill("spec-driven-development")
assert "REQ-" in content, "SKILL.md must mention REQ-ID format"
def test_skill_mentions_pr_size_limit(self) -> None:
content = _read_skill("spec-driven-development")
assert "500" in content, "SKILL.md must mention 500 line PR size limit"
def test_skill_mentions_nightly_gate(self) -> None:
content = _read_skill("spec-driven-development")
assert "nightly" in content.lower(), "SKILL.md must mention nightly gate"
# ============================================================================
# devx-workflow skill — exists in devx, mentions spec gates
# ============================================================================
class TestDevxWorkflowSkill:
def test_skill_exists(self) -> None:
path = _DEVX / ".devin" / "skills" / "devx-workflow" / "SKILL.md"
assert path.exists(), "devx-workflow SKILL.md not found in devx"
def test_mentions_spec_validation(self) -> None:
content = _read_skill("devx-workflow")
assert "validate_spec" in content, "devx-workflow skill must mention validate_spec"
def test_mentions_pr_size_check(self) -> None:
content = _read_skill("devx-workflow")
assert "check_pr_size" in content, "devx-workflow skill must mention check_pr_size"
def test_mentions_pr_workflow_commands(self) -> None:
content = _read_skill("devx-workflow")
assert "make create-pr" in content or "make push-with-pr" in content, (
"devx-workflow skill must mention PR creation commands"
)
def test_mentions_auto_merge(self) -> None:
content = _read_skill("devx-workflow")
assert "auto-merge" in content.lower() or "ready-to-merge" in content, (
"devx-workflow skill must mention auto-merge"
)
def test_has_correct_task_prefix(self) -> None:
content = _read_skill("devx-workflow")
assert "DEVX" in content, "devx-workflow skill must mention task prefix DEVX"
# ============================================================================
# testing-and-debugging skill — exists in devx, mentions spec workflow
# ============================================================================
class TestTestingAndDebuggingSkill:
def test_skill_exists(self) -> None:
path = _DEVX / ".devin" / "skills" / "testing-and-debugging" / "SKILL.md"
assert path.exists(), "testing-and-debugging SKILL.md not found in devx"
def test_has_required_sections(self) -> None:
content = _read_skill("testing-and-debugging")
assert "CI Failure Investigation" in content or "CI failure" in content, (
"testing-and-debugging skill must have CI failure section"
)
def test_mentions_spec_driven_workflow(self) -> None:
content = _read_skill("testing-and-debugging")
assert "spec" in content.lower(), "testing-and-debugging skill must mention spec-driven workflow"
# ============================================================================
# pr-review skill — deep review with auto-fix, exists in devx
# ============================================================================
class TestPrReviewSkill:
def test_skill_exists(self) -> None:
path = _DEVX / ".devin" / "skills" / "pr-review" / "SKILL.md"
assert path.exists(), "pr-review SKILL.md not found in devx"
def test_mentions_all_review_categories(self) -> None:
content = _read_skill("pr-review")
required_categories = [
"Functional Correctness",
"Completeness",
"Architecture",
"Reliability",
"Robustness",
"Security",
"Technical Excellence",
"Test Quality",
]
for cat in required_categories:
assert cat in content, f"pr-review skill missing category: {cat}"
def test_mentions_auto_fix(self) -> None:
content = _read_skill("pr-review")
assert "auto-fix" in content.lower() or "auto fix" in content.lower(), "pr-review skill must mention auto-fix"
def test_mentions_gitea_mcp(self) -> None:
content = _read_skill("pr-review")
assert "mcp" in content.lower(), "pr-review skill must mention Gitea MCP"
def test_mentions_inline_comments(self) -> None:
content = _read_skill("pr-review")
assert "inline" in content.lower(), "pr-review skill must mention inline comments"
def test_mentions_ready_to_merge(self) -> None:
content = _read_skill("pr-review")
assert "ready-to-merge" in content, "pr-review skill must mention ready-to-merge label"
def test_mentions_resolve_discussion(self) -> None:
content = _read_skill("pr-review")
assert "resolve" in content.lower(), "pr-review skill must mention resolving discussions"
def test_mentions_summary(self) -> None:
content = _read_skill("pr-review")
assert "summary" in content.lower(), "pr-review skill must mention posting a summary"
def test_no_pr_review_module_remains(self) -> None:
"""The old devx.ci.pr_review module should be deleted."""
path = _DEVX / "src" / "devx" / "ci" / "pr_review.py"
assert not path.exists(), "devx.ci.pr_review module should be deleted (replaced by pr-review skill)"
def test_no_pr_review_test_remains(self) -> None:
"""The old test_pr_review.py should be deleted."""
path = _DEVX / "tests" / "unit" / "test_pr_review.py"
assert not path.exists(), "tests/unit/test_pr_review.py should be deleted"
def test_no_pr_review_in_workflows(self) -> None:
"""No devx CI workflow should reference devx.ci.pr_review."""
wf_dir = _DEVX / ".gitea" / "workflows"
if not wf_dir.exists():
pytest.skip("No workflows directory")
for wf_file in wf_dir.glob("*.yml"):
content = wf_file.read_text(encoding="utf-8")
assert "devx.ci.pr_review" not in content, f"{wf_file.name} still references devx.ci.pr_review"
# ============================================================================
# Skill consistency — all devx skills have proper structure
# ============================================================================
class TestSkillConsistency:
DEVX_SKILLS = [
"devx-workflow",
"testing-and-debugging",
"spec-driven-development",
"pr-review",
]
@pytest.mark.parametrize("skill_name", DEVX_SKILLS)
def test_skill_has_title(self, skill_name: str) -> None:
path = _DEVX / ".devin" / "skills" / skill_name / "SKILL.md"
assert path.exists(), f"SKILL.md not found for {skill_name}"
content = path.read_text(encoding="utf-8")
first_line = content.strip().split("\n")[0]
assert first_line.startswith("# "), f"{skill_name}: SKILL.md must start with a # title"
@pytest.mark.parametrize("skill_name", DEVX_SKILLS)
def test_skill_not_empty(self, skill_name: str) -> None:
path = _DEVX / ".devin" / "skills" / skill_name / "SKILL.md"
assert path.exists(), f"SKILL.md not found for {skill_name}"
content = path.read_text(encoding="utf-8").strip()
assert len(content) > 100, f"{skill_name}: SKILL.md is too short ({len(content)} chars)"
@pytest.mark.parametrize("skill_name", DEVX_SKILLS)
def test_skill_has_sections(self, skill_name: str) -> None:
path = _DEVX / ".devin" / "skills" / skill_name / "SKILL.md"
assert path.exists(), f"SKILL.md not found for {skill_name}"
content = path.read_text(encoding="utf-8")
section_count = content.count("\n## ")
assert section_count >= 2, f"{skill_name}: SKILL.md must have at least 2 sections (found {section_count})"
# ============================================================================
# AGENTS.md — spec-driven development section in devx
# ============================================================================
class TestAgentsMdSpecSection:
def test_agents_md_has_spec_driven_section(self) -> None:
path = _DEVX / "AGENTS.md"
if not path.exists():
pytest.skip("AGENTS.md not found in devx")
content = path.read_text(encoding="utf-8")
assert "## Spec-Driven Development" in content, "AGENTS.md must have '## Spec-Driven Development' section"
def test_agents_md_mentions_validate_spec(self) -> None:
path = _DEVX / "AGENTS.md"
if not path.exists():
pytest.skip("AGENTS.md not found in devx")
content = path.read_text(encoding="utf-8")
assert "validate_spec" in content or "devx.ci.validate_spec" in content, (
"AGENTS.md must mention devx.ci.validate_spec"
)
def test_agents_md_pr_workflow_section_intact(self) -> None:
"""Ensure the PR Workflow section wasn't accidentally deleted."""
path = _DEVX / "AGENTS.md"
if not path.exists():
pytest.skip("AGENTS.md not found in devx")
content = path.read_text(encoding="utf-8")
assert "## PR Workflow" in content, "AGENTS.md must still have '## PR Workflow' section"
+1
View File
@@ -30,6 +30,7 @@ class TestHelpers:
assert CONVENTIONAL_RE.match("test: add tests")
assert CONVENTIONAL_RE.match("ci: update workflow")
assert CONVENTIONAL_RE.match("build: update deps")
assert CONVENTIONAL_RE.match("deps: bump devx from v0.50.2 to v0.51.0")
assert CONVENTIONAL_RE.match("revert: undo change")
def test_conventional_re_allows_scope(self) -> None:
+223
View File
@@ -0,0 +1,223 @@
"""Unit tests for devx.ci.validate_spec."""
from pathlib import Path
from unittest.mock import patch
from click.testing import CliRunner
from devx.ci.validate_spec import (
AC_CHECKED_RE,
AC_UNCHECKED_RE,
REQ_ID_RE,
cli,
find_spec_file,
validate_spec_content,
)
VALID_SPEC = """\
# OBL-INFRA-531: Fix sso-bridge role for pip install
## Problem
The sso-bridge role uses scripts.sso_bridge.listener but the pip
package uses sso_bridge.listener.
## Approach
REQ-1: Update molecule verify.yml to use sso_bridge.listener
REQ-2: Add infra repo clone task to sso_bridge role
## Test Plan
- Run molecule test for sso_bridge role
- Verify pip package is installed correctly
## Deploy Plan
- Merge PR
- Auto-deploy to staging
## Rollback Plan
- Revert PR
- Re-deploy previous version
## Acceptance Criteria
- [x] Molecule test passes with sso_bridge.listener
- [x] Infra repo is cloned by sso_bridge role
"""
SPEC_MISSING_SECTION = """\
# OBL-INFRA-531: Fix sso-bridge
## Problem
Something is broken.
## Approach
REQ-1: Fix it
## Test Plan
Run tests
"""
SPEC_UNCHECKED_AC = """\
# OBL-INFRA-531: Fix sso-bridge
## Problem
Broken.
## Approach
REQ-1: Fix it
## Test Plan
Run tests
## Deploy Plan
Deploy
## Rollback Plan
Revert
## Acceptance Criteria
- [x] Fixed
- [ ] Verified in staging
"""
SPEC_NO_REQ_IDS = """\
# OBL-INFRA-531: Fix sso-bridge
## Problem
Broken.
## Approach
Fix it.
## Test Plan
Run tests
## Deploy Plan
Deploy
## Rollback Plan
Revert
## Acceptance Criteria
- [x] Fixed
"""
class TestFindSpecFile:
def test_finds_exact_match(self, tmp_path: Path) -> None:
specs_dir = tmp_path / "specs"
specs_dir.mkdir()
(specs_dir / "OBL-INFRA-531.md").write_text("content")
result = find_spec_file("OBL-INFRA-531", str(specs_dir))
assert result is not None
assert result.name == "OBL-INFRA-531.md"
def test_finds_case_insensitive(self, tmp_path: Path) -> None:
specs_dir = tmp_path / "specs"
specs_dir.mkdir()
(specs_dir / "obl-infra-531.md").write_text("content")
result = find_spec_file("OBL-INFRA-531", str(specs_dir))
assert result is not None
def test_returns_none_when_not_found(self, tmp_path: Path) -> None:
specs_dir = tmp_path / "specs"
specs_dir.mkdir()
result = find_spec_file("OBL-INFRA-999", str(specs_dir))
assert result is None
def test_returns_none_when_dir_missing(self, tmp_path: Path) -> None:
result = find_spec_file("OBL-INFRA-531", str(tmp_path / "nonexistent"))
assert result is None
class TestValidateSpecContent:
def test_valid_spec_passes(self) -> None:
errors = validate_spec_content(VALID_SPEC)
assert errors == []
def test_missing_sections(self) -> None:
errors = validate_spec_content(SPEC_MISSING_SECTION)
assert len(errors) >= 3 # Missing Deploy Plan, Rollback Plan, Acceptance Criteria
assert any("Deploy Plan" in e for e in errors)
assert any("Rollback Plan" in e for e in errors)
assert any("Acceptance Criteria" in e for e in errors)
def test_unchecked_ac_fails(self) -> None:
errors = validate_spec_content(SPEC_UNCHECKED_AC)
assert len(errors) == 1
assert "unchecked" in errors[0].lower()
def test_no_req_ids_fails(self) -> None:
errors = validate_spec_content(SPEC_NO_REQ_IDS)
assert any("REQ-ID" in e for e in errors)
def test_empty_content_fails(self) -> None:
errors = validate_spec_content("")
assert len(errors) >= 2 # Missing sections + no REQ-IDs
class TestRegexPatterns:
def test_req_id_re_matches(self) -> None:
assert REQ_ID_RE.search("REQ-1: Do something")
assert REQ_ID_RE.search("REQ-42: Another thing")
assert not REQ_ID_RE.search("REQ: no number")
def test_ac_checked_re_matches(self) -> None:
assert AC_CHECKED_RE.search("- [x] Done")
assert AC_CHECKED_RE.search(" - [x] Indented")
assert not AC_CHECKED_RE.search("- [ ] Not done")
def test_ac_unchecked_re_matches(self) -> None:
assert AC_UNCHECKED_RE.search("- [ ] Not done")
assert AC_UNCHECKED_RE.search(" - [ ] Indented")
assert not AC_UNCHECKED_RE.search("- [x] Done")
class TestCli:
def test_fails_without_task_id(self) -> None:
runner = CliRunner()
result = runner.invoke(cli, ["--branch", "no-task-id"])
assert result.exit_code != 0
def test_allow_missing_succeeds_without_task_id(self) -> None:
runner = CliRunner()
result = runner.invoke(cli, ["--branch", "no-task-id", "--allow-missing"])
assert result.exit_code == 0
assert "WARNING" in result.output
def test_fails_when_spec_not_found(self, tmp_path: Path) -> None:
runner = CliRunner()
with patch("devx.ci.validate_spec.extract_task_id", return_value="OBL-INFRA-999"):
result = runner.invoke(
cli,
["--branch", "OBL-INFRA-999-test", "--specs-dir", str(tmp_path / "specs")],
)
assert result.exit_code != 0
assert "No spec file found" in result.output
def test_passes_with_valid_spec(self, tmp_path: Path) -> None:
specs_dir = tmp_path / "specs"
specs_dir.mkdir()
(specs_dir / "OBL-INFRA-531.md").write_text(VALID_SPEC)
runner = CliRunner()
with patch("devx.ci.validate_spec.extract_task_id", return_value="OBL-INFRA-531"):
result = runner.invoke(
cli,
["--branch", "OBL-INFRA-531-fix-foo", "--specs-dir", str(specs_dir)],
)
assert result.exit_code == 0
assert "Spec validated" in result.output
def test_fails_with_unchecked_ac(self, tmp_path: Path) -> None:
specs_dir = tmp_path / "specs"
specs_dir.mkdir()
(specs_dir / "OBL-INFRA-531.md").write_text(SPEC_UNCHECKED_AC)
runner = CliRunner()
with patch("devx.ci.validate_spec.extract_task_id", return_value="OBL-INFRA-531"):
result = runner.invoke(
cli,
["--branch", "OBL-INFRA-531-fix-foo", "--specs-dir", str(specs_dir)],
)
assert result.exit_code != 0
assert "unchecked" in result.output.lower()