Compare commits

..
Author SHA1 Message Date
emil 269699fd6f chore: trigger CI after branch rename to DEVX-153
CI / validate (pull_request) Successful in 1m0s
CI / auto-merge (pull_request) Failing after 21s
2026-08-09 02:58:13 +02:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> b18fef3f33 feat: sync missing features from v0.49.x line to master
CI / validate (pull_request) Failing after 56s
CI / auto-merge (pull_request) Skipped
The v0.49.x tag line diverged from origin/master, leaving many
features only accessible via tags but not on the master branch.

New modules:
- ci/cancel_superseded_runs.py — cancel superseded CI runs
- ci/check_workflow_artifact_deps.py — validate artifact deps
- ci/check_workflow_tofu_init.py — validate tofu init steps
- tools/check_alert_rules.py — validate Prometheus alert rules
- tools/check_ansible_set_fact_to_json.py — lint set_fact usage
- tools/check_docker_init.py — validate Docker init scripts
- utils/jinja.py — Jinja2 template utilities
- utils/ui.py — UI/console utilities

Modified modules:
- distribute_molecule.py: add --include-roles/--exclude-roles
- utils/api.py: add container.credentials for private registry auth
- install_tools.py: retry ansible-galaxy on transient timeouts
- setup_image.py: skip dep resolution with --no-deps
- cli.py: register new commands
- i18n.py: add new translation keys

Also removes accidentally committed .vale/styles/Google/ files.

Test results: 2195 passed, 100% coverage.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-09 02:47:04 +02:00
devx-ci-bot dd8e6c69e9 release: v0.49.5 [skip ci] 2026-08-07 21:02:12 +00:00
emil ccb7023965 DEVX-151: perf: skip dep resolution in setup-image with --no-deps 2026-08-07 21:01:06 +00:00
gitea-actions-bot 7dd15f1461 chore: update badge URLs to commit 8c02351c [skip ci] 2026-08-07 20:44:08 +00:00
devx-ci-bot d4e4621fa1 release: v0.49.4 [skip ci] 2026-08-07 20:43:19 +00:00
emil a6f814c446 DEVX-150: fix: add container.credentials for private registry auth 2026-08-07 20:42:18 +00:00
gitea-actions-bot 04aa5acb1f chore: update badge URLs to commit 40269e2e [skip ci] 2026-08-07 20:34:03 +00:00
devx-ci-bot 6973f9d851 release: v0.49.3 [skip ci] 2026-08-07 20:33:16 +00:00
emil 2669a0ea73 DEVX-149: fix: retry ansible-galaxy collection install on transient timeouts 2026-08-07 20:26:52 +00:00
gitea-actions-bot 03f057b55a chore: update badge URLs to commit b534b155 [skip ci] 2026-08-07 15:37:23 +00:00
devx-ci-bot 706d6dafe0 release: v0.49.2 [skip ci] 2026-08-07 15:36:28 +00:00
gitea-admin 03ddce427c DEVX-148: fix: add fallback URL for tea download
Co-authored-by: oblachno Admin <admin@oblachno.oblachno.fyi>
2026-08-07 15:33:54 +00:00
gitea-actions-bot 9642d6884c chore: update badge URLs to commit 3f33ebe6 [skip ci] 2026-08-07 14:32:31 +00:00
devx-ci-bot b2074d6635 release: v0.49.1 [skip ci] 2026-08-07 14:31:37 +00:00
gitea-admin a6dddf25e7 DEVX-147: fix: add container images to build-images workflow
Co-authored-by: oblachno Admin <admin@oblachno.oblachno.fyi>
2026-08-07 14:27:45 +00:00
gitea-actions-bot 01130a7385 chore: update badge URLs to commit 58fdb2b6 [skip ci] 2026-08-07 14:23:22 +00:00
devx-ci-bot 07580c9280 release: v0.49.0 [skip ci] 2026-08-07 14:22:26 +00:00
gitea-admin 6601d90bee DEVX-146: feat: add --include-roles and --exclude-roles to distribute_molecule
Co-authored-by: oblachno Admin <admin@oblachno.oblachno.fyi>
2026-08-07 14:20:54 +00:00
gitea-actions-bot 0df79fed53 chore: update badge URLs to commit b55c2d2f [skip ci] 2026-07-22 20:58:00 +00:00
devx-ci-bot cf8287e683 release: v0.48.0 [skip ci] 2026-07-22 20:57:10 +00:00
emil 9f1bdc4cf1 DEVX-145: feat: extract reusable components from infra and grm into devx
Post-merge / detect-and-configure (push) Successful in 12s
Post-merge / release-and-maintain (push) Successful in 1m19s
2026-07-22 20:56:27 +00:00
113 changed files with 4525 additions and 9991 deletions
-135
View File
@@ -1,135 +0,0 @@
# 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']"
```
@@ -1,90 +0,0 @@
# 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
+1 -9
View File
@@ -12,6 +12,7 @@ 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` |
@@ -23,15 +24,6 @@ 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
@@ -1,272 +0,0 @@
# 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
@@ -1,142 +0,0 @@
# 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
@@ -1,130 +0,0 @@
# 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,13 +45,6 @@ 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,9 +77,6 @@ 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"
+13 -42
View File
@@ -106,30 +106,14 @@ jobs:
--pr-title "$PR_TITLE" \
--repo "$REPOSITORY" \
--pr-number "$PR_NUMBER"
- name: Validate spec file
- name: Run automated PR review
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
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
set -euo pipefail
python3 -m devx.ci.pr_review \
"${{ github.event.number }}" \
"${{ github.repository }}"
# --- release-dry-run step (conditional) ---
- name: Release dry-run validation
if: steps.detect.outputs.user-facing-changed == 'true'
@@ -183,29 +167,16 @@ jobs:
REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PR_NUMBER: ${{ github.event.number }}
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
REPOSITORY: ${{ github.repository }}
run: |
. .venv/bin/activate 2>/dev/null || true
# 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
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)."
- name: Squash merge with task ID
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
-9
View File
@@ -1,9 +0,0 @@
extends: existence
message: "Use 'AM' or 'PM' (preceded by a space)."
link: "https://developers.google.com/style/word-list"
level: error
nonword: true
tokens:
- '\d{1,2}[AP]M\b'
- '\d{1,2} ?[ap]m\b'
- '\d{1,2} ?[aApP]\.[mM]\.'
-64
View File
@@ -1,64 +0,0 @@
extends: conditional
message: "Spell out '%s', if it's unfamiliar to the audience."
link: 'https://developers.google.com/style/abbreviations'
level: suggestion
ignorecase: false
# Ensures that the existence of 'first' implies the existence of 'second'.
first: '\b([A-Z]{3,5})\b'
second: '(?:\b[A-Z][a-z]+ )+\(([A-Z]{3,5})\)'
# ... with the exception of these:
exceptions:
- API
- ASP
- CLI
- CPU
- CSS
- CSV
- DEBUG
- DOM
- DPI
- FAQ
- GCC
- GDB
- GET
- GPU
- GTK
- GUI
- HTML
- HTTP
- HTTPS
- IDE
- JAR
- JSON
- JSX
- LESS
- LLDB
- NET
- NOTE
- NVDA
- OSS
- PATH
- PDF
- PHP
- POST
- RAM
- REPL
- RSA
- SCM
- SCSS
- SDK
- SQL
- SSH
- SSL
- SVG
- TBD
- TCP
- TODO
- URI
- URL
- USB
- UTF
- XML
- XSS
- YAML
- ZIP
-12
View File
@@ -1,12 +0,0 @@
extends: existence
message: "Don't attribute human qualities to software or hardware ('%s')."
link: https://developers.google.com/style/anthropomorphism
level: suggestion
ignorecase: true
# Limited to the two verbs the guide itself names. Broader lists (wants, knows,
# thinks) can't tell a software subject from a human one: on a 950-file corpus
# they produced 8 false positives ('the customer wants', 'your audience knows')
# for every 2 real ones.
tokens:
- sees
- tells
-13
View File
@@ -1,13 +0,0 @@
extends: existence
message: "'%s' should be in lowercase."
link: 'https://developers.google.com/style/colons'
level: warning
scope: sentence
# The match is the word itself, not ': X', and `nonword` is off. Both are
# required for a project Vocab to work: Vale compares accept.txt entries
# against the matched text, and `nonword: true` opts out of that entirely.
# So a proper noun after a colon can be exempted by adding it to accept.txt.
# The guide's other exemption, notice labels, is handled by the lookbehinds;
# headings are already excluded by `scope: sentence`. See issue #20.
tokens:
- '(?<!Note: )(?<!Caution: )(?<!Warning: )(?<!Success: )(?<=:\s)[A-Z]\w+'
-30
View File
@@ -1,30 +0,0 @@
extends: substitution
message: "Use '%s' instead of '%s'."
link: 'https://developers.google.com/style/contractions'
level: suggestion
ignorecase: true
action:
name: replace
swap:
are not: aren't
cannot: can't
could not: couldn't
did not: didn't
do not: don't
does not: doesn't
has not: hasn't
have not: haven't
how is: how's
is not: isn't
it is: it's
should not: shouldn't
that is: that's
they are: they're
was not: wasn't
we are: we're
we have: we've
were not: weren't
what is: what's
when is: when's
where is: where's
will not: won't
-9
View File
@@ -1,9 +0,0 @@
extends: existence
message: "Use 'July 31, 2016' format, not '%s'."
link: 'https://developers.google.com/style/dates-times'
ignorecase: true
level: error
nonword: true
tokens:
- '\d{1,2}(?:\.|/)\d{1,2}(?:\.|/)\d{4}'
- '\d{1,2} (?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?) \d{4}'
-9
View File
@@ -1,9 +0,0 @@
extends: existence
message: "In general, don't use an ellipsis."
link: 'https://developers.google.com/style/ellipses'
nonword: true
level: warning
action:
name: remove
tokens:
- '\.\.\.'
-13
View File
@@ -1,13 +0,0 @@
extends: existence
message: "Don't put a space before or after a dash."
link: "https://developers.google.com/style/dashes"
nonword: true
level: error
action:
name: edit
params:
- trim
- " "
tokens:
- '\s[—–]\s'
-14
View File
@@ -1,14 +0,0 @@
extends: existence
message: "Avoid the unverifiable claim '%s'."
link: https://developers.google.com/style/excessive-claims
level: suggestion
ignorecase: true
# The guide also names 'never', 'always', and 'ensure', but in technical writing
# those are usually legitimate instructions ('never commit secrets') rather than
# product claims: they accounted for 125 of 142 hits on a 950-file corpus.
# 'best practices' is a fixed term, not a superlative.
tokens:
- 'best(?! practices?)'
- simplest
- fastest
- guarantees?
-12
View File
@@ -1,12 +0,0 @@
extends: existence
message: "Don't use exclamation points in text."
link: "https://developers.google.com/style/exclamation-points"
nonword: true
level: error
action:
name: edit
params:
- trim_right
- "!"
tokens:
- '\w+!(?:\s|$)'
-15
View File
@@ -1,15 +0,0 @@
extends: existence
message: "Avoid first-person pronouns such as '%s'."
link: 'https://developers.google.com/style/pronouns#personal-pronouns'
ignorecase: true
level: warning
# The 'I' tokens use lookaround rather than consuming the surrounding
# whitespace. Matching ' I ' made the alert span cover both spaces, which shows
# up as a too-wide underline in editors, and read as "such as ' I '". Dropping
# `nonword` also lets a project Vocab apply, which it can't when set. See PR #50.
tokens:
- '(?<=^|\s)I(?=[\s,])'
- "\\bI'm\\b"
- \bme\b
- \bmy\b
- \bmine\b
-9
View File
@@ -1,9 +0,0 @@
extends: existence
message: "Don't use '%s' as a gender-neutral pronoun."
link: 'https://developers.google.com/style/pronouns#gender-neutral-pronouns'
level: error
ignorecase: true
tokens:
- he/she
- s/he
- \(s\)he
-43
View File
@@ -1,43 +0,0 @@
extends: substitution
message: "Consider using '%s' instead of '%s'."
ignorecase: true
link: "https://developers.google.com/style/inclusive-documentation"
level: error
action:
name: replace
swap:
(?:alumna|alumnus): graduate
(?:alumnae|alumni): graduates
air(?:m[ae]n|wom[ae]n): pilot(s)
anchor(?:m[ae]n|wom[ae]n): anchor(s)
authoress: author
camera(?:m[ae]n|wom[ae]n): camera operator(s)
door(?:m[ae]|wom[ae]n): concierge(s)
draft(?:m[ae]n|wom[ae]n): drafter(s)
fire(?:m[ae]n|wom[ae]n): firefighter(s)
fisher(?:m[ae]n|wom[ae]n): fisher(s)
fresh(?:m[ae]n|wom[ae]n): first-year student(s)
garbage(?:m[ae]n|wom[ae]n): waste collector(s)
lady lawyer: lawyer
ladylike: courteous
mail(?:m[ae]n|wom[ae]n): mail carriers
man and wife: husband and wife
man enough: strong enough
mankind: human kind|humanity
manmade: manufactured
manpower: personnel
middle(?:m[ae]n|wom[ae]n): intermediary
news(?:m[ae]n|wom[ae]n): journalist(s)
ombuds(?:man|woman): ombuds
oneupmanship: upstaging
poetess: poet
police(?:m[ae]n|wom[ae]n): police officer(s)
repair(?:m[ae]n|wom[ae]n): technician(s)
sales(?:m[ae]n|wom[ae]n): salesperson or sales people
service(?:m[ae]n|wom[ae]n): soldier(s)
steward(?:ess)?: flight attendant
tribes(?:m[ae]n|wom[ae]n): tribe member(s)
waitress: waiter
woman doctor: doctor
woman scientist[s]?: scientist(s)
work(?:m[ae]n|wom[ae]n): worker(s)
@@ -1,13 +0,0 @@
extends: existence
message: "Don't put a period at the end of a heading."
link: "https://developers.google.com/style/capitalization#capitalization-in-titles-and-headings"
nonword: true
level: warning
scope: heading
action:
name: edit
params:
- trim_right
- "."
tokens:
- '[a-z0-9][.]\s*$'
-32
View File
@@ -1,32 +0,0 @@
extends: capitalization
message: "'%s' should use sentence-style capitalization."
link: "https://developers.google.com/style/capitalization#capitalization-in-titles-and-headings"
level: warning
scope: heading
match: $sentence
# No `indicators: [":"]` here. That makes Vale require a capital after a colon,
# which is the Microsoft convention this rule was originally copied from. This
# guide says the opposite: "the first word after a colon is generally
# lowercase" (developers.google.com/style/colons), and Colons.yml enforces
# exactly that. See issue #58.
exceptions:
- Azure
- CLI
- Cosmos
- Docker
- Emmet
- gRPC
- I
- Kubernetes
- Linux
- macOS
- Marketplace
- MongoDB
- REPL
- Studio
- TypeScript
- URLs
- Visual
- VS
- Windows
- JSON
-13
View File
@@ -1,13 +0,0 @@
extends: existence
message: "Avoid the jargon '%s'."
link: https://developers.google.com/style/jargon
level: suggestion
ignorecase: true
# The guide also cites 'solution', 'support', and 'workload' as overloaded
# terms, but those have ordinary technical meanings and accounted for every hit
# on a 950-file corpus, so only the unambiguous figurative terms are listed.
tokens:
- break-glass
- camel ?case
- out-of-the-box
- swim ?lane
-15
View File
@@ -1,15 +0,0 @@
extends: substitution
message: "Use '%s' instead of '%s'."
link: 'https://developers.google.com/style/abbreviations'
ignorecase: true
level: error
nonword: true
action:
name: replace
# The delimiter is a lookahead so the replacement doesn't swallow the comma or
# space that follows (issue #18). `$` is included so the abbreviation is still
# caught at the end of a heading, table cell, or block, which accounted for 8
# of 10 occurrences on a 950-file corpus.
swap:
'\b(?:eg|e\.g\.)(?=[\s,;]|$)': for example
'\b(?:ie|i\.e\.)(?=[\s,;]|$)': that is
-14
View File
@@ -1,14 +0,0 @@
extends: existence
message: "'%s' doesn't need a hyphen."
link: "https://developers.google.com/style/hyphens"
level: error
ignorecase: false
nonword: true
action:
name: edit
params:
- regex
- "-"
- " "
tokens:
- '\b[^\s-]+ly-\w+\b'
-12
View File
@@ -1,12 +0,0 @@
extends: existence
message: "Don't use plurals in parentheses such as in '%s'."
link: "https://developers.google.com/style/plurals-parentheses"
level: error
nonword: true
action:
name: edit
params:
- trim_right
- "(s)"
tokens:
- '\b\w+\(s\)'
-7
View File
@@ -1,7 +0,0 @@
extends: existence
message: "Spell out all ordinal numbers ('%s') in text."
link: 'https://developers.google.com/style/numbers'
level: error
nonword: true
tokens:
- \d+(?:st|nd|rd|th)
-28
View File
@@ -1,28 +0,0 @@
extends: existence
message: "Use the Oxford comma in '%s'."
link: 'https://developers.google.com/style/commas'
scope: sentence
level: warning
nonword: true
# List items may be several words long, not just one. Four guards keep the
# false-positive rate down:
#
# 1. The comma can't be the one closing a fronted subordinate clause
# ('When your alarm rings, you turn it off and tumble out of bed.') --
# that comma separates clauses, not list items. Only the first comma of
# such a sentence is exempt, so 'When it rains, apples, pears or bananas
# get wet.' is still caught.
# 2. The item can't open with a clause-introducer (', which ...',
# ', specifically ...').
# 3. The item can't open with a subject pronoun followed by a verb, which
# marks a compound predicate rather than a list ('..., you walk to the
# fridge and get a snack.'). A pronoun directly followed by 'and'/'or'
# is a real list item, so ', you and me.' still matches.
# 4. Neither item may contain an auxiliary verb, which is another compound
# predicate signal (', it has some downsides and is officially
# discouraged.').
#
# The trailing anchor allows end-of-scope so list fragments ('Apples, pears
# or bananas') are still caught.
tokens:
- '(?<!^(?i:when|whenever|while|if|unless|until|although|though|because|since|after|before|once|whereas|whether|as)\b[^,]{0,80}),\s(?!(?:which|who|whom|whose|that|where|when|while|because|since|although|though|if|unless|so|but|and|or|however|therefore|thus|specifically|especially|namely|then|take|see|note|consider|make|use|either|neither)\b)(?!(?i:i|you|we|they|he|she|it)\s+(?!(?:and|or)\b))(?:(?!\b(?:is|are|was|were|has|have|had|be|been|being|will|would|can|could|should|may|might|must|do|does|did)\b)\w+ ){0,4}\w+ (?:and|or) (?:(?!\b(?:is|are|was|were|has|have|had|be|been|being|will|would|can|could|should|may|might|must|do|does|did)\b)\w+ ){0,4}\w+(?:[.?!]|$)'
-15
View File
@@ -1,15 +0,0 @@
extends: existence
message: "Use parentheses judiciously."
link: 'https://developers.google.com/style/parentheses'
nonword: true
level: suggestion
# `[^)]` rather than `.+`: a greedy match ran from the first '(' on a line to
# the last ')', so 'Text (one) and more (two).' produced a single alert
# covering everything between them. See issue #30.
# A bare 3-5 letter acronym is skipped: Acronyms.yml requires acronyms to be
# defined as 'Spelled Out Term (ACRONYM)', so flagging those parentheses would
# put the two rules in direct conflict. The acronym has to be the whole
# parenthetical — '(NASA rocket program)' is an ordinary aside and still
# flags. Length matches the {3,5} in Acronyms.yml. See PR #59.
tokens:
- '\((?![A-Z]{3,5}\))[^)]+\)'
-184
View File
@@ -1,184 +0,0 @@
extends: existence
link: 'https://developers.google.com/style/voice'
message: "In general, use active voice instead of passive voice ('%s')."
ignorecase: true
level: suggestion
raw:
- \b(am|are|were|being|is|been|was|be)\b\s*
tokens:
- '[\w]+ed'
- awoken
- beat
- become
- been
- begun
- bent
- beset
- bet
- bid
- bidden
- bitten
- bled
- blown
- born
- bought
- bound
- bred
- broadcast
- broken
- brought
- built
- burnt
- burst
- cast
- caught
- chosen
- clung
- come
- cost
- crept
- cut
- dealt
- dived
- done
- drawn
- dreamt
- driven
- drunk
- dug
- eaten
- fallen
- fed
- felt
- fit
- fled
- flown
- flung
- forbidden
- foregone
- forgiven
- forgotten
- forsaken
- fought
- found
- frozen
- given
- gone
- gotten
- ground
- grown
- heard
- held
- hidden
- hit
- hung
- hurt
- kept
- knelt
- knit
- known
- laid
- lain
- leapt
- learnt
- led
- left
- lent
- let
- lighted
- lost
- made
- meant
- met
- misspelt
- mistaken
- mown
- overcome
- overdone
- overtaken
- overthrown
- paid
- pled
- proven
- put
- quit
- read
- rid
- ridden
- risen
- run
- rung
- said
- sat
- sawn
- seen
- sent
- set
- sewn
- shaken
- shaven
- shed
- shod
- shone
- shorn
- shot
- shown
- shrunk
- shut
- slain
- slept
- slid
- slit
- slung
- smitten
- sold
- sought
- sown
- sped
- spent
- spilt
- spit
- split
- spoken
- spread
- sprung
- spun
- stolen
- stood
- stridden
- striven
- struck
- strung
- stuck
- stung
- stunk
- sung
- sunk
- swept
- swollen
- sworn
- swum
- swung
- taken
- taught
- thought
- thrived
- thrown
- thrust
- told
- torn
- trodden
- understood
- upheld
- upset
- wed
- wept
- withheld
- withstood
- woken
- won
- worn
- wound
- woven
- written
- wrung
-7
View File
@@ -1,7 +0,0 @@
extends: existence
message: "Don't use periods with acronyms or initialisms such as '%s'."
link: 'https://developers.google.com/style/abbreviations'
level: error
nonword: true
tokens:
- '\b(?:[A-Z]\.){3,}'
-7
View File
@@ -1,7 +0,0 @@
extends: existence
message: "Commas and periods go inside quotation marks."
link: 'https://developers.google.com/style/quotation-marks'
level: error
nonword: true
tokens:
- '"[^"]+"[.,?]'
-7
View File
@@ -1,7 +0,0 @@
extends: existence
message: "Don't add words such as 'from' or 'between' to describe a range of numbers."
link: 'https://developers.google.com/style/hyphens'
nonword: true
level: warning
tokens:
- '(?:from|between)\s\d+\s?-\s?\d+'
-8
View File
@@ -1,8 +0,0 @@
extends: existence
message: "Use semicolons judiciously."
link: 'https://developers.google.com/style/semicolons'
nonword: true
scope: sentence
level: suggestion
tokens:
- ';'
-11
View File
@@ -1,11 +0,0 @@
extends: existence
message: "Don't use internet slang abbreviations such as '%s'."
link: 'https://developers.google.com/style/abbreviations'
ignorecase: true
level: error
tokens:
- 'tl;dr'
- ymmv
- rtfm
- imo
- fwiw
-10
View File
@@ -1,10 +0,0 @@
extends: existence
message: "'%s' should have one space."
link: 'https://developers.google.com/style/sentence-spacing'
level: error
nonword: true
action:
name: remove
tokens:
- '[a-z][.?!] {2,}[A-Z]'
- '[a-z][.?!][A-Z]'
-10
View File
@@ -1,10 +0,0 @@
extends: existence
message: "In general, use American spelling instead of '%s'."
link: 'https://developers.google.com/style/spelling'
ignorecase: true
level: warning
tokens:
- '(?:\w+)nised?'
- 'colour'
- 'labour'
- 'centre'
-13
View File
@@ -1,13 +0,0 @@
extends: existence
message: "Avoid time-based words like '%s' in product documentation."
link: https://developers.google.com/style/timeless-documentation
level: suggestion
ignorecase: true
# The guide also names 'now' and 'new', but both have common senses that aren't
# time-anchored ('create a new project'): adding them took a 950-file corpus of
# technical documentation from 14 hits to 117. 'recently' is left out too — every
# hit in that corpus was the UI idiom 'recently used'.
tokens:
- currently
- latest
- soon
-10
View File
@@ -1,10 +0,0 @@
extends: existence
message: "Put a nonbreaking space between the number and the unit in '%s'."
link: "https://developers.google.com/style/units-of-measure"
nonword: true
level: error
tokens:
- '\b\d+(?:B|kB|MB|GB|TB)\b'
- '\b\d+(?:ns|ms|min|h|d)\b'
# Seconds are split out so a decade ('1990s') isn't read as a unit.
- '\b\d+s\b(?<!\b(?:19|20)\d\ds\b)'
-11
View File
@@ -1,11 +0,0 @@
extends: existence
message: "Try to avoid using first-person plural like '%s'."
link: 'https://developers.google.com/style/pronouns#personal-pronouns'
level: warning
ignorecase: true
tokens:
- we
- we'(?:ve|re)
- ours?
- us
- let's
-7
View File
@@ -1,7 +0,0 @@
extends: existence
message: "Avoid using '%s'."
link: 'https://developers.google.com/style/tense'
ignorecase: true
level: warning
tokens:
- will
-29
View File
@@ -1,29 +0,0 @@
extends: substitution
message: "Use '%s' instead of '%s'."
link: "https://developers.google.com/style/word-list"
level: warning
# Case matters here: each key's own capitalization is what's being corrected,
# so ignorecase would make these match their own replacements. The rest of the
# word list lives in WordListCase.yml.
ignorecase: false
action:
name: replace
swap:
Ajax: AJAX
Android device: Android-powered device
android: Android
API explorer: APIs Explorer
authN: authentication
authZ: authorization
CLI: command-line tool
Cloud: Google Cloud Platform|GCP
Container Engine: Kubernetes Engine
Developers Console: Google API Console|API Console
Google account: Google Account
Google accounts: Google Accounts
Googling: search with Google
HTTPs: HTTPS
k8s: Kubernetes
SHA1: SHA-1|HAS-SHA1
url: URL
World Wide Web: web
-68
View File
@@ -1,68 +0,0 @@
extends: substitution
message: "Use '%s' instead of '%s'."
link: "https://developers.google.com/style/word-list"
level: warning
# The case-insensitive half of the word list, so sentence-initial use is caught
# ('Touch the screen', not only 'touch the screen'). Entries that must stay
# case-sensitive are in WordList.yml.
ignorecase: true
action:
name: replace
swap:
"(?:API Console|dev|developer) key": API key
"(?:cell ?phone|smart ?phone)": phone|mobile phone
"(?:dev|developer|APIs) console": API console
"(?:e-mail|Email|E-mail)": email
"(?:file ?path|path ?name)": path
"(?:kill|terminate|abort)": stop|exit|cancel|end
# Longest form first: with the shortest alternative leading, 'OAuth 2' matched
# only 'OAuth', so applying the suggestion produced 'OAuth 2.0 2'. The rule is
# already case-insensitive, so the inline (?i) is redundant. See issue #41.
'\bOauth2\.0\b|\bOAuth ?2\b(?!\.0)|\bOauth\b(?! ?2)': OAuth 2.0
"(?:ok|Okay)": OK|okay
"(?:WiFi|wifi)": Wi-Fi
'[\.]+apk': APK
'3\-D': 3D
'Google (?:I\-O|IO)': Google I/O
"tap (?:&|and) hold": touch & hold
"un(?:check|select)": clear
above: preceding
account name: username
action bar: app bar
admin: administrator
a\.k\.a|aka: or|also known as
application: app
approx\.: approximately
autoupdate: automatically update
cellular data: mobile data
cellular network: mobile network
chapter: documents|pages|sections
check box: checkbox
click on: click|click in
content type: media type
curated roles: predefined roles
data are: data is
disabled?: turn off|off
ephemeral IP address: ephemeral external IP address
fewer data: less data
file name: filename
firewalls: firewall rules
functionality: capability|feature
grayed-out: unavailable
in order to: to
ingest: import|load
long press: touch & hold
network IP address: internal IP address
omnibox: address bar
open-source: open source
overview screen: recents screen
regex: regular expression
sign into: sign in to
'(?<!single )sign-?on': single sign-on
static IP address: static external IP address
stylesheet: style sheet
synch: sync
tablename: table name
tablet: device
'touch(?! ?(?:&|and) hold)': tap
vs\.: versus
-4
View File
@@ -1,4 +0,0 @@
{
"feed": "https://github.com/errata-ai/Google/releases.atom",
"vale_version": ">=1.0.0"
}
View File
+3 -35
View File
@@ -83,6 +83,7 @@ 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)
@@ -123,10 +124,6 @@ src/devx/
│ ├── check_docker_init.py # Check Docker Compose services with healthchecks have init: true
│ ├── check_ansible_set_fact_to_json.py # Check set_fact tasks don't misuse to_json
│ ├── check_alert_rules.py # Validate Prometheus alert rules with promtool
│ ├── check_ansible_no_log.py # Check Ansible tasks for missing no_log on secrets
│ ├── check_ansible_patterns.py # Detect dangerous failure-masking patterns
│ ├── check_jinja_expr.py # Validate Jinja2 expressions in Ansible files
│ ├── check_ansible_no_state_absent_on_db.py # Prevent state:absent on DB paths
│ └── _shared.py # Shared tool utilities
├── opentofu.py # OpenTofu output helpers (get_tofu_output, get_tofu_vm_ip, get_tofu_vm_field)
├── utils/ # Shared utilities (reusable across projects)
@@ -144,8 +141,8 @@ src/devx/
└── molecule/ # Optional molecule testing helpers (for Ansible projects)
├── discover_runners.py # Dynamic Gitea runner discovery
├── distribute_molecule.py # Distribute molecule scenarios across runners (LPT scheduling, --roles-root for multi-role)
├── molecule_ci_guard.py # Run molecule with cross-runner fail-fast (--roles-root)
├── molecule_all.py # Run all molecule scenarios locally
├── molecule_changed.py # Detect which Ansible roles changed and output molecule scenarios
├── start_docker.py # Ensure Docker daemon is running for molecule tests
└── platforms.py # Supported molecule platforms
```
@@ -157,37 +154,8 @@ 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)
@@ -238,7 +206,7 @@ docs: update README
### 6. Review the PR
**Automated review (CI `validate` job):** Every PR triggers an automated
review via the `pr-review` skill (agent-invoked, not a CI step).
review via `python -m devx.ci.pr_review` as a step in the `validate` job.
This posts a review with
`COMMENT` (no issues) or `REQUEST_CHANGES` (issues found):
+37 -136
View File
@@ -2,172 +2,73 @@
All notable changes to this project will be documented in this file.
## [0.51.10] - 2026-09-17
## [0.49.5] - 2026-08-07
### Performance
- Skip dep resolution in setup-image with --no-deps
## [0.49.4] - 2026-08-07
### Bug Fixes
- Read Gitea repo-variable data field; fail closed on unknown nightly status
- Add container.credentials for private registry auth
## [0.51.9] - 2026-08-26
## [0.49.3] - 2026-08-07
### Bug Fixes
- Exclude docs/plans/* from PR size check
- Retry ansible-galaxy collection install on transient timeouts
## [0.51.8] - 2026-08-26
## [0.49.2] - 2026-08-07
### Bug Fixes
- Accept deps: as valid conventional commit type
- Add fallback URL for tea download
## [0.51.7] - 2026-08-26
## [0.49.1] - 2026-08-07
### Bug Fixes
- Increase HTTP 500 retry count to 5 with longer backoff and visible logging
- Add container images to build-images workflow
## [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
## [0.49.0] - 2026-08-07
### 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
- Prefer rootless Docker socket over low-space inner DinD daemon
- Check /run/host-docker.sock for host Docker daemon
- Use /dev/shm/docker as data-root for inner dockerd
- Kill existing dockerd before starting /dev/shm/docker daemon
- Use /dev/shm/docker.sock socket for local dockerd
- Add --iptables=false to local dockerd
- Disable bridge and ip6tables for local dockerd
- Use host Docker when root dir is inaccessible (free=0)
- Prefer /run/host-docker.sock over inner dockerd
- Start local dockerd instead of using low-space inner dockerd
- Use container overlay for local dockerd data root
- Kill inner dockerd with SIGKILL, use alt socket if alive
- Kill dockerd by PID when pkill fails, use /dev/shm for alive daemon
- Trust /var/run/docker.sock with free=0 when no inner dockerd exists
## [0.50.0] - 2026-08-09
- Add --include-roles and --exclude-roles to distribute_molecule
## [0.48.0] - 2026-07-22
### Features
- Add 5 standalone lint scripts from infra
- Extract reusable components from infra and grm into devx
## [0.49.0] - 2026-08-09
## [0.48.0] - 2026-07-22
### Features
- Sync missing features from v0.49.x line to master
- Extract reusable components from infra and grm into devx
## [0.48.2] - 2026-08-09
### Bug Fixes
- Remove dead translation keys and add missing one
## [0.48.1] - 2026-08-08
### Bug Fixes
- *(setup)* Extract version from filename for mirror installs
## [0.48.0] - 2026-08-08
## [Unreleased]
### Features
- *(setup)* Mirror Ansible collections from Gitea registry with auth
## [0.47.10] - 2026-08-05
### Bug Fixes
- Unique molecule container names per CI runner
## [0.47.9] - 2026-08-03
### Bug Fixes
- Unique molecule container names per CI runner
## [0.47.8] - 2026-08-03
### Bug Fixes
- Increase CI_SCALE_FACTOR default from 4 to 6
## [0.47.7] - 2026-08-03
### Bug Fixes
- Scale check_test_speed limits on CI runners
## [0.47.6] - 2026-08-03
### Bug Fixes
- Configure git auth in setup_image for git+https deps
## [0.47.5] - 2026-08-03
### Bug Fixes
- Push wiki to main branch instead of master
## [0.47.4] - 2026-08-03
### Bug Fixes
- Add User-Agent header to _download in install_tools
- Extract reusable components from infra and grm into devx:
- `devx.utils.ui.say()` — unified click.echo + logging output
- `devx.utils.api.APIClient` — base HTTP API client class with retry logic
- `devx.utils.jinja` — Jinja2 environment helpers with Ansible-compatible filters
- `devx.i18n.configure_i18n()` — configurable `lang_env_var` and `translations_path_env_var`
- `devx.ci.cancel_superseded_runs` — cancel in-flight CI runs for the same PR branch
- `devx.ci.check_workflow_artifact_deps` — verify artifact download jobs depend on upload jobs
- `devx.ci.check_workflow_tofu_init` — verify tofu-state jobs have a tofu-init step
- `devx.tools.check_docker_init` — check Docker Compose services with healthchecks have init: true
- `devx.tools.check_ansible_set_fact_to_json` — check set_fact tasks don't misuse to_json
- `devx.tools.check_alert_rules` — validate Prometheus alert rules with promtool
- Add `jinja2` and `pyyaml` as core dependencies (previously in `deploy` extras only)
- Register new CLI commands: `devx ci cancel-superseded-runs`, `devx ci check-workflow-artifact-deps`,
`devx ci check-workflow-tofu-init`, `devx tools check-docker-init`,
`devx tools check-ansible-set-fact-to-json`, `devx tools check-alert-rules`
- Add Makefile targets for all new check tools
## [0.47.3] - 2026-07-17
+14 -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/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/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/8c02351c3190af51b1dfe545a7abbec665c51559/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/8c02351c3190af51b1dfe545a7abbec665c51559/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/8c02351c3190af51b1dfe545a7abbec665c51559/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/8c02351c3190af51b1dfe545a7abbec665c51559/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/8c02351c3190af51b1dfe545a7abbec665c51559/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/8c02351c3190af51b1dfe545a7abbec665c51559/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.51.10",
"devx>=0.49.5",
]
[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.51.10"`) or use a version constraint
> (for example, `"devx>=0.51.10,<0.52"`).
> `dependencies` (for example, `"devx==0.49.5"`) or use a version constraint
> (for example, `"devx>=0.49.5,<0.50"`).
### Optional extras
@@ -226,6 +226,10 @@ python -m devx.molecule.distribute_molecule --runner-index 1 --max-runners 3
python -m devx.molecule.distribute_molecule --list # list all scenarios
python -m devx.molecule.distribute_molecule --list-platforms # list platforms
# Run molecule tests with cross-runner fail-fast
python -m devx.molecule.molecule_ci_guard pair1 pair2
python -m devx.molecule.molecule_ci_guard --roles-root ansible/roles pair1 pair2
# Run all molecule scenarios locally (sequential)
python -m devx.molecule.molecule_all
python -m devx.molecule.molecule_all --bin .venv/bin
@@ -299,6 +303,7 @@ devx --version
| `devx molecule all` | Run all molecule scenarios on all supported platforms |
| `devx molecule discover-runners` | Discover available Gitea Actions runners |
| `devx molecule distribute` | Distribute molecule test pairs across parallel runners |
| `devx molecule guard` | Run molecule tests with CI failure polling |
See [CLI Commands](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki/CLI-Commands)
in the wiki for full command documentation with examples.
+11 -11
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/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/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/8c02351c3190af51b1dfe545a7abbec665c51559/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/8c02351c3190af51b1dfe545a7abbec665c51559/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/8c02351c3190af51b1dfe545a7abbec665c51559/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/8c02351c3190af51b1dfe545a7abbec665c51559/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/8c02351c3190af51b1dfe545a7abbec665c51559/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/8c02351c3190af51b1dfe545a7abbec665c51559/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.51.10",
"devx>=0.49.5",
]
[tool.pip]
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
```
Pin a specific version if needed: `"devx==0.51.10"` or `"devx>=0.51.10,<0.52"`.
Pin a specific version if needed: `"devx==0.49.5"` or `"devx>=0.49.5,<0.50"`.
### Optional extras
@@ -104,8 +104,8 @@ devx is a self-contained Python package under `src/devx/`:
- **Dev tools** (`devx.tools`) — setup, install_tools, check_test_speed,
configure_repo, generate_badges, generate_cliff_config, install_checkmake
- **Molecule tools** (`devx.molecule`) — Optional, for projects with Ansible
roles: distribute_molecule, molecule_all, discover_runners, start_docker,
platforms
roles: distribute_molecule, molecule_ci_guard, molecule_all, discover_runners,
start_docker, platforms
See [Architecture](Architecture) for the full package structure, module
descriptions, design principles, and data flow diagrams.
@@ -132,7 +132,7 @@ devx provides a `devx` CLI with three command groups:
- `devx ci <command>` — CI/CD automation (17 commands)
- `devx tools <command>` — Developer tools (9 commands)
- `devx molecule <command>` — Molecule testing (3 commands, optional)
- `devx molecule <command>` — Molecule testing (4 commands, optional)
See [CLI Commands](CLI-Commands) for full command documentation with examples.
-46
View File
@@ -1,46 +0,0 @@
# 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
@@ -1,34 +0,0 @@
# 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
@@ -1,24 +0,0 @@
# 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
@@ -1,38 +0,0 @@
# 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
@@ -1,41 +0,0 @@
# 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
@@ -1,31 +0,0 @@
# 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
@@ -1,27 +0,0 @@
# 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
@@ -1,41 +0,0 @@
# 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
@@ -1,58 +0,0 @@
# 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
@@ -1,33 +0,0 @@
# 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
@@ -1,34 +0,0 @@
# 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
@@ -1,31 +0,0 @@
# 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
@@ -1,27 +0,0 @@
# 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
@@ -1,64 +0,0 @@
# 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
+11 -3
View File
@@ -50,6 +50,7 @@ src/devx/
├── __init__.py
├── discover_runners.py # Dynamic Gitea runner discovery
├── distribute_molecule.py # Distribute scenarios across runners
├── molecule_ci_guard.py # Run molecule with cross-runner fail-fast
├── molecule_all.py # Run all molecule scenarios locally
├── start_docker.py # Ensure Docker is available for molecule
└── platforms.py # Supported molecule platforms
@@ -299,9 +300,9 @@ Distributes files matching a glob pattern across N parallel runners
### `integration_guard.py`
Runs pytest with cross-runner failure detection. A background thread polls
the Gitea API. If any other integration-tests matrix runner reports failure,
the current pytest subprocess is killed and this runner exits early.
Runs pytest with the same cross-runner failure detection mechanism used by
`molecule_ci_guard`. If any other integration-tests matrix runner reports
failure, the current pytest subprocess is killed and this runner exits early.
## Developer tools (`devx.tools`)
@@ -381,6 +382,13 @@ the supported OS platform matrix. Supports `--roles-root` for multi-role
repositories, `--list` to list scenarios, and `--list-platforms` to list
platforms.
### `molecule_ci_guard.py`
Runs molecule tests sequentially while polling the Gitea API for other runner
failures. If any other molecule matrix runner reports failure, the current
molecule subprocess is killed and this runner exits early. Supports both
single-role (4-part) and multi-role (5-part) pair encoding.
### `molecule_all.py`
Runs all molecule scenarios on all supported OS platforms sequentially.
+9
View File
@@ -478,6 +478,15 @@ python -m devx.molecule.distribute_molecule --list
python -m devx.molecule.distribute_molecule --list-platforms
```
### `molecule_ci_guard.py`
Runs molecule tests sequentially while polling the Gitea API for other runner
failures. Aborts early if another runner fails the same job.
```bash
python -m devx.molecule.molecule_ci_guard [--roles-root <dir>] pair1 pair2 ...
```
### `validate_commit_msg.py`
Validates commit messages. On feature branches: conventional commits only
+26
View File
@@ -631,3 +631,29 @@ Options:
- `--list-platforms` — list all platforms, one per line
- `--roles-root <dir>` — roles root directory for multi-role repos (default:
`ansible/roles`)
### `devx molecule guard`
Run molecule tests sequentially with CI failure polling. A background thread
polls the Gitea API. If any other molecule matrix runner reports failure, the
current molecule subprocess is killed and this runner exits early with code 1.
```bash
devx molecule guard pair1 pair2 pair3
devx molecule guard --roles-root ansible/roles pair1 pair2
```
Each pair is encoded as:
- **Single-role (4-part):** `scenario|platform_name|platform_image|platform_command`
- **Multi-role (5-part):** `role|scenario|platform_name|platform_image|platform_command`
Options:
- `--roles-root <dir>` — roles root directory for multi-role repos
Environment variables:
- `GITEA_URL` — base URL of the Gitea instance
- `CI_GITEA_TOKEN` — API token with repo access
- `RUN_ID` — workflow run ID (`GITHUB_RUN_ID`)
- `JOB_NAME` — base job name (`GITHUB_JOB`)
- `MATRIX_INDEX` — current matrix index (runner-index)
- `GITEA_REPOSITORY` — repository in `owner/repo` format
+2 -2
View File
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
```toml
[project]
dependencies = [
"devx>=0.51.10",
"devx>=0.49.5",
]
[project.optional-dependencies]
dev = [
"devx>=0.51.10",
"devx>=0.49.5",
]
```
+2 -8
View File
@@ -1,9 +1,3 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects.
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
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"
__version__ = "0.49.5"
+1 -4
View File
@@ -392,10 +392,7 @@ class GiteaClient:
"""
try:
r = self._request("GET", f"/actions/variables/{name}")
body = r.json()
if "data" in body:
return body["data"]
return body.get("value")
return r.json().get("value")
except APIError as e:
if e.status == 404:
return None
-213
View File
@@ -1,213 +0,0 @@
#!/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
@@ -1,227 +0,0 @@
#!/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()
+2
View File
@@ -44,6 +44,7 @@ REQUIRED_SCRIPTS = [
"auto_merge.py",
"release.py",
"publish.py",
"pr_review.py",
"notify_failure.py",
"post_merge.py",
"classify_changes.py",
@@ -51,6 +52,7 @@ REQUIRED_SCRIPTS = [
"detect_release_commit.py",
"push_badges.py",
"distribute_molecule.py",
"molecule_ci_guard.py",
"validate_commit_msg.py",
]
-121
View File
@@ -1,121 +0,0 @@
#!/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()
+7 -51
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
"""Run integration tests with cross-runner failure detection.
Wraps ``pytest`` with Gitea API polling. If any other integration-tests
matrix runner reports failure, the current pytest subprocess is killed
and this runner exits early with code 1.
Wraps ``pytest`` with the same Gitea API polling mechanism used by
``molecule_ci_guard``. If any other integration-tests matrix runner
reports failure, the current pytest subprocess is killed and this runner
exits early with code 1.
Usage::
@@ -34,62 +35,17 @@ import threading
import time
import click
import requests
from devx.config import REPO_NAME, REPO_OWNER
from devx.i18n import _
from devx.molecule.molecule_ci_guard import (
poll_for_other_failures,
)
from devx.tokens import get_ci_token
POLL_INTERVAL = 10
def get_running_jobs(gitea_url: str, owner: str, repo: str, token: str, run_id: int) -> list[dict]:
"""Return jobs for the given workflow run."""
url = f"{gitea_url}/api/v1/repos/{owner}/{repo}/actions/runs/{run_id}/jobs"
headers = {"Authorization": f"token {token}"}
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
data = response.json()
return data.get("jobs", [])
def any_other_runner_failed(jobs: list[dict], current_job_name: str, current_index: int) -> bool:
"""Return True if any other matrix job has failed."""
for job in jobs:
name = job.get("name", "")
if not name.startswith(current_job_name):
continue
if name == f"{current_job_name} ({current_index})" or name == current_job_name:
continue
if job.get("conclusion") == "failure":
return True
return False
def poll_for_other_failures(
gitea_url: str,
owner: str,
repo: str,
token: str,
run_id: int,
job_name: str,
current_index: int,
stop_event: threading.Event,
failed_event: threading.Event,
) -> None:
"""Background thread: poll API and signal if another runner fails."""
while not stop_event.is_set():
try:
jobs = get_running_jobs(gitea_url, owner, repo, token, run_id)
if any_other_runner_failed(jobs, job_name, current_index):
click.echo(_("Another runner failed. Stopping this runner early."))
failed_event.set()
return
except requests.RequestException as exc:
click.echo(_("API poll warning: {exc}", exc=exc))
stop_event.wait(POLL_INTERVAL)
@click.command(context_settings={"ignore_unknown_options": True})
@click.argument("pytest_args", nargs=-1, type=click.UNPROCESSED, required=True)
def cli(pytest_args: tuple[str, ...]) -> None:
-126
View File
@@ -1,126 +0,0 @@
#!/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
@@ -0,0 +1,715 @@
#!/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, deps, revert, BREAKING CHANGE",
" perf, test, ci, build, revert, BREAKING CHANGE",
subject=subject,
)
)
-157
View File
@@ -1,157 +0,0 @@
#!/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()
+14
View File
@@ -116,6 +116,13 @@ 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:
@@ -294,6 +301,13 @@ def molecule_discover_runners(args: tuple[str, ...]) -> None:
_run_module("devx.molecule.discover_runners", list(args))
@molecule.command("guard")
@click.argument("args", nargs=-1)
def molecule_guard(args: tuple[str, ...]) -> None:
"""Run molecule tests sequentially with CI failure polling."""
_run_module("devx.molecule.molecule_ci_guard", list(args))
@molecule.command("all")
@click.argument("args", nargs=-1)
def molecule_all(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|deps)(\(.+\))?: .+")
CONVENTIONAL_RE = re.compile(r"^(feat|fix|chore|docs|style|refactor|perf|test|ci|build|revert)(\(.+\))?: .+")
+11 -1
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-rebase devx-pr-rebase
.PHONY: devx-pr-status devx-pr-logs devx-pr-label devx-pr-review 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,6 +171,16 @@ 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
-158
View File
@@ -1,158 +0,0 @@
"""Detect which Ansible roles changed and output their molecule scenarios.
Usage::
python -m devx.molecule.molecule_changed --print-targets
python -m devx.molecule.molecule_changed --base origin/master --print-roles
Outputs the list of make targets (e.g. molecule-docker-base) for roles
that have changed files vs the base ref. Used by ``make molecule-changed``
to run only the molecule scenarios affected by the current diff.
Role-to-target mapping is derived from the directory structure:
ansible/roles/<role>/ molecule-<role>
For roles with multiple scenarios (e.g. app_container has customer-apps,
nextcloud, postgres-upgrade, simple-app), the base target runs all
scenarios for that role.
Playbooks that change also trigger molecule for the roles they include.
Shared infrastructure changes (ansible.cfg, requirements.yml, molecule/)
trigger all scenarios.
"""
from __future__ import annotations
import subprocess # nosec B404 — used to run git, a trusted binary
from pathlib import Path
import click
REPO_ROOT = Path.cwd()
# Map role names to make targets.
ROLE_TARGET_MAP: dict[str, str] = {
"app_container": "molecule-app-container",
"app_hardening": "molecule-app-hardening",
"crowdsec": "molecule-crowdsec",
"disk_cleanup": "molecule-disk-cleanup",
"docker_base": "molecule-docker-base",
"observability": "molecule-observability",
"restore": "molecule-restore",
"sso_config": "molecule-sso-config",
"storage": "molecule-storage",
"zitadel": "molecule-zitadel",
}
# Playbooks that map to molecule scenarios (via roles they include).
PLAYBOOK_ROLE_MAP: dict[str, list[str]] = {
"ansible/playbooks/deploy-observability.yml": ["observability", "docker_base", "zitadel", "crowdsec"],
"ansible/playbooks/deploy-customer.yml": ["app_container", "docker_base", "app_hardening", "sso_config"],
"ansible/playbooks/configure-oidc.yml": ["sso_config", "app_container"],
"ansible/playbooks/prepare-vms.yml": ["docker_base", "app_hardening", "storage", "disk_cleanup", "crowdsec"],
}
# Shared infrastructure that affects all molecule tests.
SHARED_PATHS = (
"ansible/ansible.cfg",
"ansible/requirements.yml",
"ansible/molecule/",
)
# Minimum path parts for a role file: ansible/roles/<role> (3 parts).
# Files inside the role have more parts, but we only need the role name.
_MIN_ROLE_PATH_PARTS = 3
def _run_git(args: list[str]) -> str: # pragma: no cover
"""Run a git command and return stdout."""
result = subprocess.run( # nosec
["git", *args],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=False,
)
return result.stdout
def get_changed_files(base: str) -> list[str]:
"""Get list of changed files vs base ref."""
for ref in [base, "master"]:
output = _run_git(["diff", "--name-only", f"{ref}...HEAD"])
if output.strip():
return sorted(output.strip().splitlines())
return []
def detect_changed_roles(changed_files: list[str]) -> set[str]:
"""Detect which roles have changed files."""
roles: set[str] = set()
for filepath in changed_files:
# Check if file is in a role directory
if filepath.startswith("ansible/roles/"):
parts = filepath.split("/")
if len(parts) >= _MIN_ROLE_PATH_PARTS:
roles.add(parts[2])
# Check if file is a playbook that maps to roles
if filepath in PLAYBOOK_ROLE_MAP:
roles.update(PLAYBOOK_ROLE_MAP[filepath])
# Check shared infrastructure — triggers all roles
for shared in SHARED_PATHS:
if filepath.startswith(shared):
return set(ROLE_TARGET_MAP.keys())
return roles
def roles_to_targets(roles: set[str]) -> list[str]:
"""Convert role names to make targets."""
targets = []
for role in sorted(roles):
target = ROLE_TARGET_MAP.get(role)
if target:
targets.append(target)
return targets
@click.command()
@click.option(
"--base",
default="origin/master",
help="Base ref to compare against (default: origin/master).",
)
@click.option(
"--print-targets",
is_flag=True,
help="Print make targets (e.g. molecule-docker-base).",
)
@click.option(
"--print-roles",
is_flag=True,
help="Print role names (default if no --print-targets).",
)
def main(base: str, print_targets: bool, print_roles: bool) -> None:
"""Detect which Ansible roles changed and output molecule scenarios."""
changed_files = get_changed_files(base)
if not changed_files:
click.echo("No changed files detected.", err=True)
return
roles = detect_changed_roles(changed_files)
if not roles:
click.echo("No molecule scenarios affected by changes.", err=True)
return
if print_targets:
for target in roles_to_targets(roles):
click.echo(target)
else:
for role in sorted(roles):
click.echo(role)
if __name__ == "__main__": # pragma: no cover
main()
+333
View File
@@ -0,0 +1,333 @@
#!/usr/bin/env python3
"""Run molecule tests sequentially while polling Gitea for other runner failures.
Each pair is encoded as one of:
- **Single-role (4-part):** ``scenario|platform_name|platform_image|platform_command``
- **Multi-role (5-part):** ``role|scenario|platform_name|platform_image|platform_command``
Pairs are executed one at a time (molecule scenarios share temp directories and
Docker networks, so parallel execution within a single runner is unsafe).
A background thread polls the Gitea API. If any other molecule matrix runner
reports failure, the current molecule subprocess is killed and this runner
exits early with code 1.
Usage::
# Single-role
python3 -m devx.molecule.molecule_ci_guard pair1 pair2 ...
# Multi-role
python3 -m devx.molecule.molecule_ci_guard --roles-root ansible/roles pair1 pair2 ...
Environment variables:
GITEA_URL Base URL of the Gitea instance.
CI_GITEA_API_TOKEN API token with repo access (CI_GITEA_TOKEN accepted for legacy).
RUN_ID Workflow run ID (GITHUB_RUN_ID).
JOB_NAME Base job name (GITHUB_JOB), e.g. "molecule-tests".
MATRIX_INDEX Current matrix index (runner-index).
GITEA_REPOSITORY Repository in "owner/repo" format.
"""
from __future__ import annotations
import contextlib
import os
import signal
import subprocess # nosec B404
import sys
import threading
import time
from pathlib import Path
import click
import requests
from devx.config import REPO_NAME, REPO_OWNER
from devx.i18n import _
from devx.tokens import get_ci_token
POLL_INTERVAL = 10
def get_running_jobs(gitea_url: str, owner: str, repo: str, token: str, run_id: int) -> list[dict]:
"""Return jobs for the given workflow run."""
url = f"{gitea_url}/api/v1/repos/{owner}/{repo}/actions/runs/{run_id}/jobs"
headers = {"Authorization": f"token {token}"}
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
data = response.json()
return data.get("jobs", [])
def any_other_runner_failed(jobs: list[dict], current_job_name: str, current_index: int) -> bool:
"""Return True if any other molecule matrix job has failed."""
for job in jobs:
name = job.get("name", "")
if not name.startswith(current_job_name):
continue
if name == f"{current_job_name} ({current_index})" or name == current_job_name:
continue
if job.get("conclusion") == "failure":
return True
return False
def poll_for_other_failures(
gitea_url: str,
owner: str,
repo: str,
token: str,
run_id: int,
job_name: str,
current_index: int,
stop_event: threading.Event,
failed_event: threading.Event,
) -> None:
"""Background thread: poll API and signal if another runner fails."""
while not stop_event.is_set():
try:
jobs = get_running_jobs(gitea_url, owner, repo, token, run_id)
if any_other_runner_failed(jobs, job_name, current_index):
click.echo(_("Another molecule runner failed. Stopping this runner early."))
failed_event.set()
return
except requests.RequestException as exc:
click.echo(_("API poll warning: {exc}", exc=exc))
stop_event.wait(POLL_INTERVAL)
def build_molecule_cmd(scenario: str) -> list[str]:
"""Build the molecule command for a scenario."""
cmd = ["molecule", "test"]
if scenario != "default":
cmd.extend(["-s", scenario])
return cmd
def parse_pair(pair: str) -> tuple[str, str, str, str, str]:
"""Parse a pair string into (role, scenario, platform_name, platform_image, platform_command).
Supports both 4-part (single-role) and 5-part (multi-role) formats.
For 4-part pairs, role is empty (caller uses default role dir).
Spaces in the command field are encoded as ``__SPACE__`` to survive
shell word-splitting when ``$TEST_PAIRS`` is expanded unquoted.
"""
parts = pair.split("|")
if len(parts) == 4:
return "", parts[0], parts[1], parts[2], parts[3].replace("__SPACE__", " ")
if len(parts) == 5:
return parts[0], parts[1], parts[2], parts[3], parts[4].replace("__SPACE__", " ")
raise click.ClickException(f"Invalid pair format: {pair!r} (expected 4 or 5 pipe-delimited parts)")
def build_env_for_pair(pair: str, base_env: dict[str, str]) -> dict[str, str]:
"""Build environment for a single molecule pair."""
_role, _scenario, platform_name, platform_image, platform_command = parse_pair(pair)
env = base_env.copy()
# Append runner index to platform name when running in CI matrix to avoid
# Docker container name conflicts when multiple runners share the same Docker host.
matrix_index = env.get("MATRIX_INDEX")
if matrix_index:
platform_name = f"{platform_name}-r{matrix_index}"
env["MOLECULE_PLATFORM_NAME"] = platform_name
env["MOLECULE_PLATFORM_IMAGE"] = platform_image
if platform_command:
env["MOLECULE_PLATFORM_COMMAND"] = platform_command
elif "MOLECULE_PLATFORM_COMMAND" in env:
del env["MOLECULE_PLATFORM_COMMAND"]
env["ANSIBLE_ALLOW_BROKEN_CONDITIONALS"] = "true"
# Use a fresh MOLECULE_HOME per pair to avoid stale config cache
# from previous CI runs (causes "Instances missing" errors).
if "MOLECULE_HOME" not in env:
import tempfile
env["MOLECULE_HOME"] = tempfile.mkdtemp(prefix="molecule-ci-")
return env
def resolve_role_dir(role: str, roles_root: Path | None, repo_root: Path) -> Path:
"""Resolve the working directory for a molecule pair.
For multi-role pairs (role non-empty), uses ``roles_root/role``.
For single-role pairs, auto-discovers the first role with a molecule/
subdirectory under ``repo_root/ansible/roles/``.
"""
if role:
if roles_root is None:
roles_root = repo_root / "ansible" / "roles"
return roles_root / role
roles_dir = repo_root / "ansible" / "roles"
if roles_dir.is_dir():
role_dirs = sorted(d for d in roles_dir.iterdir() if (d / "molecule").is_dir())
if role_dirs:
return role_dirs[0]
return roles_dir / "role" # will produce a clear "not found" error
@click.command()
@click.argument("pairs", nargs=-1, required=True)
@click.option(
"--roles-root",
type=click.Path(exists=True, file_okay=False, path_type=Path),
default=None,
help="Root directory for multi-role pairs (e.g. ansible/roles). Required when pairs use 5-part format.",
)
def cli(pairs: tuple[str, ...], roles_root: Path | None) -> None:
"""Run molecule pairs sequentially, stop if another CI runner fails."""
gitea_url = os.environ.get("GITEA_URL", "")
try:
token = get_ci_token()
except click.ClickException:
token = None
run_id = int(os.environ.get("RUN_ID", "0"))
job_name = os.environ.get("JOB_NAME", "molecule-tests")
current_index = int(os.environ.get("MATRIX_INDEX", "0"))
repository = os.environ.get("GITEA_REPOSITORY", "")
owner, _sep, repo = repository.partition("/")
if not owner or not repo:
owner, repo = REPO_OWNER, REPO_NAME
if not all([gitea_url, token, run_id]):
click.echo(_("GITEA_URL/CI_GITEA_TOKEN/RUN_ID not set; running without cross-runner cancellation."))
# When devx is installed as a pip package, __file__ resolves to the
# site-packages directory, not the repo root. Use GITHUB_WORKSPACE
# (set by Gitea Actions) or cwd as the repo root.
repo_root = Path(os.environ.get("GITHUB_WORKSPACE", os.getcwd())).resolve()
base_env = os.environ.copy()
base_env.setdefault("DOCKER_HOST", f"unix:///run/user/{os.getuid()}/docker.sock")
base_env.setdefault("ANSIBLE_INJECT_INVOCATION", "1")
stop_event = threading.Event()
failed_event = threading.Event()
if gitea_url and token and run_id:
poller = threading.Thread(
target=poll_for_other_failures,
args=(
gitea_url,
owner,
repo,
token,
run_id,
job_name,
current_index,
stop_event,
failed_event,
),
daemon=True,
)
poller.start()
try:
for pair in pairs:
if failed_event.is_set():
sys.exit(1)
role, scenario, platform_name, _img, _cmd = parse_pair(pair)
click.echo(_("Running: {scenario} on {platform}", scenario=scenario, platform=platform_name))
cmd = build_molecule_cmd(scenario)
env = build_env_for_pair(pair, base_env)
cwd = resolve_role_dir(role, roles_root, repo_root)
process = subprocess.Popen( # nosec B603
cmd,
cwd=str(cwd),
env=env,
preexec_fn=os.setsid,
)
try:
while process.poll() is None:
if failed_event.is_set():
with contextlib.suppress(ProcessLookupError):
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
with contextlib.suppress(ProcessLookupError):
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
process.wait()
# Clean up containers left behind by the killed test.
click.echo(_("Cleaning up: running molecule destroy for {scenario}", scenario=scenario))
destroy_cmd = ["molecule", "destroy"]
if scenario != "default":
destroy_cmd.extend(["-s", scenario])
with contextlib.suppress(subprocess.SubprocessError, OSError):
subprocess.run( # nosec B603, B607
destroy_cmd,
cwd=str(cwd),
env=env,
check=False,
capture_output=True,
timeout=120,
)
sys.exit(1)
time.sleep(1)
except KeyboardInterrupt:
with contextlib.suppress(ProcessLookupError):
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
process.wait()
# Clean up containers left behind by the interrupted test.
click.echo(_("Cleaning up: running molecule destroy for {scenario}", scenario=scenario))
destroy_cmd = ["molecule", "destroy"]
if scenario != "default":
destroy_cmd.extend(["-s", scenario])
with contextlib.suppress(subprocess.SubprocessError, OSError):
subprocess.run( # nosec B603, B607
destroy_cmd,
cwd=str(cwd),
env=env,
check=False,
capture_output=True,
timeout=120,
)
sys.exit(1)
rc = process.returncode
if rc != 0:
click.echo(_("FAILED: {pair} exited with code {code}", pair=pair, code=rc))
# Run molecule destroy to clean up containers left behind by the
# failed test. Without this, containers stay running and accumulate
# on the runner, consuming disk/memory and degrading CI performance.
click.echo(_("Cleaning up: running molecule destroy for {scenario}", scenario=scenario))
destroy_cmd = ["molecule", "destroy"]
if scenario != "default":
destroy_cmd.extend(["-s", scenario])
with contextlib.suppress(subprocess.SubprocessError, OSError):
subprocess.run( # nosec B603, B607
destroy_cmd,
cwd=str(cwd),
env=env,
check=False,
capture_output=True,
timeout=120,
)
sys.exit(rc)
click.echo(_("PASSED: {pair}", pair=pair))
# Prune Docker data between scenarios to prevent disk exhaustion
# in Docker-in-Docker molecule containers (each scenario pulls
# hundreds of MB of images that accumulate across pairs).
with contextlib.suppress(subprocess.SubprocessError, OSError):
subprocess.run( # nosec B603, B607
["docker", "system", "prune", "-af", "--volumes"],
check=False,
capture_output=True,
timeout=60,
)
click.echo(_("All molecule tests passed."))
finally:
stop_event.set()
sys.exit(0)
if __name__ == "__main__": # pragma: no cover
cli()
+27 -217
View File
@@ -10,12 +10,6 @@ If the host socket is not available, it tries the rootless socket, then
starts a local ``dockerd`` with the vfs storage driver (requires
privileged container).
When the host socket IS available but has limited disk space (e.g. an
inner DinD daemon writing to a 38 GB container overlay), the script
prefers a rootless socket that has more available space. This prevents
"no space left on device" errors during molecule tests that pull images
and create containers via the Docker daemon.
Usage::
python3 -m devx.molecule.start_docker [--timeout 30]
@@ -23,10 +17,8 @@ Usage::
from __future__ import annotations
import contextlib
import glob
import os
import shutil
import subprocess # nosec B404
import sys
import tempfile
@@ -40,16 +32,6 @@ DEFAULT_TIMEOUT = 30
DOCKER_SOCK = "/var/run/docker.sock"
# Rootless socket fallback (e.g. /run/user/994/docker.sock)
ROOTLESS_SOCK = f"/run/user/{os.getuid()}/docker.sock"
# Host Docker socket mounted by gitea_runner config (see runner config
# ``options: "-v /run/user/<uid>/docker.sock:/run/host-docker.sock"``).
# This gives CI containers access to the host's rootless Docker daemon,
# which has the full host filesystem (e.g. 455 GB) instead of the
# container's limited overlay (e.g. 38 GB).
HOST_DOCKER_SOCK = "/run/host-docker.sock"
# Minimum free bytes for a Docker daemon to be considered usable.
# Below this, image pulls and container creation will fail with ENOSPC.
# 20 GB leaves room for molecule-test-base (~500 MB) + a few containers.
MIN_FREE_BYTES = 20 * 1024**3 # 20 GB
def is_docker_ready() -> bool:
@@ -64,46 +46,6 @@ def is_docker_ready() -> bool:
return result.returncode == 0
def _get_docker_free_bytes() -> int:
"""Get free disk space (bytes) at the Docker daemon's data root.
Returns 0 if the daemon is not reachable or the data root cannot be
determined.
"""
docker_host = os.environ.get("DOCKER_HOST", f"unix://{DOCKER_SOCK}")
try:
result = subprocess.run( # nosec B603 B607
[
"docker",
"info",
"--format",
"{{.DockerRootDir}}",
],
capture_output=True,
text=True,
timeout=10,
check=False,
env={**os.environ, "DOCKER_HOST": docker_host},
)
if result.returncode != 0 or not result.stdout.strip():
return 0
data_root = result.stdout.strip()
if not os.path.exists(data_root):
return 0
return shutil.disk_usage(data_root).free
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
return 0
def _try_socket(sock_path: str) -> bool:
"""Set DOCKER_HOST to *sock_path* and check if the daemon is ready.
Returns ``True`` if the daemon responds, ``False`` otherwise.
"""
os.environ["DOCKER_HOST"] = f"unix://{sock_path}"
return is_docker_ready()
def _diagnose_socket() -> None:
"""Print diagnostic info about the Docker socket."""
click.echo(f"DOCKER_HOST = {os.environ.get('DOCKER_HOST', '(not set)')}")
@@ -155,177 +97,50 @@ def _diagnose_socket() -> None:
def start_docker_daemon(timeout: int = DEFAULT_TIMEOUT) -> bool:
"""Ensure Docker is ready for molecule tests.
Tries sockets in this order, preferring ones with enough disk space:
1. Host rootless socket (``/run/host-docker.sock``) mounted by the
gitea runner config, has access to the host's full filesystem
(e.g. 455 GB). Preferred over the inner dockerd.
2. Default socket (``/var/run/docker.sock``) may be an inner dockerd
started by the CI image (v29.5.3) with data root on the container's
limited overlay (e.g. 38 GB, often 100 % full).
3. Other rootless sockets (``/run/user/*/docker.sock``).
4. Local ``dockerd`` with vfs storage driver last resort.
First tries the host socket. If that works, sets ``DOCKER_HOST`` and
returns immediately. If not, tries the rootless socket. If neither
works, starts a local ``dockerd`` with vfs storage driver (requires
privileged container).
Returns ``True`` if Docker is ready, ``False`` if it failed to
start within the timeout.
"""
# Point Docker CLI and Python library to the socket explicitly
os.environ["DOCKER_HOST"] = f"unix://{DOCKER_SOCK}"
# Diagnose socket state
click.echo("--- Docker socket diagnostics ---")
_diagnose_socket()
click.echo("--- End diagnostics ---")
# Collect candidate sockets in priority order.
# The host's rootless Docker socket (mounted at /run/host-docker.sock
# by the gitea runner config) is preferred — it has access to the
# host's full filesystem instead of the container's limited overlay.
candidates: list[str] = []
if os.path.exists(HOST_DOCKER_SOCK):
candidates.append(HOST_DOCKER_SOCK)
if os.path.exists(DOCKER_SOCK):
candidates.append(DOCKER_SOCK)
if os.path.exists(ROOTLESS_SOCK):
candidates.append(ROOTLESS_SOCK)
for sock in sorted(glob.glob("/run/user/*/docker.sock")):
if sock not in candidates:
candidates.append(sock)
# Check if host Docker is already available
if is_docker_ready():
click.echo(_("Docker daemon already running"))
return True
# Try each candidate socket — prefer one with enough free space
for sock in candidates:
click.echo(f"Trying socket: {sock}")
if not _try_socket(sock):
# Try rootless socket (e.g. /run/user/994/docker.sock)
click.echo(f"Trying rootless socket: {ROOTLESS_SOCK}")
os.environ["DOCKER_HOST"] = f"unix://{ROOTLESS_SOCK}"
if os.path.exists(ROOTLESS_SOCK) and is_docker_ready():
click.echo(_("Docker daemon already running"))
return True
# Scan for any rootless sockets at other UIDs
for sock in sorted(glob.glob("/run/user/*/docker.sock")):
if sock == ROOTLESS_SOCK:
continue
free_bytes = _get_docker_free_bytes()
free_gb = free_bytes / 1024**3
click.echo(f" Docker daemon ready (free space: {free_gb:.1f} GB)")
if free_bytes >= MIN_FREE_BYTES:
click.echo(f"Trying alternative rootless socket: {sock}")
os.environ["DOCKER_HOST"] = f"unix://{sock}"
if is_docker_ready():
click.echo(_("Docker daemon already running"))
return True
# If free_bytes is 0, the Docker root dir is on the host filesystem
# (not accessible from inside the container). This is expected for
# the host's rootless Docker — it has the full host disk.
# Only trust this for /run/host-docker.sock (known host socket).
# For other sockets (e.g. inner dockerd), free_bytes == 0 means
# the data root path doesn't exist inside the container — the
# inner dockerd may be using the container's full overlay.
if free_bytes == 0 and sock == HOST_DOCKER_SOCK:
click.echo("Host rootless Docker root dir not accessible from container, using it")
return True
# If free_bytes is 0 and there are no dockerd processes inside the
# container, the socket is the host's Docker (mounted from outside).
# The data root is on the host filesystem and has plenty of space.
if free_bytes == 0 and sock == DOCKER_SOCK:
has_inner_dockerd = False
with contextlib.suppress(Exception):
pgrep_result = subprocess.run( # nosec B603 B607
["pgrep", "-f", "dockerd"],
capture_output=True,
text=True,
timeout=5,
)
has_inner_dockerd = pgrep_result.returncode == 0
if not has_inner_dockerd:
click.echo("No inner dockerd found, socket is host Docker (data root on host), using it")
return True
click.echo(f" Insufficient space ({free_gb:.1f} GB), trying next...")
# No socket with sufficient space found.
# Don't fall back to the low-space inner dockerd — it will fail
# on image pulls. Instead, kill the inner dockerd, clean up its
# data root to free space, and start a new dockerd using the
# freed space on the container's overlay.
click.echo(_("Host Docker not available, starting local dockerd..."))
# Kill the inner dockerd (started by the CI image) to free its
# data root and socket. The inner dockerd uses the container's
# overlay (38G, often 100% full). Killing it frees up the
# socket and any space used by its containers/volumes.
# Use SIGKILL (-9) since the inner dockerd may not respond to SIGTERM.
# Try multiple approaches to ensure the inner dockerd is killed.
with contextlib.suppress(Exception):
result = subprocess.run( # nosec B603 B607
["pgrep", "-af", "dockerd"],
capture_output=True,
text=True,
timeout=5,
)
if result.stdout.strip():
click.echo(f" dockerd processes before kill: {result.stdout.strip()}")
for pattern in ["dockerd", "dockerd-entrypoint.sh", "containerd"]:
with contextlib.suppress(Exception):
subprocess.run( # nosec B603 B607
["pkill", "-9", "-f", pattern],
check=False,
timeout=5,
)
time.sleep(3)
# Check if dockerd processes are still alive
with contextlib.suppress(Exception):
result = subprocess.run( # nosec B603 B607
["pgrep", "-af", "dockerd"],
capture_output=True,
text=True,
timeout=5,
)
if result.stdout.strip():
click.echo(f" dockerd processes after kill: {result.stdout.strip()}")
# Try killing by PID directly
for pid_str in result.stdout.split("\n"):
pid = pid_str.split()[0] if pid_str.strip() else ""
if pid:
with contextlib.suppress(Exception):
os.kill(int(pid), 9)
time.sleep(2)
# Verify the inner dockerd is actually dead. If we can still
# connect to /var/run/docker.sock, the old daemon is still running
# and we need to use a different socket path.
old_daemon_alive = False
with contextlib.suppress(Exception):
result = subprocess.run( # nosec B603 B607
["docker", "info"],
env={**os.environ, "DOCKER_HOST": f"unix://{DOCKER_SOCK}"},
capture_output=True,
timeout=5,
)
old_daemon_alive = result.returncode == 0
if old_daemon_alive:
click.echo(" Inner dockerd still alive, using alternate socket")
local_sock = "/dev/shm/docker.sock" # nosec B108
else:
local_sock = DOCKER_SOCK
# Clean up the inner dockerd's data root to free space.
# The inner dockerd stores images, containers, and volumes here.
# Removing them frees up ~2.4GB on the container's overlay.
inner_data_root = "/home/grm-ci-runner-*/.local/share/docker"
rm_paths = " ".join(f"{inner_data_root}/{d}" for d in ("overlay2", "image", "volumes", "containers"))
with contextlib.suppress(Exception):
subprocess.run( # nosec B603 B607
["sh", "-c", f"rm -rf {rm_paths}"],
check=False,
timeout=30,
)
# Use a fresh data root. If the inner dockerd is dead, use the
# container's overlay (38G, with freed space). If the inner
# dockerd is still alive, use /dev/shm (16G tmpfs) — the overlay
# is still full because the inner dockerd's data can't be cleaned.
docker_data_root = "/dev/shm/docker" if old_daemon_alive else "/tmp/docker-data" # nosec B108
# Remove stale socket if present
with contextlib.suppress(OSError):
os.unlink(local_sock)
os.environ["DOCKER_HOST"] = f"unix://{local_sock}"
# Reset DOCKER_HOST to host socket for local dockerd
os.environ["DOCKER_HOST"] = f"unix://{DOCKER_SOCK}"
# Start local dockerd (requires privileged container)
# Use /tmp/docker-data as data root on the container's overlay.
# The inner dockerd's data root has been cleaned up, freeing ~2.4GB.
# vfs storage driver is used since overlay2 may not work inside
# a Docker-in-Docker container without --privileged.
log_file = tempfile.NamedTemporaryFile( # noqa: SIM115
mode="w", suffix="dockerd.log", delete=False
)
@@ -335,13 +150,8 @@ def start_docker_daemon(timeout: int = DEFAULT_TIMEOUT) -> bool:
"dockerd",
"--storage-driver",
"vfs",
"--data-root",
docker_data_root,
"--iptables=false",
"--ip6tables=false",
"--bridge=none",
"-H",
f"unix://{local_sock}",
f"unix://{DOCKER_SOCK}",
],
stdout=log_file,
stderr=subprocess.STDOUT,
+16 -2
View File
@@ -2,16 +2,19 @@
Centralizes Gitea/Vikunja token discovery with role-based environment
variable names and backwards compatibility with the legacy
``CI_GITEA_TOKEN`` naming convention.
``CI_GITEA_TOKEN`` / ``REVIEW_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``) are accepted for backwards compatibility.
- Legacy names (``CI_GITEA_TOKEN``, ``REVIEW_GITEA_TOKEN``) are accepted for
backwards compatibility.
- If no role-specific token is set, the generic CI tokens are tried last.
"""
@@ -25,6 +28,12 @@ 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"]
@@ -52,6 +61,11 @@ 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)
+10 -168
View File
@@ -40,17 +40,13 @@ 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
@@ -192,188 +188,38 @@ 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, tag in zip(full_tags, spec.tags, strict=False):
for ft in full_tags:
cmd = ["docker", "push", ft]
if dry_run:
click.echo(f"[dry-run] {' '.join(cmd)}")
continue
click.echo(f"Pushing {ft}...")
@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,
result = subprocess.run( # nosec B603
cmd,
capture_output=True,
text=True,
check=False,
)
def _attempt(_cmd: list[str] = cmd) -> subprocess.CompletedProcess[str]:
return _run_push(_cmd)
try:
result = _attempt()
except PushHTTP500Error as e:
if result.returncode != 0:
click.echo(
_("Push failed for {tag}: {error}", tag=ft, error=str(e)),
_("Push failed for {tag}: {error}", tag=ft, error=result.stderr.strip()),
err=True,
)
all_ok = False
continue
if result.returncode == 0:
else:
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
@@ -474,15 +320,11 @@ 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, username=push_username, token=push_token): # type: ignore[arg-type]
if push and not push_image(spec, registry, dry_run=dry_run): # type: ignore[arg-type]
failed.append(spec.name)
if failed:
-232
View File
@@ -1,232 +0,0 @@
"""Check Ansible tasks for missing no_log on secret-handling tasks.
ansible-lint's built-in ``no-log-password`` rule only fires when a module
parameter is literally named ``*password*`` and there's a loop. It does
NOT catch:
- Shell/command tasks that interpolate ``{{ _secrets.* }}`` or
``{{ *password* }}`` variables
- Template/copy tasks that render secret values without ``no_log``
This script fills that gap by scanning all Ansible task files for
variables that look like secrets (``_secrets.*``, ``*password*``,
``*secret*``, ``*token*``, ``*api_key*``) and verifying that the task
has ``no_log`` set to a non-False value.
Usage::
python -m devx.tools.check_ansible_no_log
python -m devx.tools.check_ansible_no_log --path ansible/roles/my_role
python -m devx.tools.check_ansible_no_log --ansible-dir ansible/roles
Exit code 0 if all secret-handling tasks have no_log, 1 otherwise.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
import click
import yaml
REPO_ROOT = Path.cwd()
DEFAULT_ANSIBLE_DIR = REPO_ROOT / "ansible"
# Patterns that indicate a task is handling secrets.
# We only match Jinja-interpolated variables ({{ ... }}) to avoid false
# positives from field names like "password" in module params or task names.
SECRET_PATTERNS = [
# {{ _secrets.anything }} or {{ _secrets['anything'] }}
re.compile(r"\{\{[^}]*_secrets\.", re.IGNORECASE),
# {{ anything_password }} but NOT the word "password" in a string literal
re.compile(r"\{\{[^}]*password", re.IGNORECASE),
# {{ anything_secret }}
re.compile(r"\{\{[^}]*_secret\b", re.IGNORECASE),
# {{ anything_api_key }}
re.compile(r"\{\{[^}]*api_key", re.IGNORECASE),
# {{ anything_token }} (but not loop tokens like {{ loop_token }})
re.compile(r"\{\{[^}]*(?:vault_token|auth_token|access_token|bot_token)", re.IGNORECASE),
]
# Task keys whose values might contain secret references
TASK_VALUE_KEYS = {
"shell",
"command",
"ansible.builtin.shell",
"ansible.builtin.command",
"ansible.builtin.template",
"ansible.builtin.copy",
"ansible.builtin.debug",
"template",
"copy",
"debug",
"cmd",
"msg",
"content",
}
# Keys that are NOT secret-bearing (task metadata, not values)
NON_VALUE_KEYS = {
"name",
"when",
"loop",
"loop_control",
"changed_when",
"failed_when",
"no_log",
"register",
"tags",
"vars",
"become",
"become_user",
"delegate_to",
"run_once",
"environment",
"with_items",
"with_dict",
"with_list",
}
def _contains_secret(value: object) -> bool:
"""Recursively check if a value contains secret-like variable references."""
if isinstance(value, str):
return any(p.search(value) for p in SECRET_PATTERNS)
if isinstance(value, dict):
return any(_contains_secret(v) for v in value.values())
if isinstance(value, list):
return any(_contains_secret(item) for item in value)
return False
def _has_no_log(task: dict) -> bool:
"""Check if a task has no_log set to a non-False value."""
no_log = task.get("no_log", False)
# Jinja expressions (e.g. "{{ not debug_mode }}") count as set
return no_log is not False and no_log is not None
def _check_task(task: dict, file_path: Path, task_num: int) -> list[str]:
"""Check a single task for missing no_log on secret values.
Returns a list of violation messages (empty if OK).
"""
violations: list[str] = []
# Skip tasks that already have no_log
if _has_no_log(task):
return violations
# Check all string values in the task for secret references
has_secrets = False
for key, value in task.items():
if key in NON_VALUE_KEYS:
continue
# Check action module params (shell, command, copy, template, etc.)
if _contains_secret(value):
has_secrets = True
break
if has_secrets:
task_name = task.get("name", "<unnamed>")
violations.append(
f"{file_path}:{task_num}: Task '{task_name}' references secrets "
f"but has no no_log. Add `no_log: true` or "
f'`no_log: "{{{{ not (debug_mode | default(false) | bool) }}}}"` '
f"to prevent credential leakage in Ansible output."
)
return violations
def check_directory(ansible_dir: Path) -> list[str]:
"""Check all Ansible task files in a directory tree."""
all_violations: list[str] = []
# Find all task files
task_files = list(ansible_dir.rglob("tasks/*.yml"))
task_files += list(ansible_dir.rglob("tasks/*.yaml"))
# Also check playbook files
task_files += list(ansible_dir.glob("playbooks/*.yml"))
for task_file in sorted(task_files):
# Skip molecule test files
if "molecule" in task_file.parts:
continue
try:
with task_file.open() as f:
docs = list(yaml.safe_load_all(f))
except (yaml.YAMLError, OSError):
continue
for doc in docs:
if not doc:
continue
# Task files are bare lists of tasks; playbook files are
# lists of plays (each play is a dict with 'hosts' key)
if isinstance(doc, list):
is_plays = isinstance(doc[0], dict) and "hosts" in doc[0]
if not is_plays:
for i, task in enumerate(doc):
if not isinstance(task, dict):
continue
all_violations.extend(_check_task(task, task_file, i + 1))
continue
plays = doc
elif isinstance(doc, dict):
plays = [doc]
else:
continue
for play in plays:
if not isinstance(play, dict):
continue
for task_section in ("tasks", "pre_tasks", "post_tasks", "handlers"):
tasks = play.get(task_section, [])
if not isinstance(tasks, list):
continue
for i, task in enumerate(tasks):
if not isinstance(task, dict):
continue
all_violations.extend(_check_task(task, task_file, i + 1))
return all_violations
@click.command()
@click.option(
"--path",
type=click.Path(exists=True, path_type=Path),
help="Check a specific file or directory (default: ansible/).",
)
@click.option(
"--ansible-dir",
type=click.Path(exists=True, path_type=Path),
default=None,
help="Override the default ansible directory (default: ansible/).",
)
def main(path: Path | None, ansible_dir: Path | None) -> None:
"""Check that Ansible tasks handling secrets have no_log set."""
target = path or ansible_dir or DEFAULT_ANSIBLE_DIR
if not target.is_dir():
click.echo(f"Error: {target} is not a directory", err=True)
sys.exit(2)
violations = check_directory(target)
if violations:
click.echo(f"Found {len(violations)} task(s) handling secrets without no_log:\n")
for v in violations:
click.echo(f" {v}")
click.echo(f"\nTotal: {len(violations)} violation(s).")
sys.exit(1)
click.echo(f"[check-ansible-no-log] All secret-handling tasks have no_log. ({target})")
if __name__ == "__main__": # pragma: no cover
main()
@@ -1,176 +0,0 @@
"""Check Ansible tasks for ``state: absent`` on database data directories.
This is a static analysis lint check that runs in CI (``make lint-ci``)
to prevent the class of bug that caused the 2026-07-22 production outage
(ADR-0028): a ``state: absent`` on a PostgreSQL data directory path that
fired on every deploy and wiped the ZITADEL database.
The existing unit test ``scripts/tests/test_no_zitadel_db_wipe.py`` covers
the same concern as a regression test. This lint check runs earlier in
the pipeline (before tests) and covers ALL roles and playbooks, not just
the ZITADEL role.
Allowed contexts (where DB recreation is legitimate):
- PostgreSQL major version upgrades (``upgrade-postgres``, ``PG_VERSION``)
- Explicit ``# lint:allow-state-absent`` comment on the task
Usage::
python -m devx.tools.check_ansible_no_state_absent_on_db
python -m devx.tools.check_ansible_no_state_absent_on_db --path ansible/roles/zitadel/tasks/main.yml
Exit code 0 if no violations found, 1 otherwise.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
import click
REPO_ROOT = Path.cwd()
DEFAULT_ANSIBLE_DIRS: list[Path] = [
REPO_ROOT / "ansible" / "playbooks",
REPO_ROOT / "ansible" / "roles",
]
# Database data directory path patterns.
# These match the DIRECTORY path, not individual files within it.
# Removing a stale config file (e.g. postgresql.conf) is safe; removing
# the entire data directory is not.
DB_PATH_PATTERNS = (
re.compile(r"postgres/zitadel-db", re.IGNORECASE),
re.compile(r"postgres/\w+-db", re.IGNORECASE),
re.compile(r"/var/lib/postgresql/data", re.IGNORECASE),
re.compile(r"/var/lib/postgresql/data/\w+-db", re.IGNORECASE),
)
# Destructive operations
DESTRUCTIVE_PATTERNS = (
re.compile(r"state:\s*absent", re.IGNORECASE),
re.compile(r"rm\s+-rf.*\bdb\b", re.IGNORECASE),
)
# Allowed contexts where DB recreation is legitimate
ALLOWED_CONTEXT_KEYWORDS = (
"upgrade-postgres",
"PG_VERSION",
"pg_version",
)
# Comment marker to explicitly allow state: absent on a specific task
ALLOW_MARKER = "lint:allow-state-absent"
def _find_task_files(base: Path) -> list[Path]:
"""Find all YAML task files under a base directory, skipping molecule."""
if base.is_file() and base.suffix in (".yml", ".yaml"):
return [base]
if not base.is_dir():
return []
files: list[Path] = []
for f in sorted(base.rglob("*.yml")) + sorted(base.rglob("*.yaml")):
if "molecule" in f.parts:
continue
files.append(f)
return files
def _check_file(filepath: Path, repo_root: Path) -> list[str]:
"""Check a YAML file for state: absent on DB data directory paths.
Returns a list of violation messages (empty if clean).
"""
try:
content = filepath.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return []
# Quick check: if no DB path pattern appears anywhere, skip
if not any(p.search(content) for p in DB_PATH_PATTERNS):
return []
try:
display_path = filepath.relative_to(repo_root)
except ValueError:
display_path = filepath
violations: list[str] = []
lines = content.splitlines()
for i, line in enumerate(lines):
for db_pattern in DB_PATH_PATTERNS:
if not db_pattern.search(line):
continue
# Check surrounding context (±5 lines) for destructive operations
context_start = max(0, i - 5)
context_end = min(len(lines), i + 6)
context = "\n".join(lines[context_start:context_end])
# Skip if in an allowed context (PG upgrade)
if any(kw in context for kw in ALLOWED_CONTEXT_KEYWORDS):
continue
# Skip if the allow marker comment is in the context
if ALLOW_MARKER in context:
continue
for dp in DESTRUCTIVE_PATTERNS:
if dp.search(context):
violations.append(
f"{display_path}:{i + 1} — destructive operation "
f"({dp.pattern!r}) near DB data directory path "
f"({db_pattern.pattern!r}). "
f"Database directories must never be wiped automatically (ADR-0028). "
f"If this is legitimate (e.g. PG upgrade), add "
f"#{ALLOW_MARKER} to the task."
)
break
return violations
@click.command()
@click.option(
"--path",
type=click.Path(exists=True, path_type=Path),
help="Check a specific file or directory (default: ansible/playbooks + ansible/roles).",
)
@click.option(
"--ansible-dir",
"ansible_dirs",
type=click.Path(exists=True, path_type=Path),
multiple=True,
default=None,
help="Override the default ansible directories (can be repeated). Defaults to ansible/playbooks and ansible/roles.",
)
def main(path: Path | None, ansible_dirs: tuple[Path, ...]) -> None:
"""Check that no Ansible task uses state: absent on a DB data directory."""
dirs = list(ansible_dirs) if ansible_dirs else DEFAULT_ANSIBLE_DIRS
if path:
files = _find_task_files(path)
else:
files: list[Path] = []
for d in dirs:
files.extend(_find_task_files(d))
all_violations: list[str] = []
for f in files:
all_violations.extend(_check_file(f, REPO_ROOT))
if all_violations:
click.echo("[check-ansible-no-state-absent-on-db] FAIL: destructive operations on DB paths:")
for v in all_violations:
click.echo(f" - {v}")
click.echo(f"\nTotal: {len(all_violations)} violation(s).")
click.echo("Database data directories must never be wiped automatically (ADR-0028).")
sys.exit(1)
else:
click.echo("[check-ansible-no-state-absent-on-db] OK: no destructive operations on DB paths.")
if __name__ == "__main__": # pragma: no cover
main()
-345
View File
@@ -1,345 +0,0 @@
"""Check Ansible tasks for dangerous patterns that mask failures.
This check addresses the gap identified in the testing-strategy audit:
the automated PR review only checks Python files, and ``ansible-lint``
runs at ``profile: basic`` which does not catch dangerous patterns like:
- ``|| true`` on tasks that are NOT cleanup/idempotency operations
- ``failed_when: false`` on critical tasks (e.g. DB operations)
- ``2>/dev/null`` on tasks where stderr contains important diagnostics
Most ``|| true`` and ``2>/dev/null`` instances in the codebase are
legitimate (container removal, journalctl, apt-get, docker prune, SUID
removal). This check flags only instances that are NOT in a known-safe
context. Tasks can also opt out with a ``# lint:allow-failure-masking``
comment.
Usage::
python -m devx.tools.check_ansible_patterns
python -m devx.tools.check_ansible_patterns --path ansible/roles/app_container/tasks/main.yml
Exit code 0 if no violations found, 1 otherwise.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
import click
import yaml
REPO_ROOT = Path.cwd()
DEFAULT_ANSIBLE_DIRS: list[Path] = [
REPO_ROOT / "ansible" / "playbooks",
REPO_ROOT / "ansible" / "roles",
]
# Comment marker to explicitly allow a pattern on a specific task
ALLOW_MARKER = "lint:allow-failure-masking"
# Patterns that mask failures when used in shell/command tasks
OR_TRUE_PATTERN = re.compile(r"\|\|\s*true\b", re.IGNORECASE)
REDIRECT_DEVNULL_PATTERN = re.compile(r"2>/dev/null")
# Module keys that accept shell/command strings
SHELL_MODULE_KEYS = frozenset(
{
"shell",
"command",
"ansible.builtin.shell",
"ansible.builtin.command",
"cmd",
"ansible.builtin.raw",
"raw",
}
)
# Task keys whose values might contain shell commands
COMMAND_VALUE_KEYS = frozenset(
{
"shell",
"command",
"ansible.builtin.shell",
"ansible.builtin.command",
"cmd",
"raw",
"ansible.builtin.raw",
}
)
# Legitimate contexts where || true or 2>/dev/null are safe.
# These are command prefixes or task names that indicate cleanup/idempotency.
LEGITIMATE_COMMAND_PREFIXES = (
# Container/process removal (may not exist)
"docker rm",
"docker stop",
"docker rmi",
"docker network rm",
"docker volume rm",
"pkill",
"kill",
# Cleanup commands that are expected to sometimes fail
"journalctl --vacuum",
"apt-get clean",
"apt-get autoremove",
"docker image prune",
"docker container prune",
"docker volume prune",
"docker builder prune",
"find / -name",
# SUID removal (binaries may not exist)
"chmod",
"rm -f",
# Network connection checks (may fail if not connected)
"docker network connect",
# Prometheus snapshot API (may fail if no snapshot)
"curl.*api/v2/admin/tsdb/snapshot",
)
LEGITIMATE_TASK_NAME_KEYWORDS = (
"remove",
"cleanup",
"clean up",
"prune",
"purge",
"disconnect",
"stop",
"kill",
"strip suid",
"suid",
"vacuum",
"ensure.*absent",
"may not exist",
"if exists",
"optional",
"best effort",
"no-op",
"noop",
"idempotent",
"sync",
)
# Tasks with failed_when: false that are critical and should not mask failures.
# Only flag operations that SHOULD fail loudly — writing secrets, provisioning
# users, creating OIDC apps. Do NOT flag stop/start/check/wait/migrate/restore
# operations where failed_when: false is legitimate (container may not exist,
# may already be stopped, etc.).
CRITICAL_TASK_KEYWORDS = (
"password",
"secret",
"provision",
"oidc",
)
# Task name keywords that indicate failed_when: false is legitimate
LEGITIMATE_FAILED_WHEN_KEYWORDS = (
"stop",
"start",
"check",
"wait",
"migrate",
"restart",
"rebuild",
"restore",
"remove",
"cleanup",
"sync",
"download",
"extract",
"verify",
)
def _is_legitimate_or_true(command_str: str, task_name: str) -> bool:
"""Check if a || true in a command is in a legitimate context."""
# Check task name for legitimate keywords
name_lower = task_name.lower()
if any(re.search(kw, name_lower) for kw in LEGITIMATE_TASK_NAME_KEYWORDS):
return True
# Check command prefix for legitimate patterns
cmd_lower = command_str.lower()
return any(re.search(prefix, cmd_lower) for prefix in LEGITIMATE_COMMAND_PREFIXES)
def _is_legitimate_devnull(command_str: str, task_name: str) -> bool:
"""Check if a 2>/dev/null in a command is in a legitimate context."""
# 2>/dev/null is almost always safe — it suppresses stderr noise.
# Only flag it if the task is critical (DB, backup, OIDC) AND
# there's no || true (which is the more dangerous pattern).
return _is_legitimate_or_true(command_str, task_name)
def _check_task(task: dict, filepath: Path, task_num: int, repo_root: Path) -> list[str]:
"""Check a single task for dangerous failure-masking patterns."""
violations: list[str] = []
try:
display_path = filepath.relative_to(repo_root)
except ValueError:
display_path = filepath
task_name = task.get("name", "<unnamed>")
# Check for the allow marker in the task name
# (YAML comments are not preserved by safe_load, so we check the
# task name for the marker as a workaround)
if ALLOW_MARKER in task_name:
return violations
# Check for || true in command/shell values
for key in COMMAND_VALUE_KEYS:
value = task.get(key)
if value is None:
continue
value_str = str(value)
if OR_TRUE_PATTERN.search(value_str) and not _is_legitimate_or_true(value_str, task_name):
violations.append(
f"{display_path}:{task_num} — task '{task_name}' uses "
f"'|| true' in {key} which may mask real failures. "
f"If this is a cleanup/idempotency operation, rename the "
f"task to include 'remove'/'cleanup'/'prune' or add "
f"#{ALLOW_MARKER} to the task."
)
# Check for failed_when: false on critical tasks
failed_when = task.get("failed_when")
if failed_when is False:
name_lower = task_name.lower()
# Skip if the task name indicates a legitimate failed_when: false context
is_legitimate = any(kw in name_lower for kw in LEGITIMATE_FAILED_WHEN_KEYWORDS)
if not is_legitimate:
for kw in CRITICAL_TASK_KEYWORDS:
if kw in name_lower:
violations.append(
f"{display_path}:{task_num} — critical task '{task_name}' "
f"has failed_when: false, which masks failures on "
f"a {kw}-related operation. Remove failed_when: false "
f"or add #{ALLOW_MARKER} if masking is intentional."
)
break
return violations
def _check_file(filepath: Path, repo_root: Path) -> list[str]:
"""Check a YAML file for dangerous failure-masking patterns."""
try:
content = filepath.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return []
# Quick check: if no patterns appear, skip
if not (
OR_TRUE_PATTERN.search(content) or "failed_when: false" in content or REDIRECT_DEVNULL_PATTERN.search(content)
):
return []
# Check for allow markers in comments
has_allow_marker = ALLOW_MARKER in content
try:
docs = list(yaml.safe_load_all(content))
except yaml.YAMLError:
return []
violations: list[str] = []
for doc in docs:
if not doc:
continue
if isinstance(doc, list):
for i, item in enumerate(doc):
if isinstance(item, dict):
if any(k in item for k in ("tasks", "pre_tasks", "post_tasks", "handlers")):
_check_tasks(item, filepath, violations, repo_root)
else:
violations.extend(_check_task(item, filepath, i + 1, repo_root))
block = item.get("block")
if isinstance(block, list):
for j, bt in enumerate(block):
if isinstance(bt, dict):
violations.extend(_check_task(bt, filepath, i + j + 1, repo_root))
elif isinstance(doc, dict):
_check_tasks(doc, filepath, violations, repo_root)
# Filter out violations if the allow marker is present in the file
# (coarse-grained opt-out for files with many legitimate uses)
if has_allow_marker:
violations = []
return violations
def _check_tasks(doc: dict, filepath: Path, errors: list[str], repo_root: Path) -> None:
"""Check top-level tasks and nested task sections in a playbook doc."""
for section_key in ("tasks", "pre_tasks", "post_tasks", "handlers"):
section = doc.get(section_key)
if isinstance(section, list):
for i, task in enumerate(section):
if isinstance(task, dict):
errors.extend(_check_task(task, filepath, i + 1, repo_root))
block = task.get("block")
if isinstance(block, list):
for j, bt in enumerate(block):
if isinstance(bt, dict):
errors.extend(_check_task(bt, filepath, i + j + 1, repo_root))
def _find_task_files(base: Path) -> list[Path]:
"""Find all YAML task files under a base directory, skipping molecule."""
if base.is_file() and base.suffix in (".yml", ".yaml"):
return [base]
if not base.is_dir():
return []
files: list[Path] = []
for f in sorted(base.rglob("*.yml")) + sorted(base.rglob("*.yaml")):
if "molecule" in f.parts:
continue
files.append(f)
return files
@click.command()
@click.option(
"--path",
type=click.Path(exists=True, path_type=Path),
help="Check a specific file or directory (default: ansible/playbooks + ansible/roles).",
)
@click.option(
"--ansible-dir",
"ansible_dirs",
type=click.Path(exists=True, path_type=Path),
multiple=True,
default=None,
help="Override the default ansible directories (can be repeated). Defaults to ansible/playbooks and ansible/roles.",
)
def main(path: Path | None, ansible_dirs: tuple[Path, ...]) -> None:
"""Check Ansible tasks for dangerous failure-masking patterns."""
dirs = list(ansible_dirs) if ansible_dirs else DEFAULT_ANSIBLE_DIRS
if path:
files = _find_task_files(path)
else:
files: list[Path] = []
for d in dirs:
files.extend(_find_task_files(d))
all_violations: list[str] = []
for f in files:
all_violations.extend(_check_file(f, REPO_ROOT))
if all_violations:
click.echo("[check-ansible-patterns] FAIL: dangerous failure-masking patterns found:")
for v in all_violations:
click.echo(f" - {v}")
click.echo(f"\nTotal: {len(all_violations)} violation(s).")
sys.exit(1)
else:
click.echo("[check-ansible-patterns] OK: no dangerous failure-masking patterns.")
if __name__ == "__main__": # pragma: no cover
main()
-292
View File
@@ -1,292 +0,0 @@
"""Validate Jinja2 expressions in Ansible files by rendering them.
Extracts ``{{ ... }}`` expressions from Ansible YAML files and renders
each one with Ansible's Jinja2 environment using mock variables. Catches
errors like reversed filter arguments, undefined filters, and syntax
errors before pushing to CI.
The check is intentionally lightweight it doesn't need real Ansible
facts or variables. It provides common mock values (now(), ansible_*,
etc.) and renders each expression in isolation. Expressions that fail
with undefined variables that aren't in the mock set are skipped (not
all variables can be predicted).
Usage::
python -m devx.tools.check_jinja_expr
python -m devx.tools.check_jinja_expr --path ansible/playbooks/deploy-observability.yml
Exit code 0 if all renderable expressions pass, 1 if any fail.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
import click
from jinja2 import Environment
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
REPO_ROOT = Path.cwd()
def _default_ansible_dirs() -> list[Path]:
"""Return the default directories to scan for Ansible files."""
return [
REPO_ROOT / "ansible" / "playbooks",
REPO_ROOT / "ansible" / "roles",
]
# Mock context for rendering Jinja expressions.
MOCK_CONTEXT: dict[str, object] = {
"now": lambda fmt=None: (
"2026-01-01T00:00:00+00:00"
if fmt
else type(
"Now",
(),
{
"timestamp": lambda self: 1735689600.0,
"strftime": lambda self, fmt: "2026-01-01T00:00:00+00:00",
},
)()
),
"ansible_date_time": {
"iso8601": "2026-01-01T00:00:00+00:00",
"epoch": "1735689600",
},
"ansible_facts": {
"service_mgr": "systemd",
"architecture": "x86_64",
"distribution_release": "noble",
"virtualization_type": "none",
"interfaces": ["eth0", "lo"],
"hostname": "test-host",
},
"ansible_host": "10.0.0.1",
"env": "staging",
"environment": "staging",
"customer_id": "test",
"zitadel_domain": "zitadel.test",
"_env_name": "staging",
"_observability_data_root": "/opt",
"skip_zitadel_stack": False,
"skip_htpasswd": False,
"skip_observability_stack": False,
"backup_enabled": True,
"app_filter": "",
"app_domain": "test.example.com",
"oidc_client_id": "test-client-id",
"oidc_client_secret": "test-secret", # nosec B105 — mock value for Jinja rendering, not a real secret
"s3_backup_bucket": "test-bucket",
"s3_endpoint": "https://s3.test",
"s3_access_key": "test-key",
"s3_secret_key": "test-secret", # nosec B105 — mock value for Jinja rendering, not a real secret
}
# Pattern to find {{ ... }} expressions (non-greedy, single-line).
EXPR_PATTERN = re.compile(r"\{\{(.*?)\}\}", re.DOTALL)
def _find_yaml_files(path: Path) -> list[Path]:
"""Find Ansible YAML files (tasks, playbooks, handlers) in a path."""
if path.is_file():
return [path]
files: list[Path] = []
for pattern in ["**/*.yml", "**/*.yaml"]:
files.extend(path.glob(pattern))
# Exclude molecule scenarios — they have their own variables.
return [f for f in files if "molecule" not in f.parts]
def _extract_expressions(content: str) -> list[str]:
"""Extract Jinja expressions from file content.
Filters out Go template syntax (``{{.Field}}``) used in docker
inspect --format strings, and single-character fragments from
quoted strings that aren't real Jinja expressions.
"""
expressions = []
for match in EXPR_PATTERN.finditer(content):
raw = match.group(1)
# Skip multi-line expressions (often have YAML formatting artifacts).
if "\n" in raw:
continue
expr = raw.strip()
# Skip empty, control flow, and single-char fragments.
if not expr or expr.startswith("%") or len(expr) <= 1:
continue
# Skip Go template syntax (docker inspect --format).
if expr.startswith(".") or "println" in expr:
continue
# Skip expressions containing Go template dot-access patterns.
if ".State." in expr or ".NetworkSettings." in expr:
continue
# Skip expressions with unbalanced parens/brackets/braces —
# the regex captured only part of a larger expression where
# }} appears inside a dict literal (e.g. default({'k': {}})).
if expr.count("(") != expr.count(")"):
continue
if expr.count("{") != expr.count("}"):
continue
if expr.count("[") != expr.count("]"):
continue
expressions.append(expr)
return expressions
def _render_expression(expr: str) -> tuple[bool, str]:
"""Try to render a Jinja expression. Returns (success, error_msg)."""
try:
env = Environment(autoescape=False, keep_trailing_newline=True) # nosec B701 — Ansible Jinja, not web-facing # noqa: S701
# Add common Ansible filters so expressions can render.
# strftime: Ansible's signature is strftime(string_format, second, utc)
# where string_format is the piped value. If the piped value looks like
# a number (epoch) and second looks like a format string, the args are
# reversed — this is the exact bug from OBL-INFRA-508.
def _strftime(string_format: str, second: float | None = None, utc: bool = False) -> str:
if isinstance(string_format, (int, float)) and isinstance(second, str) and "%" in second:
raise ValueError( # noqa: TRY301
"Invalid value for epoch value — strftime filter arguments "
"are reversed. The format string must be the piped value: "
"'%format%' | strftime(epoch), not epoch | strftime('%format%')"
)
return str(string_format)
env.filters["strftime"] = _strftime
env.filters["b64decode"] = lambda x: x
env.filters["b64encode"] = lambda x: x
env.filters["regex_replace"] = lambda x, pattern, replacement="": x
env.filters["int"] = lambda x, default=0: (
int(x) if isinstance(x, (int, float, str)) and str(x).lstrip("-").isdigit() else default
)
env.filters["bool"] = bool
env.filters["basename"] = lambda x: str(x).rsplit("/", 1)[-1]
env.filters["dirname"] = lambda x: str(x).rsplit("/", 1)[0] if "/" in str(x) else "."
env.filters["combine"] = lambda *args, **kwargs: args[0]
env.filters["from_json"] = lambda x: x
env.filters["to_json"] = lambda x: x
env.filters["ternary"] = lambda x, true_val, false_val=None: true_val if x else false_val
env.filters["dict2items"] = lambda x: [
{"key": k, "value": v} for k, v in (x.items() if isinstance(x, dict) else [])
]
env.filters["map"] = lambda x, attribute=None: x
env.filters["default"] = lambda x, default_value="", boolean=False: x if x else default_value
env.filters["from_yaml"] = lambda x: x
env.filters["difference"] = lambda x, y: x
env.filters["join"] = lambda x, sep="": sep.join(str(i) for i in (x if isinstance(x, list) else [x]))
env.filters["list"] = lambda x: list(x) if isinstance(x, (list, tuple)) else [x]
env.filters["length"] = lambda x: len(x) if hasattr(x, "__len__") else 0
env.filters["items"] = lambda x: list(x.items()) if isinstance(x, dict) else []
env.filters["first"] = lambda x: x[0] if isinstance(x, (list, str)) and x else x
env.filters["last"] = lambda x: x[-1] if isinstance(x, (list, str)) and x else x
env.filters["upper"] = lambda x: str(x).upper()
env.filters["lower"] = lambda x: str(x).lower()
env.filters["replace"] = lambda x, old, new: str(x).replace(old, new)
env.filters["split"] = lambda x, sep=None: str(x).split(sep) if sep else str(x).split()
env.filters["trim"] = lambda x: str(x).strip()
env.filters["sort"] = lambda x: sorted(x) if isinstance(x, list) else x
env.filters["unique"] = lambda x: list(set(x)) if isinstance(x, list) else x
env.filters["count"] = lambda x: len(x) if hasattr(x, "__len__") else 0
env.filters["float"] = lambda x, default=0.0: (
float(x) if isinstance(x, (int, float, str)) and str(x).replace(".", "").lstrip("-").isdigit() else default
)
env.filters["string"] = str
env.filters["indent"] = lambda x, width=4: str(x)
env.filters["to_nice_json"] = str
env.filters["to_nice_yaml"] = str
env.filters["from_yaml_all"] = lambda x: x
env.filters["groupby"] = lambda x: x
env.filters["dictsort"] = lambda x: list(x.items()) if isinstance(x, dict) else []
env.filters["max"] = lambda x: max(x) if isinstance(x, list) and x else x
env.filters["min"] = lambda x: min(x) if isinstance(x, list) and x else x
env.filters["reverse"] = lambda x: list(reversed(x)) if isinstance(x, list) else x
env.filters["flatten"] = lambda x: x
env.filters["product"] = lambda x: x
env.filters["zip"] = lambda x: x
env.filters["subelements"] = lambda x: x
env.filters["json_query"] = lambda x: x
env.filters["type_debug"] = lambda x: type(x).__name__
env.globals["lookup"] = lambda *args, **kwargs: ""
env.globals["query"] = lambda *args, **kwargs: []
template = env.from_string("{{ " + expr + " }}")
result = template.render(**MOCK_CONTEXT)
except TemplateSyntaxError as e:
return False, f"Syntax error: {e.message}"
except UndefinedError as e:
# Undefined variable — skip, we can't mock everything.
return True, f"Skipped (undefined: {e})"
except Exception as e:
# Check if it's a filter argument error.
error_msg = str(e)
if "Invalid value for epoch" in error_msg:
return False, f"strftime filter argument error: {error_msg}"
# Other errors might be due to missing mock variables — skip.
return True, f"Skipped ({type(e).__name__}: {error_msg})"
else:
return True, result
def _check_file(filepath: Path, repo_root: Path) -> list[str]:
"""Check all Jinja expressions in a file. Returns list of violations."""
violations = []
content = filepath.read_text()
expressions = _extract_expressions(content)
for expr in expressions:
success, msg = _render_expression(expr)
if not success:
try:
rel_path = filepath.relative_to(repo_root)
except ValueError:
rel_path = filepath
violations.append(f"{rel_path}: `{{{{ {expr} }}}}` — {msg}")
return violations
@click.command()
@click.option(
"--path",
type=click.Path(exists=True, path_type=Path),
help="Check a specific file or directory (default: ansible/playbooks + ansible/roles).",
)
@click.option(
"--ansible-dir",
"ansible_dirs",
type=click.Path(exists=True, path_type=Path),
multiple=True,
default=None,
help="Override the default ansible directories (can be repeated). Defaults to ansible/playbooks and ansible/roles.",
)
def main(path: Path | None, ansible_dirs: tuple[Path, ...]) -> None:
"""Validate Jinja2 expressions in Ansible files."""
dirs = list(ansible_dirs) if ansible_dirs else _default_ansible_dirs()
if path:
files = _find_yaml_files(path)
else:
files: list[Path] = []
for d in dirs:
files.extend(_find_yaml_files(d))
all_violations: list[str] = []
for f in files:
all_violations.extend(_check_file(f, REPO_ROOT))
if all_violations:
click.echo("[check-jinja-expr] FAIL: invalid Jinja expressions found:")
for v in all_violations:
click.echo(f" - {v}")
click.echo("\nFix: test expressions with `ansible localhost -m debug -a 'msg={{ <expr> }}'`")
sys.exit(1)
else:
click.echo("[check-jinja-expr] OK: all Jinja expressions render correctly.")
if __name__ == "__main__": # pragma: no cover
main()
+1576 -1888
View File
@@ -1,51 +1,51 @@
{
"\n=== Summary ===": {
"bg": "\n=== Обобщение ===",
"de": "\n=== Zusammenfassung ===",
"bg": "\n=== Summary ===",
"de": "\n=== Summary ===",
"en": "\n=== Summary ===",
"pl": "\n=== Podsumowanie ===",
"ru": "\n=== Сводка ===",
"zh": "\n=== 摘要 ==="
"ru": "\n=== Summary ===",
"zh": "\n=== Summary ==="
},
"\nAll documentation coverage checks passed!": {
"bg": "\nВсички проверки за покритие на документацията преминаха успешно!",
"de": "\nAlle Dokumentations-Abdeckungsprüfungen bestanden!",
"bg": "\nAll documentation coverage checks passed!",
"de": "\nAll documentation coverage checks passed!",
"en": "\nAll documentation coverage checks passed!",
"pl": "\nWszystkie kontrole pokrycia dokumentacji zakończone pomyślnie!",
"ru": "\nВсе проверки покрытия документации пройдены!",
"zh": "\n所有文档覆盖率检查均已通过!"
"ru": "\nAll documentation coverage checks passed!",
"zh": "\nAll documentation coverage checks passed!"
},
"\nCHANGELOG version ordering:": {
"bg": "\nПодреждане на версиите в CHANGELOG:",
"de": "\nReihenfolge der CHANGELOG-Versionen:",
"bg": "\nCHANGELOG version ordering:",
"de": "\nCHANGELOG version ordering:",
"en": "\nCHANGELOG version ordering:",
"pl": "\nKolejność wersji w CHANGELOG:",
"ru": "\nПорядок версий в CHANGELOG:",
"zh": "\nCHANGELOG 版本顺序:"
"ru": "\nCHANGELOG version ordering:",
"zh": "\nCHANGELOG version ordering:"
},
"\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...",
"bg": "\nChecking CI script documentation in ci-cd-workflow.md...",
"de": "\nChecking CI script documentation in ci-cd-workflow.md...",
"en": "\nChecking CI script documentation in ci-cd-workflow.md...",
"pl": "\nSprawdzanie dokumentacji skryptów CI w ci-cd-workflow.md...",
"ru": "\nПроверка документации CI-скриптов в ci-cd-workflow.md...",
"zh": "\n正在检查 ci-cd-workflow.md 中的 CI 脚本文档..."
"ru": "\nChecking CI script documentation in ci-cd-workflow.md...",
"zh": "\nChecking CI script documentation in ci-cd-workflow.md..."
},
"\nChecking module documentation in architecture.md...": {
"bg": "\nПроверка на документацията за модулите в architecture.md...",
"de": "\nPrüfe Moduldokumentation in architecture.md...",
"bg": "\nChecking module documentation in architecture.md...",
"de": "\nChecking module documentation in architecture.md...",
"en": "\nChecking module documentation in architecture.md...",
"pl": "\nSprawdzanie dokumentacji modułów w architecture.md...",
"ru": "\nПроверка документации модулей в architecture.md...",
"zh": "\n正在检查 architecture.md 中的模块文档..."
"ru": "\nChecking module documentation in architecture.md...",
"zh": "\nChecking module documentation in architecture.md..."
},
"\nDoc coverage: {covered}/{total} ({pct}%)": {
"bg": "\nПокритие на документацията: {covered}/{total} ({pct}%)",
"de": "\nDokumentationsabdeckung: {covered}/{total} ({pct}%)",
"bg": "\nDoc coverage: {covered}/{total} ({pct}%)",
"de": "\nDoc coverage: {covered}/{total} ({pct}%)",
"en": "\nDoc coverage: {covered}/{total} ({pct}%)",
"pl": "\nPokrycie dokumentacji: {covered}/{total} ({pct}%)",
"ru": "\nПокрытие документации: {covered}/{total} ({pct}%)",
"zh": "\n文档覆盖率:{covered}/{total} ({pct}%)"
"ru": "\nDoc coverage: {covered}/{total} ({pct}%)",
"zh": "\nDoc coverage: {covered}/{total} ({pct}%)"
},
"\nDone! Synced: {synced}, Pruned: {pruned}": {
"bg": "",
@@ -56,20 +56,20 @@
"zh": ""
},
"\nDone. Deleted {deleted}, kept {kept}, failed {failed}.": {
"bg": "\nГотово. Изтрити: {deleted}, запазени: {kept}, неуспешни: {failed}.",
"de": "\nFertig. Gelöscht: {deleted}, behalten: {kept}, fehlgeschlagen: {failed}.",
"bg": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}.",
"de": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}.",
"en": "\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}"
"pl": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}.",
"ru": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}.",
"zh": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}."
},
"\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.",
"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.",
"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": "\nОШИБКА: Покрытие документации не составляет 100%. Используйте --fail-on-missing для принудительной проверки.",
"zh": "\n错误:文档覆盖率未达到 100%。使用 --fail-on-missing 强制执行。"
"ru": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.",
"zh": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce."
},
"\nFAIL: {n} stale version reference(s) found:": {
"bg": "",
@@ -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": "\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.",
"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.",
"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": "\nИсправьте несоответствующие теги перед созданием новых релизов. Выполните 'python3 -m devx.ci.release --verify' для полного отчёта.",
"zh": "\n请在创建新版本之前修正不匹配的标签。运行 'python3 -m devx.ci.release --verify' 获取完整报告。"
"ru": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
"zh": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report."
},
"\nFixed {n} stale version reference(s).": {
"bg": "",
@@ -96,36 +96,36 @@
"zh": ""
},
"\nGenerated {count} badges:": {
"bg": "\nГенерирани {count} значка:",
"de": "\n{count} Badges generiert:",
"bg": "\nGenerated {count} badges:",
"de": "\nGenerated {count} badges:",
"en": "\nGenerated {count} badges:",
"pl": "\nWygenerowano {count} odznak:",
"ru": "\nСгенерировано значков: {count}:",
"zh": "\n已生成 {count} 个徽章:"
"pl": "\nGenerated {count} badges:",
"ru": "\nGenerated {count} badges:",
"zh": "\nGenerated {count} badges:"
},
"\nKeeping {kept}, would delete {count}": {
"bg": "\nЗапазени {kept}, ще бъдат изтрити {count}",
"de": "\nBehalte {kept}, würde {count} löschen",
"bg": "\nKeeping {kept}, would delete {count}",
"de": "\nKeeping {kept}, would delete {count}",
"en": "\nKeeping {kept}, would delete {count}",
"pl": "\nZachowano {kept}, usunięto by {count}",
"ru": "\nСохранено {kept}, будет удалено {count}",
"zh": "\n保留 {kept},将删除 {count}"
"pl": "\nKeeping {kept}, would delete {count}",
"ru": "\nKeeping {kept}, would delete {count}",
"zh": "\nKeeping {kept}, would delete {count}"
},
"\nLatest tag: {tag}": {
"bg": "\nПоследен таг: {tag}",
"de": "\nNeuestes Tag: {tag}",
"bg": "\nLatest tag: {tag}",
"de": "\nLatest tag: {tag}",
"en": "\nLatest tag: {tag}",
"pl": "\nNajnowszy tag: {tag}",
"ru": "\nПоследний тег: {tag}",
"zh": "\n最新标签:{tag}"
"ru": "\nLatest tag: {tag}",
"zh": "\nLatest tag: {tag}"
},
"\nMissing documentation:": {
"bg": "\nЛипсваща документация:",
"de": "\nFehlende Dokumentation:",
"bg": "\nMissing documentation:",
"de": "\nMissing documentation:",
"en": "\nMissing documentation:",
"pl": "\nBrakująca dokumentacja:",
"ru": "\nОтсутствующая документация:",
"zh": "\n缺失的文档:"
"ru": "\nMissing documentation:",
"zh": "\nMissing documentation:"
},
"\nNo stale version references found.": {
"bg": "",
@@ -144,12 +144,28 @@
"zh": ""
},
"\nResult: {status}": {
"bg": "\nРезултат: {status}",
"de": "\nErgebnis: {status}",
"bg": "\nResult: {status}",
"de": "\nResult: {status}",
"en": "\nResult: {status}",
"pl": "\nWynik: {status}",
"ru": "\nРезультат: {status}",
"zh": "\n结果:{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}'."
},
"\nRun with --fix to auto-update version references.": {
"bg": "",
@@ -160,36 +176,28 @@
"zh": ""
},
"\nTag → Commit alignment:": {
"bg": "\nСъответствие таг → комит:",
"de": "\nTag-→-Commit-Zuordnung:",
"bg": "\nTag → Commit alignment:",
"de": "\nTagCommit alignment:",
"en": "\nTag → Commit alignment:",
"pl": "\nTag → Commit: zgodność:",
"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"
"ru": "\nTag → Commit alignment:",
"zh": "\nTag → Commit alignment:"
},
"\nUntagged release commits:": {
"bg": "\nРелийз комити без таг:",
"de": "\nRelease-Commits ohne Tag:",
"bg": "\nUntagged release commits:",
"de": "\nUntagged release commits:",
"en": "\nUntagged release commits:",
"pl": "\nCommity wydania bez tagu:",
"ru": "\nРелизные коммиты без тега:",
"zh": "\n未打标签的发布提交:"
"ru": "\nUntagged release commits:",
"zh": "\nUntagged release commits:"
},
"\nUser-facing changes ({count}):": {
"bg": "\nВидими за потребителя промени ({count}):",
"de": "\nNutzersichtbare Änderungen ({count}):",
"bg": "\nUser-facing changes ({count}):",
"de": "\nUser-facing changes ({count}):",
"en": "\nUser-facing changes ({count}):",
"pl": "\nZmiany widoczne dla użytkownika ({count}):",
"ru": "\nПользовательские изменения ({count}):",
"zh": "\n面向用户的更改({count}):"
"ru": "\nUser-facing changes ({count}):",
"zh": "\nUser-facing changes ({count}):"
},
"\nVerification passed — all wiki pages exist.": {
"bg": "",
@@ -208,36 +216,36 @@
"zh": ""
},
"\nWorkflow-only changes ({count}):": {
"bg": "\nПромени само в workflow ({count}):",
"de": "\nNur-Workflow-Änderungen ({count}):",
"bg": "\nWorkflow-only changes ({count}):",
"de": "\nWorkflow-only changes ({count}):",
"en": "\nWorkflow-only changes ({count}):",
"pl": "\nZmiany tylko w workflow ({count}):",
"ru": "\nИзменения только в workflow ({count}):",
"zh": "\n仅工作流更改({count}):"
"ru": "\nWorkflow-only changes ({count}):",
"zh": "\nWorkflow-only changes ({count}):"
},
"\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.",
"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.",
"en": "\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] 修复:提交前添加缺失的测试文件。"
"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."
},
"\n[dry-run] Changelog:\n{changelog}": {
"bg": "\n[dry-run] Списък на промените:\n{changelog}",
"de": "\n[dry-run] Änderungsprotokoll:\n{changelog}",
"bg": "\n[dry-run] Changelog:\n{changelog}",
"de": "\n[dry-run] Changelog:\n{changelog}",
"en": "\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}"
"pl": "\n[dry-run] Changelog:\n{changelog}",
"ru": "\n[dry-run] Changelog:\n{changelog}",
"zh": "\n[dry-run] Changelog:\n{changelog}"
},
"\n{label} files changed ({count}):": {
"bg": "\nПроменени файлове — {label} ({count}):",
"de": "\n{label} geänderte Dateien ({count}):",
"bg": "\n{label} files changed ({count}):",
"de": "\n{label} files changed ({count}):",
"en": "\n{label} files changed ({count}):",
"pl": "\n{label} plików zmienionych ({count}):",
"ru": "\nИзменённые файлы — {label} ({count}):",
"zh": "\n{label} 个已更改文件({count}):"
"ru": "\n{label} files changed ({count}):",
"zh": "\n{label} files changed ({count}):"
},
"\n{separator}": {
"bg": "\n{separator}",
@@ -248,36 +256,36 @@
"zh": "\n{separator}"
},
"\n{tag} files ({count}):": {
"bg": "\n{tag} файла ({count}):",
"de": "\n{tag} Dateien ({count}):",
"bg": "\n{tag} files ({count}):",
"de": "\n{tag} files ({count}):",
"en": "\n{tag} files ({count}):",
"pl": "\nPliki {tag} ({count}):",
"ru": "\n{tag} файлов ({count}):",
"zh": "\n{tag} 个文件({count}):"
"ru": "\n{tag} files ({count}):",
"zh": "\n{tag} files ({count}):"
},
" Could not fetch logs: {error}": {
"bg": " Неуспешно извличане на логове: {error}",
"de": " Logs konnten nicht abgerufen werden: {error}",
"bg": " Could not fetch logs: {error}",
"de": " Could not fetch logs: {error}",
"en": " Could not fetch logs: {error}",
"pl": " Nie udało się pobrać logów: {error}",
"ru": " Не удалось получить логи: {error}",
"zh": " 无法获取日志:{error}"
"pl": " Could not fetch logs: {error}",
"ru": " Could not fetch logs: {error}",
"zh": " Could not fetch logs: {error}"
},
" pytest stderr (last 300 chars): {stderr}": {
"bg": " pytest stderr (последни 300 символа): {stderr}",
"de": " pytest stderr (letzte 300 Zeichen): {stderr}",
"bg": " pytest stderr (last 300 chars): {stderr}",
"de": " pytest stderr (last 300 chars): {stderr}",
"en": " pytest stderr (last 300 chars): {stderr}",
"pl": " pytest stderr (ostatnie 300 znaków): {stderr}",
"ru": " pytest stderr (последние 300 символов): {stderr}",
"zh": " pytest stderr(最后 300 个字符):{stderr}"
"pl": " pytest stderr (last 300 chars): {stderr}",
"ru": " pytest stderr (last 300 chars): {stderr}",
"zh": " pytest stderr (last 300 chars): {stderr}"
},
" pytest stdout (last 300 chars): {stdout}": {
"bg": " pytest stdout (последни 300 символа): {stdout}",
"de": " pytest stdout (letzte 300 Zeichen): {stdout}",
"bg": " pytest stdout (last 300 chars): {stdout}",
"de": " pytest stdout (last 300 chars): {stdout}",
"en": " pytest stdout (last 300 chars): {stdout}",
"pl": " pytest stdout (ostatnie 300 znaków): {stdout}",
"ru": " pytest stdout (последние 300 символов): {stdout}",
"zh": " pytest stdout(最后 300 个字符):{stdout}"
"pl": " pytest stdout (last 300 chars): {stdout}",
"ru": " pytest stdout (last 300 chars): {stdout}",
"zh": " pytest stdout (last 300 chars): {stdout}"
},
" stderr: {stderr}": {
"bg": " stderr: {stderr}",
@@ -320,12 +328,12 @@
"zh": " - 阻止被拒绝的审查: 是"
},
" - Direct pushes: BLOCKED (require PR, whitelisted users can push)": {
"bg": " - Директни push-ове: БЛОКИРАНИ (изисква се PR; разрешени потребители могат да push-ват)",
"de": " - Direkte Pushes: BLOCKIERT (PR erforderlich, freigegebene Benutzer dürfen pushen)",
"bg": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)",
"de": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)",
"en": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)",
"pl": " - Bezpośrednie push-e: ZABLOKOWANE (wymagają PR, użytkownicy z białej listy mogą pushować)",
"ru": " - Прямые push: ЗАБЛОКИРОВАНЫ (требуется PR; разрешённые пользователи могут push)",
"zh": " - 直接推送:已阻止(需要 PR,白名单用户可推送)"
"ru": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)",
"zh": " - Direct pushes: BLOCKED (require PR, whitelisted users can push)"
},
" - Dismiss stale approvals: yes": {
"bg": " - Анулиране на остарели одобрения: да",
@@ -352,12 +360,12 @@
"zh": " - 必需状态检查: {checks}"
},
" - {count} standard labels verified": {
"bg": " - {count} стандартни етикета проверени",
"de": " - {count} Standard-Labels geprüft",
"bg": " - {count} standard labels verified",
"de": " - {count} standard labels verified",
"en": " - {count} standard labels verified",
"pl": " - {count} standardowych etykiet zweryfikowanych",
"ru": " - {count} стандартных меток проверено",
"zh": " - 已验证 {count} 个标准标签"
"pl": " - {count} standard labels verified",
"ru": " - {count} standard labels verified",
"zh": " - {count} standard labels verified"
},
" -> {dir}": {
"bg": " -> {dir}",
@@ -376,52 +384,52 @@
"zh": ""
},
" Auto-fixed trailing whitespace in {n} files": {
"bg": " Автоматично коригирани крайни интервали в {n} файла",
"de": " Abschließende Leerzeichen in {n} Dateien automatisch korrigiert",
"bg": " Auto-fixed trailing whitespace in {n} files",
"de": " Auto-fixed trailing whitespace in {n} files",
"en": " Auto-fixed trailing whitespace in {n} files",
"pl": " Automatycznie poprawiono końcowe białe znaki w {n} plikach",
"ru": " Автоматически исправлены конечные пробелы в {n} файлах",
"zh": " 已自动修复 {n} 个文件中的行尾空白"
"pl": " Auto-fixed trailing whitespace in {n} files",
"ru": " Auto-fixed trailing whitespace in {n} files",
"zh": " Auto-fixed trailing whitespace in {n} files"
},
" Collecting code quality...": {
"bg": " Събиране на качество на кода...",
"de": " Codequalität wird erfasst...",
"bg": " Collecting code quality...",
"de": " Collecting code quality...",
"en": " Collecting code quality...",
"pl": " Zbieranie jakości kodu...",
"ru": " Сбор данных о качестве кода...",
"zh": " 正在收集代码质量..."
"pl": " Collecting code quality...",
"ru": " Collecting code quality...",
"zh": " Collecting code quality..."
},
" Collecting coverage and tests...": {
"bg": " Събиране на покритие и тестове...",
"de": " Coverage und Tests werden erfasst...",
"bg": " Collecting coverage and tests...",
"de": " Collecting coverage and tests...",
"en": " Collecting coverage and tests...",
"pl": " Zbieranie pokrycia i testów...",
"ru": " Сбор покрытия и тестов...",
"zh": " 正在收集覆盖率和测试..."
"pl": " Collecting coverage and tests...",
"ru": " Collecting coverage and tests...",
"zh": " Collecting coverage and tests..."
},
" Collecting doc coverage...": {
"bg": " Събиране на покритие на документацията...",
"de": " Dokumentationsabdeckung wird erfasst...",
"bg": " Collecting doc coverage...",
"de": " Collecting doc coverage...",
"en": " Collecting doc coverage...",
"pl": " Zbieranie pokrycia dokumentacji...",
"ru": " Сбор покрытия документации...",
"zh": " 正在收集文档覆盖率..."
"pl": " Collecting doc coverage...",
"ru": " Collecting doc coverage...",
"zh": " Collecting doc coverage..."
},
" Collecting version...": {
"bg": " Събиране на версия...",
"de": " Version wird erfasst...",
"bg": " Collecting version...",
"de": " Collecting version...",
"en": " Collecting version...",
"pl": " Zbieranie wersji...",
"ru": " Сбор версии...",
"zh": " 正在收集版本..."
"pl": " Collecting version...",
"ru": " Collecting version...",
"zh": " Collecting version..."
},
" Deleted: {version}": {
"bg": " Изтрито: {version}",
"de": " Gelöscht: {version}",
"bg": " Deleted: {version}",
"de": " Deleted: {version}",
"en": " Deleted: {version}",
"pl": " Usunięto: {version}",
"ru": " Удалено: {version}",
"zh": " 已删除:{version}"
"pl": " Deleted: {version}",
"ru": " Deleted: {version}",
"zh": " Deleted: {version}"
},
" FAIL: {title} — page not found in wiki!": {
"bg": "",
@@ -432,20 +440,12 @@
"zh": ""
},
" FAILED to delete: {version}": {
"bg": " НЕУСПЕШНО изтриване: {version}",
"de": " Löschen FEHLGESCHLAGEN: {version}",
"bg": " FAILED to delete: {version}",
"de": " FAILED to delete: {version}",
"en": " 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}'"
"pl": " FAILED to delete: {version}",
"ru": " FAILED to delete: {version}",
"zh": " FAILED to delete: {version}"
},
" Fixed {fixes} version ref(s) in {file}": {
"bg": "",
@@ -456,44 +456,36 @@
"zh": ""
},
" Generated: {path}": {
"bg": " Генерирано: {path}",
"de": " Generiert: {path}",
"bg": " Generated: {path}",
"de": " Generated: {path}",
"en": " 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次尝试)..."
"pl": " Generated: {path}",
"ru": " Generated: {path}",
"zh": " Generated: {path}"
},
" MISSING: {cmd}": {
"bg": " ЛИПСВА: {cmd}",
"de": " FEHLT: {cmd}",
"bg": " MISSING: {cmd}",
"de": " MISSING: {cmd}",
"en": " MISSING: {cmd}",
"pl": " BRAKUJE: {cmd}",
"ru": " ОТСУТСТВУЕТ: {cmd}",
"zh": " 缺失:{cmd}"
"pl": " MISSING: {cmd}",
"ru": " MISSING: {cmd}",
"zh": " MISSING: {cmd}"
},
" MISSING: {module}": {
"bg": " ЛИПСВА: {module}",
"de": " FEHLT: {module}",
"bg": " MISSING: {module}",
"de": " MISSING: {module}",
"en": " MISSING: {module}",
"pl": " BRAK: {module}",
"ru": " ОТСУТСТВУЕТ: {module}",
"zh": " 缺失:{module}"
"ru": " MISSING: {module}",
"zh": " MISSING: {module}"
},
" MISSING: {script}": {
"bg": " ЛИПСВА: {script}",
"de": " FEHLT: {script}",
"bg": " MISSING: {script}",
"de": " MISSING: {script}",
"en": " MISSING: {script}",
"pl": " BRAK: {script}",
"ru": " ОТСУТСТВУЕТ: {script}",
"zh": " 缺失:{script}"
"ru": " MISSING: {script}",
"zh": " MISSING: {script}"
},
" OK: {cmd}": {
"bg": " OK: {cmd}",
@@ -501,7 +493,7 @@
"en": " OK: {cmd}",
"pl": " OK: {cmd}",
"ru": " OK: {cmd}",
"zh": " 正常:{cmd}"
"zh": " OK: {cmd}"
},
" OK: {module}": {
"bg": " OK: {module}",
@@ -509,7 +501,7 @@
"en": " OK: {module}",
"pl": " OK: {module}",
"ru": " OK: {module}",
"zh": " 正常:{module}"
"zh": " OK: {module}"
},
" OK: {script}": {
"bg": " OK: {script}",
@@ -517,7 +509,7 @@
"en": " OK: {script}",
"pl": " OK: {script}",
"ru": " OK: {script}",
"zh": " 正常:{script}"
"zh": " OK: {script}"
},
" OK: {title}": {
"bg": "",
@@ -528,12 +520,12 @@
"zh": ""
},
" Package: {pkg}": {
"bg": " Пакет: {pkg}",
"de": " Paket: {pkg}",
"bg": " Package: {pkg}",
"de": " Package: {pkg}",
"en": " Package: {pkg}",
"pl": " Pakiet: {pkg}",
"ru": " Пакет: {pkg}",
"zh": " 包:{pkg}"
"pl": " Package: {pkg}",
"ru": " Package: {pkg}",
"zh": " Package: {pkg}"
},
" Pruned: {file} (not in mapping)": {
"bg": "",
@@ -544,20 +536,20 @@
"zh": ""
},
" Quality checks: {checks}": {
"bg": " Проверки на качеството: {checks}",
"de": " Qualitätsprüfungen: {checks}",
"bg": " Quality checks: {checks}",
"de": " Quality checks: {checks}",
"en": " Quality checks: {checks}",
"pl": " Kontrole jakości: {checks}",
"ru": " Проверки качества: {checks}",
"zh": " 质量检查:{checks}"
"pl": " Quality checks: {checks}",
"ru": " Quality checks: {checks}",
"zh": " Quality checks: {checks}"
},
" Repo root: {root}": {
"bg": " Корен на репозитория: {root}",
"de": " Repo-Wurzel: {root}",
"bg": " Repo root: {root}",
"de": " Repo root: {root}",
"en": " Repo root: {root}",
"pl": " Katalog główny repo: {root}",
"ru": " Корень репозитория: {root}",
"zh": " 仓库根目录:{root}"
"pl": " Repo root: {root}",
"ru": " Repo root: {root}",
"zh": " Repo root: {root}"
},
" Run 'make install-checkmake' to install the Makefile linter.": {
"bg": " Изпълнете 'make install-checkmake' за инсталиране на Makefile линтера.",
@@ -576,12 +568,12 @@
"zh": ""
},
" Test paths: {testpaths}": {
"bg": " Тестови пътища: {testpaths}",
"de": " Testpfade: {testpaths}",
"bg": " Test paths: {testpaths}",
"de": " Test paths: {testpaths}",
"en": " Test paths: {testpaths}",
"pl": " Ścieżki testów: {testpaths}",
"ru": " Пути тестов: {testpaths}",
"zh": " 测试路径:{testpaths}"
"pl": " Test paths: {testpaths}",
"ru": " Test paths: {testpaths}",
"zh": " Test paths: {testpaths}"
},
" WARN: Mapped file {file} is empty, skipping": {
"bg": "",
@@ -600,84 +592,84 @@
"zh": ""
},
" 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})",
"bg": " WARNING: Could not extract coverage from pytest output (rc={rc})",
"de": " WARNING: Could not extract coverage from pytest output (rc={rc})",
"en": " 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})"
"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})"
},
" WARNING: Could not extract doc coverage (rc={rc})": {
"bg": " ПРЕДУПРЕЖДЕНИЕ: Не може да се извлече покритие на документацията (rc={rc})",
"de": " WARNUNG: Dokumentationsabdeckung konnte nicht extrahiert werden (rc={rc})",
"bg": " WARNING: Could not extract doc coverage (rc={rc})",
"de": " WARNING: Could not extract doc coverage (rc={rc})",
"en": " 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})"
"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})"
},
" 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})",
"bg": " WARNING: Could not extract test count from pytest output (rc={rc})",
"de": " WARNING: Could not extract test count from pytest output (rc={rc})",
"en": " 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})"
"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})"
},
" 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'",
"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'",
"en": " 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'"
"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'"
},
" 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'",
"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'",
"en": " 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'"
"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'"
},
" 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)",
"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)",
"en": " 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"
"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)"
},
" WARNING: {init_file} not found — version badge will show 'unknown'": {
"bg": " ПРЕДУПРЕЖДЕНИЕ: {init_file} не е намерен — значкът за версия ще показва 'unknown'",
"de": " WARNUNG: {init_file} nicht gefundenVersions-Badge zeigt 'unknown'",
"bg": " WARNING: {init_file} not found — version badge will show 'unknown'",
"de": " WARNING: {init_file} not found — version badge will show 'unknown'",
"en": " 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'"
"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'"
},
" WARNING: {name} failed (rc={rc})": {
"bg": " ПРЕДУПРЕЖДЕНИЕ: {name} се провали (rc={rc})",
"de": " WARNUNG: {name} fehlgeschlagen (rc={rc})",
"bg": " WARNING: {name} failed (rc={rc})",
"de": " WARNING: {name} failed (rc={rc})",
"en": " WARNING: {name} failed (rc={rc})",
"pl": " OSTRZEŻENIE: {name} nie powiodło się (rc={rc})",
"ru": " ПРЕДУПРЕЖДЕНИЕ: {name} завершился с ошибкой (rc={rc})",
"zh": " 警告:{name} 失败 (rc={rc})"
"pl": " WARNING: {name} failed (rc={rc})",
"ru": " WARNING: {name} failed (rc={rc})",
"zh": " WARNING: {name} failed (rc={rc})"
},
" WARNING: {name} not installed — skipping (counted as pass)": {
"bg": " ПРЕДУПРЕЖДЕНИЕ: {name} не е инсталиран — пропуска се (отчита се като успешно)",
"de": " WARNUNG: {name} nicht installiert — übersprungen (als bestanden gezählt)",
"bg": " WARNING: {name} not installed — skipping (counted as pass)",
"de": " WARNING: {name} not installed — skipping (counted as pass)",
"en": " WARNING: {name} not installed — skipping (counted as pass)",
"pl": " OSTRZEŻENIE: {name} nie jest zainstalowane — pomijanie (liczone jako zaliczone)",
"ru": " ПРЕДУПРЕЖДЕНИЕ: {name} не установлен — пропускается (засчитывается как успех)",
"zh": " 警告:{name} 未安装——跳过(计为通过)"
"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)"
},
" [dry-run] Would delete: {version}": {
"bg": " [dry-run] Ще бъде изтрито: {version}",
"de": " [dry-run] Würde löschen: {version}",
"bg": " [dry-run] Would delete: {version}",
"de": " [dry-run] Would delete: {version}",
"en": " [dry-run] Would delete: {version}",
"pl": " [dry-run] Usunięto by: {version}",
"ru": " [dry-run] Было бы удалено: {version}",
"zh": " [dry-run] 将删除:{version}"
"pl": " [dry-run] Would delete: {version}",
"ru": " [dry-run] Would delete: {version}",
"zh": " [dry-run] Would delete: {version}"
},
" {name}: {label}={message} ({color})": {
"bg": " {name}: {label}={message} ({color})",
@@ -704,12 +696,12 @@
"zh": ""
},
" {n} stale docs found (warnings only)": {
"bg": " Намерени {n} остарели документа (само предупреждения)",
"de": " {n} veraltete Dokumente gefunden (nur Warnungen)",
"bg": " {n} stale docs found (warnings only)",
"de": " {n} stale docs found (warnings only)",
"en": " {n} stale docs found (warnings only)",
"pl": " Znaleziono {n} nieaktualnych dokumentów (tylko ostrzeżenia)",
"ru": " Найдено {n} устаревших документов (только предупреждения)",
"zh": " 发现 {n} 个过时文档(仅警告)"
"pl": " {n} stale docs found (warnings only)",
"ru": " {n} stale docs found (warnings only)",
"zh": " {n} stale docs found (warnings only)"
},
" {tool}: found at {path}": {
"bg": " {tool}: намерен на {path}",
@@ -720,100 +712,84 @@
"zh": " {tool}: 在 {path} 找到"
},
" {version} (created: {created})": {
"bg": " {version} (създадено: {created})",
"de": " {version} (erstellt: {created})",
"bg": " {version} (created: {created})",
"de": " {version} (created: {created})",
"en": " {version} (created: {created})",
"pl": " {version} (utworzono: {created})",
"ru": " {version} (создано: {created})",
"zh": " {version}(创建于:{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."
},
"--push requires --registry": {
"bg": "--push изисква --registry",
"de": "--push erfordert --registry",
"bg": "--push requires --registry",
"de": "--push requires --registry",
"en": "--push requires --registry",
"pl": "--push wymaga --registry",
"ru": "--push требует --registry",
"zh": "--push 需要 --registry"
"pl": "--push requires --registry",
"ru": "--push requires --registry",
"zh": "--push requires --registry"
},
"--skip-build: skipping package build and PyPI publish.": {
"bg": "--skip-build: пропуска се изграждане на пакета и публикуване в PyPI.",
"de": "--skip-build: Paket-Build und PyPI-Veröffentlichung werden übersprungen.",
"bg": "--skip-build: skipping package build and PyPI publish.",
"de": "--skip-build: skipping package build and PyPI publish.",
"en": "--skip-build: skipping package build and PyPI publish.",
"pl": "--skip-build: pomijanie budowania pakietu i publikacji PyPI.",
"ru": "--skip-build: сборка пакета и публикация в PyPI пропускаются.",
"zh": "--skip-build:跳过包构建和 PyPI 发布。"
"ru": "--skip-build: skipping package build and PyPI publish.",
"zh": "--skip-build: skipping package build and PyPI publish."
},
"=== Release Alignment Verification ===\n": {
"bg": "=== Проверка на съответствието на версиите ===\n",
"de": "=== Release-Abgleich-Verifizierung ===\n",
"bg": "=== Release Alignment Verification ===\n",
"de": "=== Release Alignment Verification ===\n",
"en": "=== Release Alignment Verification ===\n",
"pl": "=== Weryfikacja zgodności wydań ===\n",
"ru": "=== Проверка соответствия релизов ===\n",
"zh": "=== 发布一致性验证 ===\n"
"ru": "=== Release Alignment Verification ===\n",
"zh": "=== Release Alignment Verification ===\n"
},
"API poll warning: {exc}": {
"bg": "Предупреждение при API запитване: {exc}",
"de": "Warnung bei API-Abfrage: {exc}",
"bg": "API poll warning: {exc}",
"de": "API poll warning: {exc}",
"en": "API poll warning: {exc}",
"pl": "Ostrzeżenie sondowania API: {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\") 或修补调用函数以修复此问题。"
"ru": "API poll warning: {exc}",
"zh": "API poll warning: {exc}"
},
"Added label '{label}' to PR #{pr}.": {
"bg": "Добавен етикет '{label}' към PR #{pr}.",
"de": "Label '{label}' zu PR #{pr} hinzugefügt.",
"bg": "Added label '{label}' to PR #{pr}.",
"de": "Added label '{label}' to PR #{pr}.",
"en": "Added label '{label}' to PR #{pr}.",
"pl": "Dodano etykietę '{label}' do PR #{pr}.",
"ru": "Добавлена метка '{label}' к PR #{pr}.",
"zh": "已向 PR #{pr} 添加标签 '{label}'。"
"pl": "Added label '{label}' to PR #{pr}.",
"ru": "Added label '{label}' to PR #{pr}.",
"zh": "Added label '{label}' to PR #{pr}."
},
"Additional directory to scan (default: scripts, tests). Can be repeated.": {
"bg": "Допълнителна директория за сканиране (по подразбиране: scripts, tests). Може да се повтаря.",
"de": "Zusätzliches zu scannendes Verzeichnis (Standard: scripts, tests). Wiederholbar.",
"bg": "Additional directory to scan (default: scripts, tests). Can be repeated.",
"de": "Additional directory to scan (default: scripts, tests). Can be repeated.",
"en": "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)。可重复使用。"
"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."
},
"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": "附加排除模式(除默认模式外)"
"All molecule tests passed.": {
"bg": "All molecule tests passed.",
"de": "All molecule tests passed.",
"en": "All molecule tests passed.",
"pl": "Wszystkie testy molecule zakończone pomyślnie.",
"ru": "All molecule tests passed.",
"zh": "All molecule tests passed."
},
"Allow empty tag (PR mode where SHA is concrete).": {
"bg": "Позволи празен таг (PR режим, където SHA е конкретен).",
@@ -823,93 +799,77 @@
"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.",
"en": "Another runner failed. Stopping this runner early.",
"pl": "Inny runner zakończył się niepowodzeniem. Wczesne zatrzymanie tego runnera.",
"ru": "Другой runner завершился с ошибкой. Останавливаю этот runner досрочно.",
"zh": "另一个 runner 失败。提前停止此 runner。"
"Another molecule runner failed. Stopping this runner early.": {
"bg": "Another molecule runner failed. Stopping this runner early.",
"de": "Another molecule runner failed. Stopping this runner early.",
"en": "Another molecule runner failed. Stopping this runner early.",
"pl": "Inny runner molecule zakończył się niepowodzeniem. Wczesne zatrzymanie tego runnera.",
"ru": "Another molecule runner failed. Stopping this runner early.",
"zh": "Another molecule runner failed. Stopping this runner early."
},
"Assigned {count} files to runner {runner_index}": {
"bg": "Разпределени {count} файла към runner {runner_index}",
"de": "{count} Dateien an Runner {runner_index} zugewiesen",
"bg": "Assigned {count} files to runner {runner_index}",
"de": "Assigned {count} files to runner {runner_index}",
"en": "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}"
"pl": "Assigned {count} files to runner {runner_index}",
"ru": "Assigned {count} files to runner {runner_index}",
"zh": "Assigned {count} files to runner {runner_index}"
},
"Assigned {count} items to runner {runner_index}: {encoded}": {
"bg": "Разпределени {count} елемента към runner {runner_index}: {encoded}",
"de": "{count} Elemente an Runner {runner_index} zugewiesen: {encoded}",
"bg": "Assigned {count} items to runner {runner_index}: {encoded}",
"de": "Assigned {count} items to runner {runner_index}: {encoded}",
"en": "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}"
"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}"
},
"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.",
"bg": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"de": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"en": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"pl": "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 标签。"
"pl": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"ru": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"zh": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label."
},
"Automated CI commit (badge) — skipping post-merge jobs.": {
"bg": "Автоматизиран CI комит (значка) — пропускат се post-merge задачите.",
"de": "Automatisierter CI-Commit (Badge) — Post-Merge-Jobs werden übersprungen.",
"bg": "Automated CI commit (badge) — skipping post-merge jobs.",
"de": "Automated CI commit (badge) — skipping post-merge jobs.",
"en": "Automated CI commit (badge) — skipping post-merge jobs.",
"pl": "Zautomatyzowany commit CI (odznaka) — pomijanie zadań post-merge.",
"ru": "Автоматический CI-коммит (значок) — post-merge задачи пропускаются.",
"zh": "自动 CI 提交(徽章)——跳过后续合并任务。"
"pl": "Automated CI commit (badge) — skipping post-merge jobs.",
"ru": "Automated CI commit (badge) — skipping post-merge jobs.",
"zh": "Automated CI commit (badge) — skipping post-merge jobs."
},
"Badge push attempt {attempt}/{retries} failed — retrying: {error}": {
"bg": "Опит {attempt}/{retries} за push на значки се провали — повторен опит: {error}",
"de": "Badge-Push-Versuch {attempt}/{retries} fehlgeschlagen — erneuter Versuch: {error}",
"bg": "Badge push attempt {attempt}/{retries} failed — retrying: {error}",
"de": "Badge push attempt {attempt}/{retries} failed — retrying: {error}",
"en": "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}"
"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}"
},
"Badge push failed after {retries} attempts: {error}": {
"bg": "Push на значки се провали след {retries} опита: {error}",
"de": "Badge-Push nach {retries} Versuchen fehlgeschlagen: {error}",
"bg": "Badge push failed after {retries} attempts: {error}",
"de": "Badge push failed after {retries} attempts: {error}",
"en": "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}"
"pl": "Badge push failed after {retries} attempts: {error}",
"ru": "Badge push failed after {retries} attempts: {error}",
"zh": "Badge push failed after {retries} attempts: {error}"
},
"Badges commit SHA: {sha}": {
"bg": "SHA на комита със значки: {sha}",
"de": "SHA des Badge-Commits: {sha}",
"bg": "Badges commit SHA: {sha}",
"de": "Badges commit SHA: {sha}",
"en": "Badges commit SHA: {sha}",
"pl": "SHA commita z odznakami: {sha}",
"ru": "SHA коммита значков: {sha}",
"zh": "徽章提交 SHA{sha}"
"pl": "Badges commit SHA: {sha}",
"ru": "Badges commit SHA: {sha}",
"zh": "Badges commit SHA: {sha}"
},
"Badges pushed to badges branch": {
"bg": "Значките са push-нати към клона badges",
"de": "Badges zum badges-Branch gepusht",
"bg": "Badges pushed to badges branch",
"de": "Badges pushed to badges branch",
"en": "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"
"pl": "Badges pushed to badges branch",
"ru": "Badges pushed to badges branch",
"zh": "Badges pushed to badges branch"
},
"Branch '{branch}' does not contain a task ID.\n Expected format: {prefix}-N-short-description": {
"bg": "Клон '{branch}' не съдържа ID на задача.\n Очакван формат: {prefix}-N-кратко-описание",
@@ -928,148 +888,108 @@
"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": "Клонът вече е актуален спрямо origin/master.",
"de": "Branch ist bereits aktuell mit origin/master.",
"bg": "Branch is already up-to-date with origin/master.",
"de": "Branch is already up-to-date with origin/master.",
"en": "Branch is already up-to-date with origin/master.",
"pl": "Gałąź jest już aktualna względem origin/master.",
"ru": "Ветка уже актуальна относительно origin/master.",
"zh": "分支已与 origin/master 同步。"
"pl": "Branch is already up-to-date with origin/master.",
"ru": "Branch is already up-to-date with origin/master.",
"zh": "Branch is already up-to-date with origin/master."
},
"Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.": {
"bg": "Клонът изостава от 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.",
"bg": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"de": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"en": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"pl": "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"
"pl": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"ru": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"zh": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR."
},
"Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master": {
"bg": "Клонът изостава от 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",
"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",
"en": "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"
"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"
},
"Branch is {count} commit(s) behind master. Rebasing...": {
"bg": "Клонът изостава с {count} комит(а) от master. Rebase...",
"de": "Branch ist {count} Commit(s) hinter master. Rebase läuft...",
"bg": "Branch is {count} commit(s) behind master. Rebasing...",
"de": "Branch is {count} commit(s) behind master. Rebasing...",
"en": "Branch is {count} commit(s) behind master. Rebasing...",
"pl": "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 自动获取)"
"pl": "Branch is {count} commit(s) behind master. Rebasing...",
"ru": "Branch is {count} commit(s) behind master. Rebasing...",
"zh": "Branch is {count} commit(s) behind master. Rebasing..."
},
"Branch name (e.g., DEVX-256-fix-foo)": {
"bg": "Име на клон (напр. DEVX-256-fix-foo)",
"de": "Branch-Name (z. B. DEVX-256-fix-foo)",
"bg": "Branch name (e.g., DEVX-256-fix-foo)",
"de": "Branch name (e.g., DEVX-256-fix-foo)",
"en": "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"
"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)"
},
"Branch name must contain a task ID.": {
"bg": "Името на клона трябва да съдържа task ID.",
"de": "Der Branch-Name muss eine Task-ID enthalten.",
"bg": "Branch name must contain a task ID.",
"de": "Branch name must contain a task ID.",
"en": "Branch name must contain a task ID.",
"pl": "Nazwa gałęzi musi zawierać ID zadania.",
"ru": "Имя ветки должно содержать ID задачи.",
"zh": "分支名称必须包含任务 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."
},
"Build failed for {name}": {
"bg": "Изграждането на {name} се провали",
"de": "Build für {name} fehlgeschlagen",
"bg": "Build failed for {name}",
"de": "Build failed for {name}",
"en": "Build failed for {name}",
"pl": "Budowanie {name} nie powiodło się",
"ru": "Сборка {name} не удалась",
"zh": "{name} 构建失败"
"pl": "Build failed for {name}",
"ru": "Build failed for {name}",
"zh": "Build failed for {name}"
},
"Bumping version: {current} -> v{new_version}": {
"bg": "Увеличаване на версията: {current} -> v{new_version}",
"de": "Version wird erhöht: {current} -> v{new_version}",
"bg": "Bumping version: {current} -> v{new_version}",
"de": "Bumping version: {current} -> v{new_version}",
"en": "Bumping version: {current} -> v{new_version}",
"pl": "Zmiana wersji: {current} -> v{new_version}",
"ru": "Повышение версии: {current} -> v{new_version}",
"zh": "升级版本:{current} -> v{new_version}"
"ru": "Bumping version: {current} -> v{new_version}",
"zh": "Bumping version: {current} -> v{new_version}"
},
"CI checks did not complete within timeout.": {
"bg": "CI проверките не завършиха в рамките на таймаута.",
"de": "CI-Checks wurden nicht innerhalb des Timeouts abgeschlossen.",
"bg": "CI checks did not complete within timeout.",
"de": "CI checks did not complete within timeout.",
"en": "CI checks did not complete within timeout.",
"pl": "Kontrole CI nie zakończyły się w ramach limitu czasu.",
"ru": "CI-проверки не завершились в течение таймаута.",
"zh": "CI 检查未在超时时间内完成。"
"pl": "CI checks did not complete within timeout.",
"ru": "CI checks did not complete within timeout.",
"zh": "CI checks did not complete within timeout."
},
"CI checks failed.": {
"bg": "CI проверките се провалиха.",
"de": "CI-Checks fehlgeschlagen.",
"bg": "CI checks failed.",
"de": "CI checks failed.",
"en": "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}"
"pl": "CI checks failed.",
"ru": "CI checks failed.",
"zh": "CI checks failed."
},
"CI_GITEA_TOKEN environment variable required": {
"bg": "Изисква се променлива на средата CI_GITEA_TOKEN",
"de": "Umgebungsvariable CI_GITEA_TOKEN erforderlich",
"bg": "CI_GITEA_TOKEN environment variable required",
"de": "CI_GITEA_TOKEN environment variable required",
"en": "CI_GITEA_TOKEN environment variable required",
"pl": "Wymagana zmienna środowiskowa CI_GITEA_TOKEN",
"ru": "Требуется переменная окружения CI_GITEA_TOKEN",
"zh": "需要环境变量 CI_GITEA_TOKEN"
"pl": "CI_GITEA_TOKEN environment variable required",
"ru": "CI_GITEA_TOKEN environment variable required",
"zh": "CI_GITEA_TOKEN environment variable required"
},
"CI_GITEA_TOKEN is not set.": {
"bg": "CI_GITEA_TOKEN не е зададен.",
"de": "CI_GITEA_TOKEN ist nicht gesetzt.",
"bg": "CI_GITEA_TOKEN is not set.",
"de": "CI_GITEA_TOKEN is not set.",
"en": "CI_GITEA_TOKEN is not set.",
"pl": "CI_GITEA_TOKEN nie jest ustawiony.",
"ru": "CI_GITEA_TOKEN не задан.",
"zh": "未设置 CI_GITEA_TOKEN"
"pl": "CI_GITEA_TOKEN is not set.",
"ru": "CI_GITEA_TOKEN is not set.",
"zh": "CI_GITEA_TOKEN is not set."
},
"CI_GITEA_TOKEN is not set. Add it to .env or export it.": {
"bg": "CI_GITEA_TOKEN не е зададен. Добавете го в .env или го експортирайте.",
"de": "CI_GITEA_TOKEN ist nicht gesetzt. Zu .env hinzufügen oder exportieren.",
"bg": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"de": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"en": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"pl": "CI_GITEA_TOKEN nie jest ustawiony. Dodaj go do .env lub wyeksportuj.",
"ru": "CI_GITEA_TOKEN не задан. Добавьте его в .env или экспортируйте.",
"zh": "未设置 CI_GITEA_TOKEN。请添加到 .env 或导出。"
"pl": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"ru": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"zh": "CI_GITEA_TOKEN is not set. Add it to .env or export it."
},
"CI_GITEA_TOKEN is not set. Required to create a PR.": {
"bg": "CI_GITEA_TOKEN не е зададен. Необходим за създаване на PR.",
@@ -1080,12 +1000,12 @@
"zh": "CI_GITEA_TOKEN 未设置。创建 PR 所需。"
},
"CI_GITEA_TOKEN not set — skipping login configuration.": {
"bg": "CI_GITEA_TOKEN не е зададен — пропуска се конфигурацията за вход.",
"de": "CI_GITEA_TOKEN nicht gesetztLogin-Konfiguration wird übersprungen.",
"bg": "CI_GITEA_TOKEN not set — skipping login configuration.",
"de": "CI_GITEA_TOKEN not set — skipping login configuration.",
"en": "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——跳过登录配置。"
"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."
},
"Cannot read __version__ from src/{pkg}/__init__.py — skipping.": {
"bg": "",
@@ -1096,20 +1016,20 @@
"zh": ""
},
"Cannot rebase: not on a branch (detached HEAD).": {
"bg": "Не може rebase: не сте на клон (detached HEAD).",
"de": "Rebase nicht möglich: nicht auf einem Branch (detached HEAD).",
"bg": "Cannot rebase: not on a branch (detached HEAD).",
"de": "Cannot rebase: not on a branch (detached HEAD).",
"en": "Cannot rebase: not on a branch (detached HEAD).",
"pl": "Nie można wykonać rebase: nie na gałęzi (detached HEAD).",
"ru": "Невозможно выполнить rebase: не на ветке (detached HEAD).",
"zh": "无法 rebase:不在分支上(detached HEAD)。"
"pl": "Cannot rebase: not on a branch (detached HEAD).",
"ru": "Cannot rebase: not on a branch (detached HEAD).",
"zh": "Cannot rebase: not on a branch (detached HEAD)."
},
"Checking CLI command documentation...": {
"bg": "Проверка на документацията за CLI команди...",
"de": "Prüfe CLI-Befehlsdokumentation...",
"bg": "Checking CLI command documentation...",
"de": "Checking CLI command documentation...",
"en": "Checking CLI command documentation...",
"pl": "Sprawdzanie dokumentacji poleceń CLI...",
"ru": "Проверка документации CLI-команд...",
"zh": "正在检查 CLI 命令文档..."
"ru": "Checking CLI command documentation...",
"zh": "Checking CLI command documentation..."
},
"Checking code block languages...": {
"bg": "",
@@ -1120,28 +1040,28 @@
"zh": ""
},
"Checking docs structure...": {
"bg": "Проверка на структурата на документацията...",
"de": "Prüfe Dokumentationsstruktur...",
"bg": "Checking docs structure...",
"de": "Checking docs structure...",
"en": "Checking docs structure...",
"pl": "Sprawdzanie struktury dokumentacji...",
"ru": "Проверка структуры документации...",
"zh": "正在检查文档结构..."
"pl": "Checking docs structure...",
"ru": "Checking docs structure...",
"zh": "Checking docs structure..."
},
"Checking duplicate headings...": {
"bg": "Проверка за дублирани заглавия...",
"de": "Prüfe auf doppelte Überschriften...",
"bg": "Checking duplicate headings...",
"de": "Checking duplicate headings...",
"en": "Checking duplicate headings...",
"pl": "Sprawdzanie zduplikowanych nagłówków...",
"ru": "Проверка дублирующихся заголовков...",
"zh": "正在检查重复标题..."
"pl": "Checking duplicate headings...",
"ru": "Checking duplicate headings...",
"zh": "Checking duplicate headings..."
},
"Checking for TODO/FIXME markers...": {
"bg": "Проверка за TODO/FIXME маркери...",
"de": "Prüfe auf TODO/FIXME-Marker...",
"bg": "Checking for TODO/FIXME markers...",
"de": "Checking for TODO/FIXME markers...",
"en": "Checking for TODO/FIXME markers...",
"pl": "Sprawdzanie znaczników TODO/FIXME...",
"ru": "Проверка меток TODO/FIXME...",
"zh": "正在检查 TODO/FIXME 标记..."
"pl": "Checking for TODO/FIXME markers...",
"ru": "Checking for TODO/FIXME markers...",
"zh": "Checking for TODO/FIXME markers..."
},
"Checking for orphan docs...": {
"bg": "",
@@ -1152,28 +1072,28 @@
"zh": ""
},
"Checking for stale docs...": {
"bg": "Проверка за остарели документи...",
"de": "Prüfe auf veraltete Dokumente...",
"bg": "Checking for stale docs...",
"de": "Checking for stale docs...",
"en": "Checking for stale docs...",
"pl": "Sprawdzanie nieaktualnych dokumentów...",
"ru": "Проверка устаревших документов...",
"zh": "正在检查过时文档..."
"pl": "Checking for stale docs...",
"ru": "Checking for stale docs...",
"zh": "Checking for stale docs..."
},
"Checking heading hierarchy...": {
"bg": "Проверка на йерархията на заглавията...",
"de": "Prüfe Überschriftenhierarchie...",
"bg": "Checking heading hierarchy...",
"de": "Checking heading hierarchy...",
"en": "Checking heading hierarchy...",
"pl": "Sprawdzanie hierarchii nagłówków...",
"ru": "Проверка иерархии заголовков...",
"zh": "正在检查标题层级..."
"pl": "Checking heading hierarchy...",
"ru": "Checking heading hierarchy...",
"zh": "Checking heading hierarchy..."
},
"Checking internal links...": {
"bg": "Проверка на вътрешни връзки...",
"de": "Prüfe interne Links...",
"bg": "Checking internal links...",
"de": "Checking internal links...",
"en": "Checking internal links...",
"pl": "Sprawdzanie linków wewnętrznych...",
"ru": "Проверка внутренних ссылок...",
"zh": "正在检查内部链接..."
"pl": "Checking internal links...",
"ru": "Checking internal links...",
"zh": "Checking internal links..."
},
"Checking line length...": {
"bg": "",
@@ -1192,12 +1112,12 @@
"zh": ""
},
"Checking required files...": {
"bg": "Проверка на задължителните файлове...",
"de": "Prüfe erforderliche Dateien...",
"bg": "Checking required files...",
"de": "Checking required files...",
"en": "Checking required files...",
"pl": "Sprawdzanie wymaganych plików...",
"ru": "Проверка обязательных файлов...",
"zh": "正在检查必需文件..."
"pl": "Checking required files...",
"ru": "Checking required files...",
"zh": "Checking required files..."
},
"Checking single H1 per file...": {
"bg": "",
@@ -1208,20 +1128,20 @@
"zh": ""
},
"Checking status for PR #{pr_number}...": {
"bg": "Проверка на статуса на PR #{pr_number}...",
"de": "Prüfe Status für PR #{pr_number}...",
"bg": "Checking status for PR #{pr_number}...",
"de": "Checking status for PR #{pr_number}...",
"en": "Checking status for PR #{pr_number}...",
"pl": "Sprawdzanie statusu PR #{pr_number}...",
"ru": "Проверка статуса PR #{pr_number}...",
"zh": "正在检查 PR #{pr_number} 的状态..."
"pl": "Checking status for PR #{pr_number}...",
"ru": "Checking status for PR #{pr_number}...",
"zh": "Checking status for PR #{pr_number}..."
},
"Checking trailing whitespace...": {
"bg": "Проверка за крайни интервали...",
"de": "Prüfe auf abschließende Leerzeichen...",
"bg": "Checking trailing whitespace...",
"de": "Checking trailing whitespace...",
"en": "Checking trailing whitespace...",
"pl": "Sprawdzanie końcowych białych znaków...",
"ru": "Проверка конечных пробелов...",
"zh": "正在检查行尾空白..."
"pl": "Checking trailing whitespace...",
"ru": "Checking trailing whitespace...",
"zh": "Checking trailing whitespace..."
},
"Checking version references for {pkg} (current: v{version})": {
"bg": "",
@@ -1231,14 +1151,6 @@
"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": "",
@@ -1256,28 +1168,28 @@
"zh": ""
},
"Command failed ({cmd}): {stderr}": {
"bg": "Командата се провали ({cmd}): {stderr}",
"de": "Befehl fehlgeschlagen ({cmd}): {stderr}",
"bg": "Command failed ({cmd}): {stderr}",
"de": "Command failed ({cmd}): {stderr}",
"en": "Command failed ({cmd}): {stderr}",
"pl": "Polecenie nie powiodło się ({cmd}): {stderr}",
"ru": "Команда завершилась с ошибкой ({cmd}): {stderr}",
"zh": "命令失败({cmd}):{stderr}"
"ru": "Command failed ({cmd}): {stderr}",
"zh": "Command failed ({cmd}): {stderr}"
},
"Commit message: {msg}": {
"bg": "Съобщение на комит: {msg}",
"de": "Commit-Nachricht: {msg}",
"bg": "Commit message: {msg}",
"de": "Commit message: {msg}",
"en": "Commit message: {msg}",
"pl": "Treść commita: {msg}",
"ru": "Сообщение коммита: {msg}",
"zh": "提交信息:{msg}"
"pl": "Commit message: {msg}",
"ru": "Commit message: {msg}",
"zh": "Commit message: {msg}"
},
"Commit: {sha}": {
"bg": "Комит: {sha}",
"bg": "Commit: {sha}",
"de": "Commit: {sha}",
"en": "Commit: {sha}",
"pl": "Commit: {sha}",
"ru": "Коммит: {sha}",
"zh": "提交:{sha}"
"ru": "Commit: {sha}",
"zh": "Commit: {sha}"
},
"Committing and pushing...": {
"bg": "",
@@ -1288,12 +1200,12 @@
"zh": ""
},
"Comparing {base}..{head} ({count} files changed)": {
"bg": "Сравняване на {base}..{head} ({count} променени файла)",
"de": "Vergleiche {base}..{head} ({count} geänderte Dateien)",
"bg": "Comparing {base}..{head} ({count} files changed)",
"de": "Comparing {base}..{head} ({count} files changed)",
"en": "Comparing {base}..{head} ({count} files changed)",
"pl": "Porównywanie {base}..{head} ({count} zmienionych plików)",
"ru": "Сравнение {base}..{head} ({count} изменённых файлов)",
"zh": "正在比较 {base}..{head}{count} 个文件已更改)"
"ru": "Comparing {base}..{head} ({count} files changed)",
"zh": "Comparing {base}..{head} ({count} files changed)"
},
"Configuration OK: [tool.devx] present, devx versions consistent.": {
"bg": "Конфигурацията е OK: [tool.devx] присъства, версиите на devx са консистентни.",
@@ -1304,12 +1216,12 @@
"zh": "配置正常: [tool.devx] 已存在, devx 版本一致。"
},
"Configuration validation failed.": {
"bg": "Валидацията на конфигурацията се провали.",
"de": "Konfigurationsvalidierung fehlgeschlagen.",
"bg": "Configuration validation failed.",
"de": "Configuration validation failed.",
"en": "Configuration validation failed.",
"pl": "Walidacja konfiguracji nie powiodła się.",
"ru": "Проверка конфигурации не удалась.",
"zh": "配置验证失败。"
"pl": "Configuration validation failed.",
"ru": "Configuration validation failed.",
"zh": "Configuration validation failed."
},
"Configuring branch protection for {branch}...": {
"bg": "Конфигуриране на защита на клона {branch}...",
@@ -1328,20 +1240,20 @@
"zh": "正在配置仓库设置..."
},
"Configuring tea login '{name}' for {url}...": {
"bg": "Конфигуриране на tea вход '{name}' за {url}...",
"de": "Konfiguriere tea-Login '{name}' für {url}...",
"bg": "Configuring tea login '{name}' for {url}...",
"de": "Configuring tea login '{name}' for {url}...",
"en": "Configuring tea login '{name}' for {url}...",
"pl": "Konfigurowanie logowania tea '{name}' dla {url}...",
"ru": "Настройка входа tea '{name}' для {url}...",
"zh": "正在为 {url} 配置 tea 登录 '{name}'..."
"pl": "Configuring tea login '{name}' for {url}...",
"ru": "Configuring tea login '{name}' for {url}...",
"zh": "Configuring tea login '{name}' for {url}..."
},
"Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.": {
"bg": "Не може да се определи номерът на 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.",
"bg": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"de": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"en": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"pl": "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 的分支上运行此命令。"
"pl": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"ru": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"zh": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR."
},
"Could not detect current branch: {error}": {
"bg": "Не може да се определи текущия клон: {error}",
@@ -1351,45 +1263,37 @@
"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": "Не може да се определи head SHA за PR #{pr_number}.",
"de": "Head-SHA für PR #{pr_number} konnte nicht ermittelt werden.",
"bg": "Could not determine head SHA for PR #{pr_number}.",
"de": "Could not determine head SHA for PR #{pr_number}.",
"en": "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。"
"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}."
},
"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.",
"bg": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"de": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"en": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"pl": "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"
"pl": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"ru": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"zh": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables."
},
"Could not extract conventional commit message from PR commits.": {
"bg": "Не може да се извлече conventional commit съобщение от PR комитите.",
"de": "Konnte keine Conventional-Commit-Nachricht aus den PR-Commits extrahieren.",
"bg": "Could not extract conventional commit message from PR commits.",
"de": "Could not extract conventional commit message from PR commits.",
"en": "Could not extract conventional commit message from PR commits.",
"pl": "Nie udało się wyodrębnić konwencjonalnej wiadomości commit z commitów PR.",
"ru": "Не удалось извлечь conventional commit сообщение из коммитов PR.",
"zh": "无法从 PR 提交中提取 conventional commit 信息。"
"ru": "Could not extract conventional commit message from PR commits.",
"zh": "Could not extract conventional commit message from PR commits."
},
"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).",
"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).",
"en": "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 未找到)。"
"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)."
},
"Could not find Vikunja task {task_id} in project {project_id}.": {
"bg": "Не е намерена Vikunja задача {task_id} в проект {project_id}.",
@@ -1400,36 +1304,28 @@
"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": "Не е намерена 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.",
"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.",
"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": "Задача Vikunja {task_id} в проекте {project_id} не найдена. Каждый PR должен иметь соответствующую задачу Vikunja.",
"zh": "在项目 {project_id} 中未找到 Vikunja 任务 {task_id}。每个 PR 必须有对应的 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."
},
"Could not find __version__ in {file}": {
"bg": "Не е намерен __version__ в {file}",
"de": "__version__ in {file} nicht gefunden",
"bg": "Could not find __version__ in {file}",
"de": "Could not find __version__ in {file}",
"en": "Could not find __version__ in {file}",
"pl": "Nie znaleziono __version__ w {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} 的固定版本"
"ru": "Could not find __version__ in {file}",
"zh": "Could not find __version__ in {file}"
},
"Could not parse test execution time from output.": {
"bg": "Не може да се извлече време за изпълнение на теста от изхода.",
"de": "Testausführungszeit konnte aus der Ausgabe nicht gelesen werden.",
"bg": "Could not parse test execution time from output.",
"de": "Could not parse test execution time from output.",
"en": "Could not parse test execution time from output.",
"pl": "Nie udało się przeanalizować czasu wykonania testu z wyjścia.",
"ru": "Не удалось извлечь время выполнения теста из вывода.",
"zh": "无法从输出中解析测试执行时间。"
"ru": "Could not parse test execution time from output.",
"zh": "Could not parse test execution time from output."
},
"Created PR #{index}: {title}\n {url}": {
"bg": "Създаден PR #{index}: {title}\n {url}",
@@ -1448,44 +1344,28 @@
"zh": "已创建 Vikunja 任务: {identifier} (id={task_id})"
},
"Created issue #{issue_id}: {title}": {
"bg": "Създадено issue #{issue_id}: {title}",
"de": "Issue #{issue_id} erstellt: {title}",
"bg": "Created issue #{issue_id}: {title}",
"de": "Created issue #{issue_id}: {title}",
"en": "Created issue #{issue_id}: {title}",
"pl": "Utworzono zgłoszenie #{issue_id}: {title}",
"ru": "Создано issue #{issue_id}: {title}",
"zh": "已创建 issue #{issue_id}{title}"
"ru": "Created issue #{issue_id}: {title}",
"zh": "Created issue #{issue_id}: {title}"
},
"Created release commit.": {
"bg": "Създаден е release комит.",
"de": "Release-Commit erstellt.",
"bg": "Created release commit.",
"de": "Created release commit.",
"en": "Created release commit.",
"pl": "Utworzono commit wydania.",
"ru": "Создан релизный коммит.",
"zh": "已创建发布提交。"
"ru": "Created release commit.",
"zh": "Created release commit."
},
"Dependencies must have documentation comments.": {
"bg": "Зависимостите трябва да имат документиращи коментари.",
"de": "Abhängigkeiten müssen Dokumentationskommentare haben.",
"bg": "Dependencies must have documentation comments.",
"de": "Dependencies must have documentation comments.",
"en": "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": "包含规范文件的目录"
"pl": "Dependencies must have documentation comments.",
"ru": "Dependencies must have documentation comments.",
"zh": "Dependencies must have documentation comments."
},
"Directory to scan (default: tests/integration). Can be repeated.": {
"bg": "Директория за сканиране (по подразбиране: tests/integration). Може да се повтаря.",
@@ -1504,7 +1384,7 @@
"zh": "Docker 守护进程已在运行"
},
"Docker daemon failed to start": {
"bg": "Docker демонът не успя да стартира",
"bg": "Docker daemon failed to start",
"de": "Docker-Daemon konnte nicht gestartet werden",
"en": "Docker daemon failed to start",
"pl": "Nie udało się uruchomić demona Docker",
@@ -1512,7 +1392,7 @@
"zh": "Docker 守护进程启动失败"
},
"Docker daemon started": {
"bg": "Docker демонът стартира",
"bg": "Docker daemon started",
"de": "Docker-Daemon gestartet",
"en": "Docker daemon started",
"pl": "Demon Docker uruchomiony",
@@ -1520,20 +1400,20 @@
"zh": "Docker 守护进程已启动"
},
"Dockerfile not found: {path}": {
"bg": "Dockerfile не е намерен: {path}",
"de": "Dockerfile nicht gefunden: {path}",
"bg": "Dockerfile not found: {path}",
"de": "Dockerfile not found: {path}",
"en": "Dockerfile not found: {path}",
"pl": "Nie znaleziono Dockerfile: {path}",
"ru": "Dockerfile не найден: {path}",
"zh": "未找到 Dockerfile{path}"
"pl": "Dockerfile not found: {path}",
"ru": "Dockerfile not found: {path}",
"zh": "Dockerfile not found: {path}"
},
"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.",
"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.",
"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: в ветке '{branch}' (не master). Некоторые проверки могут вести себя иначе.",
"zh": "Dry-run 模式:在分支 '{branch}' 上(非 master)。某些检查可能表现不同。"
"ru": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
"zh": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently."
},
"ERROR: CI_GITEA_TOKEN is not set.": {
"bg": "ГРЕШКА: CI_GITEA_TOKEN не е зададен.",
@@ -1552,12 +1432,12 @@
"zh": "错误:未指定仓库名称。请使用 --repo 或设置 DEVX_REPO_NAME。"
},
"ERROR: Tag consistency check failed. Existing tags are misaligned:": {
"bg": "ГРЕШКА: Проверката за консистентност на таговете се провали. Съществуващите тагове са несъответстващи:",
"de": "FEHLER: Tag-Konsistenzprüfung fehlgeschlagen. Bestehende Tags sind falsch zugeordnet:",
"bg": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
"de": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
"en": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
"pl": "BŁĄD: Kontrola zgodności tagów nie powiodła się. Istniejące tagi są niezgodne:",
"ru": "ОШИБКА: Проверка согласованности тегов не удалась. Существующие теги несогласованы:",
"zh": "错误:标签一致性检查失败。现有标签不匹配:"
"ru": "ERROR: Tag consistency check failed. Existing tags are misaligned:",
"zh": "ERROR: Tag consistency check failed. Existing tags are misaligned:"
},
"ERROR: VIKUNJA_TOKEN is not set.": {
"bg": "ГРЕШКА: VIKUNJA_TOKEN не е зададен.",
@@ -1568,12 +1448,12 @@
"zh": "错误:未设置 VIKUNJA_TOKEN。"
},
"ERROR: mapping.json not found at {path}": {
"bg": "ГРЕШКА: mapping.json не е намерен в {path}",
"de": "FEHLER: mapping.json nicht gefunden unter {path}",
"bg": "ERROR: mapping.json not found at {path}",
"de": "ERROR: mapping.json not found at {path}",
"en": "ERROR: mapping.json not found at {path}",
"pl": "BŁĄD: mapping.json nie znaleziono w {path}",
"ru": "ОШИБКА: mapping.json не найден по пути {path}",
"zh": "错误:在 {path} 未找到 mapping.json"
"ru": "ERROR: mapping.json not found at {path}",
"zh": "ERROR: mapping.json not found at {path}"
},
"Each item must be a string or an object with 'id', got {type}": {
"bg": "Всеки елемент трябва да е низ или обект с 'id', получено {type}",
@@ -1584,12 +1464,12 @@
"zh": "每个元素必须是字符串或带有 'id' 的对象,得到 {type}"
},
"Ensuring standard labels...": {
"bg": "Осигуряване на стандартни етикети...",
"de": "Standard-Labels werden sichergestellt...",
"bg": "Ensuring standard labels...",
"de": "Ensuring standard labels...",
"en": "Ensuring standard labels...",
"pl": "Zapewnianie standardowych etykiet...",
"ru": "Обеспечение стандартных меток...",
"zh": "正在确保标准标签..."
"pl": "Ensuring standard labels...",
"ru": "Ensuring standard labels...",
"zh": "Ensuring standard labels..."
},
"FAIL: Could not clone wiki for verification.": {
"bg": "",
@@ -1600,76 +1480,68 @@
"zh": ""
},
"FAIL: {n} documentation issues found:": {
"bg": "ГРЕШКА: Намерени {n} проблема в документацията:",
"de": "FEHLER: {n} Dokumentationsprobleme gefunden:",
"bg": "FAIL: {n} documentation issues found:",
"de": "FAIL: {n} documentation issues found:",
"en": "FAIL: {n} documentation issues found:",
"pl": "BŁĄD: Znaleziono {n} problemów z dokumentacją:",
"ru": "ОШИБКА: Найдено {n} проблем в документации:",
"zh": "失败:发现 {n} 个文档问题:"
"pl": "FAIL: {n} documentation issues found:",
"ru": "FAIL: {n} documentation issues found:",
"zh": "FAIL: {n} documentation issues found:"
},
"FAILED: {count} undocumented dependency/ies": {
"bg": "НЕУСПЕШНО: {count} недокументирани зависимости",
"de": "FEHLGESCHLAGEN: {count} undokumentierte Abhängigkeit(en)",
"bg": "FAILED: {count} undocumented dependency/ies",
"de": "FAILED: {count} undocumented dependency/ies",
"en": "FAILED: {count} undocumented dependency/ies",
"pl": "NIEUDANE: {count} nieudokumentowanych zależności",
"ru": "ПРОВАЛЕНО: {count} недокументированных зависимостей",
"zh": "失败:{count} 个未记录的依赖项"
"pl": "FAILED: {count} undocumented dependency/ies",
"ru": "FAILED: {count} undocumented dependency/ies",
"zh": "FAILED: {count} undocumented dependency/ies"
},
"FAILED: {pair} exited with code {code}": {
"bg": "FAILED: {pair} exited with code {code}",
"de": "FAILED: {pair} exited with code {code}",
"en": "FAILED: {pair} exited with code {code}",
"pl": "NIEUDANE: {pair} zakończone kodem {code}",
"ru": "FAILED: {pair} exited with code {code}",
"zh": "FAILED: {pair} exited with code {code}"
},
"Failed images: {names}": {
"bg": "Неуспешни изображения: {names}",
"de": "Fehlgeschlagene Images: {names}",
"bg": "Failed images: {names}",
"de": "Failed images: {names}",
"en": "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}"
"pl": "Failed images: {names}",
"ru": "Failed images: {names}",
"zh": "Failed images: {names}"
},
"Failed to create issue via tea: {error}": {
"bg": "Неуспешно създаване на issue чрез tea: {error}",
"de": "Issue konnte nicht via tea erstellt werden: {error}",
"bg": "Failed to create issue via tea: {error}",
"de": "Failed to create issue via tea: {error}",
"en": "Failed to create issue via tea: {error}",
"pl": "Nie udało się utworzyć zgłoszenia przez tea: {error}",
"ru": "Не удалось создать issue через tea: {error}",
"zh": "通过 tea 创建 issue 失败:{error}"
"ru": "Failed to create issue via tea: {error}",
"zh": "Failed to create issue via tea: {error}"
},
"Failed to delete {count} image version(s)": {
"bg": "Неуспешно изтриване на {count} версии на изображения",
"de": "{count} Image-Version(en) konnten nicht gelöscht werden",
"bg": "Failed to delete {count} image version(s)",
"de": "Failed to delete {count} image version(s)",
"en": "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}"
"pl": "Failed to delete {count} image version(s)",
"ru": "Failed to delete {count} image version(s)",
"zh": "Failed to delete {count} image version(s)"
},
"Failed to list versions for {name}: {error}": {
"bg": "Неуспешно изброяване на версиите за {name}: {error}",
"de": "Versionen für {name} konnten nicht aufgelistet werden: {error}",
"bg": "Failed to list versions for {name}: {error}",
"de": "Failed to list versions for {name}: {error}",
"en": "Failed to list versions for {name}: {error}",
"pl": "Nie udało się wylistować wersji dla {name}: {error}",
"ru": "Не удалось получить список версий для {name}: {error}",
"zh": "列出 {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}"
},
"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.",
"bg": "Failed to push release commit after 3 attempts. Manual intervention required.",
"de": "Failed to push release commit after 3 attempts. Manual intervention required.",
"en": "Failed to push release commit after 3 attempts. Manual intervention required.",
"pl": "Nie udało się wypchnąć commita release po 3 próbach. Wymagana ręczna interwencja.",
"ru": "Не удалось отправить релизный коммит после 3 попыток. Требуется ручное вмешательство.",
"zh": "3 次尝试后仍无法推送发布提交。需要人工干预。"
"pl": "Failed to push release commit after 3 attempts. Manual intervention required.",
"ru": "Failed to push release commit after 3 attempts. Manual intervention required.",
"zh": "Failed to push release commit after 3 attempts. Manual intervention required."
},
"Failed to start ssh-agent: {error}": {
"bg": "Неуспешно стартиране на ssh-agent: {error}",
@@ -1679,93 +1551,61 @@
"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": "Извличането се провали: {error}",
"de": "Abruf fehlgeschlagen: {error}",
"bg": "Fetch failed: {error}",
"de": "Fetch failed: {error}",
"en": "Fetch failed: {error}",
"pl": "Pobieranie nie powiodło się: {error}",
"ru": "Получение не удалось: {error}",
"zh": "获取失败:{error}"
"pl": "Fetch failed: {error}",
"ru": "Fetch failed: {error}",
"zh": "Fetch failed: {error}"
},
"Fetching logs for PR #{pr_number}...": {
"bg": "Извличане на логове за PR #{pr_number}...",
"de": "Rufe Logs für PR #{pr_number} ab...",
"bg": "Fetching logs for PR #{pr_number}...",
"de": "Fetching logs for PR #{pr_number}...",
"en": "Fetching logs for PR #{pr_number}...",
"pl": "Pobieranie logów dla PR #{pr_number}...",
"ru": "Получение логов для PR #{pr_number}...",
"zh": "正在获取 PR #{pr_number} 的日志..."
"pl": "Fetching logs for PR #{pr_number}...",
"ru": "Fetching logs for PR #{pr_number}...",
"zh": "Fetching logs for PR #{pr_number}..."
},
"Fetching origin/master...": {
"bg": "Извличане на origin/master...",
"de": "Rufe origin/master ab...",
"bg": "Fetching origin/master...",
"de": "Fetching origin/master...",
"en": "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"
"pl": "Fetching origin/master...",
"ru": "Fetching origin/master...",
"zh": "Fetching origin/master..."
},
"Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.": {
"bg": "Force-push се провали:\n{error}\nОтдалеченото репозитори може да съдържа неочаквани комити. Извличане и нов опит.",
"de": "Force-Push fehlgeschlagen:\n{error}\nDas Remote kann unerwartete Commits enthalten. Fetchen und erneut versuchen.",
"bg": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"de": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"en": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"pl": "Force-push 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 后重试。"
"pl": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"ru": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"zh": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again."
},
"Force-pushing...": {
"bg": "Force-push...",
"de": "Force-Push läuft...",
"bg": "Force-pushing...",
"de": "Force-pushing...",
"en": "Force-pushing...",
"pl": "Wymuszone wypychanie...",
"ru": "Force-push...",
"zh": "正在强制推送..."
"pl": "Force-pushing...",
"ru": "Force-pushing...",
"zh": "Force-pushing..."
},
"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.",
"bg": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
"de": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
"en": "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"
"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."
},
"Found {count} stale documentation reference(s)": {
"bg": "Намерени {count} остарели препратки в документацията",
"de": "{count} veraltete Dokumentationsreferenz(en) gefunden",
"bg": "Found {count} stale documentation reference(s)",
"de": "Found {count} stale documentation reference(s)",
"en": "Found {count} stale documentation reference(s)",
"pl": "Znaleziono {count} nieaktualnych odwołań w dokumentacji",
"ru": "Найдено {count} устаревших ссылок в документации",
"zh": "发现 {count} 个过时的文档引用"
"pl": "Found {count} stale documentation reference(s)",
"ru": "Found {count} stale documentation reference(s)",
"zh": "Found {count} stale documentation reference(s)"
},
"Found {count} unsafe identity check(s) in integration tests.": {
"bg": "Намерени са {count} небрежни проверки за идентичност в интеграционните тестове.",
@@ -1776,44 +1616,44 @@
"zh": "在集成测试中发现 {count} 个不安全的身份检查。"
},
"Found {count} version(s):": {
"bg": "Намерени {count} версии:",
"de": "{count} Version(en) gefunden:",
"bg": "Found {count} version(s):",
"de": "Found {count} version(s):",
"en": "Found {count} version(s):",
"pl": "Znaleziono {count} wersji:",
"ru": "Найдено {count} версий:",
"zh": "找到 {count} 个版本:"
"pl": "Found {count} version(s):",
"ru": "Found {count} version(s):",
"zh": "Found {count} version(s):"
},
"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.",
"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.",
"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 не заданы; работа без отмены между раннерами.",
"zh": "未设置 GITEA_URL/CI_GITEA_TOKEN/RUN_ID;运行时无法进行跨 runner 取消。"
"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."
},
"Generated {count} badge files": {
"bg": "Генерирани {count} файла със значки",
"de": "{count} Badge-Dateien generiert",
"bg": "Generated {count} badge files",
"de": "Generated {count} badge files",
"en": "Generated {count} badge files",
"pl": "Wygenerowano {count} plików odznak",
"ru": "Сгенерировано {count} файлов значков",
"zh": "已生成 {count} 个徽章文件"
"pl": "Generated {count} badge files",
"ru": "Generated {count} badge files",
"zh": "Generated {count} badge files"
},
"Generated {file} with prefix '{prefix}'.": {
"bg": "Генериран {file} с префикс '{prefix}'.",
"de": "{file} mit Präfix '{prefix}' generiert.",
"bg": "Generated {file} with prefix '{prefix}'.",
"de": "Generated {file} with prefix '{prefix}'.",
"en": "Generated {file} with prefix '{prefix}'.",
"pl": "Wygenerowano {file} z prefiksem '{prefix}'.",
"ru": "Сгенерирован {file} с префиксом '{prefix}'.",
"zh": "已生成带前缀 '{prefix}' 的 {file}。"
"ru": "Generated {file} with prefix '{prefix}'.",
"zh": "Generated {file} with prefix '{prefix}'."
},
"Generating badges in {out}...": {
"bg": "Генериране на значки в {out}...",
"de": "Generiere Badges in {out}...",
"bg": "Generating badges in {out}...",
"de": "Generating badges in {out}...",
"en": "Generating badges in {out}...",
"pl": "Generowanie odznak w {out}...",
"ru": "Генерация значков в {out}...",
"zh": "正在 {out} 中生成徽章..."
"pl": "Generating badges in {out}...",
"ru": "Generating badges in {out}...",
"zh": "Generating badges in {out}..."
},
"Git tag or ref that was deployed": {
"bg": "Git таг или референция, която беше разгърната",
@@ -1832,12 +1672,12 @@
"zh": "要部署的 Git 标签(例如 v0.28.1)。"
},
"Gitea API token not set. Set one of: {names}": {
"bg": "Gitea API токен не е зададен. Задайте един от: {names}",
"de": "Gitea-API-Token nicht gesetzt. Setzen Sie einen von: {names}",
"bg": "Gitea API token not set. Set one of: {names}",
"de": "Gitea API token not set. Set one of: {names}",
"en": "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}"
"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}"
},
"Gitea PyPI registry: {tag} already published — continuing.": {
"bg": "Gitea PyPI registry: {tag} вече е публикуван — продължава.",
@@ -1856,36 +1696,36 @@
"zh": "Gitea release {tag} 已存在 — 跳过创建。"
},
"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.",
"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.",
"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 является релизным коммитом ('{msg}'), но тег {tag} отсутствует. Восстановление созданием тега.",
"zh": "HEAD 是发布提交('{msg}'),但缺少标签 {tag}。正在通过创建标签恢复。"
"ru": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.",
"zh": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag."
},
"HEAD is 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.",
"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.",
"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 является релизным коммитом для v{version}, но тег {tag} указывает на другой коммит ({tag_commit} против HEAD {head_commit}). Это указывает на несоответствие тег/коммит.",
"zh": "HEAD 是 v{version} 的发布提交,但标签 {tag} 指向不同的提交({tag_commit} HEAD {head_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."
},
"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.",
"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.",
"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 уже является релизным коммитом ('{msg}') и тег {tag} указывает на HEAD. Пропускается.",
"zh": "HEAD 已是发布提交('{msg}')且标签 {tag} 指向 HEAD。跳过。"
"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."
},
"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.",
"bg": "HEAD is not a release commit for {tag} — skipping publish.",
"de": "HEAD is not a release commit for {tag} — skipping publish.",
"en": "HEAD is not a release commit for {tag} — skipping publish.",
"pl": "HEAD nie jest commitem wydania dla {tag} — pomijanie publikacji.",
"ru": "HEAD не является релизным коммитом для {tag} — публикация пропускается.",
"zh": "HEAD 不是 {tag} 的发布提交——跳过发布。"
"ru": "HEAD is not a release commit for {tag} — skipping publish.",
"zh": "HEAD is not a release commit for {tag} — skipping publish."
},
"HTTP error: {status} — {message}": {
"bg": "HTTP грешка: {status} — {message}",
@@ -1903,22 +1743,6 @@
"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...",
@@ -1928,28 +1752,28 @@
"zh": "主机 Docker 不可用,正在启动本地 dockerd..."
},
"Image 'tags' must be a list": {
"bg": "Полето 'tags' на изображението трябва да е списък",
"de": "Image-'tags' muss eine Liste sein",
"bg": "Image 'tags' must be a list",
"de": "Image 'tags' must be a list",
"en": "Image 'tags' must be a list",
"pl": "'tags' obrazu musi b listą",
"ru": "Поле 'tags' образа должно быть списком",
"zh": "镜像的 'tags' 必须是列表"
"pl": "Image 'tags' must be a list",
"ru": "Image 'tags' must be a list",
"zh": "Image 'tags' must be a list"
},
"Image manifest entry missing 'dockerfile'": {
"bg": "Записът в манифеста на изображението няма 'dockerfile'",
"de": "Image-Manifest-Eintrag ohne 'dockerfile'",
"bg": "Image manifest entry missing 'dockerfile'",
"de": "Image manifest entry missing 'dockerfile'",
"en": "Image manifest entry missing 'dockerfile'",
"pl": "Wpis manifestu obrazu nie zawiera 'dockerfile'",
"ru": "Запись манифеста образа не содержит 'dockerfile'",
"zh": "镜像清单条目缺少 'dockerfile'"
"pl": "Image manifest entry missing 'dockerfile'",
"ru": "Image manifest entry missing 'dockerfile'",
"zh": "Image manifest entry missing 'dockerfile'"
},
"Image manifest entry missing 'name'": {
"bg": "Записът в манифеста на изображението няма 'name'",
"de": "Image-Manifest-Eintrag ohne 'name'",
"bg": "Image manifest entry missing 'name'",
"de": "Image manifest entry missing 'name'",
"en": "Image manifest entry missing 'name'",
"pl": "Wpis manifestu obrazu nie zawiera 'name'",
"ru": "Запись манифеста образа не содержит 'name'",
"zh": "镜像清单条目缺少 'name'"
"pl": "Image manifest entry missing 'name'",
"ru": "Image manifest entry missing 'name'",
"zh": "Image manifest entry missing 'name'"
},
"Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}": {
"bg": "Инфраструктурен commit (без идентификатор на задача DEVX-N), пропускаме обновяването на Vikunja: {msg}",
@@ -1960,44 +1784,36 @@
"zh": "基础设施提交(无 DEVX-N 任务 ID),跳过 Vikunja 更新: {msg}"
},
"Integration tests cancelled — another runner failed.": {
"bg": "Интеграционните тестове са отменени — друг runner се провали.",
"de": "Integrationstests abgebrochen — ein anderer Runner ist fehlgeschlagen.",
"bg": "Integration tests cancelled — another runner failed.",
"de": "Integration tests cancelled — another runner failed.",
"en": "Integration tests cancelled — another runner failed.",
"pl": "Testy integracyjne anulowane — inny runner zakończył się niepowodzeniem.",
"ru": "Интеграционные тесты отменены — другой раннер завершился с ошибкой.",
"zh": "集成测试已取消——另一个 runner 失败。"
"ru": "Integration tests cancelled — another runner failed.",
"zh": "Integration tests cancelled — another runner failed."
},
"Integration tests failed with exit code {code}": {
"bg": "Интеграционните тестове се провалиха с изходен код {code}",
"de": "Integrationstests mit Exit-Code {code} fehlgeschlagen",
"bg": "Integration tests failed with exit code {code}",
"de": "Integration tests failed with exit code {code}",
"en": "Integration tests failed with exit code {code}",
"pl": "Testy integracyjne zakończone niepowodzeniem z kodem {code}",
"ru": "Интеграционные тесты завершились с кодом {code}",
"zh": "集成测试失败,退出码 {code}"
"ru": "Integration tests failed with exit code {code}",
"zh": "Integration tests failed with exit code {code}"
},
"Integration tests passed.": {
"bg": "Интеграционните тестове преминаха.",
"de": "Integrationstests bestanden.",
"bg": "Integration tests passed.",
"de": "Integration tests passed.",
"en": "Integration tests passed.",
"pl": "Testy integracyjne zakończone pomyślnie.",
"ru": "Интеграционные тесты пройдены.",
"zh": "集成测试通过。"
"ru": "Integration tests passed.",
"zh": "Integration tests passed."
},
"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。"
"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."
},
"Items input must be a JSON array, got {type}": {
"bg": "Входните данни трябва да са JSON масив, получено {type}",
@@ -2008,44 +1824,44 @@
"zh": "输入必须是 JSON 数组,得到 {type}"
},
"Label '{label}' already on PR #{pr}.": {
"bg": "Етикетът '{label}' вече е на PR #{pr}.",
"de": "Label '{label}' bereits auf PR #{pr}.",
"bg": "Label '{label}' already on PR #{pr}.",
"de": "Label '{label}' already on PR #{pr}.",
"en": "Label '{label}' already on PR #{pr}.",
"pl": "Etykieta '{label}' już jest na PR #{pr}.",
"ru": "Метка '{label}' уже есть на PR #{pr}.",
"zh": "标签 '{label}' 已在 PR #{pr} 上。"
"pl": "Label '{label}' already on PR #{pr}.",
"ru": "Label '{label}' already on PR #{pr}.",
"zh": "Label '{label}' already on PR #{pr}."
},
"Latest run: #{run_id} (status: {status})": {
"bg": "Последен run: #{run_id} (статус: {status})",
"de": "Letzter Lauf: #{run_id} (Status: {status})",
"bg": "Latest run: #{run_id} (status: {status})",
"de": "Latest run: #{run_id} (status: {status})",
"en": "Latest run: #{run_id} (status: {status})",
"pl": "Ostatni przebieg: #{run_id} (status: {status})",
"ru": "Последний запуск: #{run_id} (статус: {status})",
"zh": "最近运行:#{run_id}(状态:{status}"
"pl": "Latest run: #{run_id} (status: {status})",
"ru": "Latest run: #{run_id} (status: {status})",
"zh": "Latest run: #{run_id} (status: {status})"
},
"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}",
"bg": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
"de": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
"en": "Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
"pl": "Lint nie powiódł się — odmowa wydania. Najpierw napraw błędy lint.\n{stderr}",
"ru": "Lint не пройден — отказ в релизе. Сначала исправьте ошибки lint.\n{stderr}",
"zh": "Lint 失败——拒绝发布。请先修复 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}"
},
"Lint passed.": {
"bg": "Lint премина.",
"de": "Lint bestanden.",
"bg": "Lint passed.",
"de": "Lint passed.",
"en": "Lint passed.",
"pl": "Lint zakończony pomyślnie.",
"ru": "Lint пройден.",
"zh": "Lint 通过。"
"ru": "Lint passed.",
"zh": "Lint passed."
},
"Linting documentation in {root}...": {
"bg": "Lint на документацията в {root}...",
"de": "Linting der Dokumentation in {root}...",
"bg": "Linting documentation in {root}...",
"de": "Linting documentation in {root}...",
"en": "Linting documentation in {root}...",
"pl": "Lintowanie dokumentacji w {root}...",
"ru": "Проверка документации в {root}...",
"zh": "正在检查 {root} 中的文档..."
"pl": "Linting documentation in {root}...",
"ru": "Linting documentation in {root}...",
"zh": "Linting documentation in {root}..."
},
"Login to {registry} failed: {error}": {
"bg": "Влизането в {registry} не успя: {error}",
@@ -2064,36 +1880,20 @@
"zh": "测试 '{test}' 中有 {count} 次迭代的循环 — 考虑使用基于属性的测试 (hypothesis) 或减少到 <= {max} 次迭代。"
},
"Manifest file not found: {path}": {
"bg": "Файлът на манифеста не е намерен: {path}",
"de": "Manifestdatei nicht gefunden: {path}",
"bg": "Manifest file not found: {path}",
"de": "Manifest file not found: {path}",
"en": "Manifest file not found: {path}",
"pl": "Nie znaleziono pliku manifestu: {path}",
"ru": "Файл манифеста не найден: {path}",
"zh": "未找到清单文件:{path}"
"pl": "Manifest file not found: {path}",
"ru": "Manifest file not found: {path}",
"zh": "Manifest file not found: {path}"
},
"Manifest must be a JSON list": {
"bg": "Манифестът трябва да е JSON списък",
"de": "Manifest muss eine JSON-Liste sein",
"bg": "Manifest must be a JSON list",
"de": "Manifest must be a JSON list",
"en": "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": "最大更改行数(排除的文件不计入)"
"pl": "Manifest must be a JSON list",
"ru": "Manifest must be a JSON list",
"zh": "Manifest must be a JSON list"
},
"Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.": {
"bg": "Сливането неуспешно с HTTP {status}: {message}\nПроверете дали PR е готов и имате права за сливане.",
@@ -2103,21 +1903,13 @@
"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": "Липсват тестове за променените файлове.",
"de": "Tests für geänderte Dateien fehlen.",
"bg": "Missing tests for changed files.",
"de": "Missing tests for changed files.",
"en": "Missing tests for changed files.",
"pl": "Brak testów dla zmienionych plików.",
"ru": "Отсутствуют тесты для изменённых файлов.",
"zh": "缺少已更改文件的测试。"
"pl": "Missing tests for changed files.",
"ru": "Missing tests for changed files.",
"zh": "Missing tests for changed files."
},
"Module {mod} has no main() function": {
"bg": "Модул {mod} няма функция main()",
@@ -2135,14 +1927,6 @@
"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})",
@@ -2168,12 +1952,12 @@
"zh": "不错!PR #{pr_number} 已 squash 合并,标题: {merge_title}"
},
"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.",
"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.",
"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": "Релиз v{version} помечен и отправлен. Workflow публикации будет запущен.",
"zh": "发布 v{version} 已打标签并推送。发布工作流将被触发。"
"ru": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered.",
"zh": "Nice! Release v{version} tagged and pushed. The publish workflow will be triggered."
},
"Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.": {
"bg": "Отлично! Задача Vikunja {task_id} (ID {vikunja_id}) е обновена и маркирана като готова.",
@@ -2183,21 +1967,13 @@
"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": "Не са намерени CI проверки за комит {sha}.",
"de": "Keine CI-Checks für Commit {sha} gefunden.",
"bg": "No CI checks found for commit {sha}.",
"de": "No CI checks found for commit {sha}.",
"en": "No CI checks found for commit {sha}.",
"pl": "Nie znaleziono kontroli CI dla commita {sha}.",
"ru": "CI-проверки для коммита {sha} не найдены.",
"zh": "未找到提交 {sha} 的 CI 检查。"
"pl": "No CI checks found for commit {sha}.",
"ru": "No CI checks found for commit {sha}.",
"zh": "No CI checks found for commit {sha}."
},
"No Python package found under src/ — skipping version check.": {
"bg": "",
@@ -2207,29 +1983,21 @@
"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": "Не са генерирани SVG файлове със значки",
"de": "Keine Badge-SVG-Dateien generiert",
"bg": "No badge SVG files generated",
"de": "No badge SVG files generated",
"en": "No badge SVG files generated",
"pl": "Nie wygenerowano plików SVG odznak",
"ru": "SVG-файлы значков не сгенерированы",
"zh": "未生成徽章 SVG 文件"
"pl": "No badge SVG files generated",
"ru": "No badge SVG files generated",
"zh": "No badge SVG files generated"
},
"No badge URLs found to update — README already up to date": {
"bg": "Не са намерени URL на значки за обновяване — README вече е актуално",
"de": "Keine Badge-URLs zum Aktualisieren gefunden — README bereits aktuell",
"bg": "No badge URLs found to update — README already up to date",
"de": "No badge URLs found to update — README already up to date",
"en": "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 已是最新"
"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"
},
"No badge changes — skipping commit": {
"bg": "",
@@ -2240,12 +2008,12 @@
"zh": ""
},
"No changes between {base} and {head}.": {
"bg": "Няма промени между {base} и {head}.",
"de": "Keine Änderungen zwischen {base} und {head}.",
"bg": "No changes between {base} and {head}.",
"de": "No changes between {base} and {head}.",
"en": "No changes between {base} and {head}.",
"pl": "Brak zmian między {base} i {head}.",
"ru": "Нет изменений между {base} и {head}.",
"zh": "{base} {head} 之间没有更改。"
"ru": "No changes between {base} and {head}.",
"zh": "No changes between {base} and {head}."
},
"No changes to sync — wiki is up to date.": {
"bg": "",
@@ -2256,36 +2024,36 @@
"zh": ""
},
"No failed jobs.": {
"bg": "Няма неуспешни задачи.",
"de": "Keine fehlgeschlagenen Jobs.",
"bg": "No failed jobs.",
"de": "No failed jobs.",
"en": "No failed jobs.",
"pl": "Brak nieudanych zadań.",
"ru": "Нет неудавшихся задач.",
"zh": "没有失败的任务。"
"pl": "No failed jobs.",
"ru": "No failed jobs.",
"zh": "No failed jobs."
},
"No job matching '{job}' found.": {
"bg": "Не е намерена задача, съответстваща на '{job}'.",
"de": "Kein Job gefunden, der '{job}' entspricht.",
"bg": "No job matching '{job}' found.",
"de": "No job matching '{job}' found.",
"en": "No job matching '{job}' found.",
"pl": "Nie znaleziono zadania pasującego do '{job}'.",
"ru": "Задача, соответствующая '{job}', не найдена.",
"zh": "未找到匹配 '{job}' 的任务。"
"pl": "No job matching '{job}' found.",
"ru": "No job matching '{job}' found.",
"zh": "No job matching '{job}' found."
},
"No jobs found for run #{run_id}.": {
"bg": "Не са намерени задачи за run #{run_id}.",
"de": "Keine Jobs für Lauf #{run_id} gefunden.",
"bg": "No jobs found for run #{run_id}.",
"de": "No jobs found for run #{run_id}.",
"en": "No jobs found for run #{run_id}.",
"pl": "Nie znaleziono zadań dla przebiegu #{run_id}.",
"ru": "Задачи для запуска #{run_id} не найдены.",
"zh": "未找到运行 #{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}."
},
"No open PR found for branch '{branch}'.": {
"bg": "Не е намерен отворен PR за клон '{branch}'.",
"de": "Kein offener PR für Branch '{branch}' gefunden.",
"bg": "No open PR found for branch '{branch}'.",
"de": "No open PR found for branch '{branch}'.",
"en": "No open PR found for branch '{branch}'.",
"pl": "Nie znaleziono otwartego PR dla gałęzi '{branch}'.",
"ru": "Открытый PR для ветки '{branch}' не найден.",
"zh": "未找到分支 '{branch}' 的开放 PR。"
"pl": "No open PR found for branch '{branch}'.",
"ru": "No open PR found for branch '{branch}'.",
"zh": "No open PR found for branch '{branch}'."
},
"No push needed (no changes or push failed).": {
"bg": "",
@@ -2295,133 +2063,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": "Няма staged промени — версията и changelog вече са актуални.",
"de": "Keine gestagten ÄnderungenVersion und Changelog bereits aktuell.",
"bg": "No staged changes — version and changelog already up to date.",
"de": "No staged changesversion and changelog already up to date.",
"en": "No staged changes — version and changelog already up to date.",
"pl": "Brak zmian w staging — wersja i changelog są już aktualne.",
"ru": "Нет staged-изменений — версия и changelog уже актуальны.",
"zh": "没有暂存的更改——版本和 changelog 已是最新。"
"ru": "No staged changes — version and changelog already up to date.",
"zh": "No staged changes — version and changelog already up to date."
},
"No tag found — skipping publish.": {
"bg": "Не е намерен таг — публикуването се пропуска.",
"de": "Kein Tag gefundenVeröffentlichung wird übersprungen.",
"bg": "No tag found — skipping publish.",
"de": "No tag found — skipping publish.",
"en": "No tag found — skipping publish.",
"pl": "Nie znaleziono tagu — pomijanie publikacji.",
"ru": "Тег не найден — публикация пропускается.",
"zh": "未找到标签——跳过发布。"
"ru": "No tag found — skipping publish.",
"zh": "No tag found — skipping publish."
},
"No tags found — treating all changes as user-facing.": {
"bg": "Не са намерени тагове — всички промени се третират като видими за потребителя.",
"de": "Keine Tags gefundenalle Änderungen werden als nutzersichtbar behandelt.",
"bg": "No tags found — treating all changes as user-facing.",
"de": "No tags found — treating all changes as user-facing.",
"en": "No tags found — treating all changes as user-facing.",
"pl": "Nie znaleziono tagów — traktowanie wszystkich zmian jako widocznych dla użytkownika.",
"ru": "Теги не найдены — все изменения считаются пользовательскими.",
"zh": "未找到标签——所有更改视为面向用户。"
"ru": "No tags found — treating all changes as user-facing.",
"zh": "No tags found — treating all changes as user-facing."
},
"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.",
"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.",
"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": "В сообщении коммита не найден 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。"
"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."
},
"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.",
"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.",
"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"
"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."
},
"No unreleased changes found. Nothing to release.": {
"bg": "Не са намерени непубликувани промени. Няма какво да се издаде.",
"de": "Keine unveröffentlichten Änderungen gefunden. Nichts zu veröffentlichen.",
"bg": "No unreleased changes found. Nothing to release.",
"de": "No unreleased changes found. Nothing to release.",
"en": "No unreleased changes found. Nothing to release.",
"pl": "Nie znaleziono nieopublikowanych zmian. Nic do wydania.",
"ru": "Не найдено невыпущенных изменений. Нечего выпускать.",
"zh": "未找到未发布的更改。没有可发布的内容。"
"ru": "No unreleased changes found. Nothing to release.",
"zh": "No unreleased changes found. Nothing to release."
},
"No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.": {
"bg": "Няма видими за потребителя промени от {tag} — променени са само workflow/инфраструктурни файлове. Изданието се пропуска.",
"de": "Keine nutzersichtbaren Änderungen seit {tag} — nur Workflow-/Infrastrukturdateien geändert. Release wird übersprungen.",
"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.",
"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": "Нет пользовательских изменений с {tag} — изменены только workflow/инфраструктурные файлы. Релиз пропускается.",
"zh": "自 {tag} 以来没有面向用户的更改——仅更改了工作流/基础设施文件。跳过发布。"
"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."
},
"No versions found.": {
"bg": "Не са намерени версии.",
"de": "Keine Versionen gefunden.",
"bg": "No versions found.",
"de": "No versions found.",
"en": "No versions found.",
"pl": "Nie znaleziono wersji.",
"ru": "Версии не найдены.",
"zh": "未找到版本。"
"pl": "No versions found.",
"ru": "No versions found.",
"zh": "No versions found."
},
"No workflow runs found for SHA {sha}.": {
"bg": "Не са намерени workflow runs за SHA {sha}.",
"de": "Keine Workflow-Läufe für SHA {sha} gefunden.",
"bg": "No workflow runs found for SHA {sha}.",
"de": "No workflow runs found for SHA {sha}.",
"en": "No workflow runs found for SHA {sha}.",
"pl": "Nie znaleziono przebiegów workflow dla SHA {sha}.",
"ru": "Workflow-запуски для SHA {sha} не найдены.",
"zh": "未找到 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。"
},
"Nothing to push.": {
"bg": "Няма какво да се push-не.",
"de": "Nichts zu pushen.",
"bg": "Nothing to push.",
"de": "Nothing to push.",
"en": "Nothing to push.",
"pl": "Nic do wypchnięcia.",
"ru": "Нечего отправлять.",
"zh": "没有可推送的内容。"
"pl": "Nothing to push.",
"ru": "Nothing to push.",
"zh": "Nothing to push."
},
"Only check staged files (for pre-commit)": {
"bg": "Проверява само staged файлове (за pre-commit)",
"de": "Nur gestagte Dateien prüfen (für Pre-Commit)",
"bg": "Only check staged files (for pre-commit)",
"de": "Only check staged files (for pre-commit)",
"en": "Only check staged files (for pre-commit)",
"pl": "Sprawdza tylko pliki staged (dla pre-commit)",
"ru": "Проверять только staged-файлы (для pre-commit)",
"zh": "仅检查暂存文件(用于 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)"
},
"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! 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! 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.",
"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.",
"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": "Не включайте ID задачи ({prefix}-N) в коммиты feature-веток.\n ID задачи будет добавлен автоматически при merge через CI.",
"zh": "请勿在功能分支提交中包含任务 ID{prefix}-N)。\n 任务 ID 将在合并时由 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."
},
"Oops! Gitea PyPI registry publish failed:\n{stderr}": {
"bg": "Опа! Публикуването в Gitea PyPI registry неуспешно:\n{stderr}",
@@ -2432,20 +2200,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": "Комитът на 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}",
"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}",
"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": "Коммит ветки master должен следовать conventional-формату после ID задачи.\n Ожидается: {prefix}-N: <type>: <description>\n Получено: {subject}",
"zh": "master 分支提交必须在任务 ID 后遵循 conventional 格式。\n 预期:{prefix}-N: <type>: <description>\n 实际:{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}"
},
"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}",
"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}",
"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": "Коммиты ветки master должны начинаться с ID задачи.\n Ожидается: {prefix}-N: <conventional commit message>\n Получено: {subject}",
"zh": "master 分支提交必须以任务 ID 开头。\n 预期:{prefix}-N: <conventional commit message>\n 实际:{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}"
},
"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).",
@@ -2456,20 +2224,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": "Заглавието на 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}",
"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}",
"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": "Заголовок PR должен соответствовать формату '{prefix}-N: <название задачи>'.\n Ожидается: {task_id}: <название задачи>\n Получено: {pr_title}",
"zh": "PR 标题必须遵循格式 '{prefix}-N: <任务标题>'\n 预期:{task_id}: <任务标题>\n 实际:{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}"
},
"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}",
"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}",
"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": "Несоответствие ID задачи в заголовке PR.\n ID задачи ветки: {task_id}\n Заголовок PR: {pr_title}",
"zh": "PR 标题任务 ID 不匹配。\n 分支任务 ID{task_id}\n 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}"
},
"Oops! Package build failed:\n{stderr}": {
"bg": "Опа! Сборката на пакета неуспешна:\n{stderr}",
@@ -2488,20 +2256,28 @@
"zh": "哎呀!PyPI 发布失败:\n{stderr}"
},
"PASS: All documentation checks passed!": {
"bg": "УСПЕХ: Всички проверки на документацията преминаха!",
"de": "ERFOLG: Alle Dokumentationsprüfungen bestanden!",
"bg": "PASS: All documentation checks passed!",
"de": "PASS: All documentation checks passed!",
"en": "PASS: All documentation checks passed!",
"pl": "SUKCES: Wszystkie kontrole dokumentacji przeszły!",
"ru": "УСПЕШНО: Все проверки документации пройдены!",
"zh": "通过:所有文档检查均已通过!"
"pl": "PASS: All documentation checks passed!",
"ru": "PASS: All documentation checks passed!",
"zh": "PASS: All documentation checks passed!"
},
"PASSED: {pair}": {
"bg": "PASSED: {pair}",
"de": "PASSED: {pair}",
"en": "PASSED: {pair}",
"pl": "UDANE: {pair}",
"ru": "PASSED: {pair}",
"zh": "PASSED: {pair}"
},
"PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.": {
"bg": "PR #{pr} е 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.",
"bg": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"de": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"en": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"pl": "PR #{pr} rebased 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"
"pl": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"ru": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"zh": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR."
},
"PR already exists: #{index} — {url}": {
"bg": "PR вече съществува: #{index} — {url}",
@@ -2511,117 +2287,61 @@
"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 (за извличане на заглавие от Gitea)",
"de": "PR-Nummer (zum Abrufen des Titels von Gitea)",
"bg": "PR number (to fetch title from Gitea)",
"de": "PR number (to fetch title from Gitea)",
"en": "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 编号"
"pl": "PR number (to fetch title from Gitea)",
"ru": "PR number (to fetch title from Gitea)",
"zh": "PR number (to fetch title from Gitea)"
},
"PR number must be an integer, got: {pr_number}": {
"bg": "Номерът на PR трябва да е цяло число, получено: {pr_number}",
"de": "PR-Nummer muss eine Ganzzahl sein, erhalten: {pr_number}",
"bg": "PR number must be an integer, got: {pr_number}",
"de": "PR number must be an integer, got: {pr_number}",
"en": "PR number must be an integer, got: {pr_number}",
"pl": "Numer PR musi być liczbą całkowitą, otrzymano: {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 大小检查失败。"
"ru": "PR number must be an integer, got: {pr_number}",
"zh": "PR number must be an integer, got: {pr_number}"
},
"PR title (auto-fetched if --pr-number given)": {
"bg": "Заглавие на PR (извлича се автоматично, ако е зададен --pr-number)",
"de": "PR-Titel (wird automatisch abgerufen, wenn --pr-number angegeben)",
"bg": "PR title (auto-fetched if --pr-number given)",
"de": "PR title (auto-fetched if --pr-number given)",
"en": "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 时自动获取)"
"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)"
},
"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}",
"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}",
"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 не совпадает с названием задачи Vikunja.\n Ожидается: {expected}\n Получено: {pr_title}",
"zh": "PR 标题与 Vikunja 任务标题不匹配。\n 预期:{expected}\n 实际:{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}"
},
"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}",
"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}",
"en": "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}"
"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}"
},
"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}",
"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}",
"en": "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}"
"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}"
},
"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}",
"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}",
"en": "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}"
"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}"
},
"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.",
@@ -2631,29 +2351,21 @@
"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": "Собственикът на пакета не е зададен. Използвайте --owner или задайте [tool.devx] repo_owner.",
"de": "Paket-Eigentümer nicht angegeben. Verwenden Sie --owner oder setzen Sie [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.",
"en": "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"
"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."
},
"Package: {owner}/{name}": {
"bg": "Пакет: {owner}/{name}",
"de": "Paket: {owner}/{name}",
"bg": "Package: {owner}/{name}",
"de": "Package: {owner}/{name}",
"en": "Package: {owner}/{name}",
"pl": "Pakiet: {owner}/{name}",
"ru": "Пакет: {owner}/{name}",
"zh": "包:{owner}/{name}"
"pl": "Package: {owner}/{name}",
"ru": "Package: {owner}/{name}",
"zh": "Package: {owner}/{name}"
},
"Parsed owner={owner}, repo={repo} from DEVX_REPO_NAME": {
"bg": "Разбор на owner={owner}, repo={repo} от DEVX_REPO_NAME",
@@ -2664,28 +2376,28 @@
"zh": "从 DEVX_REPO_NAME 解析 owner={owner}, repo={repo}"
},
"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).",
"bg": "Path to pyproject.toml (default: pyproject.toml in CWD).",
"de": "Path to pyproject.toml (default: pyproject.toml in CWD).",
"en": "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)。"
"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)."
},
"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.",
"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.",
"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": "Проверка скорости тестов ПРОВАЛЕНА: {count} тест(ов) превышают лимит {limit}s.",
"zh": "单测试速度检查失败:{count} 个测试超过 {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."
},
"Pre-merge validation failed.": {
"bg": "Предmerge валидацията се провали.",
"de": "Pre-Merge-Validierung fehlgeschlagen.",
"bg": "Pre-merge validation failed.",
"de": "Pre-merge validation failed.",
"en": "Pre-merge validation failed.",
"pl": "Walidacja przed merge nie powiodła się.",
"ru": "Проверка перед слиянием не пройдена.",
"zh": "合并前验证失败。"
"pl": "Pre-merge validation failed.",
"ru": "Pre-merge validation failed.",
"zh": "Pre-merge validation failed."
},
"Pre-push check passed: task {task_id} exists.": {
"bg": "Pre-push проверката премина: задача {task_id} съществува.",
@@ -2696,28 +2408,28 @@
"zh": "Pre-push 检查通过: 任务 {task_id} 存在。"
},
"Print warnings but always exit 0": {
"bg": "Печатай предупреждения, но винаги излизай с код 0",
"de": "Warnungen ausgeben, aber immer mit 0 beenden",
"bg": "Print warnings but always exit 0",
"de": "Print warnings but always exit 0",
"en": "Print warnings but always exit 0",
"pl": "Wypisuj ostrzeżenia, ale zawsze kończ kodem 0",
"ru": "Выводить предупреждения, но всегда завершать с кодом 0",
"zh": "打印警告但始终以 0 退出"
"pl": "Print warnings but always exit 0",
"ru": "Print warnings but always exit 0",
"zh": "Print warnings but always exit 0"
},
"Provide --manifest or both --dockerfile and --name": {
"bg": "Задайте --manifest или и --dockerfile, и --name",
"de": "--manifest oder sowohl --dockerfile als auch --name angeben",
"bg": "Provide --manifest or both --dockerfile and --name",
"de": "Provide --manifest or both --dockerfile and --name",
"en": "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"
"pl": "Provide --manifest or both --dockerfile and --name",
"ru": "Provide --manifest or both --dockerfile and --name",
"zh": "Provide --manifest or both --dockerfile and --name"
},
"Provide a commit message file or use --git.": {
"bg": "Предоставете файл със съобщение на комит или използвайте --git.",
"de": "Commit-Nachrichtendatei bereitstellen oder --git verwenden.",
"bg": "Provide a commit message file or use --git.",
"de": "Provide a commit message file or use --git.",
"en": "Provide a commit message file or use --git.",
"pl": "Podaj plik komunikatu commitu lub użyj --git.",
"ru": "Укажите файл с сообщением коммита или используйте --git.",
"zh": "提供提交信息文件或使用 --git"
"ru": "Provide a commit message file or use --git.",
"zh": "Provide a commit message file or use --git."
},
"Published to Gitea PyPI registry.": {
"bg": "Публикувано в Gitea PyPI registry.",
@@ -2736,28 +2448,28 @@
"zh": "已发布到 PyPI。"
},
"Publishing release {tag}...": {
"bg": "Публикуване на версия {tag}...",
"de": "Veröffentliche Release {tag}...",
"bg": "Publishing release {tag}...",
"de": "Publishing release {tag}...",
"en": "Publishing release {tag}...",
"pl": "Publikowanie wydania {tag}...",
"ru": "Публикация релиза {tag}...",
"zh": "正在发布 {tag}..."
"ru": "Publishing release {tag}...",
"zh": "Publishing release {tag}..."
},
"Push attempt {n}/3 failed: {err}": {
"bg": "Опит {n}/3 за push се провали: {err}",
"de": "Push-Versuch {n}/3 fehlgeschlagen: {err}",
"bg": "Push attempt {n}/3 failed: {err}",
"de": "Push attempt {n}/3 failed: {err}",
"en": "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}"
"pl": "Push attempt {n}/3 failed: {err}",
"ru": "Push attempt {n}/3 failed: {err}",
"zh": "Push attempt {n}/3 failed: {err}"
},
"Push failed for {tag}: {error}": {
"bg": "Push за {tag} се провали: {error}",
"de": "Push für {tag} fehlgeschlagen: {error}",
"bg": "Push failed for {tag}: {error}",
"de": "Push failed for {tag}: {error}",
"en": "Push failed for {tag}: {error}",
"pl": "Push dla {tag} nie powiódł się: {error}",
"ru": "Push для {tag} не удался: {error}",
"zh": "推送 {tag} 失败:{error}"
"pl": "Push failed for {tag}: {error}",
"ru": "Push failed for {tag}: {error}",
"zh": "Push failed for {tag}: {error}"
},
"Push failed: {error}": {
"bg": "",
@@ -2768,28 +2480,28 @@
"zh": ""
},
"Pushed README update with badge SHA {sha}": {
"bg": "Push-ната е README актуализация със SHA на значката {sha}",
"de": "README-Update mit Badge-SHA {sha} gepusht",
"bg": "Pushed README update with badge SHA {sha}",
"de": "Pushed README update with badge SHA {sha}",
"en": "Pushed README update with badge SHA {sha}",
"pl": "Wypchnięto aktualizację README z SHA odznaki {sha}",
"ru": "Отправлено обновление README с SHA значка {sha}",
"zh": "已推送带徽章 SHA {sha} 的 README 更新"
"pl": "Pushed README update with badge SHA {sha}",
"ru": "Pushed README update with badge SHA {sha}",
"zh": "Pushed README update with badge SHA {sha}"
},
"Pushed release commit to master.": {
"bg": "Release комитът е push-нат към master.",
"de": "Release-Commit zu master gepusht.",
"bg": "Pushed release commit to master.",
"de": "Pushed release commit to master.",
"en": "Pushed release commit to master.",
"pl": "Wypchnięto commit wydania do master.",
"ru": "Релизный коммит отправлен в master.",
"zh": "发布提交已推送到 master"
"ru": "Pushed release commit to master.",
"zh": "Pushed release commit to master."
},
"Pushed {branch} to origin.": {
"bg": "Клонът {branch} е push-нат към origin.",
"de": "{branch} zu origin gepusht.",
"bg": "Pushed {branch} to origin.",
"de": "Pushed {branch} to origin.",
"en": "Pushed {branch} to origin.",
"pl": "Wypchnięto {branch} do origin.",
"ru": "Ветка {branch} отправлена в origin.",
"zh": "已将 {branch} 推送到 origin"
"pl": "Pushed {branch} to origin.",
"ru": "Pushed {branch} to origin.",
"zh": "Pushed {branch} to origin."
},
"PyPI publish failed (non-fatal — continuing to Gitea release):\n{error}": {
"bg": "Публикуването в PyPI неуспешно (некритично — продължава към Gitea release):\n{error}",
@@ -2800,132 +2512,116 @@
"zh": "PyPI 发布失败(非致命 — 继续创建 Gitea release):\n{error}"
},
"REPO argument is required (or set GITHUB_REPOSITORY env var).": {
"bg": "Аргументът REPO е задължителен (или задайте променливата GITHUB_REPOSITORY).",
"de": "REPO-Argument ist erforderlich (oder GITHUB_REPOSITORY-Umgebungsvariable setzen).",
"bg": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
"de": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
"en": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
"pl": "Argument REPO jest wymagany (lub ustaw zmienną GITHUB_REPOSITORY).",
"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 调用:"
"ru": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
"zh": "REPO argument is required (or set GITHUB_REPOSITORY env var)."
},
"Rebase attempt {n}/3 failed: {err}": {
"bg": "Опит {n}/3 за rebase се провали: {err}",
"de": "Rebase-Versuch {n}/3 fehlgeschlagen: {err}",
"bg": "Rebase attempt {n}/3 failed: {err}",
"de": "Rebase attempt {n}/3 failed: {err}",
"en": "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}"
"pl": "Rebase attempt {n}/3 failed: {err}",
"ru": "Rebase attempt {n}/3 failed: {err}",
"zh": "Rebase attempt {n}/3 failed: {err}"
},
"Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue": {
"bg": "Rebase се провали (конфликти или друга грешка):\n{error}\nРазрешете конфликтите и изпълнете: git rebase --continue",
"de": "Rebase fehlgeschlagen (Konflikte oder anderer Fehler):\n{error}\nKonflikte lösen und ausführen: git rebase --continue",
"bg": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"de": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"en": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"pl": "Rebase 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"
"pl": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"ru": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"zh": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue"
},
"Rebase failed with HTTP {status}: {message}": {
"bg": "Rebase се провали с HTTP {status}: {message}",
"de": "Rebase mit HTTP {status} fehlgeschlagen: {message}",
"bg": "Rebase failed with HTTP {status}: {message}",
"de": "Rebase failed with HTTP {status}: {message}",
"en": "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}"
"pl": "Rebase failed with HTTP {status}: {message}",
"ru": "Rebase failed with HTTP {status}: {message}",
"zh": "Rebase failed with HTTP {status}: {message}"
},
"Rebase successful.": {
"bg": "Rebase успешен.",
"de": "Rebase erfolgreich.",
"bg": "Rebase successful.",
"de": "Rebase successful.",
"en": "Rebase successful.",
"pl": "Rebase powiódł się.",
"ru": "Rebase успешен.",
"zh": "Rebase 成功。"
"pl": "Rebase successful.",
"ru": "Rebase successful.",
"zh": "Rebase successful."
},
"Rebasing PR #{pr} via Gitea API...": {
"bg": "Rebase на PR #{pr} чрез Gitea API...",
"de": "Rebase von PR #{pr} via Gitea API...",
"bg": "Rebasing PR #{pr} via Gitea API...",
"de": "Rebasing PR #{pr} via Gitea API...",
"en": "Rebasing PR #{pr} via Gitea API...",
"pl": "Rebase PR #{pr} przez Gitea API...",
"ru": "Rebase PR #{pr} через Gitea API...",
"zh": "正在通过 Gitea API 对 PR #{pr} 执行 rebase..."
"pl": "Rebasing PR #{pr} via Gitea API...",
"ru": "Rebasing PR #{pr} via Gitea API...",
"zh": "Rebasing PR #{pr} via Gitea API..."
},
"Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars": {
"bg": "Изискват се идентификационни данни за регистъра: задайте променливите CI_GITEA_TOKEN и CI_GITEA_USERNAME",
"de": "Registry-Anmeldedaten erforderlich: Umgebungsvariablen CI_GITEA_TOKEN und CI_GITEA_USERNAME setzen",
"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",
"en": "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"
"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"
},
"Registry login failed": {
"bg": "Входът в регистъра се провали",
"de": "Registry-Login fehlgeschlagen",
"bg": "Registry login failed",
"de": "Registry login failed",
"en": "Registry login failed",
"pl": "Logowanie do rejestru nie powiodło się",
"ru": "Вход в реестр не удался",
"zh": "注册表登录失败"
"pl": "Registry login failed",
"ru": "Registry login failed",
"zh": "Registry login failed"
},
"Registry login failed: {error}": {
"bg": "Входът в регистъра се провали: {error}",
"de": "Registry-Login fehlgeschlagen: {error}",
"bg": "Registry login failed: {error}",
"de": "Registry login failed: {error}",
"en": "Registry login failed: {error}",
"pl": "Logowanie do rejestru nie powiodło się: {error}",
"ru": "Вход в реестр не удался: {error}",
"zh": "注册表登录失败:{error}"
"pl": "Registry login failed: {error}",
"ru": "Registry login failed: {error}",
"zh": "Registry login failed: {error}"
},
"Regular merge commit — running all post-merge jobs.": {
"bg": "Обикновен merge комит — изпълняват се всички post-merge задачи.",
"de": "Regulärer Merge-Commit — alle Post-Merge-Jobs werden ausgeführt.",
"bg": "Regular merge commit — running all post-merge jobs.",
"de": "Regular merge commit — running all post-merge jobs.",
"en": "Regular merge commit — running all post-merge jobs.",
"pl": "Zwykły commit merge uruchamianie wszystkich zadań post-merge.",
"ru": "Обычный merge-коммит — выполняются все post-merge задачи.",
"zh": "常规合并提交——运行所有合并后任务。"
"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."
},
"Release commit — skipping all post-merge jobs.": {
"bg": "Release комит — всички post-merge задачи се пропускат.",
"de": "Release-Commit — alle Post-Merge-Jobs werden übersprungen.",
"bg": "Release commit — skipping all post-merge jobs.",
"de": "Release commit — skipping all post-merge jobs.",
"en": "Release commit — skipping all post-merge jobs.",
"pl": "Commit release — pomijanie wszystkich zadań post-merge.",
"ru": "Релизный коммит — все post-merge задачи пропускаются.",
"zh": "发布提交——跳过所有合并后任务。"
"pl": "Release commit — skipping all post-merge jobs.",
"ru": "Release commit — skipping all post-merge jobs.",
"zh": "Release commit — skipping all post-merge jobs."
},
"Release creation failed: {error}": {
"bg": "Създаването на версия се провали: {error}",
"de": "Release-Erstellung fehlgeschlagen: {error}",
"bg": "Release creation failed: {error}",
"de": "Release creation failed: {error}",
"en": "Release creation failed: {error}",
"pl": "Tworzenie wydania nie powiodło się: {error}",
"ru": "Создание релиза не удалось: {error}",
"zh": "创建发布失败:{error}"
"ru": "Release creation failed: {error}",
"zh": "Release creation failed: {error}"
},
"Release must be run on master, currently on '{branch}'.": {
"bg": "Release трябва да се изпълнява на master, в момента сте на '{branch}'.",
"de": "Release muss auf master ausgeführt werden, aktuell auf '{branch}'.",
"bg": "Release must be run on master, currently on '{branch}'.",
"de": "Release must be run on master, currently on '{branch}'.",
"en": "Release must be run on master, currently on '{branch}'.",
"pl": "Wydanie musi być uruchomione na master, obecnie na '{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"
"ru": "Release must be run on master, currently on '{branch}'.",
"zh": "Release must be run on master, currently on '{branch}'."
},
"Repo must be in 'owner/name' format, got: {repo}": {
"bg": "Репозиторият трябва да е във формат 'owner/name', получено: {repo}",
"de": "Repo muss im Format 'owner/name' sein, erhalten: {repo}",
"bg": "Repo must be in 'owner/name' format, got: {repo}",
"de": "Repo must be in 'owner/name' format, got: {repo}",
"en": "Repo must be in 'owner/name' format, got: {repo}",
"pl": "Repo musi być w formacie 'owner/name', otrzymano: {repo}",
"ru": "Репозиторий должен быть в формате 'owner/name', получено: {repo}",
"zh": "仓库必须为 'owner/name' 格式,实际为:{repo}"
"ru": "Repo must be in 'owner/name' format, got: {repo}",
"zh": "Repo must be in 'owner/name' format, got: {repo}"
},
"Repository configuration complete.": {
"bg": "Конфигурирането на хранилището е завършено.",
@@ -2936,20 +2632,20 @@
"zh": "仓库配置完成。"
},
"Repository in owner/name format": {
"bg": "Репозитория във формат owner/name",
"de": "Repository im Format owner/name",
"bg": "Repository in owner/name format",
"de": "Repository in owner/name format",
"en": "Repository in owner/name format",
"pl": "Repozytorium w formacie owner/name",
"ru": "Репозиторий в формате owner/name",
"zh": "owner/name 格式的仓库"
"pl": "Repository in owner/name format",
"ru": "Repository in owner/name format",
"zh": "Repository in owner/name format"
},
"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.",
"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.",
"en": "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 环境变量。"
"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."
},
"Repository owner not set. Use --owner or DEVX_REPO_OWNER env var.": {
"bg": "Собственикът на хранилището не е зададен. Използвайте --owner или DEVX_REPO_OWNER env var.",
@@ -2967,21 +2663,29 @@
"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": "Директорията с роли не е намерена: {path}",
"de": "Rollenverzeichnis nicht gefunden: {path}",
"bg": "Roles directory not found: {path}",
"de": "Roles directory not found: {path}",
"en": "Roles directory not found: {path}",
"pl": "Katalog ról nie znaleziony: {path}",
"ru": "Директория ролей не найдена: {path}",
"zh": "未找到角色目录:{path}"
"ru": "Roles directory not found: {path}",
"zh": "Roles directory not found: {path}"
},
"Runner count: {count}": {
"bg": "Брой раннъри: {count}",
"de": "Runner-Anzahl: {count}",
"bg": "Runner count: {count}",
"de": "Runner count: {count}",
"en": "Runner count: {count}",
"pl": "Liczba runnerów: {count}",
"ru": "Количество раннеров: {count}",
"zh": "Runner 数量:{count}"
"pl": "Runner count: {count}",
"ru": "Runner count: {count}",
"zh": "Runner count: {count}"
},
"Runner index {index} out of range (0..{max})": {
"bg": "Индексът на runner {index} е извън диапазона (0..{max})",
@@ -2992,52 +2696,60 @@
"zh": "Runner 索引 {index} 超出范围 (0..{max})"
},
"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)",
"bg": "Runner index {runner_index} is out of range (must be >= 1)",
"de": "Runner index {runner_index} is out of range (must be >= 1)",
"en": "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"
"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)"
},
"Runner indices: {indices}": {
"bg": "Индекси на раннъри: {indices}",
"de": "Runner-Indizes: {indices}",
"bg": "Runner indices: {indices}",
"de": "Runner indices: {indices}",
"en": "Runner indices: {indices}",
"pl": "Indeksy runnerów: {indices}",
"ru": "Индексы раннеров: {indices}",
"zh": "Runner 索引:{indices}"
"pl": "Runner indices: {indices}",
"ru": "Runner indices: {indices}",
"zh": "Runner indices: {indices}"
},
"Runner {i}: {labels}": {
"bg": "Раннер {i}: {labels}",
"bg": "Runner {i}: {labels}",
"de": "Runner {i}: {labels}",
"en": "Runner {i}: {labels}",
"pl": "Runner {i}: {labels}",
"ru": "Раннер {i}: {labels}",
"zh": "Runner {i}{labels}"
"ru": "Runner {i}: {labels}",
"zh": "Runner {i}: {labels}"
},
"Running lint checks...": {
"bg": "Изпълнение на lint проверки...",
"de": "Lint-Checks laufen...",
"bg": "Running lint checks...",
"de": "Running lint checks...",
"en": "Running lint checks...",
"pl": "Uruchamianie kontroli lint...",
"ru": "Выполнение проверок lint...",
"zh": "正在运行 lint 检查..."
"ru": "Running lint checks...",
"zh": "Running lint checks..."
},
"Running tests...": {
"bg": "Изпълнение на тестове...",
"de": "Tests laufen...",
"bg": "Running tests...",
"de": "Running tests...",
"en": "Running tests...",
"pl": "Uruchamianie testów...",
"ru": "Выполнение тестов...",
"zh": "正在运行测试..."
"ru": "Running tests...",
"zh": "Running tests..."
},
"Running: {cmd}": {
"bg": "Изпълнение: {cmd}",
"de": "Ausführen: {cmd}",
"bg": "Running: {cmd}",
"de": "Running: {cmd}",
"en": "Running: {cmd}",
"pl": "Uruchamianie: {cmd}",
"ru": "Выполнение: {cmd}",
"zh": "运行中:{cmd}"
"pl": "Running: {cmd}",
"ru": "Running: {cmd}",
"zh": "Running: {cmd}"
},
"Running: {scenario} on {platform}": {
"bg": "Running: {scenario} on {platform}",
"de": "Running: {scenario} on {platform}",
"en": "Running: {scenario} on {platform}",
"pl": "Uruchamianie: {scenario} na {platform}",
"ru": "Running: {scenario} on {platform}",
"zh": "Running: {scenario} on {platform}"
},
"SSH key set up successfully": {
"bg": "SSH ключът е настроен успешно",
@@ -3063,85 +2775,45 @@
"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": "Пропусни проверката за съвпадение на заглавието с Vikunja",
"de": "Vikunja-Titelübereinstimmungsprüfung überspringen",
"bg": "Skip Vikunja title match check",
"de": "Skip Vikunja title match check",
"en": "Skip Vikunja title match check",
"pl": "Pomiń kontrolę zgodności tytułu z Vikunja",
"ru": "Пропустить проверку совпадения заголовка с Vikunja",
"zh": "跳过 Vikunja 标题匹配检查"
"pl": "Skip Vikunja title match check",
"ru": "Skip Vikunja title match check",
"zh": "Skip Vikunja title match check"
},
"Skip branch-behind-master check": {
"bg": "Пропусни проверката дали клонът изостава от master",
"de": "Prüfung „Branch hinter master“ überspringen",
"bg": "Skip branch-behind-master check",
"de": "Skip branch-behind-master check",
"en": "Skip branch-behind-master check",
"pl": "Pomiń kontrolę czy gałąź jest za master",
"ru": "Пропустить проверку отставания ветки от master",
"zh": "跳过分支落后于 master 的检查"
"pl": "Skip branch-behind-master check",
"ru": "Skip branch-behind-master check",
"zh": "Skip branch-behind-master check"
},
"Skipping commit push — no staged changes.": {
"bg": "Пропуска се push на комита — няма staged промени.",
"de": "Commit-Push wird übersprungen — keine gestagten Änderungen.",
"bg": "Skipping commit push — no staged changes.",
"de": "Skipping commit push — no staged changes.",
"en": "Skipping commit push — no staged changes.",
"pl": "Pomijanie wypchnięcia commit — brak zmian w staging.",
"ru": "Push коммита пропускается — нет staged-изменений.",
"zh": "跳过提交推送——没有暂存的更改。"
"ru": "Skipping commit push — no staged changes.",
"zh": "Skipping commit push — no staged changes."
},
"Skipping — runner index {runner_index} > max runners {max_runners}": {
"bg": "Пропуска се — индекс на runner {runner_index} > максимум раннъри {max_runners}",
"de": "ÜbersprungenRunner-Index {runner_index} > max. Runner {max_runners}",
"bg": "Skipping — runner index {runner_index} > max runners {max_runners}",
"de": "Skippingrunner index {runner_index} > max runners {max_runners}",
"en": "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": "规范验证失败。"
"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}"
},
"Synced to latest origin/{branch}": {
"bg": "Синхронизирано към последния origin/{branch}",
"de": "Mit neuestem origin/{branch} synchronisiert",
"bg": "Synced to latest origin/{branch}",
"de": "Synced to latest origin/{branch}",
"en": "Synced to latest origin/{branch}",
"pl": "Zsynchronizowano z najnowszym origin/{branch}",
"ru": "Синхронизировано с последним origin/{branch}",
"zh": "已同步到最新的 origin/{branch}"
"pl": "Synced to latest origin/{branch}",
"ru": "Synced to latest origin/{branch}",
"zh": "Synced to latest origin/{branch}"
},
"Syncing files...": {
"bg": "",
@@ -3160,84 +2832,60 @@
"zh": ""
},
"Tag consistency check failed.": {
"bg": "Проверката за консистентност на таговете се провали.",
"de": "Tag-Konsistenzprüfung fehlgeschlagen.",
"bg": "Tag consistency check failed.",
"de": "Tag consistency check failed.",
"en": "Tag consistency check failed.",
"pl": "Kontrola zgodności tagów nie powiodła się.",
"ru": "Проверка согласованности тегов не пройдена.",
"zh": "标签一致性检查失败。"
"ru": "Tag consistency check failed.",
"zh": "Tag consistency check failed."
},
"Tag is required (or use --from-tag).": {
"bg": "Тагът е задължителен (или използвайте --from-tag).",
"de": "Tag ist erforderlich (oder --from-tag verwenden).",
"bg": "Tag is required (or use --from-tag).",
"de": "Tag is required (or use --from-tag).",
"en": "Tag is required (or use --from-tag).",
"pl": "Tag jest wymagany (lub użyj --from-tag).",
"ru": "Тег обязателен (или используйте --from-tag).",
"zh": "标签是必需的(或使用 --from-tag)。"
"ru": "Tag is required (or use --from-tag).",
"zh": "Tag is required (or use --from-tag)."
},
"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.",
"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.",
"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": "Тег v{version} уже существует. Workflow публикации уже должен был быть запущен.",
"zh": "标签 v{version} 已存在。发布工作流应已被触发。"
"ru": "Tag v{version} already existed. Publish workflow should already have been triggered.",
"zh": "Tag v{version} already existed. Publish workflow should already have been triggered."
},
"Tag {tag} already exists and points to HEAD. Skipping creation.": {
"bg": "Тагът {tag} вече съществува и сочи към HEAD. Създаването се пропуска.",
"de": "Tag {tag} existiert bereits und zeigt auf HEAD. Erstellung wird übersprungen.",
"bg": "Tag {tag} already exists and points to HEAD. Skipping creation.",
"de": "Tag {tag} already exists and points to HEAD. Skipping creation.",
"en": "Tag {tag} already exists and points to HEAD. Skipping creation.",
"pl": "Tag {tag} już istnieje i wskazuje na HEAD. Pomijanie tworzenia.",
"ru": "Тег {tag} уже существует и указывает на HEAD. Создание пропускается.",
"zh": "标签 {tag} 已存在且指向 HEAD。跳过创建。"
"ru": "Tag {tag} already exists and points to HEAD. Skipping creation.",
"zh": "Tag {tag} already exists and points to HEAD. Skipping creation."
},
"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.",
"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.",
"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_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"
"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."
},
"Task ID: {task_id}": {
"bg": "ID на задача: {task_id}",
"de": "Task-ID: {task_id}",
"bg": "Task ID: {task_id}",
"de": "Task ID: {task_id}",
"en": "Task ID: {task_id}",
"pl": "ID zadania: {task_id}",
"ru": "ID задачи: {task_id}",
"zh": "任务 ID{task_id}"
"ru": "Task ID: {task_id}",
"zh": "Task ID: {task_id}"
},
"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.",
"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.",
"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": "Тест '{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} 条建议性警告。"
"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."
},
"Test isolation check passed: {count} test files analyzed, no violations found.": {
"bg": "Проверката за изолация на тестове премина: анализирани са {count} тестови файла, няма нарушения.",
@@ -3248,76 +2896,60 @@
"zh": "测试隔离检查通过:已分析 {count} 个测试文件,未发现违规。"
},
"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}",
"bg": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
"de": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
"en": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
"pl": "Testy nie powiodły się — odmowa wydania. Najpierw napraw niepowodzenia testów.\n{stderr}",
"ru": "Тесты не пройдены — отказ в релизе. Сначала исправьте ошибки тестов.\n{stderr}",
"zh": "测试失败——拒绝发布。请先修复测试失败。\n{stderr}"
"ru": "Tests failed — refusing to release. Fix test failures first.\n{stderr}",
"zh": "Tests failed — refusing to release. Fix test failures first.\n{stderr}"
},
"Tests passed.": {
"bg": "Тестовете преминаха.",
"de": "Tests bestanden.",
"bg": "Tests passed.",
"de": "Tests passed.",
"en": "Tests passed.",
"pl": "Testy zakończone pomyślnie.",
"ru": "Тесты пройдены.",
"zh": "测试通过。"
"ru": "Tests passed.",
"zh": "Tests passed."
},
"Timeout reached after {timeout}s.": {
"bg": "Достигнат таймаут след {timeout}s.",
"de": "Timeout nach {timeout}s erreicht.",
"bg": "Timeout reached after {timeout}s.",
"de": "Timeout reached after {timeout}s.",
"en": "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 建议(以运行时审计为准):"
"pl": "Timeout reached after {timeout}s.",
"ru": "Timeout reached after {timeout}s.",
"zh": "Timeout reached after {timeout}s."
},
"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).",
"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).",
"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": "Модульные тесты пройдены за {duration:.2f}s (ниже лимита {max}s, все тесты ниже лимита {single}s на тест).",
"zh": "单元测试在 {duration:.2f}s 内通过(低于 {max}s 限制,所有测试均低于 {single}s 单测试限制)。"
"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)."
},
"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.",
"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.",
"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": "Модульные тесты слишком медленные: {duration:.2f}s (макс.: {max}s).\n Исправление: выполните 'make pytest-cov' для профилирования, затем оптимизируйте медленные тесты.\n Совет: избегайте ненужных импортов, используйте более лёгкие моки или кешируйте фикстуры.",
"zh": "单元测试过慢:{duration:.2f}s(最大允许:{max}s)。\n 修复:运行 'make pytest-cov' 进行性能分析,然后优化慢测试。\n 提示:避免不必要的导入,使用更轻的 mock 或缓存 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."
},
"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}",
"bg": "Unknown check category '{check}'. Available: all, user-facing{tags}",
"de": "Unknown check category '{check}'. Available: all, user-facing{tags}",
"en": "Unknown check category '{check}'. Available: all, user-facing{tags}",
"pl": "Nieznana kategoria kontroli '{check}'. Dostępne: all, user-facing{tags}",
"ru": "Неизвестная категория проверки '{check}'. Доступны: all, user-facing{tags}",
"zh": "未知检查类别 '{check}'。可用:all、user-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 部署已阻止。"
"ru": "Unknown check category '{check}'. Available: all, user-facing{tags}",
"zh": "Unknown check category '{check}'. Available: all, user-facing{tags}"
},
"Updated badge URLs in {filename}": {
"bg": "Обновени URL на значки в {filename}",
"de": "Badge-URLs in {filename} aktualisiert",
"bg": "Updated badge URLs in {filename}",
"de": "Updated badge URLs in {filename}",
"en": "Updated badge URLs in {filename}",
"pl": "Zaktualizowano URL-e odznak w {filename}",
"ru": "Обновлены URL значков в {filename}",
"zh": "已更新 {filename} 中的徽章 URL"
"pl": "Updated badge URLs in {filename}",
"ru": "Updated badge URLs in {filename}",
"zh": "Updated badge URLs in {filename}"
},
"Updated documentation version references to v{version}": {
"bg": "",
@@ -3328,20 +2960,20 @@
"zh": ""
},
"Updated version in {init}": {
"bg": "Обновена версия в {init}",
"de": "Version in {init} aktualisiert",
"bg": "Updated version in {init}",
"de": "Updated version in {init}",
"en": "Updated version in {init}",
"pl": "Zaktualizowano wersję w {init}",
"ru": "Версия обновлена в {init}",
"zh": "已更新 {init} 中的版本"
"ru": "Updated version in {init}",
"zh": "Updated version in {init}"
},
"Updated {changelog_file}": {
"bg": "Обновен {changelog_file}",
"de": "{changelog_file} aktualisiert",
"bg": "Updated {changelog_file}",
"de": "Updated {changelog_file}",
"en": "Updated {changelog_file}",
"pl": "Zaktualizowano {changelog_file}",
"ru": "Обновлён {changelog_file}",
"zh": "已更新 {changelog_file}"
"ru": "Updated {changelog_file}",
"zh": "Updated {changelog_file}"
},
"Use string comparison or _is_truthy()/_is_falsy() helpers instead. Add '{marker}' to suppress individual lines.": {
"bg": "Използвайте сравнение на низове или _is_truthy()/_is_falsy() помощници. Добавете '{marker}' за потискане на отделни редове.",
@@ -3368,20 +3000,20 @@
"zh": "VIKUNJA_TOKEN 未设置。在 .env 或环境中设置它。"
},
"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.",
"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.",
"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 не задан. Требуется в CI для проверки заголовков PR.",
"zh": "未设置 VIKUNJA_TOKEN。CI 中验证 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."
},
"Version file: {file}": {
"bg": "Файл с версия: {file}",
"de": "Versionsdatei: {file}",
"bg": "Version file: {file}",
"de": "Version file: {file}",
"en": "Version file: {file}",
"pl": "Plik wersji: {file}",
"ru": "Файл версии: {file}",
"zh": "版本文件:{file}"
"ru": "Version file: {file}",
"zh": "Version file: {file}"
},
"Version stays at v{version} — no version bump from git-cliff. Commits since last tag don't warrant a new release. Skipping.": {
"bg": "",
@@ -3392,12 +3024,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 (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.",
"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.",
"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 (HTTP {status}): {message}. Задача {task_id} НЕ была обновлена. Слияние прошло успешно, но задачу Vikunja нужно обновить вручную.",
"zh": "Vikunja API 错误(HTTP {status}):{message}。任务 {task_id} 未更新。合并成功,但 Vikunja 任务需要手动更新。"
"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."
},
"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, за да премахнете префикса.",
@@ -3448,12 +3080,12 @@
"zh": "警告: 无法解析 Python 版本 '{version}'。"
},
"WARNING: --skip-tests passed — skipping test verification.": {
"bg": "ПРЕДУПРЕЖДЕНИЕ: зададен е --skip-tests — проверката на тестовете се пропуска.",
"de": "WARNUNG: --skip-tests übergeben — Testverifizierung wird übersprungen.",
"bg": "WARNING: --skip-tests passed — skipping test verification.",
"de": "WARNING: --skip-tests passed — skipping test verification.",
"en": "WARNING: --skip-tests passed — skipping test verification.",
"pl": "OSTRZEŻENIE: --skip-tests przekazane — pomijanie weryfikacji testów.",
"ru": "ПРЕДУПРЕЖДЕНИЕ: передан --skip-tests — проверка тестов пропускается.",
"zh": "警告:已传入 --skip-tests——跳过测试验证。"
"ru": "WARNING: --skip-tests passed — skipping test verification.",
"zh": "WARNING: --skip-tests passed — skipping test verification."
},
"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 от хранилището — името на клона е единственият източник на истината.",
@@ -3496,68 +3128,68 @@
"zh": ""
},
"Waiting for CI checks to complete (timeout: {timeout}s)...": {
"bg": "Изчакване CI проверките да завършат (таймаут: {timeout}s)...",
"de": "Warte auf Abschluss der CI-Checks (Timeout: {timeout}s)...",
"bg": "Waiting for CI checks to complete (timeout: {timeout}s)...",
"de": "Waiting for CI checks to complete (timeout: {timeout}s)...",
"en": "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..."
"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)..."
},
"Warning: could not fetch tags from origin.": {
"bg": "Предупреждение: не могат да се извлекат таговете от origin.",
"de": "Warnung: Tags konnten nicht von origin abgerufen werden.",
"bg": "Warning: could not fetch tags from origin.",
"de": "Warning: could not fetch tags from origin.",
"en": "Warning: could not fetch tags from origin.",
"pl": "Ostrzeżenie: nie udało się pobrać tagów z origin.",
"ru": "Предупреждение: не удалось получить теги из origin.",
"zh": "警告:无法从 origin 获取标签。"
"ru": "Warning: could not fetch tags from origin.",
"zh": "Warning: could not fetch tags from origin."
},
"Warning: instance-level runners query failed: {error}": {
"bg": "Предупреждение: заявката за раннъри на ниво инстанция се провали: {error}",
"de": "Warnung: Abfrage der Runner auf Instanzebene fehlgeschlagen: {error}",
"bg": "Warning: instance-level runners query failed: {error}",
"de": "Warning: instance-level runners query failed: {error}",
"en": "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}"
"pl": "Warning: instance-level runners query failed: {error}",
"ru": "Warning: instance-level runners query failed: {error}",
"zh": "Warning: instance-level runners query failed: {error}"
},
"Warning: instance-level runners query returned HTTP {status}": {
"bg": "Предупреждение: заявката за раннъри на ниво инстанция върна HTTP {status}",
"de": "Warnung: Abfrage der Runner auf Instanzebene gab HTTP {status} zurück",
"bg": "Warning: instance-level runners query returned HTTP {status}",
"de": "Warning: instance-level runners query returned HTTP {status}",
"en": "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}"
"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}"
},
"Warning: org-level runners query failed: {error}": {
"bg": "Предупреждение: заявката за раннъри на ниво организация се провали: {error}",
"de": "Warnung: Abfrage der Runner auf Organisationsebene fehlgeschlagen: {error}",
"bg": "Warning: org-level runners query failed: {error}",
"de": "Warning: org-level runners query failed: {error}",
"en": "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}"
"pl": "Warning: org-level runners query failed: {error}",
"ru": "Warning: org-level runners query failed: {error}",
"zh": "Warning: org-level runners query failed: {error}"
},
"Warning: org-level runners query returned HTTP {status}": {
"bg": "Предупреждение: заявката за раннъри на ниво организация върна HTTP {status}",
"de": "Warnung: Abfrage der Runner auf Organisationsebene gab HTTP {status} zurück",
"bg": "Warning: org-level runners query returned HTTP {status}",
"de": "Warning: org-level runners query returned HTTP {status}",
"en": "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}"
"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}"
},
"Warning: repo-level runners query failed: {error}": {
"bg": "Предупреждение: заявката за раннъри на ниво репозитори се провали: {error}",
"de": "Warnung: Abfrage der Runner auf Repo-Ebene fehlgeschlagen: {error}",
"bg": "Warning: repo-level runners query failed: {error}",
"de": "Warning: repo-level runners query failed: {error}",
"en": "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}"
"pl": "Warning: repo-level runners query failed: {error}",
"ru": "Warning: repo-level runners query failed: {error}",
"zh": "Warning: repo-level runners query failed: {error}"
},
"Warning: repo-level runners query returned HTTP {status}": {
"bg": "Предупреждение: заявката за раннъри на ниво репозитори върна HTTP {status}",
"de": "Warnung: Abfrage der Runner auf Repo-Ebene gab HTTP {status} zurück",
"bg": "Warning: repo-level runners query returned HTTP {status}",
"de": "Warning: repo-level runners query returned HTTP {status}",
"en": "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}"
"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}"
},
"Wiki repo not found or empty — initializing fresh.": {
"bg": "",
@@ -3599,21 +3231,13 @@
"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": "Тагът {tag} е записан в GITHUB_OUTPUT.",
"de": "Tag {tag} nach GITHUB_OUTPUT geschrieben.",
"bg": "Wrote tag {tag} to GITHUB_OUTPUT.",
"de": "Wrote tag {tag} to GITHUB_OUTPUT.",
"en": "Wrote tag {tag} to GITHUB_OUTPUT.",
"pl": "Zapisano tag {tag} do GITHUB_OUTPUT.",
"ru": "Тег {tag} записан в GITHUB_OUTPUT.",
"zh": "已将标签 {tag} 写入 GITHUB_OUTPUT"
"pl": "Wrote tag {tag} to GITHUB_OUTPUT.",
"ru": "Wrote tag {tag} to GITHUB_OUTPUT.",
"zh": "Wrote tag {tag} to GITHUB_OUTPUT."
},
"[check-api-identity-checks] Passed: no unsafe identity checks found": {
"bg": "[check-api-identity-checks] Мина: не са намерени небрежни проверки за идентичност",
@@ -3624,12 +3248,12 @@
"zh": "[check-api-identity-checks] 通过:未发现不安全的身份检查"
},
"[check-dep-docs] Passed: all dependencies are documented": {
"bg": "[check-dep-docs] Успешно: всички зависимости са документирани",
"de": "[check-dep-docs] Bestanden: alle Abhängigkeiten sind dokumentiert",
"bg": "[check-dep-docs] Passed: all dependencies are documented",
"de": "[check-dep-docs] Passed: all dependencies are documented",
"en": "[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] 通过:所有依赖项均已记录"
"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"
},
"[check-deps] All core tools present.": {
"bg": "[check-deps] Всички основни инструменти са налични.",
@@ -3656,76 +3280,28 @@
"zh": "[check-deps] 虚拟环境 .venv 已就绪 (Python {version})。"
},
"[check-mutable-globals] Passed: no mutable path globals found": {
"bg": "[check-mutable-globals] Успешно: не са намерени променливи пътеки глобали",
"de": "[check-mutable-globals] Bestanden: keine mutablen Pfad-Globals gefunden",
"bg": "[check-mutable-globals] Passed: no mutable path globals found",
"de": "[check-mutable-globals] Passed: no mutable path globals found",
"en": "[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"
"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"
},
"[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",
"bg": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
"de": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
"en": "[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} 个文件,无过时引用"
"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"
},
"[check_test_coverage] No changed files to check.": {
"bg": "[check_test_coverage] Няма променени файлове за проверка.",
"de": "[check_test_coverage] Keine geänderten Dateien zu prüfen.",
"bg": "[check_test_coverage] No changed files to check.",
"de": "[check_test_coverage] No changed files to check.",
"en": "[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。"
"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."
},
"[docker-login] Logged in to {registry}.": {
"bg": "[docker-login] Влязъл в {registry}.",
@@ -3768,36 +3344,36 @@
"zh": ""
},
"[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]",
"bg": "[dry-run] Would commit: release: v{version} [skip ci]",
"de": "[dry-run] Would commit: 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] Было бы закоммичено: release: v{version} [skip ci]",
"zh": "[dry-run] 将提交: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]"
},
"[dry-run] Would create tag: v{version}": {
"bg": "[dry-run] Ще се създаде таг: v{version}",
"de": "[dry-run] Würde Tag erstellen: v{version}",
"bg": "[dry-run] Would create tag: v{version}",
"de": "[dry-run] Would create tag: v{version}",
"en": "[dry-run] Would create tag: v{version}",
"pl": "[dry-run] Utworzono by tag: v{version}",
"ru": "[dry-run] Был бы создан тег: v{version}",
"zh": "[dry-run] 将创建标签:v{version}"
"ru": "[dry-run] Would create tag: v{version}",
"zh": "[dry-run] Would create tag: v{version}"
},
"[dry-run] Would create tag: {tag}": {
"bg": "[dry-run] Ще се създаде таг: {tag}",
"de": "[dry-run] Würde Tag erstellen: {tag}",
"bg": "[dry-run] Would create tag: {tag}",
"de": "[dry-run] Would create tag: {tag}",
"en": "[dry-run] Would create tag: {tag}",
"pl": "[dry-run] Utworzono by tag: {tag}",
"ru": "[dry-run] Был бы создан тег: {tag}",
"zh": "[dry-run] 将创建标签:{tag}"
"ru": "[dry-run] Would create tag: {tag}",
"zh": "[dry-run] Would create tag: {tag}"
},
"[dry-run] Would push commit to master": {
"bg": "[dry-run] Ще се push-не комит към master",
"de": "[dry-run] Würde Commit zu master pushen",
"bg": "[dry-run] Would push commit to master",
"de": "[dry-run] Would push commit to master",
"en": "[dry-run] Would push commit to master",
"pl": "[dry-run] Wypchnięto by commit do master",
"ru": "[dry-run] Коммит был бы отправлен в master",
"zh": "[dry-run] 将推送提交到 master"
"ru": "[dry-run] Would push commit to master",
"zh": "[dry-run] Would push commit to master"
},
"[dry-run] Would update doc version references via check_doc_versions --fix": {
"bg": "",
@@ -3808,68 +3384,20 @@
"zh": ""
},
"[dry-run] Would update {changelog_file}": {
"bg": "[dry-run] Ще се обнови {changelog_file}",
"de": "[dry-run] Würde {changelog_file} aktualisieren",
"bg": "[dry-run] Would update {changelog_file}",
"de": "[dry-run] Would update {changelog_file}",
"en": "[dry-run] Would update {changelog_file}",
"pl": "[dry-run] Zaktualizowano by {changelog_file}",
"ru": "[dry-run] Был бы обновлён {changelog_file}",
"zh": "[dry-run] 将更新 {changelog_file}"
"ru": "[dry-run] Would update {changelog_file}",
"zh": "[dry-run] Would update {changelog_file}"
},
"[dry-run] Would update {init}": {
"bg": "[dry-run] Ще се обнови {init}",
"de": "[dry-run] Würde {init} aktualisieren",
"bg": "[dry-run] Would update {init}",
"de": "[dry-run] Would update {init}",
"en": "[dry-run] Would update {init}",
"pl": "[dry-run] Zaktualizowano by {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}"
"ru": "[dry-run] Would update {init}",
"zh": "[dry-run] Would update {init}"
},
"[tofu-init] Done.": {
"bg": "[tofu-init] Готово.",
@@ -3952,52 +3480,36 @@
"zh": "失败"
},
"git command failed ({cmd}): {stderr}": {
"bg": "git командата се провали ({cmd}): {stderr}",
"de": "git-Befehl fehlgeschlagen ({cmd}): {stderr}",
"bg": "git command failed ({cmd}): {stderr}",
"de": "git command failed ({cmd}): {stderr}",
"en": "git command failed ({cmd}): {stderr}",
"pl": "polecenie git nie powiodło się ({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}"
"ru": "git command failed ({cmd}): {stderr}",
"zh": "git command failed ({cmd}): {stderr}"
},
"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.",
"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.",
"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 сгенерировал пустой changelog для v{version}. Проверьте cliff.toml и историю коммитов.",
"zh": "git-cliff 为 v{version} 生成了空的 changelog。请检查 cliff.toml 和提交历史。"
"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."
},
"git-cliff returned empty version.": {
"bg": "git-cliff върна празна версия.",
"de": "git-cliff gab eine leere Version zurück.",
"bg": "git-cliff returned empty version.",
"de": "git-cliff returned empty version.",
"en": "git-cliff returned empty version.",
"pl": "git-cliff zwrócił pustą wersję.",
"ru": "git-cliff вернул пустую версию.",
"zh": "git-cliff 返回了空版本。"
"ru": "git-cliff returned empty version.",
"zh": "git-cliff returned empty version."
},
"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).",
"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).",
"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 вернул неверный формат версии: {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 包裹。"
"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)."
},
"in_progress": {
"bg": "в процес",
@@ -4024,20 +3536,20 @@
"zh": "indices={indices}"
},
"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}",
"bg": "mapping.json keys and values must be strings, got {k}={v}",
"de": "mapping.json keys and values must be strings, got {k}={v}",
"en": "mapping.json keys and values must be strings, got {k}={v}",
"pl": "klucze i wartości mapping.json muszą być ciągami znaków, otrzymano {k}={v}",
"ru": "Ключи и значения mapping.json должны быть строками, получено {k}={v}",
"zh": "mapping.json 的键和值必须是字符串,实际得到 {k}={v}"
"ru": "mapping.json keys and values must be strings, got {k}={v}",
"zh": "mapping.json keys and values must be strings, got {k}={v}"
},
"mapping.json must be a dict of file-path -> page-title, got {type}": {
"bg": "mapping.json трябва да е dict от file-path -> page-title, получено {type}",
"de": "mapping.json muss ein Dict von file-path -> page-title sein, erhalten {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}",
"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 должен быть dict вида file-path -> page-title, получено {type}",
"zh": "mapping.json 必须是 file-path -> page-title 的字典,实际得到 {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}"
},
"pending": {
"bg": "в очакване",
@@ -4056,20 +3568,20 @@
"zh": "在当前目录中未找到 pyproject.toml。"
},
"tea login '{name}' already configured.": {
"bg": "tea входът '{name}' вече е конфигуриран.",
"de": "tea-Login '{name}' bereits konfiguriert.",
"bg": "tea login '{name}' already configured.",
"de": "tea login '{name}' already configured.",
"en": "tea login '{name}' already configured.",
"pl": "Logowanie tea '{name}' już skonfigurowane.",
"ru": "Вход tea '{name}' уже настроен.",
"zh": "tea 登录 '{name}' 已配置。"
"pl": "tea login '{name}' already configured.",
"ru": "tea login '{name}' already configured.",
"zh": "tea login '{name}' already configured."
},
"tea not installed — skipping login configuration.": {
"bg": "tea не е инсталиран — пропуска се конфигурацията за вход.",
"de": "tea nicht installiert — Login-Konfiguration wird übersprungen.",
"bg": "tea not installed — skipping login configuration.",
"de": "tea not installed — skipping login configuration.",
"en": "tea not installed — skipping login configuration.",
"pl": "tea nie jest zainstalowany — pomijanie konfiguracji logowania.",
"ru": "tea не установлен — настройка входа пропускается.",
"zh": "未安装 tea——跳过登录配置。"
"pl": "tea not installed — skipping login configuration.",
"ru": "tea not installed — skipping login configuration.",
"zh": "tea not installed — skipping login configuration."
},
"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\").",
@@ -4120,12 +3632,12 @@
"zh": "{env} 未设置。请在 .env 文件中设置。"
},
"{file} already exists. Use --force to overwrite.": {
"bg": "{file} вече съществува. Използвайте --force за презаписване.",
"de": "{file} existiert bereits. Mit --force überschreiben.",
"bg": "{file} already exists. Use --force to overwrite.",
"de": "{file} already exists. Use --force to overwrite.",
"en": "{file} already exists. Use --force to overwrite.",
"pl": "{file} już istnieje. Użyj --force, aby nadpisać.",
"ru": "{file} уже существует. Используйте --force для перезаписи.",
"zh": "{file} 已存在。使用 --force 覆盖。"
"ru": "{file} already exists. Use --force to overwrite.",
"zh": "{file} already exists. Use --force to overwrite."
},
"{func} called in test '{test}' without @patch — this function {desc}. Add @patch(\"<module>.{func}\").": {
"bg": "{func} извикано в тест '{test}' без @patch — тази функция {desc}. Добавете @patch(\"<module>.{func}\").",
@@ -4150,5 +3662,181 @@
"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)"
},
"Cleaning up: running molecule destroy for {scenario}": {
"en": "Cleaning up: running molecule destroy for {scenario}",
"bg": "Изчистване: изпълнение на molecule destroy за {scenario}",
"de": "Aufräumen: molecule destroy wird ausgeführt für {scenario}",
"pl": "Czyszczenie: uruchamianie molecule destroy dla {scenario}",
"ru": "Очистка: запуск molecule destroy для {scenario}",
"zh": "清理:正在为 {scenario} 运行 molecule destroy"
}
}
-6
View File
@@ -923,12 +923,6 @@ 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")
+8 -225
View File
@@ -13,9 +13,7 @@ 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,
@@ -198,7 +196,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, stdout="")
mock_result = MagicMock(returncode=0, stderr="", 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
@@ -206,8 +204,8 @@ class TestPushImage:
def test_partial_failure(self) -> None:
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
results = [
MagicMock(returncode=0, stdout=""),
MagicMock(returncode=1, stdout="push failed"),
MagicMock(returncode=0, stderr="", stdout=""),
MagicMock(returncode=1, stderr="push failed", stdout=""),
]
with patch("devx.tools.build_image.subprocess.run", side_effect=results):
assert push_image(spec, "git.example.com") is False
@@ -218,212 +216,6 @@ 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:
@@ -796,20 +588,11 @@ class TestCLIBuildImage:
"devx.tools.build_image.subprocess.run",
side_effect=[login_result, build_result, push_result],
):
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
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
@@ -1,175 +0,0 @@
"""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
+15
View File
@@ -40,6 +40,7 @@ class TestCliGroups:
result = runner.invoke(cli, ["molecule", "--help"])
assert result.exit_code == 0
assert "distribute" in result.output
assert "guard" in result.output
assert "all" in result.output
@@ -107,6 +108,13 @@ 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()
@@ -223,6 +231,13 @@ class TestMoleculeCommands:
assert result.exit_code == 0
mock_run.assert_called_once_with("devx.molecule.discover_runners", [])
@patch("devx.cli._run_module")
def test_molecule_guard(self, mock_run: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(cli, ["molecule", "guard"])
assert result.exit_code == 0
mock_run.assert_called_once_with("devx.molecule.molecule_ci_guard", [])
@patch("devx.cli._run_module")
def test_molecule_all(self, mock_run: MagicMock) -> None:
runner = CliRunner()
-1
View File
@@ -38,7 +38,6 @@ 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
@@ -1,182 +0,0 @@
"""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
@@ -1,82 +0,0 @@
"""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

Some files were not shown because too many files have changed in this diff Show More