Compare commits

..
22 Commits
Author SHA1 Message Date
devx-ci-bot 281193c741 release: v0.38.0 [skip ci] 2026-07-08 19:30:58 +00:00
emil 0228fce5b9 DEVX-123: feat: introduce role-based Gitea API token environment variables
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 17s
Post-merge / vikunja (push) Successful in 18s
Post-merge / release (push) Successful in 36s
Post-merge / publish (push) Successful in 20s
Post-merge / badges (push) Successful in 35s
2026-07-08 19:30:10 +00:00
gitea-actions-bot 981d3e41cc chore: update badge URLs to commit fe187115 [skip ci] 2026-07-07 22:02:05 +00:00
devx-ci-bot 3cd2459eef release: v0.37.0 [skip ci] 2026-07-07 22:01:14 +00:00
emil ef08513bcf DEVX-122: feat: consolidate docs checks into devx-docs-check target
Post-merge / detect-type (push) Successful in 19s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / vikunja (push) Successful in 24s
Post-merge / configure-repo (push) Successful in 25s
Post-merge / sync-wiki (push) Successful in 32s
Post-merge / release (push) Successful in 43s
Post-merge / publish (push) Successful in 21s
Post-merge / badges (push) Successful in 38s
2026-07-07 22:00:07 +00:00
gitea-actions-bot 05922eca2f chore: update badge URLs to commit bff19e05 [skip ci] 2026-07-07 15:53:21 +00:00
devx-ci-bot 05de2b0aa9 release: v0.36.2 [skip ci] 2026-07-07 15:52:35 +00:00
emil 6a463a93d2 DEVX-121: fix: GiteaClient.set_repo_variable uses PUT instead of PATCH
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 22s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / sync-wiki (push) Successful in 31s
Post-merge / release (push) Successful in 38s
Post-merge / publish (push) Successful in 23s
Post-merge / badges (push) Successful in 39s
2026-07-07 15:51:45 +00:00
gitea-actions-bot ad7b52c368 chore: update badge URLs to commit d9423d85 [skip ci] 2026-07-07 12:05:28 +00:00
devx-ci-bot 6b81e1a50a release: v0.36.1 [skip ci] 2026-07-07 12:04:41 +00:00
emil 443dc01b4e DEVX-120: fix: preserve .badges/ dir during git clean in push_badges
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 16s
Post-merge / vikunja (push) Successful in 21s
Post-merge / sync-wiki (push) Successful in 30s
Post-merge / release (push) Successful in 40s
Post-merge / publish (push) Successful in 25s
Post-merge / badges (push) Successful in 41s
2026-07-07 12:03:51 +00:00
devx-ci-bot 1d7bf7118a release: v0.36.0 [skip ci] 2026-07-07 11:58:00 +00:00
emil f98534ebe2 DEVX-119: feat: add GiteaClient repo variable methods and parallelize pytest-cov
Post-merge / detect-type (push) Successful in 12s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / vikunja (push) Successful in 23s
Post-merge / sync-wiki (push) Successful in 29s
Post-merge / release (push) Successful in 43s
Post-merge / publish (push) Successful in 23s
Post-merge / badges (push) Failing after 31s
2026-07-07 11:57:04 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> c62b168b25 DEVX-116: chore: update grm package name references
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / vikunja (push) Successful in 22s
Post-merge / release (push) Successful in 23s
Post-merge / publish (push) Has been skipped
Post-merge / sync-wiki (push) Successful in 30s
Post-merge / badges (push) Failing after 30s
Update hardcoded path and docstring examples from
`gitea_runner_manager` to `grm` after the package rename in grm PR #203.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 15:51:10 +02:00
devx-ci-bot 40a94df029 release: v0.35.7 [skip ci] 2026-07-06 13:22:27 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> f50c4c1e00 DEVX-118: fix: use Gitea wiki dash-marker filename convention
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / vikunja (push) Successful in 24s
Post-merge / configure-repo (push) Successful in 16s
Post-merge / sync-wiki (push) Successful in 35s
Post-merge / release (push) Successful in 44s
Post-merge / publish (push) Successful in 22s
Post-merge / badges (push) Failing after 30s
Gitea appends a ".-" suffix before ".md" for wiki page titles that
contain dashes, to distinguish literal dashes from space-to-dash
conversions. For example, "Getting-Started" becomes
"Getting-Started.-.md", while "Architecture" becomes "Architecture.md".

Previously the code wrote "Getting-Started.md" which Gitea couldn't
recognize as a valid wiki page, causing verification to fail with
"page not found" for 15 of 21 pages.

Also force-push to handle concurrent CI runs that may have pushed to
the wiki repo between our clone and push.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 15:21:22 +02:00
devx-ci-bot bbb264efc9 release: v0.35.6 [skip ci] 2026-07-06 13:00:59 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> c97b249935 DEVX-118: fix: add delay before wiki verification to avoid race condition
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / vikunja (push) Successful in 20s
Post-merge / sync-wiki (push) Successful in 29s
Post-merge / release (push) Successful in 39s
Post-merge / publish (push) Successful in 25s
Post-merge / badges (push) Failing after 38s
Gitea needs a few seconds to process pushed wiki commits before a
re-clone will see them. Add a 5s sleep after a successful push before
verification re-clones the wiki.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 15:00:02 +02:00
devx-ci-bot 32b9a53151 release: v0.35.5 [skip ci] 2026-07-06 09:46:57 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> ae68df63f1 DEVX-118: fix: embed token in wiki clone URL for push auth
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 19s
Post-merge / sync-wiki (push) Successful in 24s
Post-merge / configure-repo (push) Successful in 14s
Post-merge / release (push) Successful in 34s
Post-merge / publish (push) Successful in 19s
Post-merge / badges (push) Failing after 29s
The wiki Git push failed with "could not read Username" because the
clone URL didn't include credentials. Use token@host URL format so
both clone and push authenticate properly.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 11:46:05 +02:00
devx-ci-bot 6402f31345 release: v0.35.4 [skip ci] 2026-07-06 09:42:49 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> f017fec8f5 DEVX-118: fix: configure git identity before commit in sync_wiki
Post-merge / detect-type (push) Successful in 16s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / vikunja (push) Successful in 21s
Post-merge / sync-wiki (push) Failing after 24s
Post-merge / release (push) Successful in 37s
Post-merge / publish (push) Successful in 21s
Post-merge / badges (push) Failing after 31s
CI environments may lack git user.email/user.name config, causing
git commit to fail with exit code 128. Set identity explicitly before
committing wiki changes.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 11:41:36 +02:00
63 changed files with 1819 additions and 857 deletions
+15 -2
View File
@@ -1,6 +1,19 @@
# Gitea API token (required for CI scripts that interact with Gitea) # Role-based Gitea API tokens.
# Each token serves a specific role. For small teams the developer and CI
# tokens may belong to the same user, but the reviewer token MUST belong to a
# different Gitea user than the PR author so Gitea accepts approval reviews.
# Create at: https://git.oblachno.oblachno.fyi/user/settings/applications # Create at: https://git.oblachno.oblachno.fyi/user/settings/applications
CI_GITEA_TOKEN=
# Developer token — used by local tooling: create-task, create-pr, setup, etc.
DEVELOPER_GITEA_API_TOKEN=
# CI token — used by CI workflows and scripts that do not post approvals.
# Legacy CI_GITEA_TOKEN is also accepted.
CI_GITEA_API_TOKEN=
# Reviewer token — used by the auto-merge workflow to post APPROVE reviews.
# This must be a different Gitea user from the developer/CI user.
REVIEWER_GITEA_API_TOKEN=
# Vikunja API token (required for post-merge task updates) # Vikunja API token (required for post-merge task updates)
# Create at: https://work.oblachno.oblachno.fyi/settings/tokens # Create at: https://work.oblachno.oblachno.fyi/settings/tokens
+14 -6
View File
@@ -38,6 +38,8 @@ jobs:
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Set up environment - name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-ci run: make setup-ci
- name: Check if this is a release commit - name: Check if this is a release commit
id: check id: check
@@ -62,18 +64,22 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up environment - name: Set up environment
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-release run: make setup-release
- name: Docker registry login - name: Docker registry login
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: | run: |
. .venv/bin/activate . .venv/bin/activate
echo "$CI_GITEA_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin _TOKEN="$CI_GITEA_API_TOKEN"
[ -z "$_TOKEN" ] && _TOKEN="$DEVELOPER_GITEA_API_TOKEN"
[ -z "$_TOKEN" ] && _TOKEN="$CI_GITEA_TOKEN"
if [ -z "$_TOKEN" ]; then echo "Gitea API token not set — skipping Docker login"; exit 1; fi
echo "$_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
- name: Build and push tier images - name: Build and push tier images
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
@@ -103,7 +109,7 @@ jobs:
- name: Notify on failure - name: Notify on failure
if: failure() if: failure()
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
@@ -125,10 +131,12 @@ jobs:
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Set up environment - name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-ci run: make setup-ci
- name: Clean up old image versions - name: Clean up old image versions
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
. .venv/bin/activate . .venv/bin/activate
+18 -25
View File
@@ -16,6 +16,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up environment - name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image run: make setup-image
- name: Lint all - name: Lint all
run: | run: |
@@ -32,31 +34,15 @@ jobs:
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.tools.check_test_speed --max-seconds 6 --max-single-seconds 0.5 python3 -m devx.tools.check_test_speed --max-seconds 6 --max-single-seconds 0.5
- name: Documentation coverage check - name: Documentation gate (coverage + stale refs + lint + version refs + prose)
env:
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.doc_coverage --fail-on-missing
- name: Documentation lint check
env:
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.lint_docs --root .
- name: Documentation version reference check
env:
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.tools.check_doc_versions --root .
- name: Vale prose lint check
env: env:
PYTHONPATH: src PYTHONPATH: src
DEVX_DOC_COVERAGE_STRICT: "1"
DEVX_VALE_LEVEL: warning
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
vale --minAlertLevel=error docs/ AGENTS.md README.md make devx-docs-check
- name: Translation completeness check - name: Translation completeness check
env: env:
PYTHONPATH: src PYTHONPATH: src
@@ -95,6 +81,8 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up environment - name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image run: make setup-image
- name: Detect changed paths - name: Detect changed paths
id: detect id: detect
@@ -122,10 +110,11 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up environment - name: Set up environment
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image run: make setup-image
- name: Release dry-run validation - name: Release dry-run validation
env: env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
@@ -143,10 +132,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up environment - name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image run: make setup-image
- name: Run automated PR review - name: Run automated PR review
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
set -euo pipefail set -euo pipefail
@@ -176,12 +167,14 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.CI_GITEA_TOKEN }} token: ${{ secrets.CI_GITEA_API_TOKEN }}
- name: Set up environment - name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image run: make setup-image
- name: Post approval review - name: Post approval review
env: env:
CI_GITEA_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
PR_NUMBER: ${{ github.event.number }} PR_NUMBER: ${{ github.event.number }}
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PYTHONPATH: src PYTHONPATH: src
@@ -196,7 +189,7 @@ jobs:
--body "Auto-approved: all CI checks passed (quality, pr-review, release-dry-run)." --body "Auto-approved: all CI checks passed (quality, pr-review, release-dry-run)."
- name: Squash merge with task ID - name: Squash merge with task ID
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }} VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
DEVX_VIKUNJA_PROJECT_ID: "8" DEVX_VIKUNJA_PROJECT_ID: "8"
PYTHONPATH: src PYTHONPATH: src
+28 -12
View File
@@ -47,6 +47,8 @@ jobs:
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Set up environment - name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image run: make setup-image
- name: Check if this is a release commit - name: Check if this is a release commit
id: check id: check
@@ -70,6 +72,8 @@ jobs:
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Set up environment - name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image run: make setup-image
- name: Validate latest commit message - name: Validate latest commit message
env: env:
@@ -95,10 +99,10 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.CI_GITEA_TOKEN }} token: ${{ secrets.CI_GITEA_API_TOKEN }}
- name: Set up environment - name: Set up environment
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image run: make setup-image
- name: Configure git - name: Configure git
run: | run: |
@@ -107,6 +111,7 @@ jobs:
- name: Run release - name: Run release
id: release-tag id: release-tag
env: env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
@@ -115,7 +120,7 @@ jobs:
- name: Notify on failure - name: Notify on failure
if: failure() if: failure()
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
@@ -142,10 +147,12 @@ jobs:
fetch-depth: 0 fetch-depth: 0
ref: ${{ needs.release.outputs.tag }} ref: ${{ needs.release.outputs.tag }}
- name: Set up environment - name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image EXTRAS=release run: make setup-image EXTRAS=release
- name: Build and publish release - name: Build and publish release
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
@@ -154,7 +161,7 @@ jobs:
- name: Notify on failure - name: Notify on failure
if: failure() if: failure()
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
@@ -183,10 +190,12 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up environment - name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image run: make setup-image
- name: Sync documentation to wiki - name: Sync documentation to wiki
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
@@ -194,7 +203,7 @@ jobs:
- name: Notify on failure - name: Notify on failure
if: failure() if: failure()
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
@@ -219,15 +228,18 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
ref: master ref: master
token: ${{ secrets.CI_GITEA_TOKEN }} token: ${{ secrets.CI_GITEA_API_TOKEN }}
- name: Fetch latest master - name: Fetch latest master
run: | run: |
git fetch origin master git fetch origin master
git reset --hard origin/master git reset --hard origin/master
- name: Set up environment - name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image run: make setup-image
- name: Generate and push badges - name: Generate and push badges
env: env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PRE_COMMIT_ALLOW_NO_CONFIG: "1" PRE_COMMIT_ALLOW_NO_CONFIG: "1"
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
@@ -235,7 +247,7 @@ jobs:
- name: Notify on failure - name: Notify on failure
if: failure() if: failure()
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
@@ -260,6 +272,8 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up environment - name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image run: make setup-image
- name: Update Vikunja task - name: Update Vikunja task
env: env:
@@ -272,7 +286,7 @@ jobs:
- name: Notify on failure - name: Notify on failure
if: failure() if: failure()
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
@@ -295,10 +309,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up environment - name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image run: make setup-image
- name: Ensure branch protection and labels - name: Ensure branch protection and labels
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
DEVX_REPO_NAME: devx DEVX_REPO_NAME: devx
DEVX_REPO_OWNER: oblachno-oss DEVX_REPO_OWNER: oblachno-oss
@@ -308,7 +324,7 @@ jobs:
- name: Notify on failure - name: Notify on failure
if: failure() if: failure()
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PYTHONPATH: src
run: | run: |
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
+4 -10
View File
@@ -73,18 +73,12 @@ repos:
pass_filenames: false pass_filenames: false
stages: [pre-commit] stages: [pre-commit]
- id: doc-coverage - id: docs-check
name: documentation coverage check name: documentation gate (coverage + stale refs + lint + version refs + prose)
entry: env PYTHONPATH=src .venv/bin/python -m devx.ci.doc_coverage --fail-on-missing entry: bash -c 'PYTHONPATH=src DEVX_DOC_COVERAGE_STRICT=1 DEVX_VALE_LEVEL=warning make devx-docs-check'
language: system
pass_filenames: false
stages: [pre-commit]
- id: lint-docs
name: documentation lint check
entry: env PYTHONPATH=src .venv/bin/python -m devx.ci.lint_docs --root .
language: system language: system
pass_filenames: false pass_filenames: false
always_run: true
stages: [pre-commit] stages: [pre-commit]
- id: pytest-cov - id: pytest-cov
+4 -1
View File
@@ -32,14 +32,17 @@ write-good.E-Prime = NO
write-good.So = NO write-good.So = NO
write-good.ThereIs = NO write-good.ThereIs = NO
write-good.TooWordy = NO write-good.TooWordy = NO
write-good.Passive = NO
# Vale defaults — spelling catches too many technical terms # Vale defaults — spelling catches too many technical terms
Vale.Terms = NO Vale.Terms = NO
Vale.Repetition = NO Vale.Repetition = NO
Vale.Spelling = NO Vale.Spelling = NO
# Readability — warnings only, technical docs are naturally complex # Readability — technical docs are naturally complex, downgrade to suggestions
Readability.FleschReadingEase = suggestion Readability.FleschReadingEase = suggestion
Readability.FleschKincaid = suggestion
Readability.AutomatedReadability = suggestion
Readability.ColemanLiau = suggestion Readability.ColemanLiau = suggestion
Readability.LIX = suggestion Readability.LIX = suggestion
Readability.GunningFog = suggestion Readability.GunningFog = suggestion
+1 -1
View File
@@ -3,4 +3,4 @@ message: "Unlabeled code block — add a language tag (```bash, ```yaml, etc.)"
level: warning level: warning
scope: raw scope: raw
raw: raw:
- '(?s)```\n(?!.*```)' - '(?ms)^\n```\n.*?^```\s*$'
+1 -1
View File
@@ -2,7 +2,7 @@ Based on [write-good](https://github.com/btford/write-good).
> Naive linter for English prose for developers who can't write good and wanna learn to do other stuff good too. > Naive linter for English prose for developers who can't write good and wanna learn to do other stuff good too.
```text ```
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2014 Brian Ford Copyright (c) 2014 Brian Ford
+4 -4
View File
@@ -226,14 +226,14 @@ After a PR is merged to master, the **post-merge workflow**
- Pushes both the commit and tag to master - Pushes both the commit and tag to master
3. **sync-wiki** — Syncs documentation to the Gitea wiki. Runs for ALL 3. **sync-wiki** — Syncs documentation to the Gitea wiki. Runs for ALL
non-release commits (not just when release succeeds), so docs-only non-release commits (not only when release succeeds), so docs-only
changes still update the wiki. changes still update the wiki.
4. **badges** — Generates and pushes quality badge SVGs to the `badges` branch. 4. **badges** — Generates and pushes quality badge SVGs to the `badges` branch.
Uses `if: always()` so it runs on every push, including release commits. Uses `if: always()` so it runs on every push, including release commits.
5. **vikunja** — Marks the corresponding Vikunja task as done. Runs for ALL 5. **vikunja** — Marks the corresponding Vikunja task as done. Runs for ALL
non-release commits (not just when release succeeds), so infrastructure-only non-release commits (not only when release succeeds), so infrastructure-only
changes still update the task tracker. changes still update the task tracker.
6. **publish** — Runs after release succeeds (needs: release). Builds and 6. **publish** — Runs after release succeeds (needs: release). Builds and
@@ -398,7 +398,7 @@ system loads `.env` automatically via `python-dotenv`.
### pyproject.toml [tool.devx] Configuration ### pyproject.toml [tool.devx] Configuration
In addition to `DEVX_` env vars, several devx tools read configuration from In addition to `DEVX_` env vars, many devx tools read configuration from
the `[tool.devx]` section in `pyproject.toml`. This allows per-project the `[tool.devx]` section in `pyproject.toml`. This allows per-project
customization without environment variables. customization without environment variables.
@@ -610,7 +610,7 @@ the user should not need to specify which profile to use.
2. **Background by default, foreground when blocking.** 2. **Background by default, foreground when blocking.**
3. **Provide full context in the prompt** — subagents don't inherit conversation history. 3. **Provide full context in the prompt** — subagents don't inherit conversation history.
4. **One subagent per concern.** Chain: investigate → fix in main session → review. 4. **One subagent per concern.** Chain: investigate → fix in main session → review.
5. **Don't delegate trivial work** (<30s, <50 lines of context). 5. **Don't delegate minor work** (<30s, <50 lines of context).
6. **Compact after subagent returns.** 6. **Compact after subagent returns.**
7. **Never skip delegation to save time** — it keeps main context small. 7. **Never skip delegation to save time** — it keeps main context small.
+54
View File
@@ -2,6 +2,60 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.38.0] - 2026-07-08
### Features
- Introduce role-based Gitea API token environment variables
## [0.37.0] - 2026-07-07
### Features
- Consolidate docs checks into devx-docs-check target
## [0.36.2] - 2026-07-07
### Bug Fixes
- GiteaClient.set_repo_variable uses PUT instead of PATCH
## [0.36.1] - 2026-07-07
### Bug Fixes
- Preserve .badges/ dir during git clean in push_badges
## [0.36.0] - 2026-07-07
### Features
- Add GiteaClient repo variable methods and parallelize pytest-cov
## [0.35.7] - 2026-07-06
### Bug Fixes
- Use Gitea wiki dash-marker filename convention
## [0.35.6] - 2026-07-06
### Bug Fixes
- Add delay before wiki verification to avoid race condition
## [0.35.5] - 2026-07-06
### Bug Fixes
- Embed token in wiki clone URL for push auth
## [0.35.4] - 2026-07-06
### Bug Fixes
- Configure git identity before commit in sync_wiki
## [0.35.3] - 2026-07-06 ## [0.35.3] - 2026-07-06
### Bug Fixes ### Bug Fixes
+10 -10
View File
@@ -16,12 +16,12 @@ quality badges.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE) [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/40fbd801952eefafe3876bef53a09d217267f810/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/fe1871151080d6188805f2a31916f7007d10849b/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/40fbd801952eefafe3876bef53a09d217267f810/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/fe1871151080d6188805f2a31916f7007d10849b/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/40fbd801952eefafe3876bef53a09d217267f810/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki) [![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/fe1871151080d6188805f2a31916f7007d10849b/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/40fbd801952eefafe3876bef53a09d217267f810/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/fe1871151080d6188805f2a31916f7007d10849b/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/40fbd801952eefafe3876bef53a09d217267f810/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases) [![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/fe1871151080d6188805f2a31916f7007d10849b/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/40fbd801952eefafe3876bef53a09d217267f810/python.svg)](https://www.python.org/downloads/) [![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/fe1871151080d6188805f2a31916f7007d10849b/python.svg)](https://www.python.org/downloads/)
## Why devx? ## Why devx?
@@ -87,7 +87,7 @@ extra index and list devx in your dependencies:
```toml ```toml
[project] [project]
dependencies = [ dependencies = [
"devx>=0.35.3", "devx>=0.38.0",
] ]
[tool.pip] [tool.pip]
@@ -101,8 +101,8 @@ pip install -e .
``` ```
> **Note:** If your project requires a specific devx version, pin it in > **Note:** If your project requires a specific devx version, pin it in
> `dependencies` (for example, `"devx==0.35.3"`) or use a version constraint > `dependencies` (for example, `"devx==0.38.0"`) or use a version constraint
> (for example, `"devx>=0.35.3,<0.36"`). > (for example, `"devx>=0.38.0,<0.39"`).
### Optional extras ### Optional extras
@@ -372,7 +372,7 @@ infrastructure = []
# Files that would default to user-facing but are actually infrastructure # Files that would default to user-facing but are actually infrastructure
infrastructure_overrides = [ infrastructure_overrides = [
"src/myproject/__init__.py", # only contains __version__ "src/myproject/__init__.py", # example only — only contains __version__
] ]
# Safety override for broad infrastructure patterns # Safety override for broad infrastructure patterns
+1 -1
View File
@@ -13,7 +13,7 @@ RUN pip install --no-cache-dir /tmp/devx[lint] \
&& rm -rf /tmp/devx && rm -rf /tmp/devx
# Install CI/CD binary tools # Install CI/CD binary tools
RUN python3 -m devx.tools.install_tools --tool actionlint \ RUN python3 -m devx.tools.install_tools --tool actionlint --tool vale \
&& python3 -m devx.tools.install_checkmake && python3 -m devx.tools.install_checkmake
# Install hadolint (Dockerfile linter) # Install hadolint (Dockerfile linter)
+8 -8
View File
@@ -12,12 +12,12 @@ project to be reusable across all oblachno-oss repositories.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE) [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/40fbd801952eefafe3876bef53a09d217267f810/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/fe1871151080d6188805f2a31916f7007d10849b/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/40fbd801952eefafe3876bef53a09d217267f810/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/fe1871151080d6188805f2a31916f7007d10849b/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/40fbd801952eefafe3876bef53a09d217267f810/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki) [![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/fe1871151080d6188805f2a31916f7007d10849b/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/40fbd801952eefafe3876bef53a09d217267f810/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions) [![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/fe1871151080d6188805f2a31916f7007d10849b/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/40fbd801952eefafe3876bef53a09d217267f810/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases) [![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/fe1871151080d6188805f2a31916f7007d10849b/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/40fbd801952eefafe3876bef53a09d217267f810/python.svg)](https://www.python.org/downloads/) [![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/fe1871151080d6188805f2a31916f7007d10849b/python.svg)](https://www.python.org/downloads/)
## Overview ## Overview
@@ -74,14 +74,14 @@ Add devx to your `pyproject.toml` dependencies and configure the registry:
```toml ```toml
[project] [project]
dependencies = [ dependencies = [
"devx>=0.35.3", "devx>=0.38.0",
] ]
[tool.pip] [tool.pip]
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple" extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
``` ```
Pin a specific version if needed: `"devx==0.35.3"` or `"devx>=0.35.3,<0.36"`. Pin a specific version if needed: `"devx==0.38.0"` or `"devx>=0.38.0,<0.39"`.
### Optional extras ### Optional extras
+4 -2
View File
@@ -122,7 +122,9 @@ exponential backoff (2s, 4s, 8s).
- Labels (list, create, add to issues) - Labels (list, create, add to issues)
- Issues (create, list) - Issues (create, list)
- Pull requests (get commits, merge, create review) - Pull requests (get commits, merge, create review)
- Releases (list) - Releases (list, create idempotent)
- Actions (list runs, list jobs, get job logs)
- Actions variables (get, set idempotent)
- Wiki pages (list, fetch, create, update, delete) - Wiki pages (list, fetch, create, update, delete)
**`VikunjaClient`** — Vikunja REST API wrapper: **`VikunjaClient`** — Vikunja REST API wrapper:
@@ -309,7 +311,7 @@ from `devx.api_clients`, `devx.config`, and `devx.gitea_cli`.
### `setup.py` ### `setup.py`
Project setup: installs Python dependencies (editable mode with extras), Project setup: installs Python dependencies (editable mode with extras),
Ansible Galaxy collections (if `ansible/requirements.yml` exists), pre-commit Ansible Galaxy collections (if `ansible/requirements.yml` exists in the target repo), pre-commit
hooks (pre-commit, commit-msg, pre-push), and configures the `tea` CLI login hooks (pre-commit, commit-msg, pre-push), and configures the `tea` CLI login
profile from `.env`. Supports `--extras` to specify dependency groups, profile from `.env`. Supports `--extras` to specify dependency groups,
`--no-pre-commit` to skip hook installation, and `--no-tea-login` to skip tea `--no-pre-commit` to skip hook installation, and `--no-tea-login` to skip tea
+1 -1
View File
@@ -251,7 +251,7 @@ badges using `python -m devx.ci.push_badges`:
1. **Fetch latest master** — `git fetch origin master && git reset --hard 1. **Fetch latest master** — `git fetch origin master && git reset --hard
origin/master` (ensures the version badge reflects the current state, origin/master` (ensures the version badge reflects the current state,
even if the release job just pushed a new version) even if the release job recently pushed a new version)
2. **Generate badges** — calls `devx.tools.generate_badges` which runs 2. **Generate badges** — calls `devx.tools.generate_badges` which runs
pytest-cov, doc-coverage, lint checks, and version extraction, then writes pytest-cov, doc-coverage, lint checks, and version extraction, then writes
SVG files: `coverage.svg`, `tests.svg`, `docs.svg`, `quality.svg`, SVG files: `coverage.svg`, `tests.svg`, `docs.svg`, `quality.svg`,
+1 -1
View File
@@ -405,7 +405,7 @@ devx tools install-tools --list # list status
### `devx tools setup` ### `devx tools setup`
Project setup: install Python dependencies (editable mode with extras), Project setup: install Python dependencies (editable mode with extras),
Ansible Galaxy collections (if `ansible/requirements.yml` exists), pre-commit Ansible Galaxy collections (if `ansible/requirements.yml` exists in the target repo), pre-commit
hooks (pre-commit, commit-msg, pre-push), and configure the tea CLI login hooks (pre-commit, commit-msg, pre-push), and configure the tea CLI login
profile from `.env`. profile from `.env`.
+2 -2
View File
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
```toml ```toml
[project] [project]
dependencies = [ dependencies = [
"devx>=0.35.3", "devx>=0.38.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"devx>=0.35.3", "devx>=0.38.0",
] ]
``` ```
+6
View File
@@ -121,6 +121,12 @@ vikunja_project_id = 8
repo_owner = "oblachno-oss" repo_owner = "oblachno-oss"
repo_name = "devx" repo_name = "devx"
[tool.devx.check_agent_docs]
skip_ref_prefixes = [
"src/myproject/",
"ansible/requirements.yml",
]
# 3. infrastructure (DEFAULT_INFRASTRUCTURE + project-specific patterns) # 3. infrastructure (DEFAULT_INFRASTRUCTURE + project-specific patterns)
# 4. Default: user-facing (safe) # 4. Default: user-facing (safe)
[tool.devx.classify] [tool.devx.classify]
+1 -1
View File
@@ -1,3 +1,3 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects.""" """devx — reusable development and CI/CD tools for oblachno-oss projects."""
__version__ = "0.35.3" __version__ = "0.38.0"
+30
View File
@@ -372,6 +372,36 @@ class GiteaClient:
r = self._request("GET", f"/actions/jobs/{job_id}/logs") r = self._request("GET", f"/actions/jobs/{job_id}/logs")
return r.text return r.text
# -- actions variables (repo-level) --
def get_repo_variable(self, name: str) -> str | None:
"""Read a Gitea Actions repository variable.
Returns the variable value, or ``None`` if the variable is not set.
Raises :class:`APIError` on other HTTP errors.
"""
try:
r = self._request("GET", f"/actions/variables/{name}")
return r.json().get("value")
except APIError as e:
if e.status == 404:
return None
raise
def set_repo_variable(self, name: str, value: str) -> None:
"""Create or update a Gitea Actions repository variable (idempotent).
Tries PUT first (update); if the variable doesn't exist (404),
creates it via POST. Gitea 1.26.x does not support PATCH for
action variables.
"""
try:
self._request("PUT", f"/actions/variables/{name}", json={"value": value})
except APIError as e:
if e.status != 404:
raise
self._request("POST", f"/actions/variables/{name}", json={"value": value})
class VikunjaClient: class VikunjaClient:
"""Low-level Vikunja REST API client with connection pooling.""" """Low-level Vikunja REST API client with connection pooling."""
+12 -8
View File
@@ -17,10 +17,9 @@ This allows the PR title to be a human-friendly Vikunja task title
while the squashed commit follows conventional commits. while the squashed commit follows conventional commits.
Usage: Usage:
CI_GITEA_TOKEN=<token> python3 -m devx.ci.auto_merge <branch> <pr_title> <repo> <pr_number> CI_GITEA_API_TOKEN=<token> VIKUNJA_TOKEN=<token> python3 -m devx.ci.auto_merge <branch> <pr_title> <repo> <pr_number>
""" """
import os
import re import re
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -40,6 +39,7 @@ from devx.config import (
) )
from devx.exceptions import APIError from devx.exceptions import APIError
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_ci_token, get_vikunja_token
# Strip leading task ID prefix (e.g. "DEVX-12: " or "OBL-INFRA-364: ") from commit subjects. # Strip leading task ID prefix (e.g. "DEVX-12: " or "OBL-INFRA-364: ") from commit subjects.
_TASK_ID_PREFIX_RE = re.compile(rf"^{TASK_PREFIX}-\d+:\s*") _TASK_ID_PREFIX_RE = re.compile(rf"^{TASK_PREFIX}-\d+:\s*")
@@ -115,9 +115,12 @@ def get_vikunja_task_title(task_id: str) -> str:
Raises ClickException if VIKUNJA_TOKEN is not set or the task is not found. Raises ClickException if VIKUNJA_TOKEN is not set or the task is not found.
""" """
token = os.environ.get("VIKUNJA_TOKEN", "") try:
if not token: token = get_vikunja_token()
raise click.ClickException(_("VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.")) except click.ClickException:
raise click.ClickException(
_("VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.")
) from None
client = VikunjaClient(VIKUNJA_API_URL, token) client = VikunjaClient(VIKUNJA_API_URL, token)
page = 1 page = 1
while True: while True:
@@ -197,9 +200,10 @@ def extract_conventional_msg(commits: list[dict[str, Any]]) -> str:
@click.argument("repo") @click.argument("repo")
@click.argument("pr_number") @click.argument("pr_number")
def main(branch: str, pr_title: str, repo: str, pr_number: str) -> None: def main(branch: str, pr_title: str, repo: str, pr_number: str) -> None:
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token: token = get_ci_token()
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) except click.ClickException:
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) from None
# Validate PR number is an integer # Validate PR number is an integer
try: try:
+17 -9
View File
@@ -15,7 +15,7 @@ Exit code 1 = NOT ready — fix issues before pushing.
Usage:: Usage::
# CI (with VIKUNJA_TOKEN and CI_GITEA_TOKEN): # CI (with VIKUNJA_TOKEN and CI_GITEA_API_TOKEN):
python3 -m devx.ci.check_auto_merge_ready \\ python3 -m devx.ci.check_auto_merge_ready \\
--branch "$HEAD_REF" \\ --branch "$HEAD_REF" \\
--pr-title "$PR_TITLE" \\ --pr-title "$PR_TITLE" \\
@@ -34,13 +34,12 @@ skipped (with a warning) — this allows local pre-push hooks to run
without CI secrets. In CI, the token is always set and the check is without CI secrets. In CI, the token is always set and the check is
mandatory. mandatory.
If ``CI_GITEA_TOKEN`` is not set and ``--pr-number`` is not provided, only If ``CI_GITEA_API_TOKEN`` is not set and ``--pr-number`` is not provided, only
branch-name and PR-title-format checks run (local mode). branch-name and PR-title-format checks run (local mode).
""" """
from __future__ import annotations from __future__ import annotations
import os
import subprocess # nosec B404 import subprocess # nosec B404
import click import click
@@ -55,6 +54,7 @@ from devx.config import (
) )
from devx.exceptions import APIError from devx.exceptions import APIError
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_ci_token, get_vikunja_token
load_dotenv() load_dotenv()
@@ -99,10 +99,13 @@ def is_branch_behind_master(branch: str) -> bool:
def get_pr_title_from_gitea(repo: str, pr_number: int) -> str | None: def get_pr_title_from_gitea(repo: str, pr_number: int) -> str | None:
"""Fetch the PR title from the Gitea API. """Fetch the PR title from the Gitea API.
Returns ``None`` if ``CI_GITEA_TOKEN`` is not set or the PR cannot be fetched. Returns ``None`` if no token is set or the PR cannot be fetched.
""" """
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token or "/" not in repo: token = get_ci_token()
except click.ClickException:
return None
if "/" not in repo:
return None return None
owner, repo_name = repo.split("/", 1) owner, repo_name = repo.split("/", 1)
client = GiteaClient(GITEA_API_URL, token, owner, repo_name) client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
@@ -120,8 +123,9 @@ def get_vikunja_title_optional(task_id: str) -> str | None:
raise when ``VIKUNJA_TOKEN`` is missing it returns ``None`` so the raise when ``VIKUNJA_TOKEN`` is missing it returns ``None`` so the
caller can skip the check in local mode. caller can skip the check in local mode.
""" """
token = os.environ.get("VIKUNJA_TOKEN", "") try:
if not token: token = get_vikunja_token()
except click.ClickException:
return None return None
client = VikunjaClient(VIKUNJA_API_URL, token) client = VikunjaClient(VIKUNJA_API_URL, token)
from devx.config import DEFAULT_PER_PAGE from devx.config import DEFAULT_PER_PAGE
@@ -221,7 +225,11 @@ def cli(
if not skip_vikunja: if not skip_vikunja:
vikunja_title = get_vikunja_title_optional(task_id) vikunja_title = get_vikunja_title_optional(task_id)
if vikunja_title is None: if vikunja_title is None:
token_set = bool(os.environ.get("VIKUNJA_TOKEN", "")) try:
get_vikunja_token()
token_set = True
except click.ClickException:
token_set = False
if token_set: if token_set:
errors.append( errors.append(
_( _(
+1
View File
@@ -185,6 +185,7 @@ def main(translations: tuple[Path, ...], source_dir: str | None) -> None:
# Try common locations # Try common locations
candidates = [ candidates = [
root / "src" / "devx" / "translations.json", root / "src" / "devx" / "translations.json",
root / "src" / "grm" / "translations.json",
] ]
# Also search for any translations.json in src/ # Also search for any translations.json in src/
for match in root.glob("src/*/translations.json"): for match in root.glob("src/*/translations.json"):
+6 -2
View File
@@ -31,6 +31,7 @@ import requests
from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_ci_token
DEFAULT_MAX_RUNNERS = 3 DEFAULT_MAX_RUNNERS = 3
@@ -96,7 +97,7 @@ def query_runners(api_url: str, token: str, owner: str, repo: str) -> int:
return total return total
def get_runner_count(api_url: str, token: str, owner: str, repo: str) -> int: def get_runner_count(api_url: str, token: str | None, owner: str, repo: str) -> int:
"""Determine the number of available runners. """Determine the number of available runners.
Tries the Gitea API first, then falls back to env vars, then default. Tries the Gitea API first, then falls back to env vars, then default.
@@ -152,7 +153,10 @@ def main(
output_indices: bool, output_indices: bool,
github_output: bool, github_output: bool,
) -> None: ) -> None:
token = os.environ.get("CI_GITEA_TOKEN", "") try:
token = get_ci_token()
except click.ClickException:
token = None
if owner is None: if owner is None:
owner = os.environ.get("DEVX_REPO_OWNER", "") or REPO_OWNER owner = os.environ.get("DEVX_REPO_OWNER", "") or REPO_OWNER
+63 -8
View File
@@ -21,6 +21,7 @@ from pathlib import Path
import click import click
from devx.config import _load_pyproject_devx
from devx.i18n import _ from devx.i18n import _
# Default to the current working directory (consuming repo's root) # Default to the current working directory (consuming repo's root)
@@ -71,12 +72,30 @@ def extract_cli_commands(source_dir: Path) -> list[str]:
# Matches @cli.command, @ci.command, @tools.command, @molecule.command # Matches @cli.command, @ci.command, @tools.command, @molecule.command
for match in re.finditer(r"@\w+\.command\b", content): for match in re.finditer(r"@\w+\.command\b", content):
# Check for explicit name="..." in the decorator arguments # Check for explicit name="..." in the decorator arguments
decorator_end = content.find(")", match.start()) # Use a balanced paren search to find the end of the decorator
# (handles nested parens like @cli.command(help=_("...")))
depth = 0
decorator_end = match.start()
for i in range(match.start(), len(content)):
if content[i] == "(":
depth += 1
elif content[i] == ")":
depth -= 1
if depth == 0:
decorator_end = i
break
decorator_text = content[match.start() : decorator_end + 1] decorator_text = content[match.start() : decorator_end + 1]
name_match = re.search(r'["\']([^"\']+)["\']', decorator_text) # Look for explicit name="..." parameter (not help=, not other kwargs)
name_match = re.search(r'\bname\s*=\s*["\']([^"\']+)["\']', decorator_text)
if name_match: if name_match:
commands.append(name_match.group(1)) commands.append(name_match.group(1))
continue continue
# Look for a positional string argument (e.g. @cli.command("my-cmd"))
# but skip if the only strings are in help= or other keyword args
positional_match = re.search(r'@\w+\.command\s*\(\s*["\']([^"\']+)["\']', decorator_text)
if positional_match:
commands.append(positional_match.group(1))
continue
# Find the next def statement after this decorator # Find the next def statement after this decorator
after = content[decorator_end:] after = content[decorator_end:]
def_match = re.search(r"def\s+(\w+)\s*\(", after) def_match = re.search(r"def\s+(\w+)\s*\(", after)
@@ -106,16 +125,38 @@ def check_module_documented(module: str, docs_content: str) -> bool:
@click.command() @click.command()
@click.option("--docs-dir", default=None, help="Path to the docs directory (default: ./docs).") @click.option("--docs-dir", default=None, help="Path to the docs directory (default: ./docs).")
@click.option("--source-dir", default=None, help="Path to the source directory (default: auto-detect from src/).") @click.option("--source-dir", default=None, help="Path to the source directory (default: auto-detect from src/).")
@click.option(
"--ci-scripts-dir",
default=None,
help=(
"Path to CI scripts directory (default: auto-detect from src/ci/). "
"Set to empty string to skip CI script checks."
),
)
@click.option( @click.option(
"--fail-on-missing", "--fail-on-missing",
is_flag=True, is_flag=True,
default=False, default=False,
help="Exit with non-zero status if any documentation is missing.", help="Exit with non-zero status if any documentation is missing.",
) )
def main(docs_dir: str | None, source_dir: str | None, fail_on_missing: bool) -> None: def main(docs_dir: str | None, source_dir: str | None, ci_scripts_dir: str | None, fail_on_missing: bool) -> None:
root = Path.cwd() root = Path.cwd()
docs_path = Path(docs_dir) if docs_dir else root / "docs" docs_path = Path(docs_dir) if docs_dir else root / "docs"
# Read [tool.devx.doc_coverage] config from pyproject.toml
devx_cfg = _load_pyproject_devx()
doc_cov_cfg_raw: object = devx_cfg.get("doc_coverage", {}) if isinstance(devx_cfg, dict) else {}
doc_cov_cfg: dict[str, object] = doc_cov_cfg_raw if isinstance(doc_cov_cfg_raw, dict) else {}
# CLI args override config; config overrides defaults
if ci_scripts_dir is None and "ci_scripts_dir" in doc_cov_cfg:
ci_scripts_dir = str(doc_cov_cfg["ci_scripts_dir"])
if docs_dir is None and "docs_dir" in doc_cov_cfg:
docs_dir = str(doc_cov_cfg["docs_dir"])
docs_path = Path(docs_dir)
if source_dir is None and "source_dir" in doc_cov_cfg:
source_dir = str(doc_cov_cfg["source_dir"])
# Auto-detect source directory # Auto-detect source directory
if source_dir: if source_dir:
src_path = Path(source_dir) src_path = Path(source_dir)
@@ -166,13 +207,27 @@ def main(docs_dir: str | None, source_dir: str | None, fail_on_missing: bool) ->
missing.append(f"Module: {module}") missing.append(f"Module: {module}")
# Check CI scripts in ci-cd-workflow.md # Check CI scripts in ci-cd-workflow.md
# Auto-detect CI scripts from ci/ subdirectory # Auto-detect CI scripts from ci/ subdirectory, or use explicit config
click.echo(_("\nChecking CI script documentation in ci-cd-workflow.md...")) click.echo(_("\nChecking CI script documentation in ci-cd-workflow.md..."))
ci_dir = src_path / "ci" if src_path.name != "ci" else src_path if ci_scripts_dir is not None:
if ci_dir.exists(): # Explicit config — empty string means skip CI script checks
detected_scripts = sorted(f.name for f in ci_dir.glob("*.py") if f.name != "__init__.py") if ci_scripts_dir == "":
detected_scripts = []
else:
ci_dir = Path(ci_scripts_dir)
if ci_dir.exists():
detected_scripts = sorted(f.name for f in ci_dir.glob("*.py") if f.name != "__init__.py")
else:
detected_scripts = []
else: else:
detected_scripts = REQUIRED_SCRIPTS # Auto-detect from src_path/ci/
ci_dir = src_path / "ci" if src_path.name != "ci" else src_path
if ci_dir.exists():
detected_scripts = sorted(f.name for f in ci_dir.glob("*.py") if f.name != "__init__.py")
else:
# No ci/ directory found — skip CI script checks rather than falling back
# to REQUIRED_SCRIPTS (which is devx-specific)
detected_scripts = []
total += len(detected_scripts) total += len(detected_scripts)
ci_docs = ci_cd_file.read_text() if ci_cd_file.exists() else "" ci_docs = ci_cd_file.read_text() if ci_cd_file.exists() else ""
for script in detected_scripts: for script in detected_scripts:
+6 -2
View File
@@ -17,7 +17,7 @@ Usage::
Environment variables: Environment variables:
GITEA_URL Base URL of the Gitea instance. GITEA_URL Base URL of the Gitea instance.
CI_GITEA_TOKEN API token with repo access. CI_GITEA_API_TOKEN API token with repo access (CI_GITEA_TOKEN accepted for legacy).
RUN_ID Workflow run ID (GITHUB_RUN_ID). RUN_ID Workflow run ID (GITHUB_RUN_ID).
JOB_NAME Base job name (GITHUB_JOB), e.g. "integration-tests". JOB_NAME Base job name (GITHUB_JOB), e.g. "integration-tests".
MATRIX_INDEX Current matrix index (runner-index). MATRIX_INDEX Current matrix index (runner-index).
@@ -41,6 +41,7 @@ from devx.i18n import _
from devx.molecule.molecule_ci_guard import ( from devx.molecule.molecule_ci_guard import (
poll_for_other_failures, poll_for_other_failures,
) )
from devx.tokens import get_ci_token
POLL_INTERVAL = 10 POLL_INTERVAL = 10
@@ -50,7 +51,10 @@ POLL_INTERVAL = 10
def cli(pytest_args: tuple[str, ...]) -> None: def cli(pytest_args: tuple[str, ...]) -> None:
"""Run pytest with cross-runner failure detection.""" """Run pytest with cross-runner failure detection."""
gitea_url = os.environ.get("GITEA_URL", "") gitea_url = os.environ.get("GITEA_URL", "")
token = os.environ.get("CI_GITEA_TOKEN", "") try:
token = get_ci_token()
except click.ClickException:
token = None
run_id = int(os.environ.get("RUN_ID", "0")) run_id = int(os.environ.get("RUN_ID", "0"))
job_name = os.environ.get("JOB_NAME", "integration-tests") job_name = os.environ.get("JOB_NAME", "integration-tests")
current_index = int(os.environ.get("MATRIX_INDEX", "0")) current_index = int(os.environ.get("MATRIX_INDEX", "0"))
+7 -6
View File
@@ -6,7 +6,7 @@ otherwise go unnoticed in the Actions tab. Uses the ``tea`` Gitea CLI
for issue creation tea must be installed and configured. for issue creation tea must be installed and configured.
Usage: Usage:
CI_GITEA_TOKEN=<token> python3 -m devx.ci.notify_failure \ CI_GITEA_API_TOKEN=<token> python3 -m devx.ci.notify_failure \
--repo <owner/repo> \ --repo <owner/repo> \
--run-id <run_id> \ --run-id <run_id> \
--workflow <workflow_name> \ --workflow <workflow_name> \
@@ -14,14 +14,13 @@ Usage:
--auto-login --auto-login
With ``--auto-login``, the script configures the tea CLI login profile With ``--auto-login``, the script configures the tea CLI login profile
from ``CI_GITEA_TOKEN`` and ``DEVX_GITEA_API_URL`` before creating the issue, from the CI API token and ``DEVX_GITEA_API_URL`` before creating the issue,
eliminating the need for a separate ``tea login add`` step in the workflow. eliminating the need for a separate ``tea login add`` step in the workflow.
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
import click import click
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType] from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
@@ -29,6 +28,7 @@ from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnk
from devx.config import GITEA_API_URL from devx.config import GITEA_API_URL
from devx.gitea_cli import TeaCLI, TeaCLIError, configure_tea_login from devx.gitea_cli import TeaCLI, TeaCLIError, configure_tea_login
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_ci_token
load_dotenv() load_dotenv()
@@ -74,9 +74,10 @@ def _create_issue_via_tea(repo: str, title: str, body: str) -> int:
help="Configure tea CLI login from CI_GITEA_TOKEN before creating the issue.", help="Configure tea CLI login from CI_GITEA_TOKEN before creating the issue.",
) )
def main(repo: str, run_id: str, workflow: str, commit: str, auto_login: bool) -> None: def main(repo: str, run_id: str, workflow: str, commit: str, auto_login: bool) -> None:
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token: get_ci_token()
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) except click.ClickException:
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) from None
if auto_login: if auto_login:
configure_tea_login() configure_tea_login()
+5 -4
View File
@@ -5,7 +5,6 @@ Usage:
VIKUNJA_TOKEN=<token> python3 -m devx.ci.post_merge <commit_msg> [--commit-sha <sha>] VIKUNJA_TOKEN=<token> python3 -m devx.ci.post_merge <commit_msg> [--commit-sha <sha>]
""" """
import os
import re import re
import subprocess # nosec B404 import subprocess # nosec B404
@@ -17,6 +16,7 @@ from devx.ci._shared import extract_task_id as _extract_task_id
from devx.config import DEFAULT_PER_PAGE, TASK_PREFIX, VIKUNJA_API_URL, VIKUNJA_PROJECT_ID from devx.config import DEFAULT_PER_PAGE, TASK_PREFIX, VIKUNJA_API_URL, VIKUNJA_PROJECT_ID
from devx.exceptions import APIError from devx.exceptions import APIError
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_vikunja_token
load_dotenv() load_dotenv()
@@ -127,9 +127,10 @@ def main(commit_msg: str | None, commit_sha: str, from_git: bool, git_sha: str)
commit_sha = _get_git_commit_sha() commit_sha = _get_git_commit_sha()
if not commit_msg: if not commit_msg:
raise click.ClickException("commit_msg argument is required (or use --from-git or --git-sha)") raise click.ClickException("commit_msg argument is required (or use --from-git or --git-sha)")
token = os.environ.get("VIKUNJA_TOKEN", "") try:
if not token: token = get_vikunja_token()
raise click.ClickException(_("ERROR: VIKUNJA_TOKEN is not set.")) except click.ClickException:
raise click.ClickException(_("ERROR: VIKUNJA_TOKEN is not set.")) from None
task_id = extract_task_id(commit_msg) task_id = extract_task_id(commit_msg)
if not task_id: if not task_id:
+6 -5
View File
@@ -17,12 +17,11 @@ Checks performed:
8. Commit conventions conventional commit format on branch commits 8. Commit conventions conventional commit format on branch commits
Usage: Usage:
CI_GITEA_TOKEN=<token> python3 -m devx.ci.pr_review <pr_number> <owner/repo> CI_GITEA_API_TOKEN=<token> [REVIEWER_GITEA_API_TOKEN=<token>] python3 -m devx.ci.pr_review <pr_number> <owner/repo>
""" """
from __future__ import annotations from __future__ import annotations
import os
import re import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any
@@ -34,6 +33,7 @@ from devx.api_clients import GiteaClient
from devx.config import GITEA_API_URL from devx.config import GITEA_API_URL
from devx.exceptions import APIError from devx.exceptions import APIError
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_ci_token, get_reviewer_token
load_dotenv() load_dotenv()
@@ -636,9 +636,10 @@ def main(
Without --event: runs automated checks and posts COMMENT/REQUEST_CHANGES. Without --event: runs automated checks and posts COMMENT/REQUEST_CHANGES.
With --event: posts a manual review (skips automated checks). With --event: posts a manual review (skips automated checks).
""" """
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token: token = get_reviewer_token() if (event and event.upper() == "APPROVE") else get_ci_token()
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) except click.ClickException:
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) from None
owner, repo_name = repo.split("/") owner, repo_name = repo.split("/")
client = GiteaClient(GITEA_API_URL, token, owner, repo_name) client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
+8 -6
View File
@@ -9,14 +9,14 @@ Publishing destinations (checked in order):
``DEVX_PYPI_REGISTRY_URL`` env var is set, or ``GITEA_API_URL`` ``DEVX_PYPI_REGISTRY_URL`` env var is set, or ``GITEA_API_URL``
is converted to a packages URL). Uses ``twine upload is converted to a packages URL). Uses ``twine upload
--repository-url <url> -u <token> -p <token>`` with the --repository-url <url> -u <token> -p <token>`` with the
``CI_GITEA_TOKEN`` as both username and password. CI API token as both username and password.
2. **Standard PyPI** if ``PYPI_TOKEN`` is set. Uses the standard 2. **Standard PyPI** if ``PYPI_TOKEN`` is set. Uses the standard
``twine upload -u __token__ -p <token>`` flow. ``twine upload -u __token__ -p <token>`` flow.
3. **Skip** if neither is configured, only the Gitea release is created. 3. **Skip** if neither is configured, only the Gitea release is created.
Usage: Usage:
CI_GITEA_TOKEN=<token> [PYPI_TOKEN=<token>] python3 -m devx.ci.publish <tag> <repo> CI_GITEA_API_TOKEN=<token> [PYPI_TOKEN=<token>] python3 -m devx.ci.publish <tag> <repo>
CI_GITEA_TOKEN=<token> python3 -m devx.ci.publish <tag> <repo> --registry-url https://git.example.com/api/packages/owner/pypi CI_GITEA_API_TOKEN=<token> python3 -m devx.ci.publish <tag> <repo> --registry-url https://git.example.com/api/packages/owner/pypi
""" """
import os import os
@@ -31,6 +31,7 @@ from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnk
from devx.config import GITEA_API_URL, REPO_OWNER from devx.config import GITEA_API_URL, REPO_OWNER
from devx.gitea_cli import TeaCLI, TeaCLIError, configure_tea_login from devx.gitea_cli import TeaCLI, TeaCLIError, configure_tea_login
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_ci_token
load_dotenv() load_dotenv()
@@ -253,9 +254,10 @@ def main(
if not tag: if not tag:
raise click.ClickException(_("Tag is required (or use --from-tag).")) raise click.ClickException(_("Tag is required (or use --from-tag)."))
gitea_token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not gitea_token: gitea_token = get_ci_token()
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) except click.ClickException:
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) from None
pypi_token = os.environ.get("PYPI_TOKEN", "") pypi_token = os.environ.get("PYPI_TOKEN", "")
+2 -2
View File
@@ -93,8 +93,8 @@ def push_to_badges_branch(badges_dir: str) -> str:
_run(["git", "config", "user.email", "actions@oblachno.fyi"]) # nosec B607 _run(["git", "config", "user.email", "actions@oblachno.fyi"]) # nosec B607
_run(["git", "checkout", "--orphan", "badges"]) # nosec B607 _run(["git", "checkout", "--orphan", "badges"]) # nosec B607
_run(["git", "rm", "-rf", "."]) # nosec B607 _run(["git", "rm", "-rf", "."]) # nosec B607
# Remove untracked files/dirs left behind (e.g. .badges/ from generate_badges) # Remove untracked files/dirs left behind, but preserve .badges/ for copy below
_run(["git", "clean", "-fdx", "-e", ".git"]) # nosec B607 _run(["git", "clean", "-fdx", "-e", ".git", "-e", badges_dir]) # nosec B607
# Copy badge files to root # Copy badge files to root
for svg in Path(badges_dir).glob("*.svg"): for svg in Path(badges_dir).glob("*.svg"):
+1 -1
View File
@@ -29,7 +29,7 @@ version. This prevents duplicate release commits (a common issue when CI
checkouts don't fetch tags) and ensures tag/version/commit alignment. checkouts don't fetch tags) and ensures tag/version/commit alignment.
Usage: Usage:
CI_GITEA_TOKEN=<token> python3 -m devx.ci.release [--dry-run] [--skip-tests] CI_GITEA_API_TOKEN=<token> python3 -m devx.ci.release [--dry-run] [--skip-tests]
python3 -m devx.ci.release --verify # Check tag/version/release alignment python3 -m devx.ci.release --verify # Check tag/version/release alignment
""" """
+53 -15
View File
@@ -21,7 +21,7 @@ Link transformations:
- Anchor-only links (``#section``) are preserved - Anchor-only links (``#section``) are preserved
Usage: Usage:
CI_GITEA_TOKEN=<token> python3 -m devx.ci.sync_wiki [--dry-run] [--repo owner/repo] CI_GITEA_API_TOKEN=<token> python3 -m devx.ci.sync_wiki [--dry-run] [--repo owner/repo]
""" """
from __future__ import annotations from __future__ import annotations
@@ -31,13 +31,16 @@ import os
import re import re
import subprocess # nosec B404 import subprocess # nosec B404
import tempfile import tempfile
import time
from pathlib import Path from pathlib import Path
from urllib.parse import quote, urlparse
import click import click
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType] from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_ci_token
load_dotenv() load_dotenv()
@@ -48,6 +51,23 @@ MAPPING_FILE = DOCS_DIR / "mapping.json"
_LINK_RE = re.compile(r"\[([^\]]*)\]\(([^)]+)\)") _LINK_RE = re.compile(r"\[([^\]]*)\]\(([^)]+)\)")
def wiki_filename(page_title: str) -> str:
"""Convert a wiki page title to its Gitea wiki filename.
Gitea uses a "dash marker" (``.-``) suffix to distinguish literal dashes
from space-to-dash conversions. See Gitea's ``services/wiki/wiki_path.go``.
- "Architecture" (no dashes) ``Architecture.md``
- "Getting-Started" (has dashes) ``Getting-Started.-.md``
- "Home" (no dashes) ``Home.md``
"""
name = page_title.replace(" ", "-")
if "-" in name:
name += ".-"
name += ".md"
return quote(name, safe="")
def load_mapping() -> dict[str, str]: def load_mapping() -> dict[str, str]:
"""Load the file-to-wiki-page mapping from mapping.json.""" """Load the file-to-wiki-page mapping from mapping.json."""
with open(MAPPING_FILE, encoding="utf-8") as f: with open(MAPPING_FILE, encoding="utf-8") as f:
@@ -102,7 +122,10 @@ def get_wiki_clone_url(owner: str, repo: str, token: str) -> str:
# Gitea wiki repos are at {clone_url}.wiki.git # Gitea wiki repos are at {clone_url}.wiki.git
# Extract base URL from API URL # Extract base URL from API URL
base = GITEA_API_URL.rsplit("/api/v1", 1)[0] base = GITEA_API_URL.rsplit("/api/v1", 1)[0]
return f"{base}/{owner}/{repo}.wiki.git" # Embed token in URL for both clone and push auth
# Format: https://token@host/owner/repo.wiki.git
parsed = urlparse(base)
return f"{parsed.scheme}://{token}@{parsed.hostname}/{owner}/{repo}.wiki.git"
def clone_wiki(wiki_url: str, dest: Path) -> bool: def clone_wiki(wiki_url: str, dest: Path) -> bool:
@@ -166,16 +189,15 @@ def sync_files(
# Transform links # Transform links
transformed = transform_links(content) transformed = transform_links(content)
# Wiki filename: use the page title with spaces → underscores # Wiki filename: Gitea uses a dash-marker convention for titles with dashes
# Gitea wiki uses the page title as filename (spaces become dashes) fname = wiki_filename(page_title)
wiki_filename = page_title.replace(" ", "-") + ".md" expected_files.add(fname)
expected_files.add(wiki_filename)
if not dry_run: if not dry_run:
dest = wiki_dir / wiki_filename dest = wiki_dir / fname
dest.write_text(transformed, encoding="utf-8") dest.write_text(transformed, encoding="utf-8")
synced += 1 synced += 1
click.echo(_(" Synced: {title}{file}", title=page_title, file=wiki_filename)) click.echo(_(" Synced: {title}{file}", title=page_title, file=fname))
# Prune stale pages (in wiki but not in mapping) # Prune stale pages (in wiki but not in mapping)
pruned = 0 pruned = 0
@@ -208,7 +230,19 @@ def commit_and_push(wiki_dir: Path, wiki_url: str, dry_run: bool) -> bool:
click.echo(_("No changes to sync — wiki is up to date.")) click.echo(_("No changes to sync — wiki is up to date."))
return False return False
# Commit # Commit — ensure git identity is configured (CI environments may lack it)
subprocess.run( # nosec
["git", "config", "user.email", "devin-ai-integration[bot]@users.noreply.github.com"],
cwd=wiki_dir,
capture_output=True,
check=True,
)
subprocess.run( # nosec
["git", "config", "user.name", "Devin CI"],
cwd=wiki_dir,
capture_output=True,
check=True,
)
subprocess.run( # nosec subprocess.run( # nosec
["git", "commit", "-m", "Sync wiki from docs/ [skip ci]"], ["git", "commit", "-m", "Sync wiki from docs/ [skip ci]"],
cwd=wiki_dir, cwd=wiki_dir,
@@ -218,7 +252,7 @@ def commit_and_push(wiki_dir: Path, wiki_url: str, dry_run: bool) -> bool:
# Push # Push
result = subprocess.run( # nosec result = subprocess.run( # nosec
["git", "push", wiki_url, "HEAD:master"], ["git", "push", "--force", wiki_url, "HEAD:master"],
cwd=wiki_dir, cwd=wiki_dir,
capture_output=True, capture_output=True,
text=True, text=True,
@@ -241,9 +275,10 @@ def commit_and_push(wiki_dir: Path, wiki_url: str, dry_run: bool) -> bool:
) )
def main(dry_run: bool, repo: str | None, verify: bool) -> None: def main(dry_run: bool, repo: str | None, verify: bool) -> None:
"""Sync documentation to the Gitea wiki via Git.""" """Sync documentation to the Gitea wiki via Git."""
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token: token = get_ci_token()
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) except click.ClickException:
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) from None
if repo is None: if repo is None:
owner = os.environ.get("DEVX_REPO_OWNER", "") or REPO_OWNER owner = os.environ.get("DEVX_REPO_OWNER", "") or REPO_OWNER
@@ -293,6 +328,9 @@ def main(dry_run: bool, repo: str | None, verify: bool) -> None:
# Verification # Verification
if verify and not dry_run: if verify and not dry_run:
if pushed:
click.echo(_("Waiting 5s for Gitea to process pushed commits..."))
time.sleep(5)
click.echo(_("\nVerifying wiki pages...")) click.echo(_("\nVerifying wiki pages..."))
# Re-clone to verify # Re-clone to verify
verify_dir = Path(tmpdir) / "verify" verify_dir = Path(tmpdir) / "verify"
@@ -301,8 +339,8 @@ def main(dry_run: bool, repo: str | None, verify: bool) -> None:
raise click.ClickException(_("Wiki verification failed — could not clone wiki")) raise click.ClickException(_("Wiki verification failed — could not clone wiki"))
failures = 0 failures = 0
for _file_path, page_title in sorted(mapping.items()): for _file_path, page_title in sorted(mapping.items()):
wiki_filename = page_title.replace(" ", "-") + ".md" fname = wiki_filename(page_title)
if (verify_dir / wiki_filename).exists(): if (verify_dir / fname).exists():
click.echo(_(" OK: {title}", title=page_title)) click.echo(_(" OK: {title}", title=page_title))
else: else:
click.echo(_(" FAIL: {title} — page not found in wiki!", title=page_title)) click.echo(_(" FAIL: {title} — page not found in wiki!", title=page_title))
+6 -5
View File
@@ -40,7 +40,6 @@ Usage::
from __future__ import annotations from __future__ import annotations
import json import json
import os
import shutil import shutil
import subprocess # nosec B404 import subprocess # nosec B404
from typing import Any from typing import Any
@@ -49,6 +48,7 @@ import click
from devx.config import GITEA_API_URL from devx.config import GITEA_API_URL
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_ci_token
class TeaCLIError(Exception): class TeaCLIError(Exception):
@@ -56,10 +56,10 @@ class TeaCLIError(Exception):
def configure_tea_login(login_name: str = "devx") -> None: def configure_tea_login(login_name: str = "devx") -> None:
"""Configure tea CLI login from CI_GITEA_TOKEN and DEVX_GITEA_API_URL. """Configure tea CLI login from CI_GITEA_API_TOKEN and DEVX_GITEA_API_URL.
Idempotent: if a login with the same name already exists, it is not re-added. Idempotent: if a login with the same name already exists, it is not re-added.
Skips silently if tea is not installed or CI_GITEA_TOKEN is not set. Skips silently if tea is not installed or no token is set.
Used by CI scripts (publish, notify_failure) that need tea login but Used by CI scripts (publish, notify_failure) that need tea login but
run in containerized environments where ``make setup`` was not called. run in containerized environments where ``make setup`` was not called.
@@ -69,8 +69,9 @@ def configure_tea_login(login_name: str = "devx") -> None:
click.echo(_("tea not installed — skipping login configuration.")) click.echo(_("tea not installed — skipping login configuration."))
return return
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token: token = get_ci_token()
except click.ClickException:
click.echo(_("CI_GITEA_TOKEN not set — skipping login configuration.")) click.echo(_("CI_GITEA_TOKEN not set — skipping login configuration."))
return return
+56 -12
View File
@@ -39,6 +39,9 @@
# DEVX_GITEA_PYPI_ORG — Gitea PyPI org (default: oblachno-oss) # DEVX_GITEA_PYPI_ORG — Gitea PyPI org (default: oblachno-oss)
# DEVX_ACTIONLINT_CFG — actionlint config file (default: .gitea/actionlint.yaml) # DEVX_ACTIONLINT_CFG — actionlint config file (default: .gitea/actionlint.yaml)
# DEVX_WORKFLOW_DIR — workflow directory (default: .gitea/workflows) # DEVX_WORKFLOW_DIR — workflow directory (default: .gitea/workflows)
# DEVX_DOC_COVERAGE_STRICT — fail on missing docs (default: 0)
# DEVX_DOC_VERSIONS_PKG — package name for version ref checks (default: auto)
# DEVX_VALE_LEVEL — vale alert threshold (default: warning)
DEVX_PYTHON ?= python3 DEVX_PYTHON ?= python3
DEVX_PR_BASE ?= master DEVX_PR_BASE ?= master
@@ -52,15 +55,18 @@ DEVX_GITEA_PYPI_ORG ?= oblachno-oss
DEVX_ACTIONLINT_CFG ?= .gitea/actionlint.yaml DEVX_ACTIONLINT_CFG ?= .gitea/actionlint.yaml
DEVX_WORKFLOW_DIR ?= .gitea/workflows DEVX_WORKFLOW_DIR ?= .gitea/workflows
DEVX_DOCKERFILE_PATHS ?= docker DEVX_DOCKERFILE_PATHS ?= docker
DEVX_VALE_LEVEL ?= warning
# PIP_INSTALL — helper to run pip with Gitea private PyPI registry configured. # PIP_INSTALL — helper to run pip with Gitea private PyPI registry configured.
# Usage: $(DEVX_PIP_INSTALL) install -e '.[ci,lint]' # Usage: $(DEVX_PIP_INSTALL) install -e '.[ci,lint]'
# CI_GITEA_USERNAME can be set in .env, as an env var, or as a Make variable. # CI_GITEA_USERNAME can be set in .env, as an env var, or as a Make variable.
# Projects can alias: PIP_INSTALL = $(DEVX_PIP_INSTALL) # Projects can alias: PIP_INSTALL = $(DEVX_PIP_INSTALL)
DEVX_PIP_INSTALL := if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \ DEVX_PIP_INSTALL := if [ -z "$$CI_GITEA_API_TOKEN" ] && [ -z "$$DEVELOPER_GITEA_API_TOKEN" ] && [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
CI_GITEA_TOKEN="$$CI_GITEA_TOKEN"; \ _TOKEN="$$CI_GITEA_API_TOKEN"; \
[ -z "$$_TOKEN" ] && _TOKEN="$$DEVELOPER_GITEA_API_TOKEN"; \
[ -z "$$_TOKEN" ] && _TOKEN="$$CI_GITEA_TOKEN"; \
_PYPI_USER="$${CI_GITEA_USERNAME:-emil}"; \ _PYPI_USER="$${CI_GITEA_USERNAME:-emil}"; \
if [ -n "$$CI_GITEA_TOKEN" ] && [ -n "$$_PYPI_USER" ]; then export PIP_EXTRA_INDEX_URL="https://$$_PYPI_USER:$$CI_GITEA_TOKEN@$(DEVX_GITEA_PYPI_HOST)/api/packages/$(DEVX_GITEA_PYPI_ORG)/pypi/simple/"; fi; \ if [ -n "$$_TOKEN" ] && [ -n "$$_PYPI_USER" ]; then export PIP_EXTRA_INDEX_URL="https://$$_PYPI_USER:$$_TOKEN@$(DEVX_GITEA_PYPI_HOST)/api/packages/$(DEVX_GITEA_PYPI_ORG)/pypi/simple/"; fi; \
$(DEVX_BIN)/pip $(DEVX_BIN)/pip
# ── Virtual environment management ──────────────────────────────────────────── # ── Virtual environment management ────────────────────────────────────────────
@@ -192,12 +198,14 @@ devx-pr-rebase:
# ── Environment setup ───────────────────────────────────────────────────────── # ── Environment setup ─────────────────────────────────────────────────────────
# Configure Gitea private PyPI registry so pip can find devx and other # Configure Gitea private PyPI registry so pip can find devx and other
# private packages. In CI, CI_GITEA_TOKEN is set as a secret. Locally, it's in .env. # private packages. In CI, CI_GITEA_API_TOKEN is set as a secret. Locally, DEVELOPER_GITEA_API_TOKEN or CI_GITEA_TOKEN can be used.
devx-configure-gitea-pypi: devx-configure-gitea-pypi:
@if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \ @if [ -z "$$CI_GITEA_API_TOKEN" ] && [ -z "$$DEVELOPER_GITEA_API_TOKEN" ] && [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
CI_GITEA_TOKEN="$$CI_GITEA_TOKEN"; \ _TOKEN="$$CI_GITEA_API_TOKEN"; \
if [ -z "$$CI_GITEA_TOKEN" ]; then echo "[configure-gitea-pypi] CI_GITEA_TOKEN not set — skipping (devx must be on public PyPI)"; exit 0; fi; \ [ -z "$$_TOKEN" ] && _TOKEN="$$DEVELOPER_GITEA_API_TOKEN"; \
echo "[configure-gitea-pypi] Gitea PyPI registry configured (CI_GITEA_TOKEN present)." [ -z "$$_TOKEN" ] && _TOKEN="$$CI_GITEA_TOKEN"; \
if [ -z "$$_TOKEN" ]; then echo "[configure-gitea-pypi] Gitea API token not set — skipping (devx must be on public PyPI)"; exit 0; fi; \
echo "[configure-gitea-pypi] Gitea PyPI registry configured (token present)."
# Create .env from .env.example if it doesn't exist # Create .env from .env.example if it doesn't exist
devx-env: devx-env:
@@ -264,7 +272,7 @@ devx-workflow-check: devx-workflow-lint devx-workflow-dryrun
# Notify on CI failure — creates a Gitea issue via devx.ci.notify_failure. # Notify on CI failure — creates a Gitea issue via devx.ci.notify_failure.
# Usage: make devx-notify-failure WORKFLOW=post-merge/release # Usage: make devx-notify-failure WORKFLOW=post-merge/release
# Requires: CI_GITEA_TOKEN, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_SHA # Requires: CI_GITEA_API_TOKEN, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_SHA
devx-notify-failure: devx-notify-failure:
@. $(DEVX_VENV)/bin/activate 2>/dev/null || true; \ @. $(DEVX_VENV)/bin/activate 2>/dev/null || true; \
export PATH="$(HOME)/.local/bin:$$PATH"; \ export PATH="$(HOME)/.local/bin:$$PATH"; \
@@ -304,7 +312,7 @@ devx-test-unit:
@$(DEVX_BIN)/pytest $(DEVX_TEST_PATHS) -q --no-cov @$(DEVX_BIN)/pytest $(DEVX_TEST_PATHS) -q --no-cov
devx-pytest-cov: devx-pytest-cov:
@$(DEVX_BIN)/pytest $(DEVX_TEST_PATHS) -v --cov=$(DEVX_COV_PKG) --cov-report=term-missing --cov-fail-under=100 @$(DEVX_BIN)/pytest $(DEVX_TEST_PATHS) -n auto --cov=$(DEVX_COV_PKG) --cov-report=term-missing --cov-fail-under=100
# ── Quality checks ──────────────────────────────────────────────────────────── # ── Quality checks ────────────────────────────────────────────────────────────
@@ -328,10 +336,46 @@ devx-check-docs:
devx-check-doc-versions: devx-check-doc-versions:
@$(DEVX_PYTHON) -m devx.tools.check_doc_versions --root . @$(DEVX_PYTHON) -m devx.tools.check_doc_versions --root .
# Run Vale prose linter on docs and README # Documentation coverage — checks that all modules/scripts/CLI commands
# are documented. Fails if any are missing when DEVX_DOC_COVERAGE_STRICT=1.
devx-doc-coverage:
@$(DEVX_PYTHON) -m devx.ci.doc_coverage $(if $(filter 1,$(DEVX_DOC_COVERAGE_STRICT)),--fail-on-missing)
# All-in-one documentation gate: coverage + stale refs + structural lint +
# version refs + prose lint. Use in CI and pre-commit as a single step
# instead of 5+ separate steps.
#
# Configuration via environment variables (set in Makefile before include
# or in CI env):
# DEVX_DOC_COVERAGE_STRICT=1 — fail on missing docs (recommended)
# DEVX_DOC_VERSIONS_PKG=<pkg> — enable version ref checks for a named package
# DEVX_VALE_LEVEL=<level> — vale alert threshold (error, warning, suggestion)
# default: warning (catches weasel words, unlabeled
# code blocks, etc. — not just spelling errors)
devx-docs-check: devx-doc-coverage devx-check-docs
@$(DEVX_PYTHON) -m devx.ci.lint_docs --root .
@if [ -n "$(DEVX_DOC_VERSIONS_PKG)" ]; then \
$(DEVX_PYTHON) -m devx.tools.check_doc_versions --root . --package $(DEVX_DOC_VERSIONS_PKG); \
elif $(DEVX_PYTHON) -c "import importlib.util,sys; sys.exit(0 if any(importlib.util.find_spec(p) for p in ['devx','grm','oblachno_infra']) else 1)" 2>/dev/null; then \
$(DEVX_PYTHON) -m devx.tools.check_doc_versions --root . 2>/dev/null || true; \
fi
@export PATH="$$HOME/.local/bin:$$PATH" && \
if ! command -v vale >/dev/null 2>&1; then \
echo "[devx-docs-check] vale not installed — skipping prose lint (install with 'make install-tools')"; \
else \
vale sync >/dev/null 2>&1 || true; \
vale --minAlertLevel=$(DEVX_VALE_LEVEL) docs/ AGENTS.md README.md; \
fi
# Run Vale prose linter on docs and README (skips if vale not installed)
# Legacy target — use devx-docs-check for the full documentation gate.
devx-vale: devx-vale:
@export PATH="$$HOME/.local/bin:$$PATH" && \ @export PATH="$$HOME/.local/bin:$$PATH" && \
vale --minAlertLevel=error docs/ AGENTS.md README.md if ! command -v vale >/dev/null 2>&1; then \
echo "[devx-vale] vale not installed — skipping (install with 'make install-tools')"; \
else \
vale --minAlertLevel=error docs/ AGENTS.md README.md; \
fi
# Verify test suite timing # Verify test suite timing
devx-check-test-speed: devx-check-test-speed:
+6 -2
View File
@@ -30,6 +30,7 @@ import click
import requests import requests
from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER
from devx.tokens import get_ci_token
DEFAULT_MAX_RUNNERS = 3 DEFAULT_MAX_RUNNERS = 3
@@ -86,7 +87,7 @@ def query_runners(api_url: str, token: str, owner: str, repo: str) -> int:
return total return total
def get_runner_count(api_url: str, token: str, owner: str, repo: str) -> int: def get_runner_count(api_url: str, token: str | None, owner: str, repo: str) -> int:
"""Determine the number of available runners. """Determine the number of available runners.
Tries the Gitea API first, then falls back to env vars, then default. Tries the Gitea API first, then falls back to env vars, then default.
@@ -142,7 +143,10 @@ def main(
output_indices: bool, output_indices: bool,
github_output: bool, github_output: bool,
) -> None: ) -> None:
token = os.environ.get("CI_GITEA_TOKEN", "") try:
token = get_ci_token()
except click.ClickException:
token = None
if owner is None: if owner is None:
owner = os.environ.get("DEVX_REPO_OWNER", "") or REPO_OWNER owner = os.environ.get("DEVX_REPO_OWNER", "") or REPO_OWNER
+6 -2
View File
@@ -22,7 +22,7 @@ Usage::
Environment variables: Environment variables:
GITEA_URL Base URL of the Gitea instance. GITEA_URL Base URL of the Gitea instance.
CI_GITEA_TOKEN API token with repo access. CI_GITEA_API_TOKEN API token with repo access (CI_GITEA_TOKEN accepted for legacy).
RUN_ID Workflow run ID (GITHUB_RUN_ID). RUN_ID Workflow run ID (GITHUB_RUN_ID).
JOB_NAME Base job name (GITHUB_JOB), e.g. "molecule-tests". JOB_NAME Base job name (GITHUB_JOB), e.g. "molecule-tests".
MATRIX_INDEX Current matrix index (runner-index). MATRIX_INDEX Current matrix index (runner-index).
@@ -45,6 +45,7 @@ import requests
from devx.config import REPO_NAME, REPO_OWNER from devx.config import REPO_NAME, REPO_OWNER
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_ci_token
POLL_INTERVAL = 10 POLL_INTERVAL = 10
@@ -164,7 +165,10 @@ def resolve_role_dir(role: str, roles_root: Path | None, repo_root: Path) -> Pat
def cli(pairs: tuple[str, ...], roles_root: Path | None) -> None: def cli(pairs: tuple[str, ...], roles_root: Path | None) -> None:
"""Run molecule pairs sequentially, stop if another CI runner fails.""" """Run molecule pairs sequentially, stop if another CI runner fails."""
gitea_url = os.environ.get("GITEA_URL", "") gitea_url = os.environ.get("GITEA_URL", "")
token = os.environ.get("CI_GITEA_TOKEN", "") try:
token = get_ci_token()
except click.ClickException:
token = None
run_id = int(os.environ.get("RUN_ID", "0")) run_id = int(os.environ.get("RUN_ID", "0"))
job_name = os.environ.get("JOB_NAME", "molecule-tests") job_name = os.environ.get("JOB_NAME", "molecule-tests")
current_index = int(os.environ.get("MATRIX_INDEX", "0")) current_index = int(os.environ.get("MATRIX_INDEX", "0"))
+76
View File
@@ -0,0 +1,76 @@
"""Token resolution helpers for devx tools.
Centralizes Gitea/Vikunja token discovery with role-based environment
variable names and backwards compatibility with the legacy
``CI_GITEA_TOKEN`` / ``REVIEW_GITEA_TOKEN`` naming convention.
Roles:
- ``CI_GITEA_API_TOKEN``: CI workflows (read actions, post status, merge, etc.)
- ``REVIEWER_GITEA_API_TOKEN``: PR approval reviews (must be a different user
from the PR author for Gitea to accept the review as an approval)
- ``DEVELOPER_GITEA_API_TOKEN``: local development tools (create-task,
create-pr, setup, etc.)
Fallbacks:
- New role names are checked first.
- Legacy names (``CI_GITEA_TOKEN``, ``REVIEW_GITEA_TOKEN``) are accepted for
backwards compatibility.
- If no role-specific token is set, the generic CI tokens are tried last.
"""
from __future__ import annotations
import os
import click
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"]
def get_token(*names: str) -> str:
"""Return the first non-empty value from the listed environment variables.
Raises a ``click.ClickException`` if none of the listed variables are set.
"""
for name in names:
token = os.environ.get(name, "").strip()
if token:
return token
raise click.ClickException(
_(
"Gitea API token not set. Set one of: {names}",
names=", ".join(names),
)
)
def get_ci_token() -> str:
"""Resolve the CI Gitea API token."""
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)
def get_vikunja_token() -> str:
"""Resolve the Vikunja API token."""
return get_token(*VIKUNJA_TOKEN_NAMES)
+5 -2
View File
@@ -8,6 +8,8 @@ import subprocess # nosec B404
import click import click
from devx.tokens import get_developer_token
def arch_string() -> str: def arch_string() -> str:
"""Return the architecture string used by release assets. """Return the architecture string used by release assets.
@@ -45,8 +47,9 @@ def detect_pr_number() -> int | None:
if branch == "HEAD": if branch == "HEAD":
return None return None
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token: token = get_developer_token()
except click.ClickException:
return None return None
owner = os.environ.get("DEVX_REPO_OWNER", "") owner = os.environ.get("DEVX_REPO_OWNER", "")
+8 -4
View File
@@ -34,8 +34,8 @@ The manifest file is a JSON list of dicts, each with:
- ``context``: build context directory (optional, defaults to repo root) - ``context``: build context directory (optional, defaults to repo root)
- ``tags``: list of tags (optional, defaults to ``["latest"]``) - ``tags``: list of tags (optional, defaults to ``["latest"]``)
Registry authentication uses ``CI_GITEA_TOKEN`` and ``CI_GITEA_USERNAME`` Registry authentication uses ``CI_GITEA_API_TOKEN`` (or legacy ``CI_GITEA_TOKEN``)
environment variables, matching the existing CI workflow patterns. and ``CI_GITEA_USERNAME`` environment variables, matching the existing CI workflow patterns.
""" """
from __future__ import annotations from __future__ import annotations
@@ -49,6 +49,7 @@ from pathlib import Path
import click import click
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_developer_token
@dataclass @dataclass
@@ -221,9 +222,12 @@ def push_image(
def _get_registry_creds() -> tuple[str, str]: def _get_registry_creds() -> tuple[str, str]:
"""Get registry credentials from environment variables.""" """Get registry credentials from environment variables."""
token = os.environ.get("CI_GITEA_TOKEN", "") try:
token = get_developer_token()
except click.ClickException:
token = None
username = os.environ.get("CI_GITEA_USERNAME", "") username = os.environ.get("CI_GITEA_USERNAME", "")
return username, token return username, token or ""
@click.command() @click.command()
+6 -5
View File
@@ -28,12 +28,11 @@ Usage::
--keep 2 \\ --keep 2 \\
--dry-run --dry-run
Authentication uses ``CI_GITEA_TOKEN`` environment variable. Authentication uses ``CI_GITEA_API_TOKEN`` environment variable (or legacy ``CI_GITEA_TOKEN``).
""" """
from __future__ import annotations from __future__ import annotations
import os
import time import time
from typing import Any from typing import Any
@@ -42,6 +41,7 @@ import requests
from devx.config import GITEA_API_URL, REPO_OWNER from devx.config import GITEA_API_URL, REPO_OWNER
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_developer_token
def list_package_versions( def list_package_versions(
@@ -187,9 +187,10 @@ def main(
api_url: str | None, api_url: str | None,
) -> None: ) -> None:
"""Clean up old Docker image versions from a Gitea registry.""" """Clean up old Docker image versions from a Gitea registry."""
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token: token = get_developer_token()
raise click.ClickException(_("CI_GITEA_TOKEN environment variable required")) except click.ClickException:
raise click.ClickException(_("CI_GITEA_TOKEN environment variable required")) from None
if not owner: if not owner:
owner = REPO_OWNER owner = REPO_OWNER
if not owner: if not owner:
+7 -3
View File
@@ -7,8 +7,8 @@ ci-improvement, doc-improvement, workflow-improvement) are created
idempotently via ``ensure_label``. idempotently via ``ensure_label``.
Usage: Usage:
CI_GITEA_TOKEN=<token> python3 -m devx.tools.configure_repo --repo my-repo DEVELOPER_GITEA_API_TOKEN=<token> python3 -m devx.tools.configure_repo --repo my-repo
CI_GITEA_TOKEN=<token> python3 -m devx.tools.configure_repo --repo my-repo --owner my-org DEVELOPER_GITEA_API_TOKEN=<token> python3 -m devx.tools.configure_repo --repo my-repo --owner my-org
""" """
from __future__ import annotations from __future__ import annotations
@@ -23,6 +23,7 @@ from devx.api_clients import GiteaClient
from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER
from devx.exceptions import APIError from devx.exceptions import APIError
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_developer_token
def _default_status_checks() -> list[str]: def _default_status_checks() -> list[str]:
@@ -175,7 +176,10 @@ def configure_repo(
) )
def main(repo: str | None, owner: str | None, branch: str, api_url: str | None) -> None: def main(repo: str | None, owner: str | None, branch: str, api_url: str | None) -> None:
"""Configure branch protection and repository settings via the Gitea API.""" """Configure branch protection and repository settings via the Gitea API."""
token = os.environ.get("CI_GITEA_TOKEN", "") try:
token = get_developer_token()
except click.ClickException:
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) from None
if repo is None: if repo is None:
repo = os.environ.get("DEVX_REPO_NAME", "") or REPO_NAME repo = os.environ.get("DEVX_REPO_NAME", "") or REPO_NAME
+9 -6
View File
@@ -42,6 +42,7 @@ from devx.config import (
VIKUNJA_PROJECT_ID, VIKUNJA_PROJECT_ID,
) )
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_developer_token, get_vikunja_token
load_dotenv() load_dotenv()
@@ -72,9 +73,10 @@ def get_vikunja_task_title(task_id: str) -> str:
Raises ClickException if VIKUNJA_TOKEN is not set or the task is not found. Raises ClickException if VIKUNJA_TOKEN is not set or the task is not found.
""" """
token = os.environ.get("VIKUNJA_TOKEN", "") try:
if not token: token = get_vikunja_token()
raise click.ClickException(_("VIKUNJA_TOKEN is not set. Required to derive PR title.")) except click.ClickException:
raise click.ClickException(_("VIKUNJA_TOKEN is not set. Required to derive PR title.")) from None
client = VikunjaClient(VIKUNJA_API_URL, token) client = VikunjaClient(VIKUNJA_API_URL, token)
task = client.find_task_by_identifier(VIKUNJA_PROJECT_ID, task_id, per_page=DEFAULT_PER_PAGE) task = client.find_task_by_identifier(VIKUNJA_PROJECT_ID, task_id, per_page=DEFAULT_PER_PAGE)
if not task: if not task:
@@ -118,9 +120,10 @@ def create_pr(
), ),
) )
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token: token = get_developer_token()
raise click.ClickException(_("CI_GITEA_TOKEN is not set. Required to create a PR.")) except click.ClickException:
raise click.ClickException(_("CI_GITEA_TOKEN is not set. Required to create a PR.")) from None
vikunja_title = get_vikunja_task_title(task_id) vikunja_title = get_vikunja_task_title(task_id)
pr_title = f"{task_id}: {vikunja_title}" pr_title = f"{task_id}: {vikunja_title}"
+5 -5
View File
@@ -17,14 +17,13 @@ and ``DEVX_TASK_PREFIX`` environment variables (or ``.env``).
from __future__ import annotations from __future__ import annotations
import os
import click import click
from dotenv import load_dotenv from dotenv import load_dotenv
from devx.api_clients import VikunjaClient from devx.api_clients import VikunjaClient
from devx.config import TASK_PREFIX, VIKUNJA_API_URL, VIKUNJA_PROJECT_ID from devx.config import TASK_PREFIX, VIKUNJA_API_URL, VIKUNJA_PROJECT_ID
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_vikunja_token
load_dotenv() load_dotenv()
@@ -39,9 +38,10 @@ load_dotenv()
@click.option("--project-id", type=int, default=None, help="Vikunja project ID (default: DEVX_VIKUNJA_PROJECT_ID).") @click.option("--project-id", type=int, default=None, help="Vikunja project ID (default: DEVX_VIKUNJA_PROJECT_ID).")
def cli(title: str, description: str, project_id: int | None) -> None: def cli(title: str, description: str, project_id: int | None) -> None:
"""Create a Vikunja task and print its identifier.""" """Create a Vikunja task and print its identifier."""
token = os.environ.get("VIKUNJA_TOKEN", "") try:
if not token: token = get_vikunja_token()
raise click.ClickException(_("VIKUNJA_TOKEN is not set. Set it in .env or environment.")) except click.ClickException:
raise click.ClickException(_("VIKUNJA_TOKEN is not set. Set it in .env or environment.")) from None
pid = project_id if project_id is not None else VIKUNJA_PROJECT_ID pid = project_id if project_id is not None else VIKUNJA_PROJECT_ID
+2 -2
View File
@@ -68,8 +68,8 @@ def detect_package_name(repo_root: Path) -> str | None:
Looks for the first subdirectory under ``src/`` that contains Looks for the first subdirectory under ``src/`` that contains
an ``__init__.py`` file with ``__version__``. an ``__init__.py`` file with ``__version__``.
Returns the package directory name (e.g., ``devx``) or ``None`` if Returns the package directory name (e.g., ``devx``,
no package is found. ``grm``) or ``None`` if no package is found.
""" """
src_dir = repo_root / "src" src_dir = repo_root / "src"
if not src_dir.is_dir(): if not src_dir.is_dir():
+5 -5
View File
@@ -22,14 +22,13 @@ The repository is auto-detected from ``DEVX_REPO_OWNER`` /
from __future__ import annotations from __future__ import annotations
import os
import click import click
from dotenv import load_dotenv from dotenv import load_dotenv
from devx.api_clients import GiteaClient from devx.api_clients import GiteaClient
from devx.config import GITEA_API_URL, REPO_OWNER from devx.config import GITEA_API_URL, REPO_OWNER
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_developer_token
from devx.tools.create_pr import get_repo_name from devx.tools.create_pr import get_repo_name
from devx.tools.pr_status import _get_current_branch_pr from devx.tools.pr_status import _get_current_branch_pr
@@ -48,9 +47,10 @@ def cli(
repo: str | None, repo: str | None,
) -> None: ) -> None:
"""Add one or more labels to a pull request (idempotent).""" """Add one or more labels to a pull request (idempotent)."""
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token: token = get_developer_token()
raise click.ClickException(_("CI_GITEA_TOKEN is not set.")) except click.ClickException:
raise click.ClickException(_("CI_GITEA_TOKEN is not set.")) from None
repo_owner = owner or REPO_OWNER repo_owner = owner or REPO_OWNER
if not repo_owner: if not repo_owner:
+5 -5
View File
@@ -25,14 +25,13 @@ The repository is auto-detected from ``DEVX_REPO_OWNER`` /
from __future__ import annotations from __future__ import annotations
import os
import click import click
from dotenv import load_dotenv from dotenv import load_dotenv
from devx.api_clients import APIError, GiteaClient from devx.api_clients import APIError, GiteaClient
from devx.config import GITEA_API_URL, REPO_OWNER from devx.config import GITEA_API_URL, REPO_OWNER
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_developer_token
from devx.tools.create_pr import get_repo_name from devx.tools.create_pr import get_repo_name
from devx.tools.pr_status import _get_current_branch_pr from devx.tools.pr_status import _get_current_branch_pr
@@ -126,9 +125,10 @@ def cli(
repo: str | None, repo: str | None,
) -> None: ) -> None:
"""Fetch logs for failed CI jobs on a pull request.""" """Fetch logs for failed CI jobs on a pull request."""
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token: token = get_developer_token()
raise click.ClickException(_("CI_GITEA_TOKEN is not set.")) except click.ClickException:
raise click.ClickException(_("CI_GITEA_TOKEN is not set.")) from None
repo_owner = owner or REPO_OWNER repo_owner = owner or REPO_OWNER
if not repo_owner: if not repo_owner:
+5 -3
View File
@@ -32,6 +32,7 @@ from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnk
from devx.api_clients import APIError, GiteaClient from devx.api_clients import APIError, GiteaClient
from devx.config import GITEA_API_URL from devx.config import GITEA_API_URL
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_developer_token
from devx.tools._shared import detect_pr_number from devx.tools._shared import detect_pr_number
@@ -41,9 +42,10 @@ def main(pr: int | None) -> None:
"""Rebase a pull request's head branch onto master via Gitea API.""" """Rebase a pull request's head branch onto master via Gitea API."""
load_dotenv() load_dotenv()
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token: token = get_developer_token()
raise click.ClickException(_("CI_GITEA_TOKEN is not set. Add it to .env or export it.")) except click.ClickException:
raise click.ClickException(_("CI_GITEA_TOKEN is not set. Add it to .env or export it.")) from None
pr_num = pr or detect_pr_number() pr_num = pr or detect_pr_number()
if not pr_num: if not pr_num:
+5 -4
View File
@@ -24,7 +24,6 @@ The repository is auto-detected from ``DEVX_REPO_OWNER`` /
from __future__ import annotations from __future__ import annotations
import os
import subprocess # nosec B404 import subprocess # nosec B404
import time import time
@@ -34,6 +33,7 @@ from dotenv import load_dotenv
from devx.api_clients import GiteaClient from devx.api_clients import GiteaClient
from devx.config import GITEA_API_URL, REPO_OWNER from devx.config import GITEA_API_URL, REPO_OWNER
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_developer_token
from devx.tools.create_pr import get_repo_name from devx.tools.create_pr import get_repo_name
load_dotenv() load_dotenv()
@@ -139,9 +139,10 @@ def cli(
repo: str | None, repo: str | None,
) -> None: ) -> None:
"""Check CI status for a pull request or commit.""" """Check CI status for a pull request or commit."""
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token: token = get_developer_token()
raise click.ClickException(_("CI_GITEA_TOKEN is not set.")) except click.ClickException:
raise click.ClickException(_("CI_GITEA_TOKEN is not set.")) from None
repo_owner = owner or REPO_OWNER repo_owner = owner or REPO_OWNER
if not repo_owner: if not repo_owner:
+7 -5
View File
@@ -20,7 +20,6 @@ Exit codes:
from __future__ import annotations from __future__ import annotations
import os
import subprocess # nosec B404 import subprocess # nosec B404
import click import click
@@ -29,6 +28,7 @@ from dotenv import load_dotenv
from devx.api_clients import VikunjaClient from devx.api_clients import VikunjaClient
from devx.config import DEFAULT_PER_PAGE, TASK_ID_RE, TASK_PREFIX, VIKUNJA_API_URL, VIKUNJA_PROJECT_ID from devx.config import DEFAULT_PER_PAGE, TASK_ID_RE, TASK_PREFIX, VIKUNJA_API_URL, VIKUNJA_PROJECT_ID
from devx.i18n import _ from devx.i18n import _
from devx.tokens import get_vikunja_token
load_dotenv() load_dotenv()
@@ -55,8 +55,9 @@ def task_exists(task_id: str) -> bool:
Returns ``False`` if VIKUNJA_TOKEN is not set (soft-fail in local mode). Returns ``False`` if VIKUNJA_TOKEN is not set (soft-fail in local mode).
""" """
token = os.environ.get("VIKUNJA_TOKEN", "") try:
if not token: token = get_vikunja_token()
except click.ClickException:
return False return False
client = VikunjaClient(VIKUNJA_API_URL, token) client = VikunjaClient(VIKUNJA_API_URL, token)
return client.find_task_by_identifier(VIKUNJA_PROJECT_ID, task_id, per_page=DEFAULT_PER_PAGE) is not None return client.find_task_by_identifier(VIKUNJA_PROJECT_ID, task_id, per_page=DEFAULT_PER_PAGE) is not None
@@ -84,8 +85,9 @@ def validate(branch: str) -> None:
) )
) )
token = os.environ.get("VIKUNJA_TOKEN", "") try:
if not token: get_vikunja_token()
except click.ClickException:
click.echo( click.echo(
_( _(
"WARNING: VIKUNJA_TOKEN not set — skipping task existence check. " "WARNING: VIKUNJA_TOKEN not set — skipping task existence check. "
+8 -5
View File
@@ -16,6 +16,8 @@ from pathlib import Path
import click import click
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType] from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
from devx.tokens import get_developer_token
load_dotenv() load_dotenv()
@@ -64,19 +66,20 @@ def _install_ansible_collections(bin_dir: str) -> None:
def _configure_tea_login() -> None: def _configure_tea_login() -> None:
"""Configure tea CLI login from .env if CI_GITEA_TOKEN is set. """Configure tea CLI login from .env if a Gitea token is set.
Idempotent: if a login with the same name already exists, it is not re-added. Idempotent: if a login with the same name already exists, it is not re-added.
Skips if tea is not installed or CI_GITEA_TOKEN is not set. Skips if tea is not installed or no Gitea token is set.
""" """
tea_bin = shutil.which("tea") tea_bin = shutil.which("tea")
if tea_bin is None: if tea_bin is None:
click.echo("tea: not installed — run 'make install-tools' to install it.") click.echo("tea: not installed — run 'make install-tools' to install it.")
return return
token = os.environ.get("CI_GITEA_TOKEN", "") try:
if not token: token = get_developer_token()
click.echo("tea: CI_GITEA_TOKEN not set — skipping login configuration.") except click.ClickException:
click.echo("tea: Gitea API token not set — skipping login configuration.")
return return
api_url = os.environ.get("DEVX_GITEA_API_URL", "https://git.oblachno.oblachno.fyi/api/v1") api_url = os.environ.get("DEVX_GITEA_API_URL", "https://git.oblachno.oblachno.fyi/api/v1")
+6 -1
View File
@@ -24,6 +24,8 @@ from pathlib import Path
import click import click
from devx.tokens import get_developer_token
DEFAULT_VENV = ".venv" DEFAULT_VENV = ".venv"
OPT_VENV = "/opt/venv" OPT_VENV = "/opt/venv"
FALLBACK_TARGET = "setup-ci" FALLBACK_TARGET = "setup-ci"
@@ -67,7 +69,10 @@ def _install_in_image(
cmd = [pip_bin, "install", "--no-cache-dir", "-e", spec] cmd = [pip_bin, "install", "--no-cache-dir", "-e", spec]
env = os.environ.copy() env = os.environ.copy()
token = env.get("CI_GITEA_TOKEN", "") try:
token = get_developer_token()
except click.ClickException:
token = None
if token: if token:
username = env.get("CI_GITEA_USERNAME", "emil") username = env.get("CI_GITEA_USERNAME", "emil")
env["PIP_EXTRA_INDEX_URL"] = _build_pip_extra_index_url( env["PIP_EXTRA_INDEX_URL"] = _build_pip_extra_index_url(
+824 -616
View File
@@ -47,13 +47,13 @@
"ru": "\nDoc coverage: {covered}/{total} ({pct}%)", "ru": "\nDoc coverage: {covered}/{total} ({pct}%)",
"zh": "\nDoc coverage: {covered}/{total} ({pct}%)" "zh": "\nDoc coverage: {covered}/{total} ({pct}%)"
}, },
"\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}": { "\nDone! Synced: {synced}, Pruned: {pruned}": {
"bg": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}", "bg": "",
"de": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}", "de": "",
"en": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}", "en": "\nDone! Synced: {synced}, Pruned: {pruned}",
"pl": "\nGotowe! Utworzono: {created}, Zaktualizowano: {updated}, Pominięto: {skipped}", "pl": "",
"ru": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}", "ru": "",
"zh": "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}" "zh": ""
}, },
"\nDone. Deleted {deleted}, kept {kept}, failed {failed}.": { "\nDone. Deleted {deleted}, kept {kept}, failed {failed}.": {
"bg": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}.", "bg": "\nDone. Deleted {deleted}, kept {kept}, failed {failed}.",
@@ -71,6 +71,14 @@
"ru": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.", "ru": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce.",
"zh": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce." "zh": "\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce."
}, },
"\nFAIL: {n} stale version reference(s) found:": {
"bg": "",
"de": "",
"en": "\nFAIL: {n} stale version reference(s) found:",
"pl": "",
"ru": "",
"zh": ""
},
"\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.": { "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.": {
"bg": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.", "bg": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
"de": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.", "de": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
@@ -79,6 +87,14 @@
"ru": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.", "ru": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report.",
"zh": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report." "zh": "\nFix the misaligned tags before creating new releases. Run 'python3 -m devx.ci.release --verify' for a full report."
}, },
"\nFixed {n} stale version reference(s).": {
"bg": "",
"de": "",
"en": "\nFixed {n} stale version reference(s).",
"pl": "",
"ru": "",
"zh": ""
},
"\nGenerated {count} badges:": { "\nGenerated {count} badges:": {
"bg": "\nGenerated {count} badges:", "bg": "\nGenerated {count} badges:",
"de": "\nGenerated {count} badges:", "de": "\nGenerated {count} badges:",
@@ -87,22 +103,6 @@
"ru": "\nGenerated {count} badges:", "ru": "\nGenerated {count} badges:",
"zh": "\nGenerated {count} badges:" "zh": "\nGenerated {count} badges:"
}, },
"\nIntegrity check FAILED ({count} issues):": {
"bg": "\nIntegrity check FAILED ({count} issues):",
"de": "\nIntegrity check FAILED ({count} issues):",
"en": "\nIntegrity check FAILED ({count} issues):",
"pl": "\nKontrola integralności NIEUDANA ({count} problemów):",
"ru": "\nIntegrity check FAILED ({count} issues):",
"zh": "\nIntegrity check FAILED ({count} issues):"
},
"\nIntegrity check passed — all {count} pages verified.": {
"bg": "\nIntegrity check passed — all {count} pages verified.",
"de": "\nIntegrity check passed — all {count} pages verified.",
"en": "\nIntegrity check passed — all {count} pages verified.",
"pl": "\nKontrola integralności zakończona pomyślnie — wszystkie {count} stron zweryfikowane.",
"ru": "\nIntegrity check passed — all {count} pages verified.",
"zh": "\nIntegrity check passed — all {count} pages verified."
},
"\nKeeping {kept}, would delete {count}": { "\nKeeping {kept}, would delete {count}": {
"bg": "\nKeeping {kept}, would delete {count}", "bg": "\nKeeping {kept}, would delete {count}",
"de": "\nKeeping {kept}, would delete {count}", "de": "\nKeeping {kept}, would delete {count}",
@@ -127,6 +127,22 @@
"ru": "\nMissing documentation:", "ru": "\nMissing documentation:",
"zh": "\nMissing documentation:" "zh": "\nMissing documentation:"
}, },
"\nNo stale version references found.": {
"bg": "",
"de": "",
"en": "\nNo stale version references found.",
"pl": "",
"ru": "",
"zh": ""
},
"\nPASS: All version references are current.": {
"bg": "",
"de": "",
"en": "\nPASS: All version references are current.",
"pl": "",
"ru": "",
"zh": ""
},
"\nResult: {status}": { "\nResult: {status}": {
"bg": "\nResult: {status}", "bg": "\nResult: {status}",
"de": "\nResult: {status}", "de": "\nResult: {status}",
@@ -151,13 +167,13 @@
"ru": "\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}'." "zh": "\nReview #{review_id} posted on PR #{pr_number} with event '{event}'."
}, },
"\nRunning full wiki integrity check...": { "\nRun with --fix to auto-update version references.": {
"bg": "\nRunning full wiki integrity check...", "bg": "",
"de": "\nRunning full wiki integrity check...", "de": "",
"en": "\nRunning full wiki integrity check...", "en": "\nRun with --fix to auto-update version references.",
"pl": "\nUruchamianie pełnej kontroli integralności wiki...", "pl": "",
"ru": "\nRunning full wiki integrity check...", "ru": "",
"zh": "\nRunning full wiki integrity check..." "zh": ""
}, },
"\nTag → Commit alignment:": { "\nTag → Commit alignment:": {
"bg": "\nTag → Commit alignment:", "bg": "\nTag → Commit alignment:",
@@ -183,29 +199,21 @@
"ru": "\nUser-facing changes ({count}):", "ru": "\nUser-facing changes ({count}):",
"zh": "\nUser-facing changes ({count}):" "zh": "\nUser-facing changes ({count}):"
}, },
"\nVerification FAILED: {failures} page(s) have empty or mismatched content!": { "\nVerification passed — all wiki pages exist.": {
"bg": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!", "bg": "",
"de": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!", "de": "",
"en": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!", "en": "\nVerification passed — all wiki pages exist.",
"pl": "\nWeryfikacja NIEUDANA: {failures} strona(y) ma pustą lub niezgodną treść!", "pl": "",
"ru": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!", "ru": "",
"zh": "\nVerification FAILED: {failures} page(s) have empty or mismatched content!" "zh": ""
}, },
"\nVerification passed — all wiki pages have correct content.": { "\nVerifying wiki pages...": {
"bg": "\nVerification passed — all wiki pages have correct content.", "bg": "",
"de": "\nVerification passed — all wiki pages have correct content.", "de": "",
"en": "\nVerification passed — all wiki pages have correct content.", "en": "\nVerifying wiki pages...",
"pl": "\nWeryfikacja zakończona pomyślnie — wszystkie strony wiki mają poprawną treść.", "pl": "",
"ru": "\nVerification passed — all wiki pages have correct content.", "ru": "",
"zh": "\nVerification passed — all wiki pages have correct content." "zh": ""
},
"\nVerifying wiki pages have content...": {
"bg": "\nVerifying wiki pages have content...",
"de": "\nVerifying wiki pages have content...",
"en": "\nVerifying wiki pages have content...",
"pl": "\nWeryfikowanie, czy strony wiki mają treść...",
"ru": "\nVerifying wiki pages have content...",
"zh": "\nVerifying wiki pages have content..."
}, },
"\nWorkflow-only changes ({count}):": { "\nWorkflow-only changes ({count}):": {
"bg": "\nWorkflow-only changes ({count}):", "bg": "\nWorkflow-only changes ({count}):",
@@ -351,6 +359,30 @@
"ru": " - Требуемые проверки статуса: {checks}", "ru": " - Требуемые проверки статуса: {checks}",
"zh": " - 必需状态检查: {checks}" "zh": " - 必需状态检查: {checks}"
}, },
" - {count} standard labels verified": {
"bg": " - {count} standard labels verified",
"de": " - {count} standard labels verified",
"en": " - {count} standard labels verified",
"pl": " - {count} standard labels verified",
"ru": " - {count} standard labels verified",
"zh": " - {count} standard labels verified"
},
" -> {dir}": {
"en": " -> {dir}",
"bg": " -> {dir}",
"de": " -> {dir}",
"pl": " -> {dir}",
"ru": " -> {dir}",
"zh": " -> {dir}"
},
" ... and {n} more": {
"bg": "",
"de": "",
"en": " ... and {n} more",
"pl": "",
"ru": "",
"zh": ""
},
" Auto-fixed trailing whitespace in {n} files": { " Auto-fixed trailing whitespace in {n} files": {
"bg": " Auto-fixed trailing whitespace in {n} files", "bg": " Auto-fixed trailing whitespace in {n} files",
"de": " Auto-fixed trailing whitespace in {n} files", "de": " Auto-fixed trailing whitespace in {n} files",
@@ -391,14 +423,6 @@
"ru": " Collecting version...", "ru": " Collecting version...",
"zh": " Collecting version..." "zh": " Collecting version..."
}, },
" Created: {title}": {
"bg": " Created: {title}",
"de": " Created: {title}",
"en": " Created: {title}",
"pl": " Utworzono: {title}",
"ru": " Created: {title}",
"zh": " Created: {title}"
},
" Deleted: {version}": { " Deleted: {version}": {
"bg": " Deleted: {version}", "bg": " Deleted: {version}",
"de": " Deleted: {version}", "de": " Deleted: {version}",
@@ -407,13 +431,13 @@
"ru": " Deleted: {version}", "ru": " Deleted: {version}",
"zh": " Deleted: {version}" "zh": " Deleted: {version}"
}, },
" FAIL: {title} — content mismatch or empty!": { " FAIL: {title} — page not found in wiki!": {
"bg": " FAIL: {title} — content mismatch or empty!", "bg": "",
"de": " FAIL: {title} — content mismatch or empty!", "de": "",
"en": " FAIL: {title} — content mismatch or empty!", "en": " FAIL: {title} — page not found in wiki!",
"pl": " BŁĄD: {title} — treść niezgodna lub pusta!", "pl": "",
"ru": " FAIL: {title} — content mismatch or empty!", "ru": "",
"zh": " FAIL: {title} — content mismatch or empty!" "zh": ""
}, },
" FAILED to delete: {version}": { " FAILED to delete: {version}": {
"bg": " FAILED to delete: {version}", "bg": " FAILED to delete: {version}",
@@ -423,6 +447,14 @@
"ru": " FAILED to delete: {version}", "ru": " FAILED to delete: {version}",
"zh": " FAILED to delete: {version}" "zh": " FAILED to delete: {version}"
}, },
" Fixed {fixes} version ref(s) in {file}": {
"bg": "",
"de": "",
"en": " Fixed {fixes} version ref(s) in {file}",
"pl": "",
"ru": "",
"zh": ""
},
" Generated: {path}": { " Generated: {path}": {
"bg": " Generated: {path}", "bg": " Generated: {path}",
"de": " Generated: {path}", "de": " Generated: {path}",
@@ -479,13 +511,13 @@
"ru": " OK: {script}", "ru": " OK: {script}",
"zh": " OK: {script}" "zh": " OK: {script}"
}, },
" OK: {title} ({chars} chars)": { " OK: {title}": {
"bg": " OK: {title} ({chars} chars)", "bg": "",
"de": " OK: {title} ({chars} chars)", "de": "",
"en": " OK: {title} ({chars} chars)", "en": " OK: {title}",
"pl": " OK: {title} ({chars} znaków)", "pl": "",
"ru": " OK: {title} ({chars} chars)", "ru": "",
"zh": " OK: {title} ({chars} chars)" "zh": ""
}, },
" Package: {pkg}": { " Package: {pkg}": {
"bg": " Package: {pkg}", "bg": " Package: {pkg}",
@@ -495,6 +527,14 @@
"ru": " Package: {pkg}", "ru": " Package: {pkg}",
"zh": " Package: {pkg}" "zh": " Package: {pkg}"
}, },
" Pruned: {file} (not in mapping)": {
"bg": "",
"de": "",
"en": " Pruned: {file} (not in mapping)",
"pl": "",
"ru": "",
"zh": ""
},
" Quality checks: {checks}": { " Quality checks: {checks}": {
"bg": " Quality checks: {checks}", "bg": " Quality checks: {checks}",
"de": " Quality checks: {checks}", "de": " Quality checks: {checks}",
@@ -511,6 +551,22 @@
"ru": " Repo root: {root}", "ru": " Repo root: {root}",
"zh": " Repo root: {root}" "zh": " Repo root: {root}"
}, },
" Run 'make install-checkmake' to install the Makefile linter.": {
"en": " Run 'make install-checkmake' to install the Makefile linter.",
"bg": " Изпълнете 'make install-checkmake' за инсталиране на Makefile линтера.",
"de": " Führen Sie 'make install-checkmake' aus, um den Makefile-Linter zu installieren.",
"pl": " Uruchom 'make install-checkmake', aby zainstalować linter Makefile.",
"ru": " Выполните 'make install-checkmake' для установки линтера Makefile.",
"zh": " 运行 'make install-checkmake' 来安装 Makefile 检查器。"
},
" Synced: {title} → {file}": {
"bg": "",
"de": "",
"en": " Synced: {title} → {file}",
"pl": "",
"ru": "",
"zh": ""
},
" Test paths: {testpaths}": { " Test paths: {testpaths}": {
"bg": " Test paths: {testpaths}", "bg": " Test paths: {testpaths}",
"de": " Test paths: {testpaths}", "de": " Test paths: {testpaths}",
@@ -519,13 +575,21 @@
"ru": " Test paths: {testpaths}", "ru": " Test paths: {testpaths}",
"zh": " Test paths: {testpaths}" "zh": " Test paths: {testpaths}"
}, },
" Updated: {title}": { " WARN: Mapped file {file} is empty, skipping": {
"bg": " Updated: {title}", "bg": "",
"de": " Updated: {title}", "de": "",
"en": " Updated: {title}", "en": " WARN: Mapped file {file} is empty, skipping",
"pl": " Zaktualizowano: {title}", "pl": "",
"ru": " Updated: {title}", "ru": "",
"zh": " Updated: {title}" "zh": ""
},
" WARN: Mapped file {file} not found, skipping": {
"bg": "",
"de": "",
"en": " WARN: Mapped file {file} not found, skipping",
"pl": "",
"ru": "",
"zh": ""
}, },
" WARNING: Could not extract coverage from pytest output (rc={rc})": { " WARNING: Could not extract coverage from pytest output (rc={rc})": {
"bg": " WARNING: Could not extract coverage from pytest output (rc={rc})", "bg": " WARNING: Could not extract coverage from pytest output (rc={rc})",
@@ -615,6 +679,22 @@
"ru": " {name}: {label}={message} ({color})", "ru": " {name}: {label}={message} ({color})",
"zh": " {name}: {label}={message} ({color})" "zh": " {name}: {label}={message} ({color})"
}, },
" {n} long lines found (warnings only)": {
"bg": "",
"de": "",
"en": " {n} long lines found (warnings only)",
"pl": "",
"ru": "",
"zh": ""
},
" {n} orphan docs found (warnings only)": {
"bg": "",
"de": "",
"en": " {n} orphan docs found (warnings only)",
"pl": "",
"ru": "",
"zh": ""
},
" {n} stale docs found (warnings only)": { " {n} stale docs found (warnings only)": {
"bg": " {n} stale docs found (warnings only)", "bg": " {n} stale docs found (warnings only)",
"de": " {n} stale docs found (warnings only)", "de": " {n} stale docs found (warnings only)",
@@ -623,6 +703,14 @@
"ru": " {n} stale docs found (warnings only)", "ru": " {n} stale docs found (warnings only)",
"zh": " {n} stale docs found (warnings only)" "zh": " {n} stale docs found (warnings only)"
}, },
" {tool}: found at {path}": {
"en": " {tool}: found at {path}",
"bg": " {tool}: намерен на {path}",
"de": " {tool}: gefunden unter {path}",
"pl": " {tool}: znaleziono w {path}",
"ru": " {tool}: найден в {path}",
"zh": " {tool}: 在 {path} 找到"
},
" {version} (created: {created})": { " {version} (created: {created})": {
"bg": " {version} (created: {created})", "bg": " {version} (created: {created})",
"de": " {version} (created: {created})", "de": " {version} (created: {created})",
@@ -727,6 +815,22 @@
"ru": "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}" "zh": "Assigned {count} items to runner {runner_index}: {encoded}"
}, },
"Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.": {
"bg": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"de": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"en": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"pl": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"ru": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"zh": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label."
},
"Automated CI commit (badge) — skipping post-merge jobs.": {
"bg": "Automated CI commit (badge) — skipping post-merge jobs.",
"de": "Automated CI commit (badge) — skipping post-merge jobs.",
"en": "Automated CI commit (badge) — skipping post-merge jobs.",
"pl": "Automated CI commit (badge) — skipping post-merge jobs.",
"ru": "Automated CI commit (badge) — skipping post-merge jobs.",
"zh": "Automated CI commit (badge) — skipping post-merge jobs."
},
"Badge push attempt {attempt}/{retries} failed — retrying: {error}": { "Badge push attempt {attempt}/{retries} failed — retrying: {error}": {
"bg": "Badge push attempt {attempt}/{retries} failed — retrying: {error}", "bg": "Badge push attempt {attempt}/{retries} failed — retrying: {error}",
"de": "Badge push attempt {attempt}/{retries} failed — retrying: {error}", "de": "Badge push attempt {attempt}/{retries} failed — retrying: {error}",
@@ -775,6 +879,22 @@
"ru": "Ветка '{branch}' не содержит ID задачи.\n Ожидаемый формат: {prefix}-N-краткое-описание\n Пример: {prefix}-42-add-feature\n Исправление: переименуйте ветку или создайте задачу Vikunja:\n python -m devx.tools.create_task --title \"Заголовок задачи\"", "ru": "Ветка '{branch}' не содержит ID задачи.\n Ожидаемый формат: {prefix}-N-краткое-описание\n Пример: {prefix}-42-add-feature\n Исправление: переименуйте ветку или создайте задачу Vikunja:\n python -m devx.tools.create_task --title \"Заголовок задачи\"",
"zh": "分支 '{branch}' 不包含任务 ID。\n 预期格式: {prefix}-N-简短描述\n 示例: {prefix}-42-add-feature\n 修复: 重命名分支或先创建 Vikunja 任务:\n python -m devx.tools.create_task --title \"任务标题\"" "zh": "分支 '{branch}' 不包含任务 ID。\n 预期格式: {prefix}-N-简短描述\n 示例: {prefix}-42-add-feature\n 修复: 重命名分支或先创建 Vikunja 任务:\n python -m devx.tools.create_task --title \"任务标题\""
}, },
"Branch is already up-to-date with origin/master.": {
"bg": "Branch is already up-to-date with origin/master.",
"de": "Branch is already up-to-date with origin/master.",
"en": "Branch is already up-to-date with origin/master.",
"pl": "Branch is already up-to-date with origin/master.",
"ru": "Branch is already up-to-date with origin/master.",
"zh": "Branch is already up-to-date with origin/master."
},
"Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.": {
"bg": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"de": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"en": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"pl": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"ru": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"zh": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR."
},
"Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master": { "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master": {
"bg": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master", "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", "de": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
@@ -783,6 +903,14 @@
"ru": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master", "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" "zh": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master"
}, },
"Branch is {count} commit(s) behind master. Rebasing...": {
"bg": "Branch is {count} commit(s) behind master. Rebasing...",
"de": "Branch is {count} commit(s) behind master. Rebasing...",
"en": "Branch is {count} commit(s) behind master. Rebasing...",
"pl": "Branch is {count} commit(s) behind master. Rebasing...",
"ru": "Branch is {count} commit(s) behind master. Rebasing...",
"zh": "Branch is {count} commit(s) behind master. Rebasing..."
},
"Branch name (e.g., DEVX-256-fix-foo)": { "Branch name (e.g., DEVX-256-fix-foo)": {
"bg": "Branch name (e.g., DEVX-256-fix-foo)", "bg": "Branch name (e.g., DEVX-256-fix-foo)",
"de": "Branch name (e.g., DEVX-256-fix-foo)", "de": "Branch name (e.g., DEVX-256-fix-foo)",
@@ -831,6 +959,14 @@
"ru": "CI checks failed.", "ru": "CI checks failed.",
"zh": "CI checks failed." "zh": "CI checks failed."
}, },
"Gitea API token not set. Set one of: {names}": {
"bg": "Gitea API token not set. Set one of: {names}",
"de": "Gitea API token not set. Set one of: {names}",
"en": "Gitea API token not set. Set one of: {names}",
"pl": "Gitea API token not set. Set one of: {names}",
"ru": "Gitea API token not set. Set one of: {names}",
"zh": "Gitea API token not set. Set one of: {names}"
},
"CI_GITEA_TOKEN environment variable required": { "CI_GITEA_TOKEN environment variable required": {
"bg": "CI_GITEA_TOKEN environment variable required", "bg": "CI_GITEA_TOKEN environment variable required",
"de": "CI_GITEA_TOKEN environment variable required", "de": "CI_GITEA_TOKEN environment variable required",
@@ -847,6 +983,14 @@
"ru": "CI_GITEA_TOKEN is not set.", "ru": "CI_GITEA_TOKEN is not set.",
"zh": "CI_GITEA_TOKEN is not set." "zh": "CI_GITEA_TOKEN is not set."
}, },
"CI_GITEA_TOKEN is not set. Add it to .env or export it.": {
"bg": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"de": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"en": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"pl": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"ru": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"zh": "CI_GITEA_TOKEN is not set. Add it to .env or export it."
},
"CI_GITEA_TOKEN is not set. Required to create a PR.": { "CI_GITEA_TOKEN is not set. Required to create a PR.": {
"bg": "CI_GITEA_TOKEN не е зададен. Необходим за създаване на PR.", "bg": "CI_GITEA_TOKEN не е зададен. Необходим за създаване на PR.",
"de": "CI_GITEA_TOKEN nicht gesetzt. Erforderlich zum Erstellen eines PR.", "de": "CI_GITEA_TOKEN nicht gesetzt. Erforderlich zum Erstellen eines PR.",
@@ -863,6 +1007,22 @@
"ru": "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." "zh": "CI_GITEA_TOKEN not set — skipping login configuration."
}, },
"Cannot read __version__ from src/{pkg}/__init__.py — skipping.": {
"bg": "",
"de": "",
"en": "Cannot read __version__ from src/{pkg}/__init__.py — skipping.",
"pl": "",
"ru": "",
"zh": ""
},
"Cannot rebase: not on a branch (detached HEAD).": {
"bg": "Cannot rebase: not on a branch (detached HEAD).",
"de": "Cannot rebase: not on a branch (detached HEAD).",
"en": "Cannot rebase: not on a branch (detached HEAD).",
"pl": "Cannot rebase: not on a branch (detached HEAD).",
"ru": "Cannot rebase: not on a branch (detached HEAD).",
"zh": "Cannot rebase: not on a branch (detached HEAD)."
},
"Checking CLI command documentation...": { "Checking CLI command documentation...": {
"bg": "Checking CLI command documentation...", "bg": "Checking CLI command documentation...",
"de": "Checking CLI command documentation...", "de": "Checking CLI command documentation...",
@@ -871,6 +1031,14 @@
"ru": "Checking CLI command documentation...", "ru": "Checking CLI command documentation...",
"zh": "Checking CLI command documentation..." "zh": "Checking CLI command documentation..."
}, },
"Checking code block languages...": {
"bg": "",
"de": "",
"en": "Checking code block languages...",
"pl": "",
"ru": "",
"zh": ""
},
"Checking docs structure...": { "Checking docs structure...": {
"bg": "Checking docs structure...", "bg": "Checking docs structure...",
"de": "Checking docs structure...", "de": "Checking docs structure...",
@@ -895,6 +1063,14 @@
"ru": "Checking for TODO/FIXME markers...", "ru": "Checking for TODO/FIXME markers...",
"zh": "Checking for TODO/FIXME markers..." "zh": "Checking for TODO/FIXME markers..."
}, },
"Checking for orphan docs...": {
"bg": "",
"de": "",
"en": "Checking for orphan docs...",
"pl": "",
"ru": "",
"zh": ""
},
"Checking for stale docs...": { "Checking for stale docs...": {
"bg": "Checking for stale docs...", "bg": "Checking for stale docs...",
"de": "Checking for stale docs...", "de": "Checking for stale docs...",
@@ -919,6 +1095,22 @@
"ru": "Checking internal links...", "ru": "Checking internal links...",
"zh": "Checking internal links..." "zh": "Checking internal links..."
}, },
"Checking line length...": {
"bg": "",
"de": "",
"en": "Checking line length...",
"pl": "",
"ru": "",
"zh": ""
},
"Checking max heading depth...": {
"bg": "",
"de": "",
"en": "Checking max heading depth...",
"pl": "",
"ru": "",
"zh": ""
},
"Checking required files...": { "Checking required files...": {
"bg": "Checking required files...", "bg": "Checking required files...",
"de": "Checking required files...", "de": "Checking required files...",
@@ -927,6 +1119,14 @@
"ru": "Checking required files...", "ru": "Checking required files...",
"zh": "Checking required files..." "zh": "Checking required files..."
}, },
"Checking single H1 per file...": {
"bg": "",
"de": "",
"en": "Checking single H1 per file...",
"pl": "",
"ru": "",
"zh": ""
},
"Checking status for PR #{pr_number}...": { "Checking status for PR #{pr_number}...": {
"bg": "Checking status for PR #{pr_number}...", "bg": "Checking status for PR #{pr_number}...",
"de": "Checking status for PR #{pr_number}...", "de": "Checking status for PR #{pr_number}...",
@@ -943,6 +1143,30 @@
"ru": "Checking trailing whitespace...", "ru": "Checking trailing whitespace...",
"zh": "Checking trailing whitespace..." "zh": "Checking trailing whitespace..."
}, },
"Checking version references for {pkg} (current: v{version})": {
"bg": "",
"de": "",
"en": "Checking version references for {pkg} (current: v{version})",
"pl": "",
"ru": "",
"zh": ""
},
"Cloned existing wiki.": {
"bg": "",
"de": "",
"en": "Cloned existing wiki.",
"pl": "",
"ru": "",
"zh": ""
},
"Cloning wiki repo...": {
"bg": "",
"de": "",
"en": "Cloning wiki repo...",
"pl": "",
"ru": "",
"zh": ""
},
"Command failed ({cmd}): {stderr}": { "Command failed ({cmd}): {stderr}": {
"bg": "Command failed ({cmd}): {stderr}", "bg": "Command failed ({cmd}): {stderr}",
"de": "Command failed ({cmd}): {stderr}", "de": "Command failed ({cmd}): {stderr}",
@@ -967,6 +1191,14 @@
"ru": "Commit: {sha}", "ru": "Commit: {sha}",
"zh": "Commit: {sha}" "zh": "Commit: {sha}"
}, },
"Committing and pushing...": {
"bg": "",
"de": "",
"en": "Committing and pushing...",
"pl": "",
"ru": "",
"zh": ""
},
"Comparing {base}..{head} ({count} files changed)": { "Comparing {base}..{head} ({count} files changed)": {
"bg": "Comparing {base}..{head} ({count} files changed)", "bg": "Comparing {base}..{head} ({count} files changed)",
"de": "Comparing {base}..{head} ({count} files changed)", "de": "Comparing {base}..{head} ({count} files changed)",
@@ -1015,6 +1247,14 @@
"ru": "Configuring tea login '{name}' for {url}...", "ru": "Configuring tea login '{name}' for {url}...",
"zh": "Configuring tea login '{name}' for {url}..." "zh": "Configuring tea login '{name}' for {url}..."
}, },
"Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.": {
"bg": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"de": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"en": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"pl": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"ru": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"zh": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR."
},
"Could not detect current branch: {error}": { "Could not detect current branch: {error}": {
"bg": "Не може да се определи текущия клон: {error}", "bg": "Не може да се определи текущия клон: {error}",
"de": "Aktueller Branch konnte nicht erkannt werden: {error}", "de": "Aktueller Branch konnte nicht erkannt werden: {error}",
@@ -1031,6 +1271,14 @@
"ru": "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}." "zh": "Could not determine head SHA for PR #{pr_number}."
}, },
"Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.": {
"bg": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"de": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"en": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"pl": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"ru": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"zh": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables."
},
"Could not extract conventional commit message from PR commits.": { "Could not extract conventional commit message from PR commits.": {
"bg": "Could not extract conventional commit message from PR commits.", "bg": "Could not extract conventional commit message from PR commits.",
"de": "Could not extract conventional commit message from PR commits.", "de": "Could not extract conventional commit message from PR commits.",
@@ -1119,6 +1367,14 @@
"ru": "Dependencies must have documentation comments.", "ru": "Dependencies must have documentation comments.",
"zh": "Dependencies must have documentation comments." "zh": "Dependencies must have documentation comments."
}, },
"Directory to scan (default: tests/integration). Can be repeated.": {
"en": "Directory to scan (default: tests/integration). Can be repeated.",
"bg": "Директория за сканиране (по подразбиране: tests/integration). Може да се повтаря.",
"de": "Zu scannendes Verzeichnis (Standard: tests/integration). Kann wiederholt werden.",
"pl": "Katalog do skanowania (domyślnie: tests/integration). Można powtarzać.",
"ru": "Директория для сканирования (по умолчанию: tests/integration). Можно повторять.",
"zh": "要扫描的目录(默认:tests/integration)。可重复。"
},
"Docker daemon already running": { "Docker daemon already running": {
"bg": "Докер демонът вече работи", "bg": "Докер демонът вече работи",
"de": "Docker-Daemon läuft bereits", "de": "Docker-Daemon läuft bereits",
@@ -1207,6 +1463,22 @@
"ru": "Каждый элемент должен быть строкой или объектом с 'id', получено {type}", "ru": "Каждый элемент должен быть строкой или объектом с 'id', получено {type}",
"zh": "每个元素必须是字符串或带有 'id' 的对象,得到 {type}" "zh": "每个元素必须是字符串或带有 'id' 的对象,得到 {type}"
}, },
"Ensuring standard labels...": {
"bg": "Ensuring standard labels...",
"de": "Ensuring standard labels...",
"en": "Ensuring standard labels...",
"pl": "Ensuring standard labels...",
"ru": "Ensuring standard labels...",
"zh": "Ensuring standard labels..."
},
"FAIL: Could not clone wiki for verification.": {
"bg": "",
"de": "",
"en": "FAIL: Could not clone wiki for verification.",
"pl": "",
"ru": "",
"zh": ""
},
"FAIL: {n} documentation issues found:": { "FAIL: {n} documentation issues found:": {
"bg": "FAIL: {n} documentation issues found:", "bg": "FAIL: {n} documentation issues found:",
"de": "FAIL: {n} documentation issues found:", "de": "FAIL: {n} documentation issues found:",
@@ -1263,6 +1535,30 @@
"ru": "Failed to list versions for {name}: {error}", "ru": "Failed to list versions for {name}: {error}",
"zh": "Failed to list versions for {name}: {error}" "zh": "Failed to list versions for {name}: {error}"
}, },
"Failed to push release commit after 3 attempts. Manual intervention required.": {
"bg": "Failed to push release commit after 3 attempts. Manual intervention required.",
"de": "Failed to push release commit after 3 attempts. Manual intervention required.",
"en": "Failed to push release commit after 3 attempts. Manual intervention required.",
"pl": "Failed to push release commit after 3 attempts. Manual intervention required.",
"ru": "Failed to push release commit after 3 attempts. Manual intervention required.",
"zh": "Failed to push release commit after 3 attempts. Manual intervention required."
},
"Failed to start ssh-agent: {error}": {
"en": "Failed to start ssh-agent: {error}",
"bg": "Неуспешно стартиране на ssh-agent: {error}",
"de": "Starten von ssh-agent fehlgeschlagen: {error}",
"pl": "Nie udało się uruchomić ssh-agent: {error}",
"ru": "Не удалось запустить ssh-agent: {error}",
"zh": "启动 ssh-agent 失败: {error}"
},
"Fetch failed: {error}": {
"bg": "Fetch failed: {error}",
"de": "Fetch failed: {error}",
"en": "Fetch failed: {error}",
"pl": "Fetch failed: {error}",
"ru": "Fetch failed: {error}",
"zh": "Fetch failed: {error}"
},
"Fetching logs for PR #{pr_number}...": { "Fetching logs for PR #{pr_number}...": {
"bg": "Fetching logs for PR #{pr_number}...", "bg": "Fetching logs for PR #{pr_number}...",
"de": "Fetching logs for PR #{pr_number}...", "de": "Fetching logs for PR #{pr_number}...",
@@ -1271,13 +1567,29 @@
"ru": "Fetching logs for PR #{pr_number}...", "ru": "Fetching logs for PR #{pr_number}...",
"zh": "Fetching logs for PR #{pr_number}..." "zh": "Fetching logs for PR #{pr_number}..."
}, },
"Found {count} existing wiki pages.": { "Fetching origin/master...": {
"bg": "Found {count} existing wiki pages.", "bg": "Fetching origin/master...",
"de": "Found {count} existing wiki pages.", "de": "Fetching origin/master...",
"en": "Found {count} existing wiki pages.", "en": "Fetching origin/master...",
"pl": "Znaleziono {count} istniejących stron wiki.", "pl": "Fetching origin/master...",
"ru": "Found {count} existing wiki pages.", "ru": "Fetching origin/master...",
"zh": "Found {count} existing wiki pages." "zh": "Fetching origin/master..."
},
"Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.": {
"bg": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"de": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"en": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"pl": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"ru": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"zh": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again."
},
"Force-pushing...": {
"bg": "Force-pushing...",
"de": "Force-pushing...",
"en": "Force-pushing...",
"pl": "Force-pushing...",
"ru": "Force-pushing...",
"zh": "Force-pushing..."
}, },
"Found {count} mutable global(s) — use factory functions or pytest fixtures.": { "Found {count} mutable global(s) — use factory functions or pytest fixtures.": {
"bg": "Found {count} mutable global(s) — use factory functions or pytest fixtures.", "bg": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
@@ -1295,6 +1607,14 @@
"ru": "Found {count} stale documentation reference(s)", "ru": "Found {count} stale documentation reference(s)",
"zh": "Found {count} stale documentation reference(s)" "zh": "Found {count} stale documentation reference(s)"
}, },
"Found {count} unsafe identity check(s) in integration tests.": {
"en": "Found {count} unsafe identity check(s) in integration tests.",
"bg": "Намерени са {count} небрежни проверки за идентичност в интеграционните тестове.",
"de": "{count} unsichere Identitätsprüfung(en) in Integrationstests gefunden.",
"pl": "Znaleziono {count} niebezpiecznych sprawdzeń tożsamości w testach integracyjnych.",
"ru": "Найдено {count} небезопасных проверок идентичности в интеграционных тестах.",
"zh": "在集成测试中发现 {count} 个不安全的身份检查。"
},
"Found {count} version(s):": { "Found {count} version(s):": {
"bg": "Found {count} version(s):", "bg": "Found {count} version(s):",
"de": "Found {count} version(s):", "de": "Found {count} version(s):",
@@ -1519,6 +1839,14 @@
"ru": "Linting documentation in {root}...", "ru": "Linting documentation in {root}...",
"zh": "Linting documentation in {root}..." "zh": "Linting documentation in {root}..."
}, },
"Login to {registry} failed: {error}": {
"en": "Login to {registry} failed: {error}",
"bg": "Влизането в {registry} не успя: {error}",
"de": "Anmeldung bei {registry} fehlgeschlagen: {error}",
"pl": "Logowanie do {registry} nie powiodło się: {error}",
"ru": "Ошибка входа в {registry}: {error}",
"zh": "登录 {registry} 失败: {error}"
},
"Manifest file not found: {path}": { "Manifest file not found: {path}": {
"bg": "Manifest file not found: {path}", "bg": "Manifest file not found: {path}",
"de": "Manifest file not found: {path}", "de": "Manifest file not found: {path}",
@@ -1535,22 +1863,6 @@
"ru": "Manifest must be a JSON list", "ru": "Manifest must be a JSON list",
"zh": "Manifest must be a JSON list" "zh": "Manifest must be a JSON list"
}, },
"Mapped file {file} is empty. Update the content or remove from mapping.json.": {
"bg": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
"de": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
"en": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
"pl": "Mapowany plik {file} jest pusty. Zaktualizuj treść lub usuń z mapping.json.",
"ru": "Mapped file {file} is empty. Update the content or remove from mapping.json.",
"zh": "Mapped file {file} is empty. Update the content or remove from mapping.json."
},
"Mapped file {file} not found. Update mapping.json or create the file.": {
"bg": "Mapped file {file} not found. Update mapping.json or create the file.",
"de": "Mapped file {file} not found. Update mapping.json or create the file.",
"en": "Mapped file {file} not found. Update mapping.json or create the file.",
"pl": "Mapowany plik {file} nie znaleziony. Zaktualizuj mapping.json lub utwórz plik.",
"ru": "Mapped file {file} not found. Update mapping.json or create the file.",
"zh": "Mapped file {file} not found. Update mapping.json or create the file."
},
"Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.": { "Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.": {
"bg": "Сливането неуспешно с HTTP {status}: {message}\nПроверете дали PR е готов и имате права за сливане.", "bg": "Сливането неуспешно с HTTP {status}: {message}\nПроверете дали PR е готов и имате права за сливане.",
"de": "Merge fehlgeschlagen mit HTTP {status}: {message}\nBitte prüfen Sie, ob der PR bereit ist und Sie Merge-Rechte haben.", "de": "Merge fehlgeschlagen mit HTTP {status}: {message}\nBitte prüfen Sie, ob der PR bereit ist und Sie Merge-Rechte haben.",
@@ -1631,6 +1943,14 @@
"ru": "No CI checks found for commit {sha}.", "ru": "No CI checks found for commit {sha}.",
"zh": "No CI checks found for commit {sha}." "zh": "No CI checks found for commit {sha}."
}, },
"No Python package found under src/ — skipping version check.": {
"bg": "",
"de": "",
"en": "No Python package found under src/ — skipping version check.",
"pl": "",
"ru": "",
"zh": ""
},
"No badge SVG files generated": { "No badge SVG files generated": {
"bg": "No badge SVG files generated", "bg": "No badge SVG files generated",
"de": "No badge SVG files generated", "de": "No badge SVG files generated",
@@ -1647,6 +1967,14 @@
"ru": "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" "zh": "No badge URLs found to update — README already up to date"
}, },
"No badge changes — skipping commit": {
"bg": "",
"de": "",
"en": "No badge changes — skipping commit",
"pl": "",
"ru": "",
"zh": ""
},
"No changes between {base} and {head}.": { "No changes between {base} and {head}.": {
"bg": "No changes between {base} and {head}.", "bg": "No changes between {base} and {head}.",
"de": "No changes between {base} and {head}.", "de": "No changes between {base} and {head}.",
@@ -1655,6 +1983,14 @@
"ru": "No changes between {base} and {head}.", "ru": "No changes between {base} and {head}.",
"zh": "No changes between {base} and {head}." "zh": "No changes between {base} and {head}."
}, },
"No changes to sync — wiki is up to date.": {
"bg": "",
"de": "",
"en": "No changes to sync — wiki is up to date.",
"pl": "",
"ru": "",
"zh": ""
},
"No failed jobs.": { "No failed jobs.": {
"bg": "No failed jobs.", "bg": "No failed jobs.",
"de": "No failed jobs.", "de": "No failed jobs.",
@@ -1687,6 +2023,14 @@
"ru": "No open PR found for branch '{branch}'.", "ru": "No open PR found for branch '{branch}'.",
"zh": "No open PR found for branch '{branch}'." "zh": "No open PR found for branch '{branch}'."
}, },
"No push needed (no changes or push failed).": {
"bg": "",
"de": "",
"en": "No push needed (no changes or push failed).",
"pl": "",
"ru": "",
"zh": ""
},
"No staged changes — version and changelog already up to date.": { "No staged changes — version and changelog already up to date.": {
"bg": "No staged changes — version and changelog already up to date.", "bg": "No staged changes — version and changelog already up to date.",
"de": "No staged changes — version and changelog already up to date.", "de": "No staged changes — version and changelog already up to date.",
@@ -1767,6 +2111,14 @@
"ru": "Note: Self-approval not allowed. Posting COMMENT instead.", "ru": "Note: Self-approval not allowed. Posting COMMENT instead.",
"zh": "Note: Self-approval not allowed. Posting COMMENT instead." "zh": "Note: Self-approval not allowed. Posting COMMENT instead."
}, },
"Nothing to push.": {
"bg": "Nothing to push.",
"de": "Nothing to push.",
"en": "Nothing to push.",
"pl": "Nothing to push.",
"ru": "Nothing to push.",
"zh": "Nothing to push."
},
"Only check staged files (for pre-commit)": { "Only check staged files (for pre-commit)": {
"bg": "Only check staged files (for pre-commit)", "bg": "Only check staged files (for pre-commit)",
"de": "Only check staged files (for pre-commit)", "de": "Only check staged files (for pre-commit)",
@@ -1871,6 +2223,14 @@
"ru": "PASSED: {pair}", "ru": "PASSED: {pair}",
"zh": "PASSED: {pair}" "zh": "PASSED: {pair}"
}, },
"PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.": {
"bg": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"de": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"en": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"pl": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"ru": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"zh": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR."
},
"PR already exists: #{index} — {url}": { "PR already exists: #{index} — {url}": {
"bg": "PR вече съществува: #{index} — {url}", "bg": "PR вече съществува: #{index} — {url}",
"de": "PR existiert bereits: #{index} — {url}", "de": "PR existiert bereits: #{index} — {url}",
@@ -2047,6 +2407,14 @@
"ru": "Publishing release {tag}...", "ru": "Publishing release {tag}...",
"zh": "Publishing release {tag}..." "zh": "Publishing release {tag}..."
}, },
"Push attempt {n}/3 failed: {err}": {
"bg": "Push attempt {n}/3 failed: {err}",
"de": "Push attempt {n}/3 failed: {err}",
"en": "Push attempt {n}/3 failed: {err}",
"pl": "Push attempt {n}/3 failed: {err}",
"ru": "Push attempt {n}/3 failed: {err}",
"zh": "Push attempt {n}/3 failed: {err}"
},
"Push failed for {tag}: {error}": { "Push failed for {tag}: {error}": {
"bg": "Push failed for {tag}: {error}", "bg": "Push failed for {tag}: {error}",
"de": "Push failed for {tag}: {error}", "de": "Push failed for {tag}: {error}",
@@ -2055,6 +2423,14 @@
"ru": "Push failed for {tag}: {error}", "ru": "Push failed for {tag}: {error}",
"zh": "Push failed for {tag}: {error}" "zh": "Push failed for {tag}: {error}"
}, },
"Push failed: {error}": {
"bg": "",
"de": "",
"en": "Push failed: {error}",
"pl": "",
"ru": "",
"zh": ""
},
"Pushed README update with badge SHA {sha}": { "Pushed README update with badge SHA {sha}": {
"bg": "Pushed README update with badge SHA {sha}", "bg": "Pushed README update with badge SHA {sha}",
"de": "Pushed README update with badge SHA {sha}", "de": "Pushed README update with badge SHA {sha}",
@@ -2071,6 +2447,14 @@
"ru": "Pushed release commit to master.", "ru": "Pushed release commit to master.",
"zh": "Pushed release commit to master." "zh": "Pushed release commit to master."
}, },
"Pushed {branch} to origin.": {
"bg": "Pushed {branch} to origin.",
"de": "Pushed {branch} to origin.",
"en": "Pushed {branch} to origin.",
"pl": "Pushed {branch} to origin.",
"ru": "Pushed {branch} to origin.",
"zh": "Pushed {branch} to origin."
},
"PyPI publish failed (non-fatal — continuing to Gitea release):\n{error}": { "PyPI publish failed (non-fatal — continuing to Gitea release):\n{error}": {
"bg": "Публикуването в PyPI неуспешно (некритично — продължава към Gitea release):\n{error}", "bg": "Публикуването в PyPI неуспешно (некритично — продължава към Gitea release):\n{error}",
"de": "PyPI-Veröffentlichung fehlgeschlagen (nicht fatal — Gitea-Release wird fortgesetzt):\n{error}", "de": "PyPI-Veröffentlichung fehlgeschlagen (nicht fatal — Gitea-Release wird fortgesetzt):\n{error}",
@@ -2087,6 +2471,46 @@
"ru": "REPO argument is required (or set GITHUB_REPOSITORY env var).", "ru": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
"zh": "REPO argument is required (or set GITHUB_REPOSITORY env var)." "zh": "REPO argument is required (or set GITHUB_REPOSITORY env var)."
}, },
"Rebase attempt {n}/3 failed: {err}": {
"bg": "Rebase attempt {n}/3 failed: {err}",
"de": "Rebase attempt {n}/3 failed: {err}",
"en": "Rebase attempt {n}/3 failed: {err}",
"pl": "Rebase attempt {n}/3 failed: {err}",
"ru": "Rebase attempt {n}/3 failed: {err}",
"zh": "Rebase attempt {n}/3 failed: {err}"
},
"Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue": {
"bg": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"de": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"en": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"pl": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"ru": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"zh": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue"
},
"Rebase failed with HTTP {status}: {message}": {
"bg": "Rebase failed with HTTP {status}: {message}",
"de": "Rebase failed with HTTP {status}: {message}",
"en": "Rebase failed with HTTP {status}: {message}",
"pl": "Rebase failed with HTTP {status}: {message}",
"ru": "Rebase failed with HTTP {status}: {message}",
"zh": "Rebase failed with HTTP {status}: {message}"
},
"Rebase successful.": {
"bg": "Rebase successful.",
"de": "Rebase successful.",
"en": "Rebase successful.",
"pl": "Rebase successful.",
"ru": "Rebase successful.",
"zh": "Rebase successful."
},
"Rebasing PR #{pr} via Gitea API...": {
"bg": "Rebasing PR #{pr} via Gitea API...",
"de": "Rebasing PR #{pr} via Gitea API...",
"en": "Rebasing PR #{pr} via Gitea API...",
"pl": "Rebasing PR #{pr} via Gitea API...",
"ru": "Rebasing PR #{pr} via Gitea API...",
"zh": "Rebasing PR #{pr} via Gitea API..."
},
"Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars": { "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars": {
"bg": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars", "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", "de": "Registry credentials required: set CI_GITEA_TOKEN and CI_GITEA_USERNAME env vars",
@@ -2127,14 +2551,6 @@
"ru": "Release commit — skipping all post-merge jobs.", "ru": "Release commit — skipping all post-merge jobs.",
"zh": "Release commit — skipping all post-merge jobs." "zh": "Release commit — skipping all post-merge jobs."
}, },
"Automated CI commit (badge) — skipping post-merge jobs.": {
"bg": "Automated CI commit (badge) — skipping post-merge jobs.",
"de": "Automated CI commit (badge) — skipping post-merge jobs.",
"en": "Automated CI commit (badge) — skipping post-merge jobs.",
"pl": "Automated CI commit (badge) — skipping post-merge jobs.",
"ru": "Automated CI commit (badge) — skipping post-merge jobs.",
"zh": "Automated CI commit (badge) — skipping post-merge jobs."
},
"Release creation failed: {error}": { "Release creation failed: {error}": {
"bg": "Release creation failed: {error}", "bg": "Release creation failed: {error}",
"de": "Release creation failed: {error}", "de": "Release creation failed: {error}",
@@ -2191,6 +2607,14 @@
"ru": "Владелец репозитория не установлен. Используйте --owner или DEVX_REPO_OWNER env var.", "ru": "Владелец репозитория не установлен. Используйте --owner или DEVX_REPO_OWNER env var.",
"zh": "仓库所有者未设置。使用 --owner 或 DEVX_REPO_OWNER 环境变量。" "zh": "仓库所有者未设置。使用 --owner 或 DEVX_REPO_OWNER 环境变量。"
}, },
"Required tools missing.": {
"en": "Required tools missing.",
"bg": "Липсват задължителни инструменти.",
"de": "Erforderliche Werkzeuge fehlen.",
"pl": "Brak wymaganych narzędzi.",
"ru": "Отсутствуют обязательные инструменты.",
"zh": "缺少必需的工具。"
},
"Review body must be at least 50 characters.": { "Review body must be at least 50 characters.": {
"bg": "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.", "de": "Review body must be at least 50 characters.",
@@ -2279,6 +2703,30 @@
"ru": "Running: {scenario} on {platform}", "ru": "Running: {scenario} on {platform}",
"zh": "Running: {scenario} on {platform}" "zh": "Running: {scenario} on {platform}"
}, },
"SSH key set up successfully": {
"en": "SSH key set up successfully",
"bg": "SSH ключът е настроен успешно",
"de": "SSH-Schlüssel erfolgreich eingerichtet",
"pl": "Klucz SSH skonfigurowany pomyślnie",
"ru": "SSH-ключ успешно настроен",
"zh": "SSH 密钥设置成功"
},
"SSH key setup skipped (no key provided)": {
"en": "SSH key setup skipped (no key provided)",
"bg": "Настройката на SSH ключ е пропусната (не е предоставен ключ)",
"de": "SSH-Schlüssel-Setup übersprungen (kein Schlüssel bereitgestellt)",
"pl": "Pominięto konfigurację klucza SSH (brak klucza)",
"ru": "Настройка SSH-ключа пропущена (ключ не предоставлен)",
"zh": "SSH 密钥设置已跳过(未提供密钥)"
},
"SSH_PRIVATE_KEY not set — skipping SSH key setup": {
"en": "SSH_PRIVATE_KEY not set — skipping SSH key setup",
"bg": "SSH_PRIVATE_KEY не е зададен — пропускане на SSH ключ настройката",
"de": "SSH_PRIVATE_KEY nicht gesetzt — SSH-Schlüssel-Setup übersprungen",
"pl": "SSH_PRIVATE_KEY nie ustawione — pomijanie konfiguracji klucza SSH",
"ru": "SSH_PRIVATE_KEY не задан — пропуск настройки SSH-ключа",
"zh": "SSH_PRIVATE_KEY 未设置 — 跳过 SSH 密钥设置"
},
"Skip Vikunja title match check": { "Skip Vikunja title match check": {
"bg": "Skip Vikunja title match check", "bg": "Skip Vikunja title match check",
"de": "Skip Vikunja title match check", "de": "Skip Vikunja title match check",
@@ -2319,13 +2767,21 @@
"ru": "Synced to latest origin/{branch}", "ru": "Synced to latest origin/{branch}",
"zh": "Synced to latest origin/{branch}" "zh": "Synced to latest origin/{branch}"
}, },
"Syncing {count} documentation pages to wiki...": { "Syncing files...": {
"bg": "Syncing {count} documentation pages to wiki...", "bg": "",
"de": "Syncing {count} documentation pages to wiki...", "de": "",
"en": "Syncing {count} documentation pages to wiki...", "en": "Syncing files...",
"pl": "Synchronizowanie {count} stron dokumentacji do wiki...", "pl": "",
"ru": "Syncing {count} documentation pages to wiki...", "ru": "",
"zh": "Syncing {count} documentation pages to wiki..." "zh": ""
},
"Syncing {count} documentation pages to wiki via Git...": {
"bg": "",
"de": "",
"en": "Syncing {count} documentation pages to wiki via Git...",
"pl": "",
"ru": "",
"zh": ""
}, },
"Tag consistency check failed.": { "Tag consistency check failed.": {
"bg": "Tag consistency check failed.", "bg": "Tag consistency check failed.",
@@ -2439,6 +2895,14 @@
"ru": "Updated badge URLs in {filename}", "ru": "Updated badge URLs in {filename}",
"zh": "Updated badge URLs in {filename}" "zh": "Updated badge URLs in {filename}"
}, },
"Updated documentation version references to v{version}": {
"bg": "",
"de": "",
"en": "Updated documentation version references to v{version}",
"pl": "",
"ru": "",
"zh": ""
},
"Updated version in {init}": { "Updated version in {init}": {
"bg": "Updated version in {init}", "bg": "Updated version in {init}",
"de": "Updated version in {init}", "de": "Updated version in {init}",
@@ -2455,6 +2919,14 @@
"ru": "Updated {changelog_file}", "ru": "Updated {changelog_file}",
"zh": "Updated {changelog_file}" "zh": "Updated {changelog_file}"
}, },
"Use string comparison or _is_truthy()/_is_falsy() helpers instead. Add '{marker}' to suppress individual lines.": {
"en": "Use string comparison or _is_truthy()/_is_falsy() helpers instead. Add '{marker}' to suppress individual lines.",
"bg": "Използвайте сравнение на низове или _is_truthy()/_is_falsy() помощници. Добавете '{marker}' за потискане на отделни редове.",
"de": "Verwenden Sie String-Vergleich oder _is_truthy()/_is_falsy() Hilfsfunktionen. Fügen Sie '{marker}' hinzu, um einzelne Zeilen zu unterdrücken.",
"pl": "Użyj porównania ciągów lub pomocników _is_truthy()/_is_falsy(). Dodaj '{marker}', aby pominąć pojedyncze linie.",
"ru": "Используйте строковое сравнение или помощники _is_truthy()/_is_falsy(). Добавьте '{marker}' для подавления отдельных строк.",
"zh": "使用字符串比较或 _is_truthy()/_is_falsy() 辅助函数。添加 '{marker}' 以抑制个别行。"
},
"VIKUNJA_TOKEN is not set. Required to derive PR title.": { "VIKUNJA_TOKEN is not set. Required to derive PR title.": {
"bg": "VIKUNJA_TOKEN не е зададен. Необходим за извличане на PR заглавие.", "bg": "VIKUNJA_TOKEN не е зададен. Необходим за извличане на PR заглавие.",
"de": "VIKUNJA_TOKEN nicht gesetzt. Erforderlich zum Ableiten des PR-Titels.", "de": "VIKUNJA_TOKEN nicht gesetzt. Erforderlich zum Ableiten des PR-Titels.",
@@ -2511,6 +2983,38 @@
"ru": "Задача Vikunja {task_id} не найдена в проекте {project_id}.\n Сначала создайте её:\n python -m devx.tools.create_task --title \"Заголовок задачи\"\n Или проверьте, что ID задачи в имени ветки корректен.", "ru": "Задача Vikunja {task_id} не найдена в проекте {project_id}.\n Сначала создайте её:\n python -m devx.tools.create_task --title \"Заголовок задачи\"\n Или проверьте, что ID задачи в имени ветки корректен.",
"zh": "在项目 {project_id} 中找不到 Vikunja 任务 {task_id}。\n 请先创建:\n python -m devx.tools.create_task --title \"任务标题\"\n 或检查分支名称中的任务 ID 是否正确。" "zh": "在项目 {project_id} 中找不到 Vikunja 任务 {task_id}。\n 请先创建:\n python -m devx.tools.create_task --title \"任务标题\"\n 或检查分支名称中的任务 ID 是否正确。"
}, },
"WARN: .venv has Python {version}, but >={req} is required.": {
"en": "WARN: .venv has Python {version}, but >={req} is required.",
"bg": "ПРЕДУПРЕЖДЕНИЕ: .venv има Python {version}, но се изисква >={req}.",
"de": "WARNUNG: .venv hat Python {version}, aber >={req} ist erforderlich.",
"pl": "OSTRZEŻENIE: .venv ma Python {version}, ale wymagane jest >={req}.",
"ru": "ПРЕДУПРЕЖДЕНИЕ: в .venv установлен Python {version}, но требуется >={req}.",
"zh": "警告: .venv 的 Python 版本为 {version},但要求 >={req}。"
},
"WARN: .venv not found. Run 'make setup-venv' to create it.": {
"en": "WARN: .venv not found. Run 'make setup-venv' to create it.",
"bg": "ПРЕДУПРЕЖДЕНИЕ: .venv не е намерен. Изпълнете 'make setup-venv' за създаване.",
"de": "WARNUNG: .venv nicht gefunden. Führen Sie 'make setup-venv' aus, um es zu erstellen.",
"pl": "OSTRZEŻENIE: Nie znaleziono .venv. Uruchom 'make setup-venv', aby utworzyć.",
"ru": "ПРЕДУПРЕЖДЕНИЕ: .venv не найден. Выполните 'make setup-venv' для создания.",
"zh": "警告: 未找到 .venv。运行 'make setup-venv' 来创建。"
},
"WARN: Could not determine Python version in .venv.": {
"en": "WARN: Could not determine Python version in .venv.",
"bg": "ПРЕДУПРЕЖДЕНИЕ: Не може да се определи версията на Python в .venv.",
"de": "WARNUNG: Python-Version in .venv konnte nicht bestimmt werden.",
"pl": "OSTRZEŻENIE: Nie można określić wersji Python w .venv.",
"ru": "ПРЕДУПРЕЖДЕНИЕ: Не удалось определить версию Python в .venv.",
"zh": "警告: 无法确定 .venv 中的 Python 版本。"
},
"WARN: Could not parse Python version '{version}'.": {
"en": "WARN: Could not parse Python version '{version}'.",
"bg": "ПРЕДУПРЕЖДЕНИЕ: Не може да се анализира версията на Python '{version}'.",
"de": "WARNUNG: Python-Version '{version}' konnte nicht analysiert werden.",
"pl": "OSTRZEŻENIE: Nie można przeanalizować wersji Python '{version}'.",
"ru": "ПРЕДУПРЕЖДЕНИЕ: Не удалось разобрать версию Python '{version}'.",
"zh": "警告: 无法解析 Python 版本 '{version}'。"
},
"WARNING: --skip-tests passed — skipping test verification.": { "WARNING: --skip-tests passed — skipping test verification.": {
"bg": "WARNING: --skip-tests passed — skipping test verification.", "bg": "WARNING: --skip-tests passed — skipping test verification.",
"de": "WARNING: --skip-tests passed — skipping test verification.", "de": "WARNING: --skip-tests passed — skipping test verification.",
@@ -2527,22 +3031,6 @@
"ru": "ВНИМАНИЕ: Файл .taskid ({file_id}) устарел и не совпадает с именем ветки ({branch_id}). Удалите .taskid из репозитория — имя ветки — единственный источник истины.", "ru": "ВНИМАНИЕ: Файл .taskid ({file_id}) устарел и не совпадает с именем ветки ({branch_id}). Удалите .taskid из репозитория — имя ветки — единственный источник истины.",
"zh": "警告:.taskid 文件 ({file_id}) 已弃用,与分支名称 ({branch_id}) 不一致。请从仓库中删除 .taskid — 分支名称是唯一的真实来源。" "zh": "警告:.taskid 文件 ({file_id}) 已弃用,与分支名称 ({branch_id}) 不一致。请从仓库中删除 .taskid — 分支名称是唯一的真实来源。"
}, },
"WARNING: Could not fetch wiki page list after retries. The sync itself succeeded ({count} pages updated), but the integrity check could not verify them due to a transient API issue.": {
"bg": "WARNING: Could not fetch wiki page list after retries. The sync itself succeeded ({count} pages updated), but the integrity check could not verify them due to a transient API issue.",
"de": "WARNING: Could not fetch wiki page list after retries. The sync itself succeeded ({count} pages updated), but the integrity check could not verify them due to a transient API issue.",
"en": "WARNING: Could not fetch wiki page list after retries. The sync itself succeeded ({count} pages updated), but the integrity check could not verify them due to a transient API issue.",
"pl": "OSTRZEŻENIE: Nie można pobrać listy stron wiki po ponownych próbach. Sama synchronizacja zakończyła się sukcesem (zaktualizowano {count} stron), ale kontrola integralności nie mogła ich zweryfikować z powodu przejściowego problemu z API.",
"ru": "WARNING: Could not fetch wiki page list after retries. The sync itself succeeded ({count} pages updated), but the integrity check could not verify them due to a transient API issue.",
"zh": "WARNING: Could not fetch wiki page list after retries. The sync itself succeeded ({count} pages updated), but the integrity check could not verify them due to a transient API issue."
},
"WARNING: Could not re-fetch wiki page list for verification. Skipping content verification due to transient API issue.": {
"bg": "WARNING: Could not re-fetch wiki page list for verification. Skipping content verification due to transient API issue.",
"de": "WARNING: Could not re-fetch wiki page list for verification. Skipping content verification due to transient API issue.",
"en": "WARNING: Could not re-fetch wiki page list for verification. Skipping content verification due to transient API issue.",
"pl": "OSTRZEŻENIE: Nie można ponownie pobrać listy stron wiki do weryfikacji. Pomijanie weryfikacji treści z powodu przejściowego problemu z API.",
"ru": "WARNING: Could not re-fetch wiki page list for verification. Skipping content verification due to transient API issue.",
"zh": "WARNING: Could not re-fetch wiki page list for verification. Skipping content verification due to transient API issue."
},
"WARNING: VIKUNJA_TOKEN not set — skipping task existence check. Set it in .env to enable full validation.": { "WARNING: VIKUNJA_TOKEN not set — skipping task existence check. Set it in .env to enable full validation.": {
"bg": "ПРЕДУПРЕЖДЕНИЕ: VIKUNJA_TOKEN не е зададен — пропускане на проверката за съществуване на задача. Задайте го в .env за пълна валидация.", "bg": "ПРЕДУПРЕЖДЕНИЕ: VIKUNJA_TOKEN не е зададен — пропускане на проверката за съществуване на задача. Задайте го в .env за пълна валидация.",
"de": "WARNUNG: VIKUNJA_TOKEN nicht gesetzt — Task-Existenzprüfung übersprungen. In .env setzen für volle Validierung.", "de": "WARNUNG: VIKUNJA_TOKEN nicht gesetzt — Task-Existenzprüfung übersprungen. In .env setzen für volle Validierung.",
@@ -2551,6 +3039,30 @@
"ru": "ПРЕДУПРЕЖДЕНИЕ: VIKUNJA_TOKEN не установлен — пропуск проверки существования задачи. Установите в .env для полной проверки.", "ru": "ПРЕДУПРЕЖДЕНИЕ: VIKUNJA_TOKEN не установлен — пропуск проверки существования задачи. Установите в .env для полной проверки.",
"zh": "警告: VIKUNJA_TOKEN 未设置 — 跳过任务存在性检查。在 .env 中设置以启用完整验证。" "zh": "警告: VIKUNJA_TOKEN 未设置 — 跳过任务存在性检查。在 .env 中设置以启用完整验证。"
}, },
"WARNING: Version badge shows stale version (expected v{version}) — regenerating": {
"bg": "",
"de": "",
"en": "WARNING: Version badge shows stale version (expected v{version}) — regenerating",
"pl": "",
"ru": "",
"zh": ""
},
"WARNING: check_doc_versions --fix failed (rc={rc}): {err}": {
"bg": "",
"de": "",
"en": "WARNING: check_doc_versions --fix failed (rc={rc}): {err}",
"pl": "",
"ru": "",
"zh": ""
},
"Waiting 5s for Gitea to process pushed commits...": {
"bg": "",
"de": "",
"en": "Waiting 5s for Gitea to process pushed commits...",
"pl": "",
"ru": "",
"zh": ""
},
"Waiting for CI checks to complete (timeout: {timeout}s)...": { "Waiting for CI checks to complete (timeout: {timeout}s)...": {
"bg": "Waiting for CI checks to complete (timeout: {timeout}s)...", "bg": "Waiting for CI checks to complete (timeout: {timeout}s)...",
"de": "Waiting for CI checks to complete (timeout: {timeout}s)...", "de": "Waiting for CI checks to complete (timeout: {timeout}s)...",
@@ -2615,21 +3127,37 @@
"ru": "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}" "zh": "Warning: repo-level runners query returned HTTP {status}"
}, },
"Wiki integrity check failed — {count} issue(s)": { "Wiki repo not found or empty — initializing fresh.": {
"bg": "Wiki integrity check failed — {count} issue(s)", "bg": "",
"de": "Wiki integrity check failed — {count} issue(s)", "de": "",
"en": "Wiki integrity check failed — {count} issue(s)", "en": "Wiki repo not found or empty — initializing fresh.",
"pl": "Kontrola integralności wiki nie powiodła się — {count} problem(ów)", "pl": "",
"ru": "Wiki integrity check failed — {count} issue(s)", "ru": "",
"zh": "Wiki integrity check failed — {count} issue(s)" "zh": ""
}, },
"Wiki verification failed — {failures} page(s) empty or mismatched": { "Wiki synced successfully.": {
"bg": "Wiki verification failed — {failures} page(s) empty or mismatched", "bg": "",
"de": "Wiki verification failed — {failures} page(s) empty or mismatched", "de": "",
"en": "Wiki verification failed — {failures} page(s) empty or mismatched", "en": "Wiki synced successfully.",
"pl": "Weryfikacja wiki nie powiodła się — {failures} strona(y) pusta lub niezgodna", "pl": "",
"ru": "Wiki verification failed — {failures} page(s) empty or mismatched", "ru": "",
"zh": "Wiki verification failed — {failures} page(s) empty or mismatched" "zh": ""
},
"Wiki verification failed — could not clone wiki": {
"bg": "",
"de": "",
"en": "Wiki verification failed — could not clone wiki",
"pl": "",
"ru": "",
"zh": ""
},
"Wiki verification failed — {failures} page(s) missing": {
"bg": "",
"de": "",
"en": "Wiki verification failed — {failures} page(s) missing",
"pl": "",
"ru": "",
"zh": ""
}, },
"Wrote tag {tag} to GITHUB_OUTPUT.": { "Wrote tag {tag} to GITHUB_OUTPUT.": {
"bg": "Wrote tag {tag} to GITHUB_OUTPUT.", "bg": "Wrote tag {tag} to GITHUB_OUTPUT.",
@@ -2639,6 +3167,14 @@
"ru": "Wrote tag {tag} to GITHUB_OUTPUT.", "ru": "Wrote tag {tag} to GITHUB_OUTPUT.",
"zh": "Wrote tag {tag} to GITHUB_OUTPUT." "zh": "Wrote tag {tag} to GITHUB_OUTPUT."
}, },
"[check-api-identity-checks] Passed: no unsafe identity checks found": {
"en": "[check-api-identity-checks] Passed: no unsafe identity checks found",
"bg": "[check-api-identity-checks] Мина: не са намерени небрежни проверки за идентичност",
"de": "[check-api-identity-checks] Bestanden: keine unsicheren Identitätsprüfungen gefunden",
"pl": "[check-api-identity-checks] Passed: nie znaleziono niebezpiecznych sprawdzeń tożsamości",
"ru": "[check-api-identity-checks] Пройдено: небезопасных проверок идентичности не найдено",
"zh": "[check-api-identity-checks] 通过:未发现不安全的身份检查"
},
"[check-dep-docs] Passed: all dependencies are documented": { "[check-dep-docs] Passed: all dependencies are documented": {
"bg": "[check-dep-docs] Passed: all dependencies are documented", "bg": "[check-dep-docs] Passed: all dependencies are documented",
"de": "[check-dep-docs] Passed: all dependencies are documented", "de": "[check-dep-docs] Passed: all dependencies are documented",
@@ -2647,6 +3183,30 @@
"ru": "[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" "zh": "[check-dep-docs] Passed: all dependencies are documented"
}, },
"[check-deps] All core tools present.": {
"en": "[check-deps] All core tools present.",
"bg": "[check-deps] Всички основни инструменти са налични.",
"de": "[check-deps] Alle Kernwerkzeuge vorhanden.",
"pl": "[check-deps] Wszystkie podstawowe narzędzia są dostępne.",
"ru": "[check-deps] Все основные инструменты доступны.",
"zh": "[check-deps] 所有核心工具均已就绪。"
},
"[check-deps] Verifying tools...": {
"en": "[check-deps] Verifying tools...",
"bg": "[check-deps] Проверка на инструментите...",
"de": "[check-deps] Werkzeuge werden überprüft...",
"pl": "[check-deps] Sprawdzanie narzędzi...",
"ru": "[check-deps] Проверка инструментов...",
"zh": "[check-deps] 正在验证工具..."
},
"[check-deps] Virtualenv .venv ready (Python {version}).": {
"en": "[check-deps] Virtualenv .venv ready (Python {version}).",
"bg": "[check-deps] Виртуална среда .venv готова (Python {version}).",
"de": "[check-deps] Virtuelle Umgebung .venv bereit (Python {version}).",
"pl": "[check-deps] Środowisko wirtualne .venv gotowe (Python {version}).",
"ru": "[check-deps] Виртуальное окружение .venv готово (Python {version}).",
"zh": "[check-deps] 虚拟环境 .venv 已就绪 (Python {version})。"
},
"[check-mutable-globals] Passed: no mutable path globals found": { "[check-mutable-globals] Passed: no mutable path globals found": {
"bg": "[check-mutable-globals] Passed: no mutable path globals found", "bg": "[check-mutable-globals] Passed: no mutable path globals found",
"de": "[check-mutable-globals] Passed: no mutable path globals found", "de": "[check-mutable-globals] Passed: no mutable path globals found",
@@ -2671,6 +3231,46 @@
"ru": "[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." "zh": "[check_test_coverage] No changed files to check."
}, },
"[docker-login] Logged in to {registry}.": {
"en": "[docker-login] Logged in to {registry}.",
"bg": "[docker-login] Влязъл в {registry}.",
"de": "[docker-login] Angemeldet bei {registry}.",
"pl": "[docker-login] Zalogowano do {registry}.",
"ru": "[docker-login] Выполнен вход в {registry}.",
"zh": "[docker-login] 已登录到 {registry}。"
},
"[docker-login] Login to {registry} failed (continuing).": {
"en": "[docker-login] Login to {registry} failed (continuing).",
"bg": "[docker-login] Влизането в {registry} не успя (продължава).",
"de": "[docker-login] Anmeldung bei {registry} fehlgeschlagen (wird fortgesetzt).",
"pl": "[docker-login] Logowanie do {registry} nie powiodło się (kontynuowanie).",
"ru": "[docker-login] Ошибка входа в {registry} (продолжаем).",
"zh": "[docker-login] 登录 {registry} 失败(继续)。"
},
"[docker-login] Skipping {registry} (token {env} not set).": {
"en": "[docker-login] Skipping {registry} (token {env} not set).",
"bg": "[docker-login] Пропускане на {registry} (токен {env} не е зададен).",
"de": "[docker-login] {registry} übersprungen (Token {env} nicht gesetzt).",
"pl": "[docker-login] Pomijanie {registry} (token {env} nie ustawiony).",
"ru": "[docker-login] Пропуск {registry} (токен {env} не задан).",
"zh": "[docker-login] 跳过 {registry}(未设置令牌 {env})。"
},
"[dry-run] No changes pushed.": {
"bg": "",
"de": "",
"en": "[dry-run] No changes pushed.",
"pl": "",
"ru": "",
"zh": ""
},
"[dry-run] Would commit and push wiki changes": {
"bg": "",
"de": "",
"en": "[dry-run] Would commit and push wiki changes",
"pl": "",
"ru": "",
"zh": ""
},
"[dry-run] Would commit: release: v{version} [skip ci]": { "[dry-run] Would commit: release: v{version} [skip ci]": {
"bg": "[dry-run] Would commit: release: v{version} [skip ci]", "bg": "[dry-run] Would commit: release: v{version} [skip ci]",
"de": "[dry-run] Would commit: release: v{version} [skip ci]", "de": "[dry-run] Would commit: release: v{version} [skip ci]",
@@ -2703,13 +3303,13 @@
"ru": "[dry-run] Would push commit to master", "ru": "[dry-run] Would push commit to master",
"zh": "[dry-run] Would push commit to master" "zh": "[dry-run] Would push commit to master"
}, },
"[dry-run] Would sync page: {title} ({chars} chars)": { "[dry-run] Would update doc version references via check_doc_versions --fix": {
"bg": "[dry-run] Would sync page: {title} ({chars} chars)", "bg": "",
"de": "[dry-run] Would sync page: {title} ({chars} chars)", "de": "",
"en": "[dry-run] Would sync page: {title} ({chars} chars)", "en": "[dry-run] Would update doc version references via check_doc_versions --fix",
"pl": "[dry-run] Zsynchronizowano by stronę: {title} ({chars} znaków)", "pl": "",
"ru": "[dry-run] Would sync page: {title} ({chars} chars)", "ru": "",
"zh": "[dry-run] Would sync page: {title} ({chars} chars)" "zh": ""
}, },
"[dry-run] Would update {changelog_file}": { "[dry-run] Would update {changelog_file}": {
"bg": "[dry-run] Would update {changelog_file}", "bg": "[dry-run] Would update {changelog_file}",
@@ -2727,6 +3327,38 @@
"ru": "[dry-run] Would update {init}", "ru": "[dry-run] Would update {init}",
"zh": "[dry-run] Would update {init}" "zh": "[dry-run] Would update {init}"
}, },
"[tofu-init] Done.": {
"en": "[tofu-init] Done.",
"bg": "[tofu-init] Готово.",
"de": "[tofu-init] Fertig.",
"pl": "[tofu-init] Gotowe.",
"ru": "[tofu-init] Готово.",
"zh": "[tofu-init] 完成。"
},
"[tofu-init] Initializing {dir}...": {
"en": "[tofu-init] Initializing {dir}...",
"bg": "[tofu-init] Инициализиране на {dir}...",
"de": "[tofu-init] Initialisiere {dir}...",
"pl": "[tofu-init] Inicjalizacja {dir}...",
"ru": "[tofu-init] Инициализация {dir}...",
"zh": "[tofu-init] 正在初始化 {dir}..."
},
"[tofu-{mode}] All configurations valid.": {
"en": "[tofu-{mode}] All configurations valid.",
"bg": "[tofu-{mode}] Всички конфигурации са валидни.",
"de": "[tofu-{mode}] Alle Konfigurationen gültig.",
"pl": "[tofu-{mode}] Wszystkie konfiguracje są poprawne.",
"ru": "[tofu-{mode}] Все конфигурации валидны.",
"zh": "[tofu-{mode}] 所有配置有效。"
},
"[tofu-{mode}] Validating OpenTofu configurations...": {
"en": "[tofu-{mode}] Validating OpenTofu configurations...",
"bg": "[tofu-{mode}] Проверка на OpenTofu конфигурациите...",
"de": "[tofu-{mode}] Validiere OpenTofu-Konfigurationen...",
"pl": "[tofu-{mode}] Sprawdzanie konfiguracji OpenTofu...",
"ru": "[tofu-{mode}] Проверка конфигураций OpenTofu...",
"zh": "[tofu-{mode}] 正在验证 OpenTofu 配置..."
},
"[tool.devx] missing required keys: {keys}": { "[tool.devx] missing required keys: {keys}": {
"bg": "[tool.devx] липсват задължителни ключове: {keys}", "bg": "[tool.devx] липсват задължителни ключове: {keys}",
"de": "[tool.devx] fehlt erforderliche Schlüssel: {keys}", "de": "[tool.devx] fehlt erforderliche Schlüssel: {keys}",
@@ -2879,6 +3511,14 @@
"ru": "tea not installed — skipping login configuration.", "ru": "tea not installed — skipping login configuration.",
"zh": "tea not installed — skipping login configuration." "zh": "tea not installed — skipping login configuration."
}, },
"tofu command failed in {dir}: {error}": {
"en": "tofu command failed in {dir}: {error}",
"bg": "командата tofu не успя в {dir}: {error}",
"de": "tofu-Befehl fehlgeschlagen in {dir}: {error}",
"pl": "polecenie tofu nie powiodło się w {dir}: {error}",
"ru": "команда tofu не удалась в {dir}: {error}",
"zh": "tofu 命令在 {dir} 中失败: {error}"
},
"unknown": { "unknown": {
"bg": "неизвестен", "bg": "неизвестен",
"de": "unbekannt", "de": "unbekannt",
@@ -2887,286 +3527,6 @@
"ru": "неизвестно", "ru": "неизвестно",
"zh": "未知" "zh": "未知"
}, },
"{file} already exists. Use --force to overwrite.": {
"bg": "{file} already exists. Use --force to overwrite.",
"de": "{file} already exists. Use --force to overwrite.",
"en": "{file} already exists. Use --force to overwrite.",
"pl": "{file} już istnieje. Użyj --force, aby nadpisać.",
"ru": "{file} already exists. Use --force to overwrite.",
"zh": "{file} already exists. Use --force to overwrite."
},
"{separator}": {
"bg": "{separator}",
"de": "{separator}",
"en": "{separator}",
"pl": "{separator}",
"ru": "{separator}",
"zh": "{separator}"
},
"Failed to push release commit after 3 attempts. Manual intervention required.": {
"bg": "Failed to push release commit after 3 attempts. Manual intervention required.",
"de": "Failed to push release commit after 3 attempts. Manual intervention required.",
"en": "Failed to push release commit after 3 attempts. Manual intervention required.",
"pl": "Failed to push release commit after 3 attempts. Manual intervention required.",
"ru": "Failed to push release commit after 3 attempts. Manual intervention required.",
"zh": "Failed to push release commit after 3 attempts. Manual intervention required."
},
"Push attempt {n}/3 failed: {err}": {
"bg": "Push attempt {n}/3 failed: {err}",
"de": "Push attempt {n}/3 failed: {err}",
"en": "Push attempt {n}/3 failed: {err}",
"pl": "Push attempt {n}/3 failed: {err}",
"ru": "Push attempt {n}/3 failed: {err}",
"zh": "Push attempt {n}/3 failed: {err}"
},
"Rebase attempt {n}/3 failed: {err}": {
"bg": "Rebase attempt {n}/3 failed: {err}",
"de": "Rebase attempt {n}/3 failed: {err}",
"en": "Rebase attempt {n}/3 failed: {err}",
"pl": "Rebase attempt {n}/3 failed: {err}",
"ru": "Rebase attempt {n}/3 failed: {err}",
"zh": "Rebase attempt {n}/3 failed: {err}"
},
"Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.": {
"bg": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"de": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"en": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"pl": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"ru": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
"zh": "Auto-rebase failed with HTTP {status}: {message}\nRebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label."
},
"Branch is already up-to-date with origin/master.": {
"bg": "Branch is already up-to-date with origin/master.",
"de": "Branch is already up-to-date with origin/master.",
"en": "Branch is already up-to-date with origin/master.",
"pl": "Branch is already up-to-date with origin/master.",
"ru": "Branch is already up-to-date with origin/master.",
"zh": "Branch is already up-to-date with origin/master."
},
"Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.": {
"bg": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"de": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"en": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"pl": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"ru": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR.",
"zh": "Branch is behind master. Auto-rebasing via Gitea API...\nA new CI run will start automatically after the rebase.\nThe next auto-merge attempt will merge this PR."
},
"Branch is {count} commit(s) behind master. Rebasing...": {
"bg": "Branch is {count} commit(s) behind master. Rebasing...",
"de": "Branch is {count} commit(s) behind master. Rebasing...",
"en": "Branch is {count} commit(s) behind master. Rebasing...",
"pl": "Branch is {count} commit(s) behind master. Rebasing...",
"ru": "Branch is {count} commit(s) behind master. Rebasing...",
"zh": "Branch is {count} commit(s) behind master. Rebasing..."
},
"CI_GITEA_TOKEN is not set. Add it to .env or export it.": {
"bg": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"de": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"en": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"pl": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"ru": "CI_GITEA_TOKEN is not set. Add it to .env or export it.",
"zh": "CI_GITEA_TOKEN is not set. Add it to .env or export it."
},
"Cannot rebase: not on a branch (detached HEAD).": {
"bg": "Cannot rebase: not on a branch (detached HEAD).",
"de": "Cannot rebase: not on a branch (detached HEAD).",
"en": "Cannot rebase: not on a branch (detached HEAD).",
"pl": "Cannot rebase: not on a branch (detached HEAD).",
"ru": "Cannot rebase: not on a branch (detached HEAD).",
"zh": "Cannot rebase: not on a branch (detached HEAD)."
},
"Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.": {
"bg": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"de": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"en": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"pl": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"ru": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR.",
"zh": "Could not detect PR number. Use --pr to specify it explicitly,\nor run this command from a branch with an open PR."
},
"Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.": {
"bg": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"de": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"en": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"pl": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"ru": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables.",
"zh": "Could not determine repository. Set DEVX_REPO_OWNER and DEVX_REPO_NAME\nor GITHUB_REPOSITORY environment variables."
},
"Fetch failed: {error}": {
"bg": "Fetch failed: {error}",
"de": "Fetch failed: {error}",
"en": "Fetch failed: {error}",
"pl": "Fetch failed: {error}",
"ru": "Fetch failed: {error}",
"zh": "Fetch failed: {error}"
},
"Fetching origin/master...": {
"bg": "Fetching origin/master...",
"de": "Fetching origin/master...",
"en": "Fetching origin/master...",
"pl": "Fetching origin/master...",
"ru": "Fetching origin/master...",
"zh": "Fetching origin/master..."
},
"Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.": {
"bg": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"de": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"en": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"pl": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"ru": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again.",
"zh": "Force-push failed:\n{error}\nThe remote may have unexpected commits. Fetch and try again."
},
"Force-pushing...": {
"bg": "Force-pushing...",
"de": "Force-pushing...",
"en": "Force-pushing...",
"pl": "Force-pushing...",
"ru": "Force-pushing...",
"zh": "Force-pushing..."
},
"Nothing to push.": {
"bg": "Nothing to push.",
"de": "Nothing to push.",
"en": "Nothing to push.",
"pl": "Nothing to push.",
"ru": "Nothing to push.",
"zh": "Nothing to push."
},
"PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.": {
"bg": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"de": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"en": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"pl": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"ru": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR.",
"zh": "PR #{pr} rebased successfully. A new CI run will start automatically.\nIf auto-merge is enabled (ready-to-merge label), the next CI run\nwill attempt to merge this PR."
},
"Pushed {branch} to origin.": {
"bg": "Pushed {branch} to origin.",
"de": "Pushed {branch} to origin.",
"en": "Pushed {branch} to origin.",
"pl": "Pushed {branch} to origin.",
"ru": "Pushed {branch} to origin.",
"zh": "Pushed {branch} to origin."
},
"Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue": {
"bg": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"de": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"en": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"pl": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"ru": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue",
"zh": "Rebase failed (conflicts or other error):\n{error}\nResolve conflicts and run: git rebase --continue"
},
"Rebase failed with HTTP {status}: {message}": {
"bg": "Rebase failed with HTTP {status}: {message}",
"de": "Rebase failed with HTTP {status}: {message}",
"en": "Rebase failed with HTTP {status}: {message}",
"pl": "Rebase failed with HTTP {status}: {message}",
"ru": "Rebase failed with HTTP {status}: {message}",
"zh": "Rebase failed with HTTP {status}: {message}"
},
"Rebase successful.": {
"bg": "Rebase successful.",
"de": "Rebase successful.",
"en": "Rebase successful.",
"pl": "Rebase successful.",
"ru": "Rebase successful.",
"zh": "Rebase successful."
},
"Rebasing PR #{pr} via Gitea API...": {
"bg": "Rebasing PR #{pr} via Gitea API...",
"de": "Rebasing PR #{pr} via Gitea API...",
"en": "Rebasing PR #{pr} via Gitea API...",
"pl": "Rebasing PR #{pr} via Gitea API...",
"ru": "Rebasing PR #{pr} via Gitea API...",
"zh": "Rebasing PR #{pr} via Gitea API..."
},
"Ensuring standard labels...": {
"bg": "Ensuring standard labels...",
"de": "Ensuring standard labels...",
"en": "Ensuring standard labels...",
"pl": "Ensuring standard labels...",
"ru": "Ensuring standard labels...",
"zh": "Ensuring standard labels..."
},
" - {count} standard labels verified": {
"bg": " - {count} standard labels verified",
"de": " - {count} standard labels verified",
"en": " - {count} standard labels verified",
"pl": " - {count} standard labels verified",
"ru": " - {count} standard labels verified",
"zh": " - {count} standard labels verified"
},
"[check-deps] Virtualenv .venv ready (Python {version}).": {
"en": "[check-deps] Virtualenv .venv ready (Python {version}).",
"bg": "[check-deps] Виртуална среда .venv готова (Python {version}).",
"de": "[check-deps] Virtuelle Umgebung .venv bereit (Python {version}).",
"pl": "[check-deps] Środowisko wirtualne .venv gotowe (Python {version}).",
"ru": "[check-deps] Виртуальное окружение .venv готово (Python {version}).",
"zh": "[check-deps] 虚拟环境 .venv 已就绪 (Python {version})。"
},
"{level}: {tool} not found.{hint}": {
"en": "{level}: {tool} not found.{hint}",
"bg": "{level}: {tool} не е намерен.{hint}",
"de": "{level}: {tool} nicht gefunden.{hint}",
"pl": "{level}: {tool} nie znaleziono.{hint}",
"ru": "{level}: {tool} не найден.{hint}",
"zh": "{level}: 未找到 {tool}。{hint}"
},
"WARN: Could not determine Python version in .venv.": {
"en": "WARN: Could not determine Python version in .venv.",
"bg": "ПРЕДУПРЕЖДЕНИЕ: Не може да се определи версията на Python в .venv.",
"de": "WARNUNG: Python-Version in .venv konnte nicht bestimmt werden.",
"pl": "OSTRZEŻENIE: Nie można określić wersji Python w .venv.",
"ru": "ПРЕДУПРЕЖДЕНИЕ: Не удалось определить версию Python в .venv.",
"zh": "警告: 无法确定 .venv 中的 Python 版本。"
},
"WARN: Could not parse Python version '{version}'.": {
"en": "WARN: Could not parse Python version '{version}'.",
"bg": "ПРЕДУПРЕЖДЕНИЕ: Не може да се анализира версията на Python '{version}'.",
"de": "WARNUNG: Python-Version '{version}' konnte nicht analysiert werden.",
"pl": "OSTRZEŻENIE: Nie można przeanalizować wersji Python '{version}'.",
"ru": "ПРЕДУПРЕЖДЕНИЕ: Не удалось разобрать версию Python '{version}'.",
"zh": "警告: 无法解析 Python 版本 '{version}'。"
},
"WARN: .venv not found. Run 'make setup-venv' to create it.": {
"en": "WARN: .venv not found. Run 'make setup-venv' to create it.",
"bg": "ПРЕДУПРЕЖДЕНИЕ: .venv не е намерен. Изпълнете 'make setup-venv' за създаване.",
"de": "WARNUNG: .venv nicht gefunden. Führen Sie 'make setup-venv' aus, um es zu erstellen.",
"pl": "OSTRZEŻENIE: Nie znaleziono .venv. Uruchom 'make setup-venv', aby utworzyć.",
"ru": "ПРЕДУПРЕЖДЕНИЕ: .venv не найден. Выполните 'make setup-venv' для создания.",
"zh": "警告: 未找到 .venv。运行 'make setup-venv' 来创建。"
},
"[docker-login] Logged in to {registry}.": {
"en": "[docker-login] Logged in to {registry}.",
"bg": "[docker-login] Влязъл в {registry}.",
"de": "[docker-login] Angemeldet bei {registry}.",
"pl": "[docker-login] Zalogowano do {registry}.",
"ru": "[docker-login] Выполнен вход в {registry}.",
"zh": "[docker-login] 已登录到 {registry}。"
},
"[docker-login] Login to {registry} failed (continuing).": {
"en": "[docker-login] Login to {registry} failed (continuing).",
"bg": "[docker-login] Влизането в {registry} не успя (продължава).",
"de": "[docker-login] Anmeldung bei {registry} fehlgeschlagen (wird fortgesetzt).",
"pl": "[docker-login] Logowanie do {registry} nie powiodło się (kontynuowanie).",
"ru": "[docker-login] Ошибка входа в {registry} (продолжаем).",
"zh": "[docker-login] 登录 {registry} 失败(继续)。"
},
"[docker-login] Skipping {registry} (token {env} not set).": {
"en": "[docker-login] Skipping {registry} (token {env} not set).",
"bg": "[docker-login] Пропускане на {registry} (токен {env} не е зададен).",
"de": "[docker-login] {registry} übersprungen (Token {env} nicht gesetzt).",
"pl": "[docker-login] Pomijanie {registry} (token {env} nie ustawiony).",
"ru": "[docker-login] Пропуск {registry} (токен {env} не задан).",
"zh": "[docker-login] 跳过 {registry}(未设置令牌 {env})。"
},
"{env} is not set. Set it in your .env file.": {
"en": "{env} is not set. Set it in your .env file.",
"bg": "{env} не е зададен. Задайте го във вашия .env файл.",
"de": "{env} ist nicht gesetzt. Setzen Sie es in Ihrer .env-Datei.",
"pl": "{env} nie jest ustawiony. Ustaw go w pliku .env.",
"ru": "{env} не задан. Установите его в файле .env.",
"zh": "{env} 未设置。请在 .env 文件中设置。"
},
"{env} is not set. Set it in your .env file or pass it as an environment variable.": { "{env} is not set. Set it in your .env file or pass it as an environment variable.": {
"en": "{env} is not set. Set it in your .env file or pass it as an environment variable.", "en": "{env} is not set. Set it in your .env file or pass it as an environment variable.",
"bg": "{env} не е зададен. Задайте го във вашия .env файл или го подайте като променлива на средата.", "bg": "{env} не е зададен. Задайте го във вашия .env файл или го подайте като променлива на средата.",
@@ -3175,188 +3535,36 @@
"ru": "{env} не задан. Установите его в файле .env или передайте как переменную окружения.", "ru": "{env} не задан. Установите его в файле .env или передайте как переменную окружения.",
"zh": "{env} 未设置。请在 .env 文件中设置或作为环境变量传递。" "zh": "{env} 未设置。请在 .env 文件中设置或作为环境变量传递。"
}, },
"Login to {registry} failed: {error}": { "{env} is not set. Set it in your .env file.": {
"en": "Login to {registry} failed: {error}", "en": "{env} is not set. Set it in your .env file.",
"bg": "Влизането в {registry} не успя: {error}", "bg": "{env} не е зададен. Задайте го във вашия .env файл.",
"de": "Anmeldung bei {registry} fehlgeschlagen: {error}", "de": "{env} ist nicht gesetzt. Setzen Sie es in Ihrer .env-Datei.",
"pl": "Logowanie do {registry} nie powiodło się: {error}", "pl": "{env} nie jest ustawiony. Ustaw go w pliku .env.",
"ru": "Ошибка входа в {registry}: {error}", "ru": "{env} не задан. Установите его в файле .env.",
"zh": "登录 {registry} 失败: {error}" "zh": "{env} 未设置。请在 .env 文件中设置。"
}, },
"tofu command failed in {dir}: {error}": { "{file} already exists. Use --force to overwrite.": {
"en": "tofu command failed in {dir}: {error}", "bg": "{file} already exists. Use --force to overwrite.",
"bg": "командата tofu не успя в {dir}: {error}", "de": "{file} already exists. Use --force to overwrite.",
"de": "tofu-Befehl fehlgeschlagen in {dir}: {error}", "en": "{file} already exists. Use --force to overwrite.",
"pl": "polecenie tofu nie powiodło się w {dir}: {error}", "pl": "{file} już istnieje. Użyj --force, aby nadpisać.",
"ru": "команда tofu не удалась в {dir}: {error}", "ru": "{file} already exists. Use --force to overwrite.",
"zh": "tofu 命令在 {dir} 中失败: {error}" "zh": "{file} already exists. Use --force to overwrite."
}, },
"WARN: .venv has Python {version}, but >={req} is required.": { "{level}: {tool} not found.{hint}": {
"en": "WARN: .venv has Python {version}, but >={req} is required.", "en": "{level}: {tool} not found.{hint}",
"bg": "ПРЕДУПРЕЖДЕНИЕ: .venv има Python {version}, но се изисква >={req}.", "bg": "{level}: {tool} не е намерен.{hint}",
"de": "WARNUNG: .venv hat Python {version}, aber >={req} ist erforderlich.", "de": "{level}: {tool} nicht gefunden.{hint}",
"pl": "OSTRZEŻENIE: .venv ma Python {version}, ale wymagane jest >={req}.", "pl": "{level}: {tool} nie znaleziono.{hint}",
"ru": "ПРЕДУПРЕЖДЕНИЕ: в .venv установлен Python {version}, но требуется >={req}.", "ru": "{level}: {tool} не найден.{hint}",
"zh": "警告: .venv 的 Python 版本为 {version},但要求 >={req}。" "zh": "{level}: 未找到 {tool}。{hint}"
}, },
" -> {dir}": { "{separator}": {
"en": " -> {dir}", "bg": "{separator}",
"bg": " -> {dir}", "de": "{separator}",
"de": " -> {dir}", "en": "{separator}",
"pl": " -> {dir}", "pl": "{separator}",
"ru": " -> {dir}", "ru": "{separator}",
"zh": " -> {dir}" "zh": "{separator}"
},
"[tofu-init] Initializing {dir}...": {
"en": "[tofu-init] Initializing {dir}...",
"bg": "[tofu-init] Инициализиране на {dir}...",
"de": "[tofu-init] Initialisiere {dir}...",
"pl": "[tofu-init] Inicjalizacja {dir}...",
"ru": "[tofu-init] Инициализация {dir}...",
"zh": "[tofu-init] 正在初始化 {dir}..."
},
"[tofu-init] Done.": {
"en": "[tofu-init] Done.",
"bg": "[tofu-init] Готово.",
"de": "[tofu-init] Fertig.",
"pl": "[tofu-init] Gotowe.",
"ru": "[tofu-init] Готово.",
"zh": "[tofu-init] 完成。"
},
"[tofu-{mode}] Validating OpenTofu configurations...": {
"en": "[tofu-{mode}] Validating OpenTofu configurations...",
"bg": "[tofu-{mode}] Проверка на OpenTofu конфигурациите...",
"de": "[tofu-{mode}] Validiere OpenTofu-Konfigurationen...",
"pl": "[tofu-{mode}] Sprawdzanie konfiguracji OpenTofu...",
"ru": "[tofu-{mode}] Проверка конфигураций OpenTofu...",
"zh": "[tofu-{mode}] 正在验证 OpenTofu 配置..."
},
"[tofu-{mode}] All configurations valid.": {
"en": "[tofu-{mode}] All configurations valid.",
"bg": "[tofu-{mode}] Всички конфигурации са валидни.",
"de": "[tofu-{mode}] Alle Konfigurationen gültig.",
"pl": "[tofu-{mode}] Wszystkie konfiguracje są poprawne.",
"ru": "[tofu-{mode}] Все конфигурации валидны.",
"zh": "[tofu-{mode}] 所有配置有效。"
},
"[check-deps] Verifying tools...": {
"en": "[check-deps] Verifying tools...",
"bg": "[check-deps] Проверка на инструментите...",
"de": "[check-deps] Werkzeuge werden überprüft...",
"pl": "[check-deps] Sprawdzanie narzędzi...",
"ru": "[check-deps] Проверка инструментов...",
"zh": "[check-deps] 正在验证工具..."
},
" {tool}: found at {path}": {
"en": " {tool}: found at {path}",
"bg": " {tool}: намерен на {path}",
"de": " {tool}: gefunden unter {path}",
"pl": " {tool}: znaleziono w {path}",
"ru": " {tool}: найден в {path}",
"zh": " {tool}: 在 {path} 找到"
},
" Run 'make install-checkmake' to install the Makefile linter.": {
"en": " Run 'make install-checkmake' to install the Makefile linter.",
"bg": " Изпълнете 'make install-checkmake' за инсталиране на Makefile линтера.",
"de": " Führen Sie 'make install-checkmake' aus, um den Makefile-Linter zu installieren.",
"pl": " Uruchom 'make install-checkmake', aby zainstalować linter Makefile.",
"ru": " Выполните 'make install-checkmake' для установки линтера Makefile.",
"zh": " 运行 'make install-checkmake' 来安装 Makefile 检查器。"
},
"Required tools missing.": {
"en": "Required tools missing.",
"bg": "Липсват задължителни инструменти.",
"de": "Erforderliche Werkzeuge fehlen.",
"pl": "Brak wymaganych narzędzi.",
"ru": "Отсутствуют обязательные инструменты.",
"zh": "缺少必需的工具。"
},
"[check-deps] All core tools present.": {
"en": "[check-deps] All core tools present.",
"bg": "[check-deps] Всички основни инструменти са налични.",
"de": "[check-deps] Alle Kernwerkzeuge vorhanden.",
"pl": "[check-deps] Wszystkie podstawowe narzędzia są dostępne.",
"ru": "[check-deps] Все основные инструменты доступны.",
"zh": "[check-deps] 所有核心工具均已就绪。"
},
"SSH_PRIVATE_KEY not set — skipping SSH key setup": {
"en": "SSH_PRIVATE_KEY not set — skipping SSH key setup",
"bg": "SSH_PRIVATE_KEY не е зададен — пропускане на SSH ключ настройката",
"de": "SSH_PRIVATE_KEY nicht gesetzt — SSH-Schlüssel-Setup übersprungen",
"pl": "SSH_PRIVATE_KEY nie ustawione — pomijanie konfiguracji klucza SSH",
"ru": "SSH_PRIVATE_KEY не задан — пропуск настройки SSH-ключа",
"zh": "SSH_PRIVATE_KEY 未设置 — 跳过 SSH 密钥设置"
},
"Failed to start ssh-agent: {error}": {
"en": "Failed to start ssh-agent: {error}",
"bg": "Неуспешно стартиране на ssh-agent: {error}",
"de": "Starten von ssh-agent fehlgeschlagen: {error}",
"pl": "Nie udało się uruchomić ssh-agent: {error}",
"ru": "Не удалось запустить ssh-agent: {error}",
"zh": "启动 ssh-agent 失败: {error}"
},
"SSH key set up successfully": {
"en": "SSH key set up successfully",
"bg": "SSH ключът е настроен успешно",
"de": "SSH-Schlüssel erfolgreich eingerichtet",
"pl": "Klucz SSH skonfigurowany pomyślnie",
"ru": "SSH-ключ успешно настроен",
"zh": "SSH 密钥设置成功"
},
"SSH key setup skipped (no key provided)": {
"en": "SSH key setup skipped (no key provided)",
"bg": "Настройката на SSH ключ е пропусната (не е предоставен ключ)",
"de": "SSH-Schlüssel-Setup übersprungen (kein Schlüssel bereitgestellt)",
"pl": "Pominięto konfigurację klucza SSH (brak klucza)",
"ru": "Настройка SSH-ключа пропущена (ключ не предоставлен)",
"zh": "SSH 密钥设置已跳过(未提供密钥)"
},
"Found {count} unsafe identity check(s) in integration tests.": {
"en": "Found {count} unsafe identity check(s) in integration tests.",
"bg": "Намерени са {count} небрежни проверки за идентичност в интеграционните тестове.",
"de": "{count} unsichere Identitätsprüfung(en) in Integrationstests gefunden.",
"pl": "Znaleziono {count} niebezpiecznych sprawdzeń tożsamości w testach integracyjnych.",
"ru": "Найдено {count} небезопасных проверок идентичности в интеграционных тестах.",
"zh": "在集成测试中发现 {count} 个不安全的身份检查。"
},
"Use string comparison or _is_truthy()/_is_falsy() helpers instead. Add '{marker}' to suppress individual lines.": {
"en": "Use string comparison or _is_truthy()/_is_falsy() helpers instead. Add '{marker}' to suppress individual lines.",
"bg": "Използвайте сравнение на низове или _is_truthy()/_is_falsy() помощници. Добавете '{marker}' за потискане на отделни редове.",
"de": "Verwenden Sie String-Vergleich oder _is_truthy()/_is_falsy() Hilfsfunktionen. Fügen Sie '{marker}' hinzu, um einzelne Zeilen zu unterdrücken.",
"pl": "Użyj porównania ciągów lub pomocników _is_truthy()/_is_falsy(). Dodaj '{marker}', aby pominąć pojedyncze linie.",
"ru": "Используйте строковое сравнение или помощники _is_truthy()/_is_falsy(). Добавьте '{marker}' для подавления отдельных строк.",
"zh": "使用字符串比较或 _is_truthy()/_is_falsy() 辅助函数。添加 '{marker}' 以抑制个别行。"
},
"[check-api-identity-checks] Passed: no unsafe identity checks found": {
"en": "[check-api-identity-checks] Passed: no unsafe identity checks found",
"bg": "[check-api-identity-checks] Мина: не са намерени небрежни проверки за идентичност",
"de": "[check-api-identity-checks] Bestanden: keine unsicheren Identitätsprüfungen gefunden",
"pl": "[check-api-identity-checks] Passed: nie znaleziono niebezpiecznych sprawdzeń tożsamości",
"ru": "[check-api-identity-checks] Пройдено: небезопасных проверок идентичности не найдено",
"zh": "[check-api-identity-checks] 通过:未发现不安全的身份检查"
},
"Directory to scan (default: tests/integration). Can be repeated.": {
"en": "Directory to scan (default: tests/integration). Can be repeated.",
"bg": "Директория за сканиране (по подразбиране: tests/integration). Може да се повтаря.",
"de": "Zu scannendes Verzeichnis (Standard: tests/integration). Kann wiederholt werden.",
"pl": "Katalog do skanowania (domyślnie: tests/integration). Można powtarzać.",
"ru": "Директория для сканирования (по умолчанию: tests/integration). Можно повторять.",
"zh": "要扫描的目录(默认:tests/integration)。可重复。"
},
"Failed to list existing wiki pages after retries: {error}. Aborting to avoid creating duplicate pages.": {
"bg": "Неуспешно извличане на съществуващи wiki страници след повторни опити: {error}. Прекратяване, за да се избегне създаване на дублирани страници.",
"de": "Abrufen bestehender Wiki-Seiten nach Wiederholungen fehlgeschlagen: {error}. Abbruch, um doppelte Seiten zu vermeiden.",
"en": "Failed to list existing wiki pages after retries: {error}. Aborting to avoid creating duplicate pages.",
"pl": "Nie udało się wylistować istniejących stron wiki po ponownych próbach: {error}. Przerywanie, aby uniknąć tworzenia zduplikowanych stron.",
"ru": "Не удалось получить список существующих wiki-страниц после повторных попыток: {error}. Прерывание, чтобы избежать создания дубликатов страниц.",
"zh": "重试后列出现有 wiki 页面失败:{error}。正在中止以避免创建重复页面。"
},
" Page '{title}' already exists (stale list). Re-listing and updating...": {
"bg": " Страницата '{title}' вече съществува (остарял списък). Пресписване и обновяване...",
"de": " Seite '{title}' existiert bereits (veraltete Liste). Neu auflisten und aktualisieren...",
"en": " Page '{title}' already exists (stale list). Re-listing and updating...",
"pl": " Strona '{title}' już istnieje (nieaktualna lista). Ponowne listowanie i aktualizacja...",
"ru": " Страница '{title}' уже существует (устаревший список). Повторное получение списка и обновление...",
"zh": " 页面 '{title}' 已存在(列表过期)。重新列出并更新..."
} }
} }
+62
View File
@@ -909,3 +909,65 @@ class TestGiteaClientActions:
"https://git.example.com/repos/owner/repo/actions/jobs/10026/logs", "https://git.example.com/repos/owner/repo/actions/jobs/10026/logs",
timeout=DEFAULT_TIMEOUT, timeout=DEFAULT_TIMEOUT,
) )
def test_get_repo_variable_returns_value(self) -> None:
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
client._session.request = MagicMock(return_value=_mock_response({"value": "v0.28.1"}))
result = client.get_repo_variable("PRODUCTION_DEPLOY_TAG")
assert result == "v0.28.1"
client._session.request.assert_called_once_with(
"GET",
"https://git.example.com/repos/owner/repo/actions/variables/PRODUCTION_DEPLOY_TAG",
timeout=DEFAULT_TIMEOUT,
)
def test_get_repo_variable_returns_none_on_404(self) -> None:
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
not_found = MagicMock()
not_found.raise_for_status.side_effect = _mock_http_error(404, "not found")
client._session.request = MagicMock(return_value=not_found)
result = client.get_repo_variable("MISSING_VAR")
assert result is None
@patch("time.sleep")
def test_get_repo_variable_reraises_non_404(self, mock_sleep: MagicMock) -> None:
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
server_error = MagicMock()
server_error.raise_for_status.side_effect = _mock_http_error(500, "server error")
client._session.request = MagicMock(return_value=server_error)
with pytest.raises(APIError) as exc_info:
client.get_repo_variable("SOME_VAR")
assert exc_info.value.status == 500
def test_set_repo_variable_updates_existing(self) -> None:
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
client._session.request = MagicMock(return_value=_mock_response({}))
client.set_repo_variable("PRODUCTION_DEPLOY_TAG", "v0.28.2")
client._session.request.assert_called_once_with(
"PUT",
"https://git.example.com/repos/owner/repo/actions/variables/PRODUCTION_DEPLOY_TAG",
timeout=DEFAULT_TIMEOUT,
json={"value": "v0.28.2"},
)
def test_set_repo_variable_creates_on_404(self) -> None:
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
not_found = MagicMock()
not_found.raise_for_status.side_effect = _mock_http_error(404, "not found")
created = _mock_response({})
client._session.request = MagicMock(side_effect=[not_found, created])
client.set_repo_variable("NEW_VAR", "v0.29.0")
assert client._session.request.call_count == 2
second_call = client._session.request.call_args_list[1]
assert second_call.args[0] == "POST"
assert second_call.args[1] == "https://git.example.com/repos/owner/repo/actions/variables/NEW_VAR"
assert second_call.kwargs["json"] == {"value": "v0.29.0"}
def test_set_repo_variable_reraises_non_404(self) -> None:
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
forbidden = MagicMock()
forbidden.raise_for_status.side_effect = _mock_http_error(403, "forbidden")
client._session.request = MagicMock(return_value=forbidden)
with pytest.raises(APIError) as exc_info:
client.set_repo_variable("SOME_VAR", "val")
assert exc_info.value.status == 403
+12
View File
@@ -4,6 +4,7 @@ import json
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import click
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
@@ -249,3 +250,14 @@ class TestMain:
args, kwargs = mock_count.call_args args, kwargs = mock_count.call_args
assert "myorg" in args assert "myorg" in args
assert "myrepo" in args assert "myrepo" in args
@patch("devx.ci.discover_runners.get_ci_token", side_effect=click.ClickException("no token"))
@patch("devx.ci.discover_runners.get_runner_count", return_value=3)
def test_missing_token_runs_without_api(self, mock_count: MagicMock, mock_token: MagicMock) -> None:
"""When no token is available, runner discovery falls back to env/default."""
runner = CliRunner()
result = runner.invoke(main, ["--count"])
assert result.exit_code == 0
assert result.output.strip() == "3"
args, _ = mock_count.call_args
assert args[1] is None # token passed as None when missing
+200
View File
@@ -55,6 +55,38 @@ class TestExtractCliCommands:
assert "real_cmd" in commands assert "real_cmd" in commands
assert "pass" not in commands assert "pass" not in commands
def test_command_with_explicit_name_param(self, tmp_path: Path) -> None:
"""When a command uses name="explicit-name", that name is extracted."""
fake_cli = tmp_path / "cli.py"
fake_cli.write_text(
'@click.group()\ndef cli():\n pass\n@cli.command(name="my-command")\ndef my_command():\n pass\n'
)
commands = extract_cli_commands(tmp_path)
assert "my-command" in commands
assert "my_command" not in commands
def test_command_with_help_kwarg_uses_def_name(self, tmp_path: Path) -> None:
"""When a command uses help= kwarg but no name=, falls back to def name."""
fake_cli = tmp_path / "cli.py"
fake_cli.write_text(
"@click.group()\ndef cli():\n pass\n"
'@cli.command(help="Do something useful")\ndef do_something():\n pass\n'
)
commands = extract_cli_commands(tmp_path)
assert "do_something" in commands
assert "Do something useful" not in commands
def test_command_with_help_translation_uses_def_name(self, tmp_path: Path) -> None:
"""When a command uses help=_() translation, falls back to def name."""
fake_cli = tmp_path / "cli.py"
fake_cli.write_text(
"@click.group()\ndef cli():\n pass\n"
'@cli.command(help=_("Install and configure things"))\ndef install():\n pass\n'
)
commands = extract_cli_commands(tmp_path)
assert "install" in commands
assert "Install and configure things" not in commands
class TestCheckCommandDocumented: class TestCheckCommandDocumented:
def test_finds_command_in_heading(self) -> None: def test_finds_command_in_heading(self) -> None:
@@ -195,3 +227,171 @@ class TestMain:
result = runner.invoke(main, ["--docs-dir", str(docs)]) result = runner.invoke(main, ["--docs-dir", str(docs)])
# No source dir found, so no CLI commands, but modules/scripts from REQUIRED lists # No source dir found, so no CLI commands, but modules/scripts from REQUIRED lists
assert result.exit_code == 0 assert result.exit_code == 0
def test_ci_scripts_dir_empty_skips_ci_checks(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""When --ci-scripts-dir is empty string, CI script checks are skipped."""
monkeypatch.chdir(tmp_path)
docs = tmp_path / "docs"
(docs / "user").mkdir(parents=True)
(docs / "tech").mkdir(parents=True)
src = tmp_path / "src" / "devx"
src.mkdir(parents=True)
(src / "__init__.py").write_text("")
(src / "cli.py").write_text(
"@click.group()\ndef cli():\n pass\n@cli.command('release')\ndef release():\n pass\n"
)
(src / "config.py").write_text("# config")
(docs / "user" / "cli-commands.md").write_text("## release\n")
(docs / "tech" / "architecture.md").write_text("config.py")
(docs / "tech" / "ci-cd-workflow.md").write_text("")
runner = CliRunner()
result = runner.invoke(main, ["--docs-dir", str(docs), "--source-dir", str(src), "--ci-scripts-dir", ""])
assert result.exit_code == 0
assert "100%" in result.output
# Should not mention any CI scripts
assert "MISSING" not in result.output or "CI script" not in result.output
def test_ci_scripts_dir_explicit_path(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""When --ci-scripts-dir points to a directory, scripts are detected from there."""
monkeypatch.chdir(tmp_path)
docs = tmp_path / "docs"
(docs / "user").mkdir(parents=True)
(docs / "tech").mkdir(parents=True)
src = tmp_path / "src" / "myapp"
src.mkdir(parents=True)
(src / "__init__.py").write_text("")
(src / "cli.py").write_text(
"@click.group()\ndef cli():\n pass\n@cli.command('release')\ndef release():\n pass\n"
)
ci_dir = tmp_path / "ci"
ci_dir.mkdir()
(ci_dir / "my_script.py").write_text("# my script")
(ci_dir / "__init__.py").write_text("")
(docs / "user" / "cli-commands.md").write_text("## release\n")
(docs / "tech" / "architecture.md").write_text("")
(docs / "tech" / "ci-cd-workflow.md").write_text("my_script.py")
runner = CliRunner()
result = runner.invoke(
main, ["--docs-dir", str(docs), "--source-dir", str(src), "--ci-scripts-dir", str(ci_dir)]
)
assert result.exit_code == 0
assert "my_script.py" in result.output
assert "OK: my_script.py" in result.output
def test_ci_scripts_dir_nonexistent_skips(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""When --ci-scripts-dir points to a non-existent path, CI checks are skipped."""
monkeypatch.chdir(tmp_path)
docs = tmp_path / "docs"
(docs / "user").mkdir(parents=True)
(docs / "tech").mkdir(parents=True)
src = tmp_path / "src" / "myapp"
src.mkdir(parents=True)
(src / "__init__.py").write_text("")
(src / "cli.py").write_text(
"@click.group()\ndef cli():\n pass\n@cli.command('release')\ndef release():\n pass\n"
)
(docs / "user" / "cli-commands.md").write_text("## release\n")
(docs / "tech" / "architecture.md").write_text("")
(docs / "tech" / "ci-cd-workflow.md").write_text("")
runner = CliRunner()
result = runner.invoke(
main, ["--docs-dir", str(docs), "--source-dir", str(src), "--ci-scripts-dir", "/nonexistent"]
)
assert result.exit_code == 0
assert "100%" in result.output
def test_config_from_pyproject_ci_scripts_dir(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""When [tool.devx.doc_coverage] ci_scripts_dir is set in pyproject.toml, it's used."""
monkeypatch.chdir(tmp_path)
docs = tmp_path / "docs"
(docs / "user").mkdir(parents=True)
(docs / "tech").mkdir(parents=True)
src = tmp_path / "src" / "myapp"
src.mkdir(parents=True)
(src / "__init__.py").write_text("")
(src / "cli.py").write_text(
"@click.group()\ndef cli():\n pass\n@cli.command('release')\ndef release():\n pass\n"
)
(src / "config.py").write_text("# config")
(docs / "user" / "cli-commands.md").write_text("## release\n")
(docs / "tech" / "architecture.md").write_text("config.py")
(docs / "tech" / "ci-cd-workflow.md").write_text("")
# Write pyproject.toml with ci_scripts_dir = ""
(tmp_path / "pyproject.toml").write_text('[tool.devx.doc_coverage]\nci_scripts_dir = ""\n')
runner = CliRunner()
result = runner.invoke(main, ["--docs-dir", str(docs), "--source-dir", str(src)])
assert result.exit_code == 0
assert "100%" in result.output
def test_config_from_pyproject_docs_dir(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""When [tool.devx.doc_coverage] docs_dir is set in pyproject.toml, it's used."""
monkeypatch.chdir(tmp_path)
custom_docs = tmp_path / "custom-docs"
(custom_docs / "user").mkdir(parents=True)
(custom_docs / "tech").mkdir(parents=True)
src = tmp_path / "src" / "myapp"
src.mkdir(parents=True)
(src / "__init__.py").write_text("")
(src / "cli.py").write_text(
"@click.group()\ndef cli():\n pass\n@cli.command('release')\ndef release():\n pass\n"
)
(src / "config.py").write_text("# config")
(custom_docs / "user" / "cli-commands.md").write_text("## release\n")
(custom_docs / "tech" / "architecture.md").write_text("config.py")
(custom_docs / "tech" / "ci-cd-workflow.md").write_text("")
# Write pyproject.toml with custom docs_dir
(tmp_path / "pyproject.toml").write_text(
f'[tool.devx.doc_coverage]\ndocs_dir = "{custom_docs}"\nci_scripts_dir = ""\n'
)
runner = CliRunner()
result = runner.invoke(main, ["--source-dir", str(src)])
assert result.exit_code == 0
assert "100%" in result.output
def test_config_from_pyproject_source_dir(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""When [tool.devx.doc_coverage] source_dir is set in pyproject.toml, it's used."""
monkeypatch.chdir(tmp_path)
docs = tmp_path / "docs"
(docs / "user").mkdir(parents=True)
(docs / "tech").mkdir(parents=True)
custom_src = tmp_path / "custom-src" / "myapp"
custom_src.mkdir(parents=True)
(custom_src / "__init__.py").write_text("")
(custom_src / "cli.py").write_text(
"@click.group()\ndef cli():\n pass\n@cli.command('release')\ndef release():\n pass\n"
)
(custom_src / "config.py").write_text("# config")
(docs / "user" / "cli-commands.md").write_text("## release\n")
(docs / "tech" / "architecture.md").write_text("config.py")
(docs / "tech" / "ci-cd-workflow.md").write_text("")
# Write pyproject.toml with custom source_dir
(tmp_path / "pyproject.toml").write_text(
f'[tool.devx.doc_coverage]\nsource_dir = "{custom_src}"\nci_scripts_dir = ""\n'
)
runner = CliRunner()
result = runner.invoke(main, ["--docs-dir", str(docs)])
assert result.exit_code == 0
assert "100%" in result.output
def test_config_doc_coverage_not_dict(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""When [tool.devx.doc_coverage] is not a dict, falls back to defaults."""
monkeypatch.chdir(tmp_path)
docs = tmp_path / "docs"
(docs / "user").mkdir(parents=True)
(docs / "tech").mkdir(parents=True)
src = tmp_path / "src" / "myapp"
src.mkdir(parents=True)
(src / "__init__.py").write_text("")
(src / "cli.py").write_text(
"@click.group()\ndef cli():\n pass\n@cli.command('release')\ndef release():\n pass\n"
)
(src / "config.py").write_text("# config")
(docs / "user" / "cli-commands.md").write_text("## release\n")
(docs / "tech" / "architecture.md").write_text("config.py")
(docs / "tech" / "ci-cd-workflow.md").write_text("")
# Write pyproject.toml with doc_coverage as a non-dict value
(tmp_path / "pyproject.toml").write_text('[tool.devx]\ndoc_coverage = "not-a-dict"\n')
runner = CliRunner()
result = runner.invoke(main, ["--docs-dir", str(docs), "--source-dir", str(src)])
assert result.exit_code == 0
assert "100%" in result.output
+22
View File
@@ -186,6 +186,28 @@ class TestCli:
timeout=60, timeout=60,
) )
@patch("devx.molecule.molecule_ci_guard.get_ci_token", side_effect=click.ClickException("no token"))
def test_missing_token_runs_without_polling(self, mock_token: MagicMock) -> None:
"""When no token is available, cross-runner polling is skipped."""
from click.testing import CliRunner
with (
patch("devx.molecule.molecule_ci_guard.subprocess.Popen") as mock_popen,
patch("devx.molecule.molecule_ci_guard.subprocess.run") as mock_run,
patch("devx.molecule.molecule_ci_guard.poll_for_other_failures") as mock_poll,
patch("time.sleep"),
):
proc = MagicMock()
proc.poll.return_value = 0
proc.returncode = 0
mock_popen.return_value = proc
mock_run.return_value = MagicMock(returncode=0)
runner = CliRunner()
result = runner.invoke(cli, ["default|ubuntu-2204|img:latest|"])
assert result.exit_code == 0
mock_poll.assert_not_called()
def test_invalid_pair_format_raises(self) -> None: def test_invalid_pair_format_raises(self) -> None:
"""Pair with fewer than 2 parts should raise.""" """Pair with fewer than 2 parts should raise."""
from click.testing import CliRunner from click.testing import CliRunner
@@ -4,6 +4,7 @@ import json
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import click
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
@@ -219,3 +220,14 @@ class TestMain:
args, kwargs = mock_count.call_args args, kwargs = mock_count.call_args
assert "myorg" in args assert "myorg" in args
assert "myrepo" in args assert "myrepo" in args
@patch("devx.molecule.discover_runners.get_ci_token", side_effect=click.ClickException("no token"))
@patch("devx.molecule.discover_runners.get_runner_count", return_value=3)
def test_missing_token_runs_without_api(self, mock_count: MagicMock, mock_token: MagicMock) -> None:
"""When no token is available, runner discovery falls back to env/default."""
runner = CliRunner()
result = runner.invoke(main, ["--count"])
assert result.exit_code == 0
assert result.output.strip() == "3"
args, _ = mock_count.call_args
assert args[1] is None # token passed as None when missing
+7 -2
View File
@@ -12,9 +12,14 @@ from devx.tools.pr_label import cli
class TestCli: class TestCli:
def test_no_token_raises(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_no_token_raises(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("CI_GITEA_TOKEN", raising=False) for name in ("DEVELOPER_GITEA_API_TOKEN", "CI_GITEA_API_TOKEN", "CI_GITEA_TOKEN"):
monkeypatch.delenv(name, raising=False)
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli, ["--pr", "42", "--label", "ready-to-merge"]) result = runner.invoke(
cli,
["--pr", "42", "--label", "ready-to-merge"],
env={"DEVELOPER_GITEA_API_TOKEN": "", "CI_GITEA_API_TOKEN": "", "CI_GITEA_TOKEN": ""},
)
assert result.exit_code != 0 assert result.exit_code != 0
assert "CI_GITEA_TOKEN" in result.output assert "CI_GITEA_TOKEN" in result.output
+7 -2
View File
@@ -153,9 +153,14 @@ class TestPrintLogs:
class TestCli: class TestCli:
def test_no_token_raises(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_no_token_raises(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("CI_GITEA_TOKEN", raising=False) for name in ("DEVELOPER_GITEA_API_TOKEN", "CI_GITEA_API_TOKEN", "CI_GITEA_TOKEN"):
monkeypatch.delenv(name, raising=False)
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli, ["--pr", "42"]) result = runner.invoke(
cli,
["--pr", "42"],
env={"DEVELOPER_GITEA_API_TOKEN": "", "CI_GITEA_API_TOKEN": "", "CI_GITEA_TOKEN": ""},
)
assert result.exit_code != 0 assert result.exit_code != 0
assert "CI_GITEA_TOKEN" in result.output assert "CI_GITEA_TOKEN" in result.output
+2 -1
View File
@@ -756,9 +756,10 @@ class TestMain:
result = runner.invoke(main, ["42", "my-org/my-repo"], env={"CI_GITEA_TOKEN": "fake"}) result = runner.invoke(main, ["42", "my-org/my-repo"], env={"CI_GITEA_TOKEN": "fake"})
assert result.exit_code != 0 assert result.exit_code != 0
@patch.dict("os.environ", {"CI_GITEA_API_TOKEN": "", "CI_GITEA_TOKEN": ""})
def test_no_token_raises(self) -> None: def test_no_token_raises(self) -> None:
runner = CliRunner() runner = CliRunner()
result = runner.invoke(main, ["42", "my-org/my-repo"], env={"CI_GITEA_TOKEN": ""}) result = runner.invoke(main, ["42", "my-org/my-repo"], env={"CI_GITEA_API_TOKEN": "", "CI_GITEA_TOKEN": ""})
assert result.exit_code != 0 assert result.exit_code != 0
assert "CI_GITEA_TOKEN" in result.output assert "CI_GITEA_TOKEN" in result.output
+7 -2
View File
@@ -122,9 +122,14 @@ class TestWaitForCompletion:
class TestCli: class TestCli:
def test_no_token_raises(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_no_token_raises(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("CI_GITEA_TOKEN", raising=False) for name in ("DEVELOPER_GITEA_API_TOKEN", "CI_GITEA_API_TOKEN", "CI_GITEA_TOKEN"):
monkeypatch.delenv(name, raising=False)
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli, ["--pr", "42"]) result = runner.invoke(
cli,
["--pr", "42"],
env={"DEVELOPER_GITEA_API_TOKEN": "", "CI_GITEA_API_TOKEN": "", "CI_GITEA_TOKEN": ""},
)
assert result.exit_code != 0 assert result.exit_code != 0
assert "CI_GITEA_TOKEN" in result.output assert "CI_GITEA_TOKEN" in result.output
+37 -2
View File
@@ -19,6 +19,7 @@ from devx.ci.sync_wiki import (
main, main,
sync_files, sync_files,
transform_links, transform_links,
wiki_filename,
) )
@@ -96,6 +97,29 @@ class TestGetWikiCloneUrl:
assert "owner/repo.wiki.git" in url assert "owner/repo.wiki.git" in url
class TestWikiFilename:
def test_no_dashes(self) -> None:
assert wiki_filename("Home") == "Home.md"
def test_single_word(self) -> None:
assert wiki_filename("Architecture") == "Architecture.md"
def test_with_dashes_adds_marker(self) -> None:
assert wiki_filename("Getting-Started") == "Getting-Started.-.md"
def test_spaces_become_dashes_with_marker(self) -> None:
# "Getting Started" → "Getting-Started" (has dash) → marker added
assert wiki_filename("Getting Started") == "Getting-Started.-.md"
def test_spaces_no_dashes_no_marker(self) -> None:
# "Foo Bar" → "Foo-Bar" (has dash) → marker added
assert wiki_filename("Foo Bar") == "Foo-Bar.-.md"
def test_single_word_with_spaces_no_dash(self) -> None:
# No dash at all after conversion → no marker
assert wiki_filename("HelloWorld") == "HelloWorld.md"
class TestCloneWiki: class TestCloneWiki:
@patch("devx.ci.sync_wiki.subprocess.run") @patch("devx.ci.sync_wiki.subprocess.run")
def test_clone_success(self, mock_run: MagicMock, tmp_path: Path) -> None: def test_clone_success(self, mock_run: MagicMock, tmp_path: Path) -> None:
@@ -208,6 +232,8 @@ class TestCommitAndPush:
mock_run.side_effect = [ mock_run.side_effect = [
MagicMock(returncode=0), # git add MagicMock(returncode=0), # git add
MagicMock(returncode=1), # git diff --cached --quiet (has changes) MagicMock(returncode=1), # git diff --cached --quiet (has changes)
MagicMock(returncode=0), # git config user.email
MagicMock(returncode=0), # git config user.name
MagicMock(returncode=0), # git commit MagicMock(returncode=0), # git commit
MagicMock(returncode=0, stdout="", stderr=""), # git push MagicMock(returncode=0, stdout="", stderr=""), # git push
] ]
@@ -219,6 +245,8 @@ class TestCommitAndPush:
mock_run.side_effect = [ mock_run.side_effect = [
MagicMock(returncode=0), # git add MagicMock(returncode=0), # git add
MagicMock(returncode=1), # git diff --cached --quiet (has changes) MagicMock(returncode=1), # git diff --cached --quiet (has changes)
MagicMock(returncode=0), # git config user.email
MagicMock(returncode=0), # git config user.name
MagicMock(returncode=0), # git commit MagicMock(returncode=0), # git commit
MagicMock(returncode=1, stdout="", stderr="push failed"), # git push MagicMock(returncode=1, stdout="", stderr="push failed"), # git push
] ]
@@ -228,9 +256,10 @@ class TestCommitAndPush:
class TestMain: class TestMain:
def test_no_token_raises(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: def test_no_token_raises(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("CI_GITEA_TOKEN", raising=False) for name in ("CI_GITEA_API_TOKEN", "CI_GITEA_TOKEN"):
monkeypatch.delenv(name, raising=False)
runner = CliRunner() runner = CliRunner()
result = runner.invoke(main, []) result = runner.invoke(main, [], env={"CI_GITEA_API_TOKEN": "", "CI_GITEA_TOKEN": ""})
assert result.exit_code != 0 assert result.exit_code != 0
assert "CI_GITEA_TOKEN" in result.output assert "CI_GITEA_TOKEN" in result.output
@@ -319,6 +348,7 @@ class TestMain:
assert result.exit_code == 0 assert result.exit_code == 0
mock_init.assert_called_once() mock_init.assert_called_once()
@patch("devx.ci.sync_wiki.time.sleep")
@patch("devx.ci.sync_wiki.clone_wiki") @patch("devx.ci.sync_wiki.clone_wiki")
@patch("devx.ci.sync_wiki.commit_and_push", return_value=True) @patch("devx.ci.sync_wiki.commit_and_push", return_value=True)
@patch("devx.ci.sync_wiki.sync_files", return_value=(1, 0)) @patch("devx.ci.sync_wiki.sync_files", return_value=(1, 0))
@@ -327,6 +357,7 @@ class TestMain:
mock_sync: MagicMock, mock_sync: MagicMock,
mock_push: MagicMock, mock_push: MagicMock,
mock_clone: MagicMock, mock_clone: MagicMock,
mock_sleep: MagicMock,
tmp_path: Path, tmp_path: Path,
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
@@ -376,6 +407,7 @@ class TestMain:
assert result.exit_code == 0 assert result.exit_code == 0
assert "No push needed" in result.output assert "No push needed" in result.output
@patch("devx.ci.sync_wiki.time.sleep")
@patch("devx.ci.sync_wiki.clone_wiki", side_effect=[True, False]) @patch("devx.ci.sync_wiki.clone_wiki", side_effect=[True, False])
@patch("devx.ci.sync_wiki.commit_and_push", return_value=True) @patch("devx.ci.sync_wiki.commit_and_push", return_value=True)
@patch("devx.ci.sync_wiki.sync_files", return_value=(1, 0)) @patch("devx.ci.sync_wiki.sync_files", return_value=(1, 0))
@@ -384,6 +416,7 @@ class TestMain:
mock_sync: MagicMock, mock_sync: MagicMock,
mock_push: MagicMock, mock_push: MagicMock,
mock_clone: MagicMock, mock_clone: MagicMock,
mock_sleep: MagicMock,
tmp_path: Path, tmp_path: Path,
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
@@ -400,6 +433,7 @@ class TestMain:
assert result.exit_code != 0 assert result.exit_code != 0
assert "could not clone" in result.output assert "could not clone" in result.output
@patch("devx.ci.sync_wiki.time.sleep")
@patch("devx.ci.sync_wiki.clone_wiki") @patch("devx.ci.sync_wiki.clone_wiki")
@patch("devx.ci.sync_wiki.commit_and_push", return_value=True) @patch("devx.ci.sync_wiki.commit_and_push", return_value=True)
@patch("devx.ci.sync_wiki.sync_files", return_value=(1, 0)) @patch("devx.ci.sync_wiki.sync_files", return_value=(1, 0))
@@ -408,6 +442,7 @@ class TestMain:
mock_sync: MagicMock, mock_sync: MagicMock,
mock_push: MagicMock, mock_push: MagicMock,
mock_clone: MagicMock, mock_clone: MagicMock,
mock_sleep: MagicMock,
tmp_path: Path, tmp_path: Path,
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None: