Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84958fab7 | ||
|
|
c0238e75df | ||
|
|
d4ddbd7e7a | ||
|
|
08b993fc4a |
@@ -0,0 +1,20 @@
|
||||
.venv/
|
||||
.git/
|
||||
.gitea/
|
||||
tests/
|
||||
docs/
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
htmlcov/
|
||||
.coverage
|
||||
dist/
|
||||
build/
|
||||
*.md
|
||||
!README.md
|
||||
.env
|
||||
.env.example
|
||||
activate.sh
|
||||
activate.fish
|
||||
activate.zsh
|
||||
hooks/
|
||||
.devin/
|
||||
@@ -0,0 +1,131 @@
|
||||
name: Build Images
|
||||
|
||||
# Builds and pushes pre-built Docker runner images to the Gitea registry.
|
||||
# These images eliminate the 40-120s setup tax on every CI job by baking
|
||||
# devx and all dependencies into the image.
|
||||
#
|
||||
# Triggers:
|
||||
# - On push to master (after post-merge release completes)
|
||||
# - Manually via workflow_dispatch
|
||||
#
|
||||
# The workflow builds 3 tier images in sequence:
|
||||
# ci-base → ci-quality → ci-full
|
||||
#
|
||||
# Each tier builds FROM the previous one, so they must be built in order.
|
||||
# After pushing, a cleanup job removes old versions (keeps last 2 + latest).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- docker/**
|
||||
- pyproject.toml
|
||||
- src/devx/**
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
detect-type:
|
||||
runs-on: docker
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
is-release: ${{ steps.check.outputs.is-release }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Set up environment
|
||||
run: make setup-ci
|
||||
- name: Check if this is a release commit
|
||||
id: check
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.ci.detect_release_commit
|
||||
|
||||
build-and-push:
|
||||
needs: [detect-type]
|
||||
if: needs.detect-type.outputs.is-release == 'false'
|
||||
runs-on: docker
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up environment
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
run: make setup-release
|
||||
- name: Docker registry login
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }}
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
echo "$REPO_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$REGISTRY_USERNAME" --password-stdin
|
||||
- name: Build and push tier images
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
# Build ci-base first (it's the base for ci-quality and ci-full)
|
||||
python3 -m devx.tools.build_image \
|
||||
--dockerfile docker/ci-base/Dockerfile \
|
||||
--name oblachno-oss/runner-images/ci-base \
|
||||
--tag latest \
|
||||
--registry git.oblachno.oblachno.fyi \
|
||||
--push --pull
|
||||
# Build ci-quality (FROM ci-base-latest)
|
||||
python3 -m devx.tools.build_image \
|
||||
--dockerfile docker/ci-quality/Dockerfile \
|
||||
--name oblachno-oss/runner-images/ci-quality \
|
||||
--tag latest \
|
||||
--registry git.oblachno.oblachno.fyi \
|
||||
--push
|
||||
# Build ci-full (FROM ci-quality-latest)
|
||||
python3 -m devx.tools.build_image \
|
||||
--dockerfile docker/ci-full/Dockerfile \
|
||||
--name oblachno-oss/runner-images/ci-full \
|
||||
--tag latest \
|
||||
--registry git.oblachno.oblachno.fyi \
|
||||
--push
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.notify_failure \
|
||||
--repo "${{ github.repository }}" \
|
||||
--run-id "${{ github.run_id }}" \
|
||||
--workflow "build-images/build-and-push" \
|
||||
--commit "${{ github.sha }}"
|
||||
|
||||
cleanup:
|
||||
needs: [build-and-push]
|
||||
if: always() && needs.build-and-push.result == 'success'
|
||||
runs-on: docker
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Set up environment
|
||||
run: make setup-ci
|
||||
- name: Clean up old image versions
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.tools.clean_images \
|
||||
--owner oblachno-oss \
|
||||
--name oblachno-oss/runner-images/ci-base \
|
||||
--name oblachno-oss/runner-images/ci-quality \
|
||||
--name oblachno-oss/runner-images/ci-full \
|
||||
--keep 2
|
||||
@@ -74,6 +74,9 @@ src/devx/
|
||||
├── tools/ # Developer tooling modules (run locally or by CI)
|
||||
│ ├── setup.py # Environment setup (venv, deps, hooks)
|
||||
│ ├── install_tools.py # Install actionlint, git-cliff, act_runner, tea
|
||||
│ ├── install_checkmake.py # Install checkmake (Makefile linter)
|
||||
│ ├── build_image.py # Build and push Docker images to Gitea registry
|
||||
│ ├── clean_images.py # Clean up old Docker image versions from Gitea registry
|
||||
│ ├── check_test_speed.py # Measure unit test execution time
|
||||
│ ├── check_mutable_globals.py # Detect module-level mutable globals (test isolation bugs)
|
||||
│ ├── check_pyproject_deps.py # Validate pyproject.toml deps have documentation comments
|
||||
@@ -424,6 +427,11 @@ projects.
|
||||
| `devx-check-test-speed` | Verify test suite timing |
|
||||
| `devx-pre-push` | Run lint + tests before push |
|
||||
| `devx-clean` | Remove caches, build artifacts, coverage data |
|
||||
| `devx-setup-image` | Link /opt/venv + install project (for pre-built image CI jobs) |
|
||||
| `devx-build-images` | Build Docker images from manifest (no push) |
|
||||
| `devx-push-images` | Build and push Docker images to Gitea registry |
|
||||
| `devx-build-images-dry-run` | Show what would be built/pushed |
|
||||
| `devx-clean-images` | Delete old image versions (keep last 2 + latest) |
|
||||
|
||||
**Variables** (set BEFORE including devx.mak):
|
||||
- `DEVX_PYTHON` — Python executable (default: `python3`)
|
||||
@@ -433,6 +441,44 @@ projects.
|
||||
- `DEVX_COV_PKG` — coverage package (default: `src/devx`)
|
||||
- `DEVX_TEST_PATHS` — pytest paths (default: `tests/`)
|
||||
- `DEVX_PR_BASE` — PR base branch (default: `master`)
|
||||
- `DEVX_GITEA_REGISTRY` — registry URL (default: `git.oblachno.oblachno.fyi`)
|
||||
- `DEVX_IMAGE_MANIFEST` — path to JSON manifest (default: `docker/images.json`)
|
||||
- `DEVX_IMAGE_OWNER` — package owner for cleanup (default: `oblachno-oss`)
|
||||
|
||||
## Pre-built Docker Runner Images
|
||||
|
||||
devx builds and publishes three tier images to the Gitea container registry
|
||||
to eliminate the 40-120s setup tax on every CI job:
|
||||
|
||||
| Image | Contains | Used by jobs |
|
||||
|-------|----------|-------------|
|
||||
| `ci-base-latest` | Python 3.12 + devx[ci] + tea | detect-changes, detect-type, validate-commit-msg, pr-review, auto-merge, sync-wiki, vikunja, configure-repo |
|
||||
| `ci-quality-latest` | ci-base + devx[lint] + actionlint + checkmake | quality, badges |
|
||||
| `ci-full-latest` | ci-quality + devx[release,molecule,deploy] + git-cliff + OpenTofu | release, publish, release-dry-run, molecule-tests, deploy jobs |
|
||||
|
||||
**Build process** (in `build-images.yml` workflow):
|
||||
1. `ci-base` builds FROM `gitea/runner-images:ubuntu-latest`
|
||||
2. `ci-quality` builds FROM `ci-base-latest`
|
||||
3. `ci-full` builds FROM `ci-quality-latest`
|
||||
|
||||
Each image is tagged `latest` and pushed to
|
||||
`git.oblachno.oblachno.fyi/oblachno-oss/runner-images:<tier>-latest`.
|
||||
|
||||
**Using images in workflows**:
|
||||
```yaml
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: docker
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images:ci-quality-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
run: make setup-image # links /opt/venv, installs project (no-deps)
|
||||
```
|
||||
|
||||
**Image build/push tools** (tested Python modules):
|
||||
- `devx.tools.build_image` — Build and push Docker images from Dockerfile or manifest
|
||||
- `devx.tools.clean_images` — Delete old image versions via Gitea API (keep last N + latest)
|
||||
|
||||
**Usage in project Makefile**:
|
||||
```makefile
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.20.0] - 2026-06-27
|
||||
|
||||
### Features
|
||||
|
||||
- Add pre-built Docker runner images and tested image build/push tools
|
||||
|
||||
## [0.19.3] - 2026-06-26
|
||||
|
||||
### Refactor
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: all setup setup-ci setup-quality setup-release install update lint lint-all test test-unit pytest-cov clean install-tools install-hooks activate-scripts checkmake check-mutable-globals check-dep-docs check-test-speed
|
||||
.PHONY: all setup setup-ci setup-quality setup-release setup-image install update lint lint-all test test-unit pytest-cov clean install-tools install-hooks activate-scripts checkmake check-mutable-globals check-dep-docs check-test-speed build-images push-images build-images-dry-run clean-images
|
||||
|
||||
PYTHON := python3
|
||||
VENV := .venv
|
||||
@@ -30,6 +30,11 @@ setup-release: $(VENV)/bin/activate .env
|
||||
export PATH="$(HOME)/.local/bin:$$PATH"; \
|
||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --no-pre-commit
|
||||
|
||||
# Setup for pre-built image jobs (deps already in image, just link venv + install project)
|
||||
setup-image:
|
||||
@if [ -d /opt/venv ]; then ln -sf /opt/venv .venv; . .venv/bin/activate && pip install -e . --no-deps 2>/dev/null; \
|
||||
else echo "[setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
|
||||
|
||||
.env:
|
||||
@if [ ! -f .env ]; then cp .env.example .env; echo "Created .env from .env.example — please edit it."; fi
|
||||
|
||||
@@ -105,3 +110,17 @@ pre-push: lint-all pytest-cov
|
||||
|
||||
clean: devx-clean
|
||||
@echo "[clean] Done."
|
||||
|
||||
# ── Docker image management ──────────────────────────────────────────────────
|
||||
|
||||
build-images: devx-build-images
|
||||
@echo "[build-images] Done."
|
||||
|
||||
push-images: devx-push-images
|
||||
@echo "[push-images] Done."
|
||||
|
||||
build-images-dry-run: devx-build-images-dry-run
|
||||
@echo "[build-images-dry-run] Done."
|
||||
|
||||
clean-images: devx-clean-images
|
||||
@echo "[clean-images] Done."
|
||||
|
||||
@@ -16,12 +16,12 @@ quality badges.
|
||||
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Why devx?
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# ci-base — lightweight image for CI jobs that only need devx core + tea.
|
||||
#
|
||||
# Used by: detect-type, detect-changes, validate-commit-msg, pr-review,
|
||||
# auto-merge, sync-wiki, vikunja, configure-repo, discover-runners,
|
||||
# molecule-report, discover-integration-runners
|
||||
#
|
||||
# Jobs using this image: setup is instant (ln -s /opt/venv .venv)
|
||||
# No pip install needed — devx and all deps are pre-installed.
|
||||
|
||||
FROM gitea/runner-images:ubuntu-latest
|
||||
|
||||
# Create a virtual environment with all deps pre-installed
|
||||
RUN python3 -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:/root/.local/bin:$PATH"
|
||||
|
||||
# Install devx from local source (build context = devx repo root)
|
||||
COPY . /tmp/devx
|
||||
RUN pip install --no-cache-dir --upgrade pip setuptools wheel \
|
||||
&& pip install --no-cache-dir /tmp/devx[ci] \
|
||||
&& rm -rf /tmp/devx
|
||||
|
||||
# Install tea CLI (for Gitea API operations in CI)
|
||||
RUN python3 -m devx.tools.install_tools --tool tea
|
||||
|
||||
# Workspace directory (actions/checkout mounts repo here)
|
||||
WORKDIR /workspace
|
||||
@@ -0,0 +1,24 @@
|
||||
# ci-full — heaviest image, includes everything for release, molecule, deploy.
|
||||
#
|
||||
# Used by: release, publish, release-dry-run, molecule-tests,
|
||||
# provision-infra, deploy-observability, provision-zitadel,
|
||||
# deploy-customer, integration-tests
|
||||
#
|
||||
# Layers on top of ci-quality: adds release tools, molecule, deploy deps,
|
||||
# git-cliff, and OpenTofu.
|
||||
|
||||
FROM git.oblachno.oblachno.fyi/oblachno-oss/runner-images:ci-quality-latest
|
||||
|
||||
# Install devx[release,molecule,deploy] from local source
|
||||
COPY . /tmp/devx
|
||||
RUN pip install --no-cache-dir /tmp/devx[release,molecule,deploy] \
|
||||
&& rm -rf /tmp/devx
|
||||
|
||||
# Install git-cliff (changelog generator for release job)
|
||||
RUN python3 -m devx.tools.install_tools --tool git-cliff
|
||||
|
||||
# Install OpenTofu (for infra deploy jobs)
|
||||
RUN ARCH=$(uname -m | sed 's/x86_64/amd64') \
|
||||
&& VERSION=1.12.3 \
|
||||
&& curl -fsSL "https://github.com/opentofu/opentofu/releases/download/v${VERSION}/tofu_${VERSION}_$(uname -s | tr '[:upper:]' '[:lower:]')_${ARCH}.tar.gz" \
|
||||
| tar -xz -C /usr/local/bin tofu
|
||||
@@ -0,0 +1,17 @@
|
||||
# ci-quality — image for lint, type-checking, badge generation.
|
||||
#
|
||||
# Used by: quality (lint-all + pytest-cov + checks), badges (generate_badges
|
||||
# runs ruff/pyright/bandit to produce quality badge)
|
||||
#
|
||||
# Layers on top of ci-base: adds lint tools + actionlint + checkmake.
|
||||
|
||||
FROM git.oblachno.oblachno.fyi/oblachno-oss/runner-images:ci-base-latest
|
||||
|
||||
# Install devx[lint] from local source (adds ruff, pyright, bandit, etc.)
|
||||
COPY . /tmp/devx
|
||||
RUN pip install --no-cache-dir /tmp/devx[lint] \
|
||||
&& rm -rf /tmp/devx
|
||||
|
||||
# Install CI/CD binary tools
|
||||
RUN python3 -m devx.tools.install_tools --tool actionlint \
|
||||
&& python3 -m devx.tools.install_checkmake
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"name": "oblachno-oss/runner-images/ci-base",
|
||||
"dockerfile": "docker/ci-base/Dockerfile",
|
||||
"context": ".",
|
||||
"tags": ["latest"]
|
||||
},
|
||||
{
|
||||
"name": "oblachno-oss/runner-images/ci-quality",
|
||||
"dockerfile": "docker/ci-quality/Dockerfile",
|
||||
"context": ".",
|
||||
"tags": ["latest"]
|
||||
},
|
||||
{
|
||||
"name": "oblachno-oss/runner-images/ci-full",
|
||||
"dockerfile": "docker/ci-full/Dockerfile",
|
||||
"context": ".",
|
||||
"tags": ["latest"]
|
||||
}
|
||||
]
|
||||
+6
-6
@@ -12,12 +12,12 @@ project to be reusable across all oblachno-oss repositories.
|
||||
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
+20
-7
@@ -26,14 +26,13 @@ devx = "devx.cli:cli"
|
||||
version = {attr = "devx.__version__"}
|
||||
|
||||
[project.optional-dependencies]
|
||||
# Minimal deps for CI scripts that only need click/dotenv/requests
|
||||
# Test runners (pytest + coverage + parallel execution)
|
||||
ci = [
|
||||
"pytest>=9.1.0",
|
||||
"pytest-cov>=7.1.0",
|
||||
"build>=1.5.0",
|
||||
"twine>=6.2.0",
|
||||
"pytest-xdist>=3.8",
|
||||
]
|
||||
# Lint and type-checking tools (quality job)
|
||||
# Lint and type-checking tools (quality job, badge generation)
|
||||
lint = [
|
||||
"ruff>=0.15.17",
|
||||
"pyright>=1.1.410",
|
||||
@@ -41,16 +40,30 @@ lint = [
|
||||
"pip-audit>=2.10",
|
||||
"pre-commit>=4.6.0",
|
||||
]
|
||||
# Molecule testing (optional — for projects with Ansible roles)
|
||||
# Release tools (build + publish to PyPI/Gitea registry)
|
||||
release = [
|
||||
"build>=1.5.0",
|
||||
"twine>=6.2.0",
|
||||
]
|
||||
# Molecule testing (for projects with Ansible roles)
|
||||
molecule = [
|
||||
"molecule>=26.4.0",
|
||||
"molecule-docker>=2.1.0",
|
||||
"ansible-lint>=26.4.0",
|
||||
"ansible>=14.0.0",
|
||||
"ansible-core>=2.15,<2.17",
|
||||
]
|
||||
# Deploy tools (for infra staging/production deployments)
|
||||
deploy = [
|
||||
"ansible-core>=2.15,<2.17",
|
||||
"boto3>=1.34",
|
||||
"docker>=7.0",
|
||||
"jinja2>=3.1",
|
||||
"pyyaml>=6.0",
|
||||
"cryptography>=41.0",
|
||||
]
|
||||
# Full dev environment (local development)
|
||||
dev = [
|
||||
"devx[ci,lint]",
|
||||
"devx[ci,lint,release,molecule]",
|
||||
"build>=1.3.0",
|
||||
"twine>=6.2.0",
|
||||
]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
||||
|
||||
__version__ = "0.19.3"
|
||||
__version__ = "0.20.0"
|
||||
|
||||
@@ -249,3 +249,58 @@ devx-clean:
|
||||
@find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
||||
@find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||
@rm -rf .coverage htmlcov/ dist/ build/ *.egg-info/ .molecule/ 2>/dev/null || true
|
||||
|
||||
# ── Pre-built image setup ─────────────────────────────────────────────────────
|
||||
#
|
||||
# When running inside a pre-built Docker runner image (ci-base, ci-quality,
|
||||
# ci-full), all deps are already installed in /opt/venv. This target links
|
||||
# the venv and installs the project itself (no-deps, fast).
|
||||
# Falls back to devx-setup-ci if /opt/venv is not present (local dev).
|
||||
|
||||
devx-setup-image:
|
||||
@if [ -d /opt/venv ]; then \
|
||||
ln -sf /opt/venv $(DEVX_VENV); \
|
||||
. $(DEVX_BIN)/activate && pip install -e . --no-deps 2>/dev/null; \
|
||||
echo "[devx-setup-image] Linked /opt/venv and installed project (no-deps)."; \
|
||||
else \
|
||||
echo "[devx-setup-image] /opt/venv not found — falling back to devx-setup-ci"; \
|
||||
$(MAKE) devx-setup-ci; \
|
||||
fi
|
||||
|
||||
# ── Docker image build / push / cleanup ───────────────────────────────────────
|
||||
#
|
||||
# Variables:
|
||||
# DEVX_GITEA_REGISTRY — registry URL (default: git.oblachno.oblachno.fyi)
|
||||
# DEVX_IMAGE_MANIFEST — path to JSON manifest (default: docker/images.json)
|
||||
# DEVX_IMAGE_OWNER — package owner for cleanup (default: oblachno-oss)
|
||||
|
||||
DEVX_GITEA_REGISTRY ?= git.oblachno.oblachno.fyi
|
||||
DEVX_IMAGE_MANIFEST ?= docker/images.json
|
||||
DEVX_IMAGE_OWNER ?= oblachno-oss
|
||||
|
||||
# Build all images from manifest (no push)
|
||||
devx-build-images:
|
||||
@$(DEVX_PYTHON) -m devx.tools.build_image --manifest $(DEVX_IMAGE_MANIFEST) --pull
|
||||
|
||||
# Build and push all images to the Gitea registry
|
||||
devx-push-images:
|
||||
@$(DEVX_PYTHON) -m devx.tools.build_image \
|
||||
--manifest $(DEVX_IMAGE_MANIFEST) \
|
||||
--registry $(DEVX_GITEA_REGISTRY) \
|
||||
--push --pull
|
||||
|
||||
# Dry-run: show what would be built/pushed
|
||||
devx-build-images-dry-run:
|
||||
@$(DEVX_PYTHON) -m devx.tools.build_image \
|
||||
--manifest $(DEVX_IMAGE_MANIFEST) \
|
||||
--registry $(DEVX_GITEA_REGISTRY) \
|
||||
--push --dry-run
|
||||
|
||||
# Clean up old image versions (keep last 2 + latest)
|
||||
devx-clean-images:
|
||||
@$(DEVX_PYTHON) -m devx.tools.clean_images \
|
||||
--owner $(DEVX_IMAGE_OWNER) \
|
||||
--name oblachno-oss/runner-images/ci-base \
|
||||
--name oblachno-oss/runner-images/ci-quality \
|
||||
--name oblachno-oss/runner-images/ci-full \
|
||||
--keep 2
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and push Docker images to a Gitea container registry.
|
||||
|
||||
Replaces raw ``docker build`` / ``docker push`` shell commands with a
|
||||
tested Python tool. Supports:
|
||||
|
||||
- Building from any Dockerfile with a configurable context directory
|
||||
- Tagging with multiple tags (e.g. ``latest`` + version)
|
||||
- Optional push to a Gitea registry (with login)
|
||||
- Dry-run mode (prints commands without executing)
|
||||
|
||||
Usage::
|
||||
|
||||
# Build a single image
|
||||
python3 -m devx.tools.build_image \\
|
||||
--dockerfile docker/ci-base/Dockerfile \\
|
||||
--tag ci-base:latest \\
|
||||
--tag ci-base:0.19.3
|
||||
|
||||
# Build and push to registry
|
||||
python3 -m devx.tools.build_image \\
|
||||
--dockerfile docker/ci-base/Dockerfile \\
|
||||
--tag ci-base:latest \\
|
||||
--tag ci-base:0.19.3 \\
|
||||
--registry git.oblachno.oblachno.fyi \\
|
||||
--push
|
||||
|
||||
# Build multiple images (from a manifest file)
|
||||
python3 -m devx.tools.build_image --manifest docker/images.json --push
|
||||
|
||||
The manifest file is a JSON list of dicts, each with:
|
||||
- ``name``: image name (e.g. ``ci-base``)
|
||||
- ``dockerfile``: path to Dockerfile (relative to repo root)
|
||||
- ``context``: build context directory (optional, defaults to repo root)
|
||||
- ``tags``: list of tags (optional, defaults to ``["latest"]``)
|
||||
|
||||
Registry authentication uses ``REPO_TOKEN`` (or ``GITEA_REGISTRY_TOKEN``)
|
||||
and ``REGISTRY_USERNAME`` (or ``GITEA_REGISTRY_USERNAME``) environment
|
||||
variables, matching the existing CI workflow patterns.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess # nosec B404
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from devx.i18n import _
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageSpec:
|
||||
"""Specification for a single Docker image to build."""
|
||||
|
||||
name: str
|
||||
dockerfile: str
|
||||
context: str = "."
|
||||
tags: list[str] = field(default_factory=lambda: ["latest"])
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, object]) -> ImageSpec:
|
||||
"""Create an ImageSpec from a dict (e.g. from a JSON manifest)."""
|
||||
name = str(data.get("name", ""))
|
||||
if not name:
|
||||
raise ValueError(_("Image manifest entry missing 'name'"))
|
||||
dockerfile = str(data.get("dockerfile", ""))
|
||||
if not dockerfile:
|
||||
raise ValueError(_("Image manifest entry missing 'dockerfile'"))
|
||||
context = str(data.get("context", "."))
|
||||
tags_raw = data.get("tags", ["latest"])
|
||||
if not isinstance(tags_raw, list):
|
||||
raise ValueError(_("Image 'tags' must be a list"))
|
||||
tags = [str(t) for t in tags_raw] if tags_raw else ["latest"]
|
||||
return cls(name=name, dockerfile=dockerfile, context=context, tags=tags)
|
||||
|
||||
|
||||
def load_manifest(path: str | Path) -> list[ImageSpec]:
|
||||
"""Load a JSON manifest file describing images to build.
|
||||
|
||||
The file must contain a JSON list of dicts with at least ``name`` and
|
||||
``dockerfile`` keys. ``context`` and ``tags`` are optional.
|
||||
|
||||
Returns a list of :class:`ImageSpec` instances.
|
||||
"""
|
||||
p = Path(path)
|
||||
if not p.is_file():
|
||||
raise click.ClickException(_("Manifest file not found: {path}", path=p))
|
||||
with p.open() as f: # noqa: PTH123
|
||||
data = json.load(f)
|
||||
if not isinstance(data, list):
|
||||
raise click.ClickException(_("Manifest must be a JSON list"))
|
||||
return [ImageSpec.from_dict(entry) for entry in data]
|
||||
|
||||
|
||||
def build_full_tag(registry: str | None, name: str, tag: str) -> str:
|
||||
"""Build a full image tag, optionally prefixed with a registry.
|
||||
|
||||
>>> build_full_tag(None, "ci-base", "latest")
|
||||
'ci-base:latest'
|
||||
>>> build_full_tag("git.example.com", "ci-base", "0.1.0")
|
||||
'git.example.com/ci-base:0.1.0'
|
||||
"""
|
||||
if registry:
|
||||
return f"{registry}/{name}:{tag}"
|
||||
return f"{name}:{tag}"
|
||||
|
||||
|
||||
def registry_login(
|
||||
registry: str,
|
||||
username: str,
|
||||
token: str,
|
||||
*,
|
||||
dry_run: bool = False,
|
||||
) -> bool:
|
||||
"""Log in to a Docker registry.
|
||||
|
||||
Returns True on success, False on failure.
|
||||
In dry-run mode, prints the command without executing.
|
||||
"""
|
||||
cmd = ["docker", "login", registry, "-u", username, "--password-stdin"]
|
||||
if dry_run:
|
||||
click.echo(f"[dry-run] {' '.join(cmd)}")
|
||||
return True
|
||||
result = subprocess.run( # nosec B603
|
||||
cmd,
|
||||
input=token,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
click.echo(
|
||||
_("Registry login failed: {error}", error=result.stderr.strip()),
|
||||
err=True,
|
||||
)
|
||||
return False
|
||||
click.echo(f"Logged in to {registry}")
|
||||
return True
|
||||
|
||||
|
||||
def build_image(
|
||||
spec: ImageSpec,
|
||||
registry: str | None = None,
|
||||
*,
|
||||
dry_run: bool = False,
|
||||
pull: bool = False,
|
||||
) -> bool:
|
||||
"""Build a Docker image from a Dockerfile.
|
||||
|
||||
Tags the image with all specified tags, optionally prefixed with the
|
||||
registry. Returns True on success, False on failure.
|
||||
"""
|
||||
if not Path(spec.dockerfile).is_file():
|
||||
click.echo(
|
||||
_("Dockerfile not found: {path}", path=spec.dockerfile),
|
||||
err=True,
|
||||
)
|
||||
return False
|
||||
|
||||
full_tags = [build_full_tag(registry, spec.name, t) for t in spec.tags]
|
||||
cmd = ["docker", "build"]
|
||||
if pull:
|
||||
cmd.append("--pull")
|
||||
for ft in full_tags:
|
||||
cmd.extend(["-t", ft])
|
||||
cmd.extend(["-f", spec.dockerfile, spec.context])
|
||||
|
||||
if dry_run:
|
||||
click.echo(f"[dry-run] {' '.join(cmd)}")
|
||||
return True
|
||||
|
||||
click.echo(f"Building {spec.name} ({len(full_tags)} tag(s))...")
|
||||
result = subprocess.run( # nosec B603
|
||||
cmd,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
click.echo(_("Build failed for {name}", name=spec.name), err=True)
|
||||
return False
|
||||
click.echo(f"Built {spec.name}")
|
||||
return True
|
||||
|
||||
|
||||
def push_image(
|
||||
spec: ImageSpec,
|
||||
registry: str,
|
||||
*,
|
||||
dry_run: bool = False,
|
||||
) -> bool:
|
||||
"""Push all tags of a Docker image to the registry.
|
||||
|
||||
Returns True if all pushes succeed, False if any fail.
|
||||
"""
|
||||
full_tags = [build_full_tag(registry, spec.name, t) for t in spec.tags]
|
||||
all_ok = True
|
||||
for ft in full_tags:
|
||||
cmd = ["docker", "push", ft]
|
||||
if dry_run:
|
||||
click.echo(f"[dry-run] {' '.join(cmd)}")
|
||||
continue
|
||||
click.echo(f"Pushing {ft}...")
|
||||
result = subprocess.run( # nosec B603
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
click.echo(
|
||||
_("Push failed for {tag}: {error}", tag=ft, error=result.stderr.strip()),
|
||||
err=True,
|
||||
)
|
||||
all_ok = False
|
||||
else:
|
||||
click.echo(f"Pushed {ft}")
|
||||
return all_ok
|
||||
|
||||
|
||||
def _get_registry_creds() -> tuple[str, str]:
|
||||
"""Get registry credentials from environment variables.
|
||||
|
||||
Supports both REPO_TOKEN/GITEA_REGISTRY_TOKEN and
|
||||
REGISTRY_USERNAME/GITEA_REGISTRY_USERNAME patterns.
|
||||
"""
|
||||
token = os.environ.get("REPO_TOKEN") or os.environ.get("GITEA_REGISTRY_TOKEN", "")
|
||||
username = os.environ.get("REGISTRY_USERNAME") or os.environ.get("GITEA_REGISTRY_USERNAME", "")
|
||||
return username, token
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--dockerfile",
|
||||
"dockerfile",
|
||||
default=None,
|
||||
help="Path to Dockerfile (for single-image build).",
|
||||
)
|
||||
@click.option(
|
||||
"--context",
|
||||
"context",
|
||||
default=".",
|
||||
help="Build context directory (for single-image build).",
|
||||
)
|
||||
@click.option(
|
||||
"--name",
|
||||
"name",
|
||||
default=None,
|
||||
help="Image name (for single-image build).",
|
||||
)
|
||||
@click.option(
|
||||
"--tag",
|
||||
"tags",
|
||||
multiple=True,
|
||||
help="Tag(s) for the image. Can be repeated. Defaults to 'latest'.",
|
||||
)
|
||||
@click.option(
|
||||
"--manifest",
|
||||
"manifest",
|
||||
default=None,
|
||||
help="Path to JSON manifest file listing images to build.",
|
||||
)
|
||||
@click.option(
|
||||
"--registry",
|
||||
"registry",
|
||||
default=None,
|
||||
help="Registry URL (e.g. git.example.com). If set with --push, images are tagged and pushed there.",
|
||||
)
|
||||
@click.option(
|
||||
"--push",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Push images to the registry after building.",
|
||||
)
|
||||
@click.option(
|
||||
"--dry-run",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Print commands without executing.",
|
||||
)
|
||||
@click.option(
|
||||
"--pull",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Pass --pull to docker build (always fetch latest base image).",
|
||||
)
|
||||
def main(
|
||||
dockerfile: str | None,
|
||||
context: str,
|
||||
name: str | None,
|
||||
tags: tuple[str, ...],
|
||||
manifest: str | None,
|
||||
registry: str | None,
|
||||
push: bool,
|
||||
dry_run: bool,
|
||||
pull: bool,
|
||||
) -> None:
|
||||
"""Build and optionally push Docker images to a Gitea registry."""
|
||||
if manifest:
|
||||
specs = load_manifest(manifest)
|
||||
elif dockerfile and name:
|
||||
tag_list = list(tags) if tags else ["latest"]
|
||||
specs = [ImageSpec(name=name, dockerfile=dockerfile, context=context, tags=tag_list)]
|
||||
else:
|
||||
raise click.ClickException(_("Provide --manifest or both --dockerfile and --name"))
|
||||
|
||||
if push:
|
||||
if not registry:
|
||||
raise click.ClickException(_("--push requires --registry"))
|
||||
username, token = _get_registry_creds()
|
||||
if not token or not username:
|
||||
raise click.ClickException(
|
||||
_("Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars")
|
||||
)
|
||||
if not registry_login(registry, username, token, dry_run=dry_run):
|
||||
raise click.ClickException(_("Registry login failed"))
|
||||
|
||||
failed: list[str] = []
|
||||
for spec in specs:
|
||||
if not build_image(spec, registry, dry_run=dry_run, pull=pull):
|
||||
failed.append(spec.name)
|
||||
continue
|
||||
if push and not push_image(spec, registry, dry_run=dry_run): # type: ignore[arg-type]
|
||||
failed.append(spec.name)
|
||||
|
||||
if failed:
|
||||
raise click.ClickException(_("Failed images: {names}", names=", ".join(failed)))
|
||||
click.echo(f"\nDone. {len(specs)} image(s) processed.")
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main() # pragma: no cover
|
||||
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Clean up old Docker images from a Gitea container registry.
|
||||
|
||||
Queries the Gitea API for all versions of a package (container type) and
|
||||
deletes all but the most recent N versions. The ``latest`` tag is always
|
||||
preserved if present.
|
||||
|
||||
Usage::
|
||||
|
||||
# Clean up ci-base images, keep last 2 versions
|
||||
python3 -m devx.tools.clean_images \\
|
||||
--owner oblachno-oss \\
|
||||
--name ci-base \\
|
||||
--keep 2
|
||||
|
||||
# Clean up multiple images
|
||||
python3 -m devx.tools.clean_images \\
|
||||
--owner oblachno-oss \\
|
||||
--name ci-base \\
|
||||
--name ci-quality \\
|
||||
--name ci-full \\
|
||||
--keep 2
|
||||
|
||||
# Dry run (list what would be deleted)
|
||||
python3 -m devx.tools.clean_images \\
|
||||
--owner oblachno-oss \\
|
||||
--name ci-base \\
|
||||
--keep 2 \\
|
||||
--dry-run
|
||||
|
||||
Authentication uses ``REPO_TOKEN`` environment variable.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
import requests
|
||||
|
||||
from devx.config import GITEA_API_URL
|
||||
from devx.i18n import _
|
||||
|
||||
|
||||
def list_package_versions(
|
||||
api_url: str,
|
||||
owner: str,
|
||||
name: str,
|
||||
token: str,
|
||||
*,
|
||||
timeout: int = 30,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""List all versions of a container package from the Gitea API.
|
||||
|
||||
Returns a list of version dicts, each containing at least ``version``
|
||||
and ``created_at`` fields.
|
||||
"""
|
||||
url = f"{api_url}/packages/{owner}?type=container&name={name}"
|
||||
headers = {"Authorization": f"token {token}"}
|
||||
all_versions: list[dict[str, Any]] = []
|
||||
page = 1
|
||||
while True:
|
||||
resp = requests.get(
|
||||
f"{url}&page={page}&limit=50",
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if not data:
|
||||
break
|
||||
all_versions.extend(data)
|
||||
if len(data) < 50:
|
||||
break
|
||||
page += 1
|
||||
return all_versions
|
||||
|
||||
|
||||
def delete_package_version(
|
||||
api_url: str,
|
||||
owner: str,
|
||||
name: str,
|
||||
version: str,
|
||||
token: str,
|
||||
*,
|
||||
timeout: int = 30,
|
||||
) -> bool:
|
||||
"""Delete a specific version of a container package.
|
||||
|
||||
Returns True on success, False on failure.
|
||||
"""
|
||||
url = f"{api_url}/packages/{owner}/{name}/{version}"
|
||||
headers = {"Authorization": f"token {token}"}
|
||||
resp = requests.delete(url, headers=headers, timeout=timeout)
|
||||
return resp.status_code in (204, 200)
|
||||
|
||||
|
||||
def sort_versions_by_date(
|
||||
versions: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Sort package versions by creation date, newest first.
|
||||
|
||||
Falls back to version string comparison if created_at is missing.
|
||||
"""
|
||||
|
||||
def _sort_key(v: dict[str, Any]) -> str:
|
||||
return str(v.get("created_at", v.get("version", "")))
|
||||
|
||||
return sorted(versions, key=_sort_key, reverse=True)
|
||||
|
||||
|
||||
def select_for_deletion(
|
||||
versions: list[dict[str, Any]],
|
||||
keep: int,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Select versions to delete, keeping the most recent ``keep`` versions.
|
||||
|
||||
Versions named ``latest`` are always preserved.
|
||||
"""
|
||||
sorted_versions = sort_versions_by_date(versions)
|
||||
to_delete = sorted_versions[keep:]
|
||||
# Always preserve 'latest' tag
|
||||
to_delete = [v for v in to_delete if v.get("version") != "latest"]
|
||||
return to_delete
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--owner",
|
||||
required=True,
|
||||
help="Package owner (user or org).",
|
||||
)
|
||||
@click.option(
|
||||
"--name",
|
||||
"names",
|
||||
multiple=True,
|
||||
required=True,
|
||||
help="Package name(s). Can be repeated.",
|
||||
)
|
||||
@click.option(
|
||||
"--keep",
|
||||
default=2,
|
||||
type=int,
|
||||
show_default=True,
|
||||
help="Number of recent versions to keep (excluding 'latest').",
|
||||
)
|
||||
@click.option(
|
||||
"--dry-run",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="List versions that would be deleted without actually deleting.",
|
||||
)
|
||||
@click.option(
|
||||
"--api-url",
|
||||
default=None,
|
||||
help="Gitea API URL (defaults to DEVX_GITEA_API_URL or built-in default).",
|
||||
)
|
||||
def main(
|
||||
owner: str,
|
||||
names: tuple[str, ...],
|
||||
keep: int,
|
||||
dry_run: bool,
|
||||
api_url: str | None,
|
||||
) -> None:
|
||||
"""Clean up old Docker image versions from a Gitea registry."""
|
||||
token = os.environ.get("REPO_TOKEN", "")
|
||||
if not token:
|
||||
raise click.ClickException(_("REPO_TOKEN environment variable required"))
|
||||
base_url = api_url or GITEA_API_URL
|
||||
|
||||
total_deleted = 0
|
||||
total_kept = 0
|
||||
for name in names:
|
||||
click.echo(f"\n{'=' * 60}")
|
||||
click.echo(f"Package: {owner}/{name}")
|
||||
click.echo(f"{'=' * 60}")
|
||||
try:
|
||||
versions = list_package_versions(base_url, owner, name, token)
|
||||
except requests.RequestException as exc:
|
||||
click.echo(
|
||||
_("Failed to list versions for {name}: {error}", name=name, error=exc),
|
||||
err=True,
|
||||
)
|
||||
continue
|
||||
|
||||
if not versions:
|
||||
click.echo(_("No versions found."))
|
||||
continue
|
||||
|
||||
click.echo(f"Found {len(versions)} version(s):")
|
||||
for v in sort_versions_by_date(versions):
|
||||
click.echo(f" {v.get('version', '?')} (created: {v.get('created_at', '?')})")
|
||||
|
||||
to_delete = select_for_deletion(versions, keep)
|
||||
kept_count = len(versions) - len(to_delete)
|
||||
click.echo(f"\nKeeping {kept_count}, would delete {len(to_delete)}")
|
||||
|
||||
if dry_run:
|
||||
for v in to_delete:
|
||||
click.echo(f" [dry-run] Would delete: {v.get('version', '?')}")
|
||||
total_kept += kept_count
|
||||
continue
|
||||
|
||||
deleted_count = 0
|
||||
for v in to_delete:
|
||||
version = str(v.get("version", ""))
|
||||
if delete_package_version(base_url, owner, name, version, token):
|
||||
click.echo(f" Deleted: {version}")
|
||||
deleted_count += 1
|
||||
else:
|
||||
click.echo(f" FAILED to delete: {version}", err=True)
|
||||
|
||||
total_deleted += deleted_count
|
||||
total_kept += kept_count
|
||||
|
||||
click.echo(f"\nDone. Deleted {total_deleted}, kept {total_kept}.")
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main() # pragma: no cover
|
||||
+517
-381
@@ -343,6 +343,14 @@
|
||||
"ru": " Updated: {title}",
|
||||
"zh": " Updated: {title}"
|
||||
},
|
||||
"--push requires --registry": {
|
||||
"bg": "--push requires --registry",
|
||||
"de": "--push requires --registry",
|
||||
"en": "--push requires --registry",
|
||||
"pl": "--push requires --registry",
|
||||
"ru": "--push requires --registry",
|
||||
"zh": "--push requires --registry"
|
||||
},
|
||||
"--skip-build: skipping package build and PyPI publish.": {
|
||||
"bg": "--skip-build: skipping package build and PyPI publish.",
|
||||
"de": "--skip-build: skipping package build and PyPI publish.",
|
||||
@@ -367,6 +375,14 @@
|
||||
"ru": "API poll warning: {exc}",
|
||||
"zh": "API poll warning: {exc}"
|
||||
},
|
||||
"Additional directory to scan (default: scripts, tests). Can be repeated.": {
|
||||
"bg": "Additional directory to scan (default: scripts, tests). Can be repeated.",
|
||||
"de": "Additional directory to scan (default: scripts, tests). Can be repeated.",
|
||||
"en": "Additional directory to scan (default: scripts, tests). Can be repeated.",
|
||||
"pl": "Additional directory to scan (default: scripts, tests). Can be repeated.",
|
||||
"ru": "Additional directory to scan (default: scripts, tests). Can be repeated.",
|
||||
"zh": "Additional directory to scan (default: scripts, tests). Can be repeated."
|
||||
},
|
||||
"All molecule tests passed.": {
|
||||
"bg": "All molecule tests passed.",
|
||||
"de": "All molecule tests passed.",
|
||||
@@ -383,6 +399,22 @@
|
||||
"ru": "Another molecule runner failed. Stopping this runner early.",
|
||||
"zh": "Another molecule runner failed. Stopping this runner early."
|
||||
},
|
||||
"Branch '{branch}' does not contain a task ID.\n Expected format: {prefix}-N-short-description": {
|
||||
"bg": "Клон '{branch}' не съдържа ID на задача.\n Очакван формат: {prefix}-N-кратко-описание",
|
||||
"de": "Branch '{branch}' enthält keine Task-ID.\n Erwartetes Format: {prefix}-N-kurz-beschreibung",
|
||||
"en": "Branch '{branch}' does not contain a task ID.\n Expected format: {prefix}-N-short-description",
|
||||
"pl": "Gałąź '{branch}' nie zawiera ID zadania.\n Oczekiwany format: {prefix}-N-krótki-opis",
|
||||
"ru": "Ветка '{branch}' не содержит ID задачи.\n Ожидаемый формат: {prefix}-N-краткое-описание",
|
||||
"zh": "分支 '{branch}' 不包含任务 ID。\n 预期格式: {prefix}-N-简短描述"
|
||||
},
|
||||
"Branch '{branch}' does not contain a task ID.\n Expected format: {prefix}-N-short-description\n Example: {prefix}-42-add-feature\n Fix: rename the branch or create a Vikunja task first:\n python -m devx.tools.create_task --title \"Task title\"": {
|
||||
"bg": "Клон '{branch}' не съдържа ID на задача.\n Очакван формат: {prefix}-N-кратко-описание\n Пример: {prefix}-42-add-feature\n Решение: преименувайте клона или създайте Vikunja задача:\n python -m devx.tools.create_task --title \"Заглавие на задача\"",
|
||||
"de": "Branch '{branch}' enthält keine Task-ID.\n Erwartetes Format: {prefix}-N-kurz-beschreibung\n Beispiel: {prefix}-42-add-feature\n Fix: Branch umbenennen oder Vikunja-Task erstellen:\n python -m devx.tools.create_task --title \"Task-Titel\"",
|
||||
"en": "Branch '{branch}' does not contain a task ID.\n Expected format: {prefix}-N-short-description\n Example: {prefix}-42-add-feature\n Fix: rename the branch or create a Vikunja task first:\n python -m devx.tools.create_task --title \"Task title\"",
|
||||
"pl": "Gałąź '{branch}' nie zawiera ID zadania.\n Oczekiwany format: {prefix}-N-krótki-opis\n Przykład: {prefix}-42-add-feature\n Naprawa: zmień nazwę gałęzi lub utwórz zadanie Vikunja:\n python -m devx.tools.create_task --title \"Tytuł zadania\"",
|
||||
"ru": "Ветка '{branch}' не содержит ID задачи.\n Ожидаемый формат: {prefix}-N-краткое-описание\n Пример: {prefix}-42-add-feature\n Исправление: переименуйте ветку или создайте задачу Vikunja:\n python -m devx.tools.create_task --title \"Заголовок задачи\"",
|
||||
"zh": "分支 '{branch}' 不包含任务 ID。\n 预期格式: {prefix}-N-简短描述\n 示例: {prefix}-42-add-feature\n 修复: 重命名分支或先创建 Vikunja 任务:\n python -m devx.tools.create_task --title \"任务标题\""
|
||||
},
|
||||
"Branch is behind master. Rebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.": {
|
||||
"bg": "Branch is behind master. Rebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
|
||||
"de": "Branch is behind master. Rebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
|
||||
@@ -391,6 +423,38 @@
|
||||
"ru": "Branch is behind master. Rebase manually:\n git fetch origin master && git rebase origin/master && git push --force-with-lease\nThen re-add the ready-to-merge label.",
|
||||
"zh": "Branch is behind master. Rebase 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 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",
|
||||
"en": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
|
||||
"pl": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
|
||||
"ru": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
|
||||
"zh": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master"
|
||||
},
|
||||
"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)",
|
||||
"en": "Branch name (e.g., DEVX-256-fix-foo)",
|
||||
"pl": "Branch name (e.g., DEVX-256-fix-foo)",
|
||||
"ru": "Branch name (e.g., DEVX-256-fix-foo)",
|
||||
"zh": "Branch name (e.g., DEVX-256-fix-foo)"
|
||||
},
|
||||
"Branch name must contain a task ID.": {
|
||||
"bg": "Branch name must contain a task ID.",
|
||||
"de": "Branch name must contain a task ID.",
|
||||
"en": "Branch name must contain a task ID.",
|
||||
"pl": "Branch name must contain a task ID.",
|
||||
"ru": "Branch name must contain a task ID.",
|
||||
"zh": "Branch name must contain a task ID."
|
||||
},
|
||||
"Build failed for {name}": {
|
||||
"bg": "Build failed for {name}",
|
||||
"de": "Build failed for {name}",
|
||||
"en": "Build failed for {name}",
|
||||
"pl": "Build failed for {name}",
|
||||
"ru": "Build failed for {name}",
|
||||
"zh": "Build failed for {name}"
|
||||
},
|
||||
"Bumping version: {current} -> v{new_version}": {
|
||||
"bg": "Bumping version: {current} -> v{new_version}",
|
||||
"de": "Bumping version: {current} -> v{new_version}",
|
||||
@@ -399,6 +463,14 @@
|
||||
"ru": "Bumping version: {current} -> v{new_version}",
|
||||
"zh": "Bumping version: {current} -> v{new_version}"
|
||||
},
|
||||
"Check that changed files have corresponding tests": {
|
||||
"bg": "Check that changed files have corresponding tests",
|
||||
"de": "Check that changed files have corresponding tests",
|
||||
"en": "Check that changed files have corresponding tests",
|
||||
"pl": "Check that changed files have corresponding tests",
|
||||
"ru": "Check that changed files have corresponding tests",
|
||||
"zh": "Check that changed files have corresponding tests"
|
||||
},
|
||||
"Checking CLI command documentation...": {
|
||||
"bg": "Checking CLI command documentation...",
|
||||
"de": "Checking CLI command documentation...",
|
||||
@@ -423,6 +495,14 @@
|
||||
"ru": "Comparing {base}..{head} ({count} files changed)",
|
||||
"zh": "Comparing {base}..{head} ({count} files changed)"
|
||||
},
|
||||
"Configuration OK: [tool.devx] present, devx versions consistent.": {
|
||||
"bg": "Конфигурацията е OK: [tool.devx] присъства, версиите на devx са консистентни.",
|
||||
"de": "Konfiguration OK: [tool.devx] vorhanden, devx-Versionen konsistent.",
|
||||
"en": "Configuration OK: [tool.devx] present, devx versions consistent.",
|
||||
"pl": "Konfiguracja OK: [tool.devx] obecne, wersje devx spójne.",
|
||||
"ru": "Конфигурация OK: [tool.devx] присутствует, версии devx согласованы.",
|
||||
"zh": "配置正常: [tool.devx] 已存在, devx 版本一致。"
|
||||
},
|
||||
"Configuring branch protection for {branch}...": {
|
||||
"bg": "Конфигуриране на защита на клона {branch}...",
|
||||
"de": "Konfiguriere Branch-Schutz für {branch}...",
|
||||
@@ -439,13 +519,13 @@
|
||||
"ru": "Настройка параметров репозитория...",
|
||||
"zh": "正在配置仓库设置..."
|
||||
},
|
||||
"Configuration OK: [tool.devx] present, devx versions consistent.": {
|
||||
"bg": "Конфигурацията е OK: [tool.devx] присъства, версиите на devx са консистентни.",
|
||||
"de": "Konfiguration OK: [tool.devx] vorhanden, devx-Versionen konsistent.",
|
||||
"en": "Configuration OK: [tool.devx] present, devx versions consistent.",
|
||||
"pl": "Konfiguracja OK: [tool.devx] obecne, wersje devx spójne.",
|
||||
"ru": "Конфигурация OK: [tool.devx] присутствует, версии devx согласованы.",
|
||||
"zh": "配置正常: [tool.devx] 已存在, devx 版本一致。"
|
||||
"Could not detect current branch: {error}": {
|
||||
"bg": "Не може да се определи текущия клон: {error}",
|
||||
"de": "Aktueller Branch konnte nicht erkannt werden: {error}",
|
||||
"en": "Could not detect current branch: {error}",
|
||||
"pl": "Nie można wykryć bieżącej gałęzi: {error}",
|
||||
"ru": "Не удалось определить текущую ветку: {error}",
|
||||
"zh": "无法检测当前分支: {error}"
|
||||
},
|
||||
"Could not extract conventional commit message from PR commits.": {
|
||||
"bg": "Could not extract conventional commit message from PR commits.",
|
||||
@@ -455,6 +535,22 @@
|
||||
"ru": "Could not extract conventional commit message from PR commits.",
|
||||
"zh": "Could not extract conventional commit message from PR commits."
|
||||
},
|
||||
"Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).": {
|
||||
"bg": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
|
||||
"de": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
|
||||
"en": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
|
||||
"pl": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
|
||||
"ru": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
|
||||
"zh": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found)."
|
||||
},
|
||||
"Could not find Vikunja task {task_id} in project {project_id}.": {
|
||||
"bg": "Не е намерена Vikunja задача {task_id} в проект {project_id}.",
|
||||
"de": "Vikunja-Task {task_id} in Projekt {project_id} nicht gefunden.",
|
||||
"en": "Could not find Vikunja task {task_id} in project {project_id}.",
|
||||
"pl": "Nie znaleziono zadania Vikunja {task_id} w projekcie {project_id}.",
|
||||
"ru": "Не найдена задача Vikunja {task_id} в проекте {project_id}.",
|
||||
"zh": "在项目 {project_id} 中找不到 Vikunja 任务 {task_id}。"
|
||||
},
|
||||
"Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.": {
|
||||
"bg": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
|
||||
"de": "Could not find Vikunja task {task_id} in project {project_id}. Every PR must have a corresponding Vikunja task.",
|
||||
@@ -479,6 +575,22 @@
|
||||
"ru": "Could not parse test execution time from output.",
|
||||
"zh": "Could not parse test execution time from output."
|
||||
},
|
||||
"Created PR #{index}: {title}\n {url}": {
|
||||
"bg": "Създаден PR #{index}: {title}\n {url}",
|
||||
"de": "PR erstellt #{index}: {title}\n {url}",
|
||||
"en": "Created PR #{index}: {title}\n {url}",
|
||||
"pl": "Utworzono PR #{index}: {title}\n {url}",
|
||||
"ru": "Создан PR #{index}: {title}\n {url}",
|
||||
"zh": "已创建 PR #{index}: {title}\n {url}"
|
||||
},
|
||||
"Created Vikunja task: {identifier} (id={task_id})": {
|
||||
"bg": "Създадена Vikunja задача: {identifier} (id={task_id})",
|
||||
"de": "Vikunja-Task erstellt: {identifier} (id={task_id})",
|
||||
"en": "Created Vikunja task: {identifier} (id={task_id})",
|
||||
"pl": "Utworzono zadanie Vikunja: {identifier} (id={task_id})",
|
||||
"ru": "Создана задача Vikunja: {identifier} (id={task_id})",
|
||||
"zh": "已创建 Vikunja 任务: {identifier} (id={task_id})"
|
||||
},
|
||||
"Created issue #{issue_id}: {title}": {
|
||||
"bg": "Created issue #{issue_id}: {title}",
|
||||
"de": "Created issue #{issue_id}: {title}",
|
||||
@@ -495,13 +607,13 @@
|
||||
"ru": "Created release commit.",
|
||||
"zh": "Created release commit."
|
||||
},
|
||||
"devx version mismatch across extras: {detail}": {
|
||||
"bg": "несъответствие на версията на devx между extras: {detail}",
|
||||
"de": "devx-Versionskonflikt zwischen Extras: {detail}",
|
||||
"en": "devx version mismatch across extras: {detail}",
|
||||
"pl": "niezgodność wersji devx między extras: {detail}",
|
||||
"ru": "несоответствие версии devx между extras: {detail}",
|
||||
"zh": "devx 版本在 extras 之间不一致: {detail}"
|
||||
"Dependencies must have documentation comments.": {
|
||||
"bg": "Dependencies must have documentation comments.",
|
||||
"de": "Dependencies must have documentation comments.",
|
||||
"en": "Dependencies must have documentation comments.",
|
||||
"pl": "Dependencies must have documentation comments.",
|
||||
"ru": "Dependencies must have documentation comments.",
|
||||
"zh": "Dependencies must have documentation comments."
|
||||
},
|
||||
"Docker daemon already running": {
|
||||
"bg": "Докер демонът вече работи",
|
||||
@@ -527,6 +639,14 @@
|
||||
"ru": "Docker-демон запущен",
|
||||
"zh": "Docker 守护进程已启动"
|
||||
},
|
||||
"Dockerfile not found: {path}": {
|
||||
"bg": "Dockerfile not found: {path}",
|
||||
"de": "Dockerfile not found: {path}",
|
||||
"en": "Dockerfile not found: {path}",
|
||||
"pl": "Dockerfile not found: {path}",
|
||||
"ru": "Dockerfile not found: {path}",
|
||||
"zh": "Dockerfile not found: {path}"
|
||||
},
|
||||
"Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.": {
|
||||
"bg": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
|
||||
"de": "Dry-run mode: on branch '{branch}' (not master). Some checks may behave differently.",
|
||||
@@ -575,6 +695,14 @@
|
||||
"ru": "ERROR: mapping.json not found at {path}",
|
||||
"zh": "ERROR: mapping.json not found at {path}"
|
||||
},
|
||||
"FAILED: {count} undocumented dependency/ies": {
|
||||
"bg": "FAILED: {count} undocumented dependency/ies",
|
||||
"de": "FAILED: {count} undocumented dependency/ies",
|
||||
"en": "FAILED: {count} undocumented dependency/ies",
|
||||
"pl": "FAILED: {count} undocumented dependency/ies",
|
||||
"ru": "FAILED: {count} undocumented dependency/ies",
|
||||
"zh": "FAILED: {count} undocumented dependency/ies"
|
||||
},
|
||||
"FAILED: {pair} exited with code {code}": {
|
||||
"bg": "FAILED: {pair} exited with code {code}",
|
||||
"de": "FAILED: {pair} exited with code {code}",
|
||||
@@ -583,6 +711,14 @@
|
||||
"ru": "FAILED: {pair} exited with code {code}",
|
||||
"zh": "FAILED: {pair} exited with code {code}"
|
||||
},
|
||||
"Failed images: {names}": {
|
||||
"bg": "Failed images: {names}",
|
||||
"de": "Failed images: {names}",
|
||||
"en": "Failed images: {names}",
|
||||
"pl": "Failed images: {names}",
|
||||
"ru": "Failed images: {names}",
|
||||
"zh": "Failed images: {names}"
|
||||
},
|
||||
"Failed to create issue via tea: {error}": {
|
||||
"bg": "Failed to create issue via tea: {error}",
|
||||
"de": "Failed to create issue via tea: {error}",
|
||||
@@ -591,6 +727,14 @@
|
||||
"ru": "Failed to create issue via tea: {error}",
|
||||
"zh": "Failed to create issue via tea: {error}"
|
||||
},
|
||||
"Failed to list versions for {name}: {error}": {
|
||||
"bg": "Failed to list versions for {name}: {error}",
|
||||
"de": "Failed to list versions for {name}: {error}",
|
||||
"en": "Failed to list versions for {name}: {error}",
|
||||
"pl": "Failed to list versions for {name}: {error}",
|
||||
"ru": "Failed to list versions for {name}: {error}",
|
||||
"zh": "Failed to list versions for {name}: {error}"
|
||||
},
|
||||
"Found {count} existing wiki pages.": {
|
||||
"bg": "Found {count} existing wiki pages.",
|
||||
"de": "Found {count} existing wiki pages.",
|
||||
@@ -599,6 +743,22 @@
|
||||
"ru": "Found {count} existing wiki pages.",
|
||||
"zh": "Found {count} existing wiki pages."
|
||||
},
|
||||
"Found {count} mutable global(s) — use factory functions or pytest fixtures.": {
|
||||
"bg": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
|
||||
"de": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
|
||||
"en": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
|
||||
"pl": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
|
||||
"ru": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
|
||||
"zh": "Found {count} mutable global(s) — use factory functions or pytest fixtures."
|
||||
},
|
||||
"Found {count} stale documentation reference(s)": {
|
||||
"bg": "Found {count} stale documentation reference(s)",
|
||||
"de": "Found {count} stale documentation reference(s)",
|
||||
"en": "Found {count} stale documentation reference(s)",
|
||||
"pl": "Found {count} stale documentation reference(s)",
|
||||
"ru": "Found {count} stale documentation reference(s)",
|
||||
"zh": "Found {count} stale documentation reference(s)"
|
||||
},
|
||||
"GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.": {
|
||||
"bg": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
||||
"de": "GITEA_URL/REPO_TOKEN/RUN_ID not set; running without cross-runner cancellation.",
|
||||
@@ -687,6 +847,30 @@
|
||||
"ru": "Хост Docker недоступен, запускается локальный dockerd...",
|
||||
"zh": "主机 Docker 不可用,正在启动本地 dockerd..."
|
||||
},
|
||||
"Image 'tags' must be a list": {
|
||||
"bg": "Image 'tags' must be a list",
|
||||
"de": "Image 'tags' must be a list",
|
||||
"en": "Image 'tags' must be a list",
|
||||
"pl": "Image 'tags' must be a list",
|
||||
"ru": "Image 'tags' must be a list",
|
||||
"zh": "Image 'tags' must be a list"
|
||||
},
|
||||
"Image manifest entry missing 'dockerfile'": {
|
||||
"bg": "Image manifest entry missing 'dockerfile'",
|
||||
"de": "Image manifest entry missing 'dockerfile'",
|
||||
"en": "Image manifest entry missing 'dockerfile'",
|
||||
"pl": "Image manifest entry missing 'dockerfile'",
|
||||
"ru": "Image manifest entry missing 'dockerfile'",
|
||||
"zh": "Image manifest entry missing 'dockerfile'"
|
||||
},
|
||||
"Image manifest entry missing 'name'": {
|
||||
"bg": "Image manifest entry missing 'name'",
|
||||
"de": "Image manifest entry missing 'name'",
|
||||
"en": "Image manifest entry missing 'name'",
|
||||
"pl": "Image manifest entry missing 'name'",
|
||||
"ru": "Image manifest entry missing 'name'",
|
||||
"zh": "Image manifest entry missing 'name'"
|
||||
},
|
||||
"Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}": {
|
||||
"bg": "Инфраструктурен commit (без идентификатор на задача DEVX-N), пропускаме обновяването на Vikunja: {msg}",
|
||||
"de": "Infrastruktur-Commit (keine DEVX-N Task-ID), Vikunja-Update wird übersprungen: {msg}",
|
||||
@@ -735,6 +919,22 @@
|
||||
"ru": "Lint passed.",
|
||||
"zh": "Lint passed."
|
||||
},
|
||||
"Manifest file not found: {path}": {
|
||||
"bg": "Manifest file not found: {path}",
|
||||
"de": "Manifest file not found: {path}",
|
||||
"en": "Manifest file not found: {path}",
|
||||
"pl": "Manifest file not found: {path}",
|
||||
"ru": "Manifest file not found: {path}",
|
||||
"zh": "Manifest file not found: {path}"
|
||||
},
|
||||
"Manifest must be a JSON list": {
|
||||
"bg": "Manifest must be a JSON list",
|
||||
"de": "Manifest must be a JSON list",
|
||||
"en": "Manifest must be a JSON list",
|
||||
"pl": "Manifest must be a JSON list",
|
||||
"ru": "Manifest must be a JSON list",
|
||||
"zh": "Manifest must be a JSON list"
|
||||
},
|
||||
"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.",
|
||||
@@ -775,6 +975,14 @@
|
||||
"ru": "Директория molecule не найдена: {path}",
|
||||
"zh": "未找到 molecule 目录: {path}"
|
||||
},
|
||||
"Next steps:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-short-description\n 3. Implement changes, commit with conventional commit format\n 4. git push -u origin HEAD\n 5. make create-pr (creates PR with title: {identifier}: {title})": {
|
||||
"bg": "Следващи стъпки:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-кратко-описание\n 3. Имплементирайте промените, commit с conventional commit формат\n 4. git push -u origin HEAD\n 5. make create-pr (създава PR с заглавие: {identifier}: {title})",
|
||||
"de": "Nächste Schritte:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-kurz-beschreibung\n 3. Änderungen implementieren, mit Conventional-Commit-Format committen\n 4. git push -u origin HEAD\n 5. make create-pr (erstellt PR mit Titel: {identifier}: {title})",
|
||||
"en": "Next steps:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-short-description\n 3. Implement changes, commit with conventional commit format\n 4. git push -u origin HEAD\n 5. make create-pr (creates PR with title: {identifier}: {title})",
|
||||
"pl": "Następne kroki:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-krótki-opis\n 3. Wprowadź zmiany, commituj w formacie conventional commit\n 4. git push -u origin HEAD\n 5. make create-pr (tworzy PR z tytułem: {identifier}: {title})",
|
||||
"ru": "Следующие шаги:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-краткое-описание\n 3. Реализуйте изменения, коммитьте в conventional commit формате\n 4. git push -u origin HEAD\n 5. make create-pr (создаёт PR с заголовком: {identifier}: {title})",
|
||||
"zh": "后续步骤:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-简短描述\n 3. 实现更改,使用 conventional commit 格式提交\n 4. git push -u origin HEAD\n 5. make create-pr (创建 PR,标题: {identifier}: {title})"
|
||||
},
|
||||
"Nice! Gitea release {tag} created.": {
|
||||
"bg": "Отлично! Gitea release {tag} е създаден.",
|
||||
"de": "Prima! Gitea-Release {tag} erstellt.",
|
||||
@@ -847,6 +1055,14 @@
|
||||
"ru": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID.",
|
||||
"zh": "No task ID ({prefix}-N) found in commit message: {msg}. Every non-infrastructure commit must have a task ID."
|
||||
},
|
||||
"No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.": {
|
||||
"bg": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
|
||||
"de": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
|
||||
"en": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
|
||||
"pl": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
|
||||
"ru": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
|
||||
"zh": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description."
|
||||
},
|
||||
"No unreleased changes found. Nothing to release.": {
|
||||
"bg": "No unreleased changes found. Nothing to release.",
|
||||
"de": "No unreleased changes found. Nothing to release.",
|
||||
@@ -863,6 +1079,14 @@
|
||||
"ru": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release.",
|
||||
"zh": "No user-facing changes since {tag} — only workflow/infrastructure files changed. Skipping release."
|
||||
},
|
||||
"No versions found.": {
|
||||
"bg": "No versions found.",
|
||||
"de": "No versions found.",
|
||||
"en": "No versions found.",
|
||||
"pl": "No versions found.",
|
||||
"ru": "No versions found.",
|
||||
"zh": "No versions found."
|
||||
},
|
||||
"Note: Self-approval not allowed. Posting COMMENT instead.": {
|
||||
"bg": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
||||
"de": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
||||
@@ -871,6 +1095,14 @@
|
||||
"ru": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
||||
"zh": "Note: Self-approval not allowed. Posting COMMENT instead."
|
||||
},
|
||||
"Only check staged files (for pre-commit)": {
|
||||
"bg": "Only check staged files (for pre-commit)",
|
||||
"de": "Only check staged files (for pre-commit)",
|
||||
"en": "Only check staged files (for pre-commit)",
|
||||
"pl": "Only check staged files (for pre-commit)",
|
||||
"ru": "Only check staged files (for pre-commit)",
|
||||
"zh": "Only check staged files (for pre-commit)"
|
||||
},
|
||||
"Oops! Commit message must follow conventional commit format.\n Expected: <type>: <description>\n Got: {subject}\n Allowed types: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE": {
|
||||
"bg": "Опа! Съобщението за commit трябва да следва конвенционален формат.\n Очаква се: <type>: <description>\n Получено: {subject}\n Разрешени типове: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
||||
"de": "Ups! Commit-Nachricht muss dem konventionellen Commit-Format folgen.\n Erwartet: <type>: <description>\n Erhalten: {subject}\n Erlaubte Typen: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
||||
@@ -959,6 +1191,22 @@
|
||||
"ru": "PASSED: {pair}",
|
||||
"zh": "PASSED: {pair}"
|
||||
},
|
||||
"PR already exists: #{index} — {url}": {
|
||||
"bg": "PR вече съществува: #{index} — {url}",
|
||||
"de": "PR existiert bereits: #{index} — {url}",
|
||||
"en": "PR already exists: #{index} — {url}",
|
||||
"pl": "PR już istnieje: #{index} — {url}",
|
||||
"ru": "PR уже существует: #{index} — {url}",
|
||||
"zh": "PR 已存在: #{index} — {url}"
|
||||
},
|
||||
"PR number (to fetch title from Gitea)": {
|
||||
"bg": "PR number (to fetch title from Gitea)",
|
||||
"de": "PR number (to fetch title from Gitea)",
|
||||
"en": "PR number (to fetch title from Gitea)",
|
||||
"pl": "PR number (to fetch title from Gitea)",
|
||||
"ru": "PR number (to fetch title from Gitea)",
|
||||
"zh": "PR number (to fetch title from Gitea)"
|
||||
},
|
||||
"PR number must be an integer, got: {pr_number}": {
|
||||
"bg": "PR number must be an integer, got: {pr_number}",
|
||||
"de": "PR number must be an integer, got: {pr_number}",
|
||||
@@ -967,6 +1215,14 @@
|
||||
"ru": "PR number must be an integer, got: {pr_number}",
|
||||
"zh": "PR number must be an integer, got: {pr_number}"
|
||||
},
|
||||
"PR title (auto-fetched if --pr-number given)": {
|
||||
"bg": "PR title (auto-fetched if --pr-number given)",
|
||||
"de": "PR title (auto-fetched if --pr-number given)",
|
||||
"en": "PR title (auto-fetched if --pr-number given)",
|
||||
"pl": "PR title (auto-fetched if --pr-number given)",
|
||||
"ru": "PR title (auto-fetched if --pr-number given)",
|
||||
"zh": "PR title (auto-fetched if --pr-number given)"
|
||||
},
|
||||
"PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}": {
|
||||
"bg": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
||||
"de": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
||||
@@ -975,6 +1231,30 @@
|
||||
"ru": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}",
|
||||
"zh": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}"
|
||||
},
|
||||
"PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}": {
|
||||
"bg": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
|
||||
"de": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
|
||||
"en": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
|
||||
"pl": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
|
||||
"ru": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
|
||||
"zh": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}"
|
||||
},
|
||||
"PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}": {
|
||||
"bg": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
|
||||
"de": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
|
||||
"en": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
|
||||
"pl": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
|
||||
"ru": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
|
||||
"zh": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}"
|
||||
},
|
||||
"PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}": {
|
||||
"bg": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
|
||||
"de": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
|
||||
"en": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
|
||||
"pl": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
|
||||
"ru": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
|
||||
"zh": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}"
|
||||
},
|
||||
"PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.": {
|
||||
"bg": "PYPI_TOKEN не е зададен и няма конфигуриран URL на registry — пропускаме публикуването в PyPI. Без притеснения, просто ще създадем Gitea release.",
|
||||
"de": "PYPI_TOKEN nicht gesetzt und keine Registry-URL konfiguriert — PyPI-Veröffentlichung wird übersprungen. Keine Sorge, wir erstellen einfach das Gitea-Release.",
|
||||
@@ -991,6 +1271,14 @@
|
||||
"ru": "Извлечён owner={owner}, repo={repo} из DEVX_REPO_NAME",
|
||||
"zh": "从 DEVX_REPO_NAME 解析 owner={owner}, repo={repo}"
|
||||
},
|
||||
"Path to pyproject.toml (default: pyproject.toml in CWD).": {
|
||||
"bg": "Path to pyproject.toml (default: pyproject.toml in CWD).",
|
||||
"de": "Path to pyproject.toml (default: pyproject.toml in CWD).",
|
||||
"en": "Path to pyproject.toml (default: pyproject.toml in CWD).",
|
||||
"pl": "Path to pyproject.toml (default: pyproject.toml in CWD).",
|
||||
"ru": "Path to pyproject.toml (default: pyproject.toml in CWD).",
|
||||
"zh": "Path to pyproject.toml (default: pyproject.toml in CWD)."
|
||||
},
|
||||
"Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.": {
|
||||
"bg": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
|
||||
"de": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
|
||||
@@ -999,6 +1287,38 @@
|
||||
"ru": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit.",
|
||||
"zh": "Per-test speed check FAILED: {count} test(s) exceed {limit}s limit."
|
||||
},
|
||||
"Pre-merge validation failed.": {
|
||||
"bg": "Pre-merge validation failed.",
|
||||
"de": "Pre-merge validation failed.",
|
||||
"en": "Pre-merge validation failed.",
|
||||
"pl": "Pre-merge validation failed.",
|
||||
"ru": "Pre-merge validation failed.",
|
||||
"zh": "Pre-merge validation failed."
|
||||
},
|
||||
"Pre-push check passed: task {task_id} exists.": {
|
||||
"bg": "Pre-push проверката премина: задача {task_id} съществува.",
|
||||
"de": "Pre-push-Prüfung bestanden: Task {task_id} existiert.",
|
||||
"en": "Pre-push check passed: task {task_id} exists.",
|
||||
"pl": "Sprawdzanie pre-push zakończone: zadanie {task_id} istnieje.",
|
||||
"ru": "Pre-push проверка пройдена: задача {task_id} существует.",
|
||||
"zh": "Pre-push 检查通过: 任务 {task_id} 存在。"
|
||||
},
|
||||
"Print warnings but always exit 0": {
|
||||
"bg": "Print warnings but always exit 0",
|
||||
"de": "Print warnings but always exit 0",
|
||||
"en": "Print warnings but always exit 0",
|
||||
"pl": "Print warnings but always exit 0",
|
||||
"ru": "Print warnings but always exit 0",
|
||||
"zh": "Print warnings but always exit 0"
|
||||
},
|
||||
"Provide --manifest or both --dockerfile and --name": {
|
||||
"bg": "Provide --manifest or both --dockerfile and --name",
|
||||
"de": "Provide --manifest or both --dockerfile and --name",
|
||||
"en": "Provide --manifest or both --dockerfile and --name",
|
||||
"pl": "Provide --manifest or both --dockerfile and --name",
|
||||
"ru": "Provide --manifest or both --dockerfile and --name",
|
||||
"zh": "Provide --manifest or both --dockerfile and --name"
|
||||
},
|
||||
"Provide a commit message file or use --git.": {
|
||||
"bg": "Provide a commit message file or use --git.",
|
||||
"de": "Provide a commit message file or use --git.",
|
||||
@@ -1031,6 +1351,14 @@
|
||||
"ru": "Publishing release {tag}...",
|
||||
"zh": "Publishing release {tag}..."
|
||||
},
|
||||
"Push failed for {tag}: {error}": {
|
||||
"bg": "Push failed for {tag}: {error}",
|
||||
"de": "Push failed for {tag}: {error}",
|
||||
"en": "Push failed for {tag}: {error}",
|
||||
"pl": "Push failed for {tag}: {error}",
|
||||
"ru": "Push failed for {tag}: {error}",
|
||||
"zh": "Push failed for {tag}: {error}"
|
||||
},
|
||||
"Pushed release commit to master.": {
|
||||
"bg": "Pushed release commit to master.",
|
||||
"de": "Pushed release commit to master.",
|
||||
@@ -1047,6 +1375,54 @@
|
||||
"ru": "Публикация в PyPI не удалась (некритично — продолжаем создание Gitea release):\n{error}",
|
||||
"zh": "PyPI 发布失败(非致命 — 继续创建 Gitea release):\n{error}"
|
||||
},
|
||||
"REPO argument is required (or set GITHUB_REPOSITORY env var).": {
|
||||
"bg": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
|
||||
"de": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
|
||||
"en": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
|
||||
"pl": "Argument REPO jest wymagany (lub ustaw zmienną GITHUB_REPOSITORY).",
|
||||
"ru": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
|
||||
"zh": "REPO argument is required (or set GITHUB_REPOSITORY env var)."
|
||||
},
|
||||
"REPO_TOKEN environment variable required": {
|
||||
"bg": "REPO_TOKEN environment variable required",
|
||||
"de": "REPO_TOKEN environment variable required",
|
||||
"en": "REPO_TOKEN environment variable required",
|
||||
"pl": "REPO_TOKEN environment variable required",
|
||||
"ru": "REPO_TOKEN environment variable required",
|
||||
"zh": "REPO_TOKEN environment variable required"
|
||||
},
|
||||
"REPO_TOKEN is not set. Required to create a PR.": {
|
||||
"bg": "REPO_TOKEN не е зададен. Необходим за създаване на PR.",
|
||||
"de": "REPO_TOKEN nicht gesetzt. Erforderlich zum Erstellen eines PR.",
|
||||
"en": "REPO_TOKEN is not set. Required to create a PR.",
|
||||
"pl": "REPO_TOKEN nie jest ustawiony. Wymagany do utworzenia PR.",
|
||||
"ru": "REPO_TOKEN не установлен. Требуется для создания PR.",
|
||||
"zh": "REPO_TOKEN 未设置。创建 PR 所需。"
|
||||
},
|
||||
"Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars": {
|
||||
"bg": "Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars",
|
||||
"de": "Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars",
|
||||
"en": "Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars",
|
||||
"pl": "Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars",
|
||||
"ru": "Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars",
|
||||
"zh": "Registry credentials required: set REPO_TOKEN and REGISTRY_USERNAME env vars"
|
||||
},
|
||||
"Registry login failed": {
|
||||
"bg": "Registry login failed",
|
||||
"de": "Registry login failed",
|
||||
"en": "Registry login failed",
|
||||
"pl": "Registry login failed",
|
||||
"ru": "Registry login failed",
|
||||
"zh": "Registry login failed"
|
||||
},
|
||||
"Registry login failed: {error}": {
|
||||
"bg": "Registry login failed: {error}",
|
||||
"de": "Registry login failed: {error}",
|
||||
"en": "Registry login failed: {error}",
|
||||
"pl": "Registry login failed: {error}",
|
||||
"ru": "Registry login failed: {error}",
|
||||
"zh": "Registry login failed: {error}"
|
||||
},
|
||||
"Release creation failed: {error}": {
|
||||
"bg": "Release creation failed: {error}",
|
||||
"de": "Release creation failed: {error}",
|
||||
@@ -1079,6 +1455,30 @@
|
||||
"ru": "Конфигурация репозитория завершена.",
|
||||
"zh": "仓库配置完成。"
|
||||
},
|
||||
"Repository in owner/name format": {
|
||||
"bg": "Repository in owner/name format",
|
||||
"de": "Repository in owner/name format",
|
||||
"en": "Repository in owner/name format",
|
||||
"pl": "Repository in owner/name format",
|
||||
"ru": "Repository in owner/name format",
|
||||
"zh": "Repository in owner/name format"
|
||||
},
|
||||
"Repository name not set. Use DEVX_REPO_NAME or GITHUB_REPOSITORY env var.": {
|
||||
"bg": "Името на хранилището не е зададено. Използвайте DEVX_REPO_NAME или GITHUB_REPOSITORY env var.",
|
||||
"de": "Repository-Name nicht gesetzt. Verwende DEVX_REPO_NAME oder GITHUB_REPOSITORY env var.",
|
||||
"en": "Repository name not set. Use DEVX_REPO_NAME or GITHUB_REPOSITORY env var.",
|
||||
"pl": "Nazwa repozytorium nie jest ustawiona. Użyj DEVX_REPO_NAME lub GITHUB_REPOSITORY env var.",
|
||||
"ru": "Имя репозитория не установлено. Используйте DEVX_REPO_NAME или GITHUB_REPOSITORY env var.",
|
||||
"zh": "仓库名称未设置。使用 DEVX_REPO_NAME 或 GITHUB_REPOSITORY 环境变量。"
|
||||
},
|
||||
"Repository owner not set. Use --owner or DEVX_REPO_OWNER env var.": {
|
||||
"bg": "Собственикът на хранилището не е зададен. Използвайте --owner или DEVX_REPO_OWNER env var.",
|
||||
"de": "Repository-Owner nicht gesetzt. Verwende --owner oder DEVX_REPO_OWNER env var.",
|
||||
"en": "Repository owner not set. Use --owner or DEVX_REPO_OWNER env var.",
|
||||
"pl": "Właściciel repozytorium nie jest ustawiony. Użyj --owner lub DEVX_REPO_OWNER env var.",
|
||||
"ru": "Владелец репозитория не установлен. Используйте --owner или DEVX_REPO_OWNER env var.",
|
||||
"zh": "仓库所有者未设置。使用 --owner 或 DEVX_REPO_OWNER 环境变量。"
|
||||
},
|
||||
"Roles directory not found: {path}": {
|
||||
"bg": "Roles directory not found: {path}",
|
||||
"de": "Roles directory not found: {path}",
|
||||
@@ -1119,6 +1519,22 @@
|
||||
"ru": "Running: {scenario} on {platform}",
|
||||
"zh": "Running: {scenario} on {platform}"
|
||||
},
|
||||
"Skip Vikunja title match check": {
|
||||
"bg": "Skip Vikunja title match check",
|
||||
"de": "Skip Vikunja title match check",
|
||||
"en": "Skip Vikunja title match check",
|
||||
"pl": "Skip Vikunja title match check",
|
||||
"ru": "Skip Vikunja title match check",
|
||||
"zh": "Skip Vikunja title match check"
|
||||
},
|
||||
"Skip branch-behind-master check": {
|
||||
"bg": "Skip branch-behind-master check",
|
||||
"de": "Skip branch-behind-master check",
|
||||
"en": "Skip branch-behind-master check",
|
||||
"pl": "Skip branch-behind-master check",
|
||||
"ru": "Skip branch-behind-master check",
|
||||
"zh": "Skip branch-behind-master check"
|
||||
},
|
||||
"Skipping commit push — no staged changes.": {
|
||||
"bg": "Skipping commit push — no staged changes.",
|
||||
"de": "Skipping commit push — no staged changes.",
|
||||
@@ -1151,14 +1567,6 @@
|
||||
"ru": "Tag is required (or use --from-tag).",
|
||||
"zh": "Tag is required (or use --from-tag)."
|
||||
},
|
||||
"REPO argument is required (or set GITHUB_REPOSITORY env var).": {
|
||||
"bg": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
|
||||
"de": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
|
||||
"en": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
|
||||
"pl": "Argument REPO jest wymagany (lub ustaw zmienną GITHUB_REPOSITORY).",
|
||||
"ru": "REPO argument is required (or set GITHUB_REPOSITORY env var).",
|
||||
"zh": "REPO argument is required (or set GITHUB_REPOSITORY env var)."
|
||||
},
|
||||
"Tag v{version} already existed. Publish workflow should already have been triggered.": {
|
||||
"bg": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
||||
"de": "Tag v{version} already existed. Publish workflow should already have been triggered.",
|
||||
@@ -1255,6 +1663,22 @@
|
||||
"ru": "Updated {changelog_file}",
|
||||
"zh": "Updated {changelog_file}"
|
||||
},
|
||||
"VIKUNJA_TOKEN is not set. Required to derive PR title.": {
|
||||
"bg": "VIKUNJA_TOKEN не е зададен. Необходим за извличане на PR заглавие.",
|
||||
"de": "VIKUNJA_TOKEN nicht gesetzt. Erforderlich zum Ableiten des PR-Titels.",
|
||||
"en": "VIKUNJA_TOKEN is not set. Required to derive PR title.",
|
||||
"pl": "VIKUNJA_TOKEN nie jest ustawiony. Wymagany do pobrania tytułu PR.",
|
||||
"ru": "VIKUNJA_TOKEN не установлен. Требуется для получения заголовка PR.",
|
||||
"zh": "VIKUNJA_TOKEN 未设置。推导 PR 标题所需。"
|
||||
},
|
||||
"VIKUNJA_TOKEN is not set. Set it in .env or environment.": {
|
||||
"bg": "VIKUNJA_TOKEN не е зададен. Задайте го в .env или средата.",
|
||||
"de": "VIKUNJA_TOKEN nicht gesetzt. In .env oder Umgebung setzen.",
|
||||
"en": "VIKUNJA_TOKEN is not set. Set it in .env or environment.",
|
||||
"pl": "VIKUNJA_TOKEN nie jest ustawiony. Ustaw go w .env lub środowisku.",
|
||||
"ru": "VIKUNJA_TOKEN не установлен. Установите его в .env или среде.",
|
||||
"zh": "VIKUNJA_TOKEN 未设置。在 .env 或环境中设置它。"
|
||||
},
|
||||
"VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.": {
|
||||
"bg": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
||||
"de": "VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.",
|
||||
@@ -1279,6 +1703,14 @@
|
||||
"ru": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update.",
|
||||
"zh": "Vikunja API error (HTTP {status}): {message}. Task {task_id} was NOT updated. The merge succeeded but the Vikunja task needs manual update."
|
||||
},
|
||||
"Vikunja task {task_id} not found in project {project_id}.\n Create it first:\n python -m devx.tools.create_task --title \"Task title\"\n Or check that the task ID in the branch name is correct.": {
|
||||
"bg": "Vikunja задача {task_id} не е намерена в проект {project_id}.\n Създайте я първо:\n python -m devx.tools.create_task --title \"Заглавие на задача\"\n Или проверете че ID на задачата в името на клона е правилно.",
|
||||
"de": "Vikunja-Task {task_id} in Projekt {project_id} nicht gefunden.\n Zuerst erstellen:\n python -m devx.tools.create_task --title \"Task-Titel\"\n Oder prüfen, ob die Task-ID im Branch-Namen korrekt ist.",
|
||||
"en": "Vikunja task {task_id} not found in project {project_id}.\n Create it first:\n python -m devx.tools.create_task --title \"Task title\"\n Or check that the task ID in the branch name is correct.",
|
||||
"pl": "Zadanie Vikunja {task_id} nie znalezione w projekcie {project_id}.\n Utwórz je najpierw:\n python -m devx.tools.create_task --title \"Tytuł zadania\"\n Lub sprawdź, czy ID zadania w nazwie gałęzi jest poprawne.",
|
||||
"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 是否正确。"
|
||||
},
|
||||
"WARNING: --skip-tests passed — skipping test verification.": {
|
||||
"bg": "WARNING: --skip-tests passed — skipping test verification.",
|
||||
"de": "WARNING: --skip-tests passed — skipping test verification.",
|
||||
@@ -1295,6 +1727,14 @@
|
||||
"ru": "ВНИМАНИЕ: Файл .taskid ({file_id}) устарел и не совпадает с именем ветки ({branch_id}). Удалите .taskid из репозитория — имя ветки — единственный источник истины.",
|
||||
"zh": "警告:.taskid 文件 ({file_id}) 已弃用,与分支名称 ({branch_id}) 不一致。请从仓库中删除 .taskid — 分支名称是唯一的真实来源。"
|
||||
},
|
||||
"WARNING: VIKUNJA_TOKEN not set — skipping task existence check. Set it in .env to enable full validation.": {
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: VIKUNJA_TOKEN не е зададен — пропускане на проверката за съществуване на задача. Задайте го в .env за пълна валидация.",
|
||||
"de": "WARNUNG: VIKUNJA_TOKEN nicht gesetzt — Task-Existenzprüfung übersprungen. In .env setzen für volle Validierung.",
|
||||
"en": "WARNING: VIKUNJA_TOKEN not set — skipping task existence check. Set it in .env to enable full validation.",
|
||||
"pl": "OSTRZEŻENIE: VIKUNJA_TOKEN nie jest ustawiony — pomijanie sprawdzania istnienia zadania. Ustaw w .env, aby włączyć pełną walidację.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: VIKUNJA_TOKEN не установлен — пропуск проверки существования задачи. Установите в .env для полной проверки.",
|
||||
"zh": "警告: VIKUNJA_TOKEN 未设置 — 跳过任务存在性检查。在 .env 中设置以启用完整验证。"
|
||||
},
|
||||
"Warning: could not fetch tags from origin.": {
|
||||
"bg": "Warning: could not fetch tags from origin.",
|
||||
"de": "Warning: could not fetch tags from origin.",
|
||||
@@ -1319,13 +1759,45 @@
|
||||
"ru": "Wiki verification failed — {failures} page(s) empty or mismatched",
|
||||
"zh": "Wiki verification failed — {failures} page(s) empty or mismatched"
|
||||
},
|
||||
"[tool.devx] missing required keys: {keys}": {
|
||||
"bg": "[tool.devx] липсват задължителни ключове: {keys}",
|
||||
"de": "[tool.devx] fehlt erforderliche Schlüssel: {keys}",
|
||||
"en": "[tool.devx] missing required keys: {keys}",
|
||||
"pl": "[tool.devx] brak wymaganych kluczy: {keys}",
|
||||
"ru": "[tool.devx] отсутствуют обязательные ключи: {keys}",
|
||||
"zh": "[tool.devx] 缺少必需的键: {keys}"
|
||||
"Wrote tag {tag} to GITHUB_OUTPUT.": {
|
||||
"bg": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"de": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"en": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"ru": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"zh": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"pl": "Wrote tag {tag} to GITHUB_OUTPUT."
|
||||
},
|
||||
"[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",
|
||||
"en": "[check-dep-docs] Passed: all dependencies are documented",
|
||||
"pl": "[check-dep-docs] Passed: all dependencies are documented",
|
||||
"ru": "[check-dep-docs] Passed: all dependencies are documented",
|
||||
"zh": "[check-dep-docs] Passed: all dependencies are documented"
|
||||
},
|
||||
"[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",
|
||||
"en": "[check-mutable-globals] Passed: no mutable path globals found",
|
||||
"pl": "[check-mutable-globals] Passed: no mutable path globals found",
|
||||
"ru": "[check-mutable-globals] Passed: no mutable path globals found",
|
||||
"zh": "[check-mutable-globals] Passed: no mutable path globals found"
|
||||
},
|
||||
"[check_agent_docs] Passed: scanned {count} file(s), no stale references": {
|
||||
"bg": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
|
||||
"de": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
|
||||
"en": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
|
||||
"pl": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
|
||||
"ru": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
|
||||
"zh": "[check_agent_docs] Passed: scanned {count} file(s), no stale references"
|
||||
},
|
||||
"[check_test_coverage] No changed files to check.": {
|
||||
"bg": "[check_test_coverage] No changed files to check.",
|
||||
"de": "[check_test_coverage] No changed files to check.",
|
||||
"en": "[check_test_coverage] No changed files to check.",
|
||||
"pl": "[check_test_coverage] No changed files to check.",
|
||||
"ru": "[check_test_coverage] No changed files to check.",
|
||||
"zh": "[check_test_coverage] No changed files to check."
|
||||
},
|
||||
"[dry-run] Would commit: release: v{version}": {
|
||||
"bg": "[dry-run] Would commit: release: v{version}",
|
||||
@@ -1383,6 +1855,14 @@
|
||||
"ru": "[dry-run] Would update {init}",
|
||||
"zh": "[dry-run] Would update {init}"
|
||||
},
|
||||
"[tool.devx] missing required keys: {keys}": {
|
||||
"bg": "[tool.devx] липсват задължителни ключове: {keys}",
|
||||
"de": "[tool.devx] fehlt erforderliche Schlüssel: {keys}",
|
||||
"en": "[tool.devx] missing required keys: {keys}",
|
||||
"pl": "[tool.devx] brak wymaganych kluczy: {keys}",
|
||||
"ru": "[tool.devx] отсутствуют обязательные ключи: {keys}",
|
||||
"zh": "[tool.devx] 缺少必需的键: {keys}"
|
||||
},
|
||||
"active": {
|
||||
"bg": "активен",
|
||||
"de": "aktiv",
|
||||
@@ -1399,6 +1879,14 @@
|
||||
"ru": "завершён",
|
||||
"zh": "已完成"
|
||||
},
|
||||
"devx version mismatch across extras: {detail}": {
|
||||
"bg": "несъответствие на версията на devx между extras: {detail}",
|
||||
"de": "devx-Versionskonflikt zwischen Extras: {detail}",
|
||||
"en": "devx version mismatch across extras: {detail}",
|
||||
"pl": "niezgodność wersji devx między extras: {detail}",
|
||||
"ru": "несоответствие версии devx между extras: {detail}",
|
||||
"zh": "devx 版本在 extras 之间不一致: {detail}"
|
||||
},
|
||||
"failed": {
|
||||
"bg": "неуспешен",
|
||||
"de": "fehlgeschlagen",
|
||||
@@ -1502,357 +1990,5 @@
|
||||
"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."
|
||||
},
|
||||
"Branch '{branch}' does not contain a task ID.\n Expected format: {prefix}-N-short-description": {
|
||||
"bg": "Клон '{branch}' не съдържа ID на задача.\n Очакван формат: {prefix}-N-кратко-описание",
|
||||
"de": "Branch '{branch}' enthält keine Task-ID.\n Erwartetes Format: {prefix}-N-kurz-beschreibung",
|
||||
"en": "Branch '{branch}' does not contain a task ID.\n Expected format: {prefix}-N-short-description",
|
||||
"pl": "Gałąź '{branch}' nie zawiera ID zadania.\n Oczekiwany format: {prefix}-N-krótki-opis",
|
||||
"ru": "Ветка '{branch}' не содержит ID задачи.\n Ожидаемый формат: {prefix}-N-краткое-описание",
|
||||
"zh": "分支 '{branch}' 不包含任务 ID。\n 预期格式: {prefix}-N-简短描述"
|
||||
},
|
||||
"Branch '{branch}' does not contain a task ID.\n Expected format: {prefix}-N-short-description\n Example: {prefix}-42-add-feature\n Fix: rename the branch or create a Vikunja task first:\n python -m devx.tools.create_task --title \"Task title\"": {
|
||||
"bg": "Клон '{branch}' не съдържа ID на задача.\n Очакван формат: {prefix}-N-кратко-описание\n Пример: {prefix}-42-add-feature\n Решение: преименувайте клона или създайте Vikunja задача:\n python -m devx.tools.create_task --title \"Заглавие на задача\"",
|
||||
"de": "Branch '{branch}' enthält keine Task-ID.\n Erwartetes Format: {prefix}-N-kurz-beschreibung\n Beispiel: {prefix}-42-add-feature\n Fix: Branch umbenennen oder Vikunja-Task erstellen:\n python -m devx.tools.create_task --title \"Task-Titel\"",
|
||||
"en": "Branch '{branch}' does not contain a task ID.\n Expected format: {prefix}-N-short-description\n Example: {prefix}-42-add-feature\n Fix: rename the branch or create a Vikunja task first:\n python -m devx.tools.create_task --title \"Task title\"",
|
||||
"pl": "Gałąź '{branch}' nie zawiera ID zadania.\n Oczekiwany format: {prefix}-N-krótki-opis\n Przykład: {prefix}-42-add-feature\n Naprawa: zmień nazwę gałęzi lub utwórz zadanie Vikunja:\n python -m devx.tools.create_task --title \"Tytuł zadania\"",
|
||||
"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 \"任务标题\""
|
||||
},
|
||||
"Could not find Vikunja task {task_id} in project {project_id}.": {
|
||||
"bg": "Не е намерена Vikunja задача {task_id} в проект {project_id}.",
|
||||
"de": "Vikunja-Task {task_id} in Projekt {project_id} nicht gefunden.",
|
||||
"en": "Could not find Vikunja task {task_id} in project {project_id}.",
|
||||
"pl": "Nie znaleziono zadania Vikunja {task_id} w projekcie {project_id}.",
|
||||
"ru": "Не найдена задача Vikunja {task_id} в проекте {project_id}.",
|
||||
"zh": "在项目 {project_id} 中找不到 Vikunja 任务 {task_id}。"
|
||||
},
|
||||
"Could not detect current branch: {error}": {
|
||||
"bg": "Не може да се определи текущия клон: {error}",
|
||||
"de": "Aktueller Branch konnte nicht erkannt werden: {error}",
|
||||
"en": "Could not detect current branch: {error}",
|
||||
"pl": "Nie można wykryć bieżącej gałęzi: {error}",
|
||||
"ru": "Не удалось определить текущую ветку: {error}",
|
||||
"zh": "无法检测当前分支: {error}"
|
||||
},
|
||||
"Created PR #{index}: {title}\n {url}": {
|
||||
"bg": "Създаден PR #{index}: {title}\n {url}",
|
||||
"de": "PR erstellt #{index}: {title}\n {url}",
|
||||
"en": "Created PR #{index}: {title}\n {url}",
|
||||
"pl": "Utworzono PR #{index}: {title}\n {url}",
|
||||
"ru": "Создан PR #{index}: {title}\n {url}",
|
||||
"zh": "已创建 PR #{index}: {title}\n {url}"
|
||||
},
|
||||
"Created Vikunja task: {identifier} (id={task_id})": {
|
||||
"bg": "Създадена Vikunja задача: {identifier} (id={task_id})",
|
||||
"de": "Vikunja-Task erstellt: {identifier} (id={task_id})",
|
||||
"en": "Created Vikunja task: {identifier} (id={task_id})",
|
||||
"pl": "Utworzono zadanie Vikunja: {identifier} (id={task_id})",
|
||||
"ru": "Создана задача Vikunja: {identifier} (id={task_id})",
|
||||
"zh": "已创建 Vikunja 任务: {identifier} (id={task_id})"
|
||||
},
|
||||
"Next steps:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-short-description\n 3. Implement changes, commit with conventional commit format\n 4. git push -u origin HEAD\n 5. make create-pr (creates PR with title: {identifier}: {title})": {
|
||||
"bg": "Следващи стъпки:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-кратко-описание\n 3. Имплементирайте промените, commit с conventional commit формат\n 4. git push -u origin HEAD\n 5. make create-pr (създава PR с заглавие: {identifier}: {title})",
|
||||
"de": "Nächste Schritte:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-kurz-beschreibung\n 3. Änderungen implementieren, mit Conventional-Commit-Format committen\n 4. git push -u origin HEAD\n 5. make create-pr (erstellt PR mit Titel: {identifier}: {title})",
|
||||
"en": "Next steps:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-short-description\n 3. Implement changes, commit with conventional commit format\n 4. git push -u origin HEAD\n 5. make create-pr (creates PR with title: {identifier}: {title})",
|
||||
"pl": "Następne kroki:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-krótki-opis\n 3. Wprowadź zmiany, commituj w formacie conventional commit\n 4. git push -u origin HEAD\n 5. make create-pr (tworzy PR z tytułem: {identifier}: {title})",
|
||||
"ru": "Следующие шаги:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-краткое-описание\n 3. Реализуйте изменения, коммитьте в conventional commit формате\n 4. git push -u origin HEAD\n 5. make create-pr (создаёт PR с заголовком: {identifier}: {title})",
|
||||
"zh": "后续步骤:\n 1. git checkout master && git pull\n 2. git checkout -b {prefix}-{num}-简短描述\n 3. 实现更改,使用 conventional commit 格式提交\n 4. git push -u origin HEAD\n 5. make create-pr (创建 PR,标题: {identifier}: {title})"
|
||||
},
|
||||
"PR already exists: #{index} — {url}": {
|
||||
"bg": "PR вече съществува: #{index} — {url}",
|
||||
"de": "PR existiert bereits: #{index} — {url}",
|
||||
"en": "PR already exists: #{index} — {url}",
|
||||
"pl": "PR już istnieje: #{index} — {url}",
|
||||
"ru": "PR уже существует: #{index} — {url}",
|
||||
"zh": "PR 已存在: #{index} — {url}"
|
||||
},
|
||||
"Pre-push check passed: task {task_id} exists.": {
|
||||
"bg": "Pre-push проверката премина: задача {task_id} съществува.",
|
||||
"de": "Pre-push-Prüfung bestanden: Task {task_id} existiert.",
|
||||
"en": "Pre-push check passed: task {task_id} exists.",
|
||||
"pl": "Sprawdzanie pre-push zakończone: zadanie {task_id} istnieje.",
|
||||
"ru": "Pre-push проверка пройдена: задача {task_id} существует.",
|
||||
"zh": "Pre-push 检查通过: 任务 {task_id} 存在。"
|
||||
},
|
||||
"REPO_TOKEN is not set. Required to create a PR.": {
|
||||
"bg": "REPO_TOKEN не е зададен. Необходим за създаване на PR.",
|
||||
"de": "REPO_TOKEN nicht gesetzt. Erforderlich zum Erstellen eines PR.",
|
||||
"en": "REPO_TOKEN is not set. Required to create a PR.",
|
||||
"pl": "REPO_TOKEN nie jest ustawiony. Wymagany do utworzenia PR.",
|
||||
"ru": "REPO_TOKEN не установлен. Требуется для создания PR.",
|
||||
"zh": "REPO_TOKEN 未设置。创建 PR 所需。"
|
||||
},
|
||||
"Repository name not set. Use DEVX_REPO_NAME or GITHUB_REPOSITORY env var.": {
|
||||
"bg": "Името на хранилището не е зададено. Използвайте DEVX_REPO_NAME или GITHUB_REPOSITORY env var.",
|
||||
"de": "Repository-Name nicht gesetzt. Verwende DEVX_REPO_NAME oder GITHUB_REPOSITORY env var.",
|
||||
"en": "Repository name not set. Use DEVX_REPO_NAME or GITHUB_REPOSITORY env var.",
|
||||
"pl": "Nazwa repozytorium nie jest ustawiona. Użyj DEVX_REPO_NAME lub GITHUB_REPOSITORY env var.",
|
||||
"ru": "Имя репозитория не установлено. Используйте DEVX_REPO_NAME или GITHUB_REPOSITORY env var.",
|
||||
"zh": "仓库名称未设置。使用 DEVX_REPO_NAME 或 GITHUB_REPOSITORY 环境变量。"
|
||||
},
|
||||
"Repository owner not set. Use --owner or DEVX_REPO_OWNER env var.": {
|
||||
"bg": "Собственикът на хранилището не е зададен. Използвайте --owner или DEVX_REPO_OWNER env var.",
|
||||
"de": "Repository-Owner nicht gesetzt. Verwende --owner oder DEVX_REPO_OWNER env var.",
|
||||
"en": "Repository owner not set. Use --owner or DEVX_REPO_OWNER env var.",
|
||||
"pl": "Właściciel repozytorium nie jest ustawiony. Użyj --owner lub DEVX_REPO_OWNER env var.",
|
||||
"ru": "Владелец репозитория не установлен. Используйте --owner или DEVX_REPO_OWNER env var.",
|
||||
"zh": "仓库所有者未设置。使用 --owner 或 DEVX_REPO_OWNER 环境变量。"
|
||||
},
|
||||
"VIKUNJA_TOKEN is not set. Required to derive PR title.": {
|
||||
"bg": "VIKUNJA_TOKEN не е зададен. Необходим за извличане на PR заглавие.",
|
||||
"de": "VIKUNJA_TOKEN nicht gesetzt. Erforderlich zum Ableiten des PR-Titels.",
|
||||
"en": "VIKUNJA_TOKEN is not set. Required to derive PR title.",
|
||||
"pl": "VIKUNJA_TOKEN nie jest ustawiony. Wymagany do pobrania tytułu PR.",
|
||||
"ru": "VIKUNJA_TOKEN не установлен. Требуется для получения заголовка PR.",
|
||||
"zh": "VIKUNJA_TOKEN 未设置。推导 PR 标题所需。"
|
||||
},
|
||||
"VIKUNJA_TOKEN is not set. Set it in .env or environment.": {
|
||||
"bg": "VIKUNJA_TOKEN не е зададен. Задайте го в .env или средата.",
|
||||
"de": "VIKUNJA_TOKEN nicht gesetzt. In .env oder Umgebung setzen.",
|
||||
"en": "VIKUNJA_TOKEN is not set. Set it in .env or environment.",
|
||||
"pl": "VIKUNJA_TOKEN nie jest ustawiony. Ustaw go w .env lub środowisku.",
|
||||
"ru": "VIKUNJA_TOKEN не установлен. Установите его в .env или среде.",
|
||||
"zh": "VIKUNJA_TOKEN 未设置。在 .env 或环境中设置它。"
|
||||
},
|
||||
"Vikunja task {task_id} not found in project {project_id}.\n Create it first:\n python -m devx.tools.create_task --title \"Task title\"\n Or check that the task ID in the branch name is correct.": {
|
||||
"bg": "Vikunja задача {task_id} не е намерена в проект {project_id}.\n Създайте я първо:\n python -m devx.tools.create_task --title \"Заглавие на задача\"\n Или проверете че ID на задачата в името на клона е правилно.",
|
||||
"de": "Vikunja-Task {task_id} in Projekt {project_id} nicht gefunden.\n Zuerst erstellen:\n python -m devx.tools.create_task --title \"Task-Titel\"\n Oder prüfen, ob die Task-ID im Branch-Namen korrekt ist.",
|
||||
"en": "Vikunja task {task_id} not found in project {project_id}.\n Create it first:\n python -m devx.tools.create_task --title \"Task title\"\n Or check that the task ID in the branch name is correct.",
|
||||
"pl": "Zadanie Vikunja {task_id} nie znalezione w projekcie {project_id}.\n Utwórz je najpierw:\n python -m devx.tools.create_task --title \"Tytuł zadania\"\n Lub sprawdź, czy ID zadania w nazwie gałęzi jest poprawne.",
|
||||
"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 是否正确。"
|
||||
},
|
||||
"WARNING: VIKUNJA_TOKEN not set — skipping task existence check. Set it in .env to enable full validation.": {
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: VIKUNJA_TOKEN не е зададен — пропускане на проверката за съществуване на задача. Задайте го в .env за пълна валидация.",
|
||||
"de": "WARNUNG: VIKUNJA_TOKEN nicht gesetzt — Task-Existenzprüfung übersprungen. In .env setzen für volle Validierung.",
|
||||
"en": "WARNING: VIKUNJA_TOKEN not set — skipping task existence check. Set it in .env to enable full validation.",
|
||||
"pl": "OSTRZEŻENIE: VIKUNJA_TOKEN nie jest ustawiony — pomijanie sprawdzania istnienia zadania. Ustaw w .env, aby włączyć pełną walidację.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: VIKUNJA_TOKEN не установлен — пропуск проверки существования задачи. Установите в .env для полной проверки.",
|
||||
"zh": "警告: VIKUNJA_TOKEN 未设置 — 跳过任务存在性检查。在 .env 中设置以启用完整验证。"
|
||||
},
|
||||
"[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",
|
||||
"en": "[check-mutable-globals] Passed: no mutable path globals found",
|
||||
"pl": "[check-mutable-globals] Passed: no mutable path globals found",
|
||||
"ru": "[check-mutable-globals] Passed: no mutable path globals found",
|
||||
"zh": "[check-mutable-globals] Passed: no mutable path globals found"
|
||||
},
|
||||
"[check_agent_docs] Passed: scanned {count} file(s), no stale references": {
|
||||
"bg": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
|
||||
"de": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
|
||||
"en": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
|
||||
"pl": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
|
||||
"ru": "[check_agent_docs] Passed: scanned {count} file(s), no stale references",
|
||||
"zh": "[check_agent_docs] Passed: scanned {count} file(s), no stale references"
|
||||
},
|
||||
"[check_test_coverage] No changed files to check.": {
|
||||
"bg": "[check_test_coverage] No changed files to check.",
|
||||
"de": "[check_test_coverage] No changed files to check.",
|
||||
"en": "[check_test_coverage] No changed files to check.",
|
||||
"pl": "[check_test_coverage] No changed files to check.",
|
||||
"ru": "[check_test_coverage] No changed files to check.",
|
||||
"zh": "[check_test_coverage] No changed files to check."
|
||||
},
|
||||
"Additional directory to scan (default: scripts, tests). Can be repeated.": {
|
||||
"bg": "Additional directory to scan (default: scripts, tests). Can be repeated.",
|
||||
"de": "Additional directory to scan (default: scripts, tests). Can be repeated.",
|
||||
"en": "Additional directory to scan (default: scripts, tests). Can be repeated.",
|
||||
"pl": "Additional directory to scan (default: scripts, tests). Can be repeated.",
|
||||
"ru": "Additional directory to scan (default: scripts, tests). Can be repeated.",
|
||||
"zh": "Additional directory to scan (default: scripts, tests). Can be repeated."
|
||||
},
|
||||
"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",
|
||||
"en": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
|
||||
"pl": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
|
||||
"ru": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master",
|
||||
"zh": "Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master"
|
||||
},
|
||||
"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)",
|
||||
"en": "Branch name (e.g., DEVX-256-fix-foo)",
|
||||
"pl": "Branch name (e.g., DEVX-256-fix-foo)",
|
||||
"ru": "Branch name (e.g., DEVX-256-fix-foo)",
|
||||
"zh": "Branch name (e.g., DEVX-256-fix-foo)"
|
||||
},
|
||||
"Branch name must contain a task ID.": {
|
||||
"bg": "Branch name must contain a task ID.",
|
||||
"de": "Branch name must contain a task ID.",
|
||||
"en": "Branch name must contain a task ID.",
|
||||
"pl": "Branch name must contain a task ID.",
|
||||
"ru": "Branch name must contain a task ID.",
|
||||
"zh": "Branch name must contain a task ID."
|
||||
},
|
||||
"Check that changed files have corresponding tests": {
|
||||
"bg": "Check that changed files have corresponding tests",
|
||||
"de": "Check that changed files have corresponding tests",
|
||||
"en": "Check that changed files have corresponding tests",
|
||||
"pl": "Check that changed files have corresponding tests",
|
||||
"ru": "Check that changed files have corresponding tests",
|
||||
"zh": "Check that changed files have corresponding tests"
|
||||
},
|
||||
"Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).": {
|
||||
"bg": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
|
||||
"de": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
|
||||
"en": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
|
||||
"pl": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
|
||||
"ru": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found).",
|
||||
"zh": "Could not fetch PR title from Gitea (REPO_TOKEN not set or PR not found)."
|
||||
},
|
||||
"Dependencies must have documentation comments.": {
|
||||
"bg": "Dependencies must have documentation comments.",
|
||||
"de": "Dependencies must have documentation comments.",
|
||||
"en": "Dependencies must have documentation comments.",
|
||||
"pl": "Dependencies must have documentation comments.",
|
||||
"ru": "Dependencies must have documentation comments.",
|
||||
"zh": "Dependencies must have documentation comments."
|
||||
},
|
||||
"FAILED: {count} undocumented dependency/ies": {
|
||||
"bg": "FAILED: {count} undocumented dependency/ies",
|
||||
"de": "FAILED: {count} undocumented dependency/ies",
|
||||
"en": "FAILED: {count} undocumented dependency/ies",
|
||||
"pl": "FAILED: {count} undocumented dependency/ies",
|
||||
"ru": "FAILED: {count} undocumented dependency/ies",
|
||||
"zh": "FAILED: {count} undocumented dependency/ies"
|
||||
},
|
||||
"Found {count} mutable global(s) — use factory functions or pytest fixtures.": {
|
||||
"bg": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
|
||||
"de": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
|
||||
"en": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
|
||||
"pl": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
|
||||
"ru": "Found {count} mutable global(s) — use factory functions or pytest fixtures.",
|
||||
"zh": "Found {count} mutable global(s) — use factory functions or pytest fixtures."
|
||||
},
|
||||
"Found {count} stale documentation reference(s)": {
|
||||
"bg": "Found {count} stale documentation reference(s)",
|
||||
"de": "Found {count} stale documentation reference(s)",
|
||||
"en": "Found {count} stale documentation reference(s)",
|
||||
"pl": "Found {count} stale documentation reference(s)",
|
||||
"ru": "Found {count} stale documentation reference(s)",
|
||||
"zh": "Found {count} stale documentation reference(s)"
|
||||
},
|
||||
"No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.": {
|
||||
"bg": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
|
||||
"de": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
|
||||
"en": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
|
||||
"pl": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
|
||||
"ru": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
|
||||
"zh": "No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description."
|
||||
},
|
||||
"Only check staged files (for pre-commit)": {
|
||||
"bg": "Only check staged files (for pre-commit)",
|
||||
"de": "Only check staged files (for pre-commit)",
|
||||
"en": "Only check staged files (for pre-commit)",
|
||||
"pl": "Only check staged files (for pre-commit)",
|
||||
"ru": "Only check staged files (for pre-commit)",
|
||||
"zh": "Only check staged files (for pre-commit)"
|
||||
},
|
||||
"PR number (to fetch title from Gitea)": {
|
||||
"bg": "PR number (to fetch title from Gitea)",
|
||||
"de": "PR number (to fetch title from Gitea)",
|
||||
"en": "PR number (to fetch title from Gitea)",
|
||||
"pl": "PR number (to fetch title from Gitea)",
|
||||
"ru": "PR number (to fetch title from Gitea)",
|
||||
"zh": "PR number (to fetch title from Gitea)"
|
||||
},
|
||||
"PR title (auto-fetched if --pr-number given)": {
|
||||
"bg": "PR title (auto-fetched if --pr-number given)",
|
||||
"de": "PR title (auto-fetched if --pr-number given)",
|
||||
"en": "PR title (auto-fetched if --pr-number given)",
|
||||
"pl": "PR title (auto-fetched if --pr-number given)",
|
||||
"ru": "PR title (auto-fetched if --pr-number given)",
|
||||
"zh": "PR title (auto-fetched if --pr-number given)"
|
||||
},
|
||||
"PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}": {
|
||||
"bg": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
|
||||
"de": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
|
||||
"en": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
|
||||
"pl": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
|
||||
"ru": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
|
||||
"zh": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}"
|
||||
},
|
||||
"PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}": {
|
||||
"bg": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
|
||||
"de": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
|
||||
"en": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
|
||||
"pl": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
|
||||
"ru": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
|
||||
"zh": "PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}"
|
||||
},
|
||||
"PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}": {
|
||||
"bg": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
|
||||
"de": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
|
||||
"en": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
|
||||
"pl": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
|
||||
"ru": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
|
||||
"zh": "PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}"
|
||||
},
|
||||
"Path to pyproject.toml (default: pyproject.toml in CWD).": {
|
||||
"bg": "Path to pyproject.toml (default: pyproject.toml in CWD).",
|
||||
"de": "Path to pyproject.toml (default: pyproject.toml in CWD).",
|
||||
"en": "Path to pyproject.toml (default: pyproject.toml in CWD).",
|
||||
"pl": "Path to pyproject.toml (default: pyproject.toml in CWD).",
|
||||
"ru": "Path to pyproject.toml (default: pyproject.toml in CWD).",
|
||||
"zh": "Path to pyproject.toml (default: pyproject.toml in CWD)."
|
||||
},
|
||||
"Pre-merge validation failed.": {
|
||||
"bg": "Pre-merge validation failed.",
|
||||
"de": "Pre-merge validation failed.",
|
||||
"en": "Pre-merge validation failed.",
|
||||
"pl": "Pre-merge validation failed.",
|
||||
"ru": "Pre-merge validation failed.",
|
||||
"zh": "Pre-merge validation failed."
|
||||
},
|
||||
"Print warnings but always exit 0": {
|
||||
"bg": "Print warnings but always exit 0",
|
||||
"de": "Print warnings but always exit 0",
|
||||
"en": "Print warnings but always exit 0",
|
||||
"pl": "Print warnings but always exit 0",
|
||||
"ru": "Print warnings but always exit 0",
|
||||
"zh": "Print warnings but always exit 0"
|
||||
},
|
||||
"Repository in owner/name format": {
|
||||
"bg": "Repository in owner/name format",
|
||||
"de": "Repository in owner/name format",
|
||||
"en": "Repository in owner/name format",
|
||||
"pl": "Repository in owner/name format",
|
||||
"ru": "Repository in owner/name format",
|
||||
"zh": "Repository in owner/name format"
|
||||
},
|
||||
"Skip Vikunja title match check": {
|
||||
"bg": "Skip Vikunja title match check",
|
||||
"de": "Skip Vikunja title match check",
|
||||
"en": "Skip Vikunja title match check",
|
||||
"pl": "Skip Vikunja title match check",
|
||||
"ru": "Skip Vikunja title match check",
|
||||
"zh": "Skip Vikunja title match check"
|
||||
},
|
||||
"Skip branch-behind-master check": {
|
||||
"bg": "Skip branch-behind-master check",
|
||||
"de": "Skip branch-behind-master check",
|
||||
"en": "Skip branch-behind-master check",
|
||||
"pl": "Skip branch-behind-master check",
|
||||
"ru": "Skip branch-behind-master check",
|
||||
"zh": "Skip branch-behind-master check"
|
||||
},
|
||||
"[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",
|
||||
"en": "[check-dep-docs] Passed: all dependencies are documented",
|
||||
"pl": "[check-dep-docs] Passed: all dependencies are documented",
|
||||
"ru": "[check-dep-docs] Passed: all dependencies are documented",
|
||||
"zh": "[check-dep-docs] Passed: all dependencies are documented"
|
||||
},
|
||||
"Wrote tag {tag} to GITHUB_OUTPUT.": {
|
||||
"bg": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"de": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"en": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"ru": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"zh": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"pl": "Wrote tag {tag} to GITHUB_OUTPUT."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,583 @@
|
||||
"""Unit tests for devx.tools.build_image."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from click import ClickException
|
||||
from click.testing import CliRunner
|
||||
|
||||
import devx.tools.build_image as build_image
|
||||
from devx.tools.build_image import (
|
||||
ImageSpec,
|
||||
build_full_tag,
|
||||
load_manifest,
|
||||
push_image,
|
||||
registry_login,
|
||||
)
|
||||
from devx.tools.build_image import (
|
||||
build_image as do_build,
|
||||
)
|
||||
from devx.tools.clean_images import select_for_deletion, sort_versions_by_date
|
||||
|
||||
|
||||
class TestImageSpec:
|
||||
def test_from_dict_minimal(self) -> None:
|
||||
spec = ImageSpec.from_dict({"name": "ci-base", "dockerfile": "docker/ci-base/Dockerfile"})
|
||||
assert spec.name == "ci-base"
|
||||
assert spec.dockerfile == "docker/ci-base/Dockerfile"
|
||||
assert spec.context == "."
|
||||
assert spec.tags == ["latest"]
|
||||
|
||||
def test_from_dict_full(self) -> None:
|
||||
spec = ImageSpec.from_dict(
|
||||
{
|
||||
"name": "ci-quality",
|
||||
"dockerfile": "docker/ci-quality/Dockerfile",
|
||||
"context": ".",
|
||||
"tags": ["latest", "0.19.3"],
|
||||
}
|
||||
)
|
||||
assert spec.name == "ci-quality"
|
||||
assert spec.dockerfile == "docker/ci-quality/Dockerfile"
|
||||
assert spec.context == "."
|
||||
assert spec.tags == ["latest", "0.19.3"]
|
||||
|
||||
def test_from_dict_missing_name(self) -> None:
|
||||
with pytest.raises(ValueError, match="missing 'name'"):
|
||||
ImageSpec.from_dict({"dockerfile": "Dockerfile"})
|
||||
|
||||
def test_from_dict_missing_dockerfile(self) -> None:
|
||||
with pytest.raises(ValueError, match="missing 'dockerfile'"):
|
||||
ImageSpec.from_dict({"name": "ci-base"})
|
||||
|
||||
def test_from_dict_tags_not_list(self) -> None:
|
||||
with pytest.raises(ValueError, match="tags.*must be a list"):
|
||||
ImageSpec.from_dict(
|
||||
{
|
||||
"name": "ci-base",
|
||||
"dockerfile": "Dockerfile",
|
||||
"tags": "latest",
|
||||
}
|
||||
)
|
||||
|
||||
def test_from_dict_empty_tags_defaults_to_latest(self) -> None:
|
||||
spec = ImageSpec.from_dict(
|
||||
{
|
||||
"name": "ci-base",
|
||||
"dockerfile": "Dockerfile",
|
||||
"tags": [],
|
||||
}
|
||||
)
|
||||
assert spec.tags == ["latest"]
|
||||
|
||||
|
||||
class TestBuildFullTag:
|
||||
def test_no_registry(self) -> None:
|
||||
assert build_full_tag(None, "ci-base", "latest") == "ci-base:latest"
|
||||
|
||||
def test_with_registry(self) -> None:
|
||||
assert build_full_tag("git.example.com", "ci-base", "0.1.0") == "git.example.com/ci-base:0.1.0"
|
||||
|
||||
def test_with_registry_and_path(self) -> None:
|
||||
assert (
|
||||
build_full_tag("git.example.com", "oblachno/ci-base", "latest") == "git.example.com/oblachno/ci-base:latest"
|
||||
)
|
||||
|
||||
|
||||
class TestLoadManifest:
|
||||
def test_load_valid_manifest(self, tmp_path: Path) -> None:
|
||||
manifest = tmp_path / "images.json"
|
||||
manifest.write_text(
|
||||
json.dumps(
|
||||
[
|
||||
{"name": "ci-base", "dockerfile": "docker/ci-base/Dockerfile"},
|
||||
{"name": "ci-quality", "dockerfile": "docker/ci-quality/Dockerfile", "tags": ["latest", "1.0"]},
|
||||
]
|
||||
)
|
||||
)
|
||||
specs = load_manifest(manifest)
|
||||
assert len(specs) == 2
|
||||
assert specs[0].name == "ci-base"
|
||||
assert specs[1].tags == ["latest", "1.0"]
|
||||
|
||||
def test_load_missing_file(self, tmp_path: Path) -> None:
|
||||
with pytest.raises(ClickException, match="not found"):
|
||||
load_manifest(tmp_path / "nonexistent.json")
|
||||
|
||||
def test_load_not_a_list(self, tmp_path: Path) -> None:
|
||||
manifest = tmp_path / "images.json"
|
||||
manifest.write_text(json.dumps({"name": "ci-base"}))
|
||||
with pytest.raises(ClickException, match="must be a JSON list"):
|
||||
load_manifest(manifest)
|
||||
|
||||
|
||||
class TestRegistryLogin:
|
||||
def test_success(self) -> None:
|
||||
mock_result = MagicMock(returncode=0, stderr="", stdout="")
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run:
|
||||
assert registry_login("git.example.com", "user", "token") is True
|
||||
assert mock_run.call_args.args[0] == [
|
||||
"docker",
|
||||
"login",
|
||||
"git.example.com",
|
||||
"-u",
|
||||
"user",
|
||||
"--password-stdin",
|
||||
]
|
||||
assert mock_run.call_args.kwargs["input"] == "token"
|
||||
|
||||
def test_failure(self) -> None:
|
||||
mock_result = MagicMock(returncode=1, stderr="auth failed", stdout="")
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
||||
assert registry_login("git.example.com", "user", "bad") is False
|
||||
|
||||
def test_dry_run(self) -> None:
|
||||
with patch("devx.tools.build_image.subprocess.run") as mock_run:
|
||||
assert registry_login("git.example.com", "user", "token", dry_run=True) is True
|
||||
mock_run.assert_not_called()
|
||||
|
||||
|
||||
class TestBuildImage:
|
||||
def test_success(self, tmp_path: Path) -> None:
|
||||
dockerfile = tmp_path / "Dockerfile"
|
||||
dockerfile.touch()
|
||||
spec = ImageSpec(name="ci-base", dockerfile=str(dockerfile), context=".", tags=["latest"])
|
||||
mock_result = MagicMock(returncode=0)
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
||||
assert do_build(spec) is True
|
||||
|
||||
def test_dockerfile_not_found(self) -> None:
|
||||
spec = ImageSpec(name="ci-base", dockerfile="nonexistent/Dockerfile")
|
||||
assert do_build(spec) is False
|
||||
|
||||
def test_build_failure(self, tmp_path: Path) -> None:
|
||||
dockerfile = tmp_path / "Dockerfile"
|
||||
dockerfile.touch()
|
||||
spec = ImageSpec(name="ci-base", dockerfile=str(dockerfile))
|
||||
mock_result = MagicMock(returncode=1)
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
||||
assert do_build(spec) is False
|
||||
|
||||
def test_dry_run(self, tmp_path: Path) -> None:
|
||||
dockerfile = tmp_path / "Dockerfile"
|
||||
dockerfile.touch()
|
||||
spec = ImageSpec(name="ci-base", dockerfile=str(dockerfile), tags=["latest", "1.0"])
|
||||
with patch("devx.tools.build_image.subprocess.run") as mock_run:
|
||||
assert do_build(spec, dry_run=True) is True
|
||||
mock_run.assert_not_called()
|
||||
|
||||
def test_with_registry(self, tmp_path: Path) -> None:
|
||||
dockerfile = tmp_path / "Dockerfile"
|
||||
dockerfile.touch()
|
||||
spec = ImageSpec(name="ci-base", dockerfile=str(dockerfile), tags=["latest"])
|
||||
mock_result = MagicMock(returncode=0)
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run:
|
||||
assert do_build(spec, registry="git.example.com") is True
|
||||
cmd = mock_run.call_args.args[0]
|
||||
assert "-t" in cmd
|
||||
idx = cmd.index("-t")
|
||||
assert cmd[idx + 1] == "git.example.com/ci-base:latest"
|
||||
|
||||
def test_pull_flag(self, tmp_path: Path) -> None:
|
||||
dockerfile = tmp_path / "Dockerfile"
|
||||
dockerfile.touch()
|
||||
spec = ImageSpec(name="ci-base", dockerfile=str(dockerfile))
|
||||
mock_result = MagicMock(returncode=0)
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run:
|
||||
assert do_build(spec, pull=True) is True
|
||||
cmd = mock_run.call_args.args[0]
|
||||
assert "--pull" in cmd
|
||||
|
||||
|
||||
class TestPushImage:
|
||||
def test_success(self) -> None:
|
||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
|
||||
mock_result = MagicMock(returncode=0, stderr="", stdout="")
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run:
|
||||
assert push_image(spec, "git.example.com") is True
|
||||
assert mock_run.call_count == 2
|
||||
|
||||
def test_partial_failure(self) -> None:
|
||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
|
||||
results = [
|
||||
MagicMock(returncode=0, stderr="", stdout=""),
|
||||
MagicMock(returncode=1, stderr="push failed", stdout=""),
|
||||
]
|
||||
with patch("devx.tools.build_image.subprocess.run", side_effect=results):
|
||||
assert push_image(spec, "git.example.com") is False
|
||||
|
||||
def test_dry_run(self) -> None:
|
||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
||||
with patch("devx.tools.build_image.subprocess.run") as mock_run:
|
||||
assert push_image(spec, "git.example.com", dry_run=True) is True
|
||||
mock_run.assert_not_called()
|
||||
|
||||
|
||||
class TestSortVersions:
|
||||
def test_sort_by_created_at_desc(self) -> None:
|
||||
versions = [
|
||||
{"version": "0.1.0", "created_at": "2025-01-01T00:00:00Z"},
|
||||
{"version": "0.3.0", "created_at": "2025-03-01T00:00:00Z"},
|
||||
{"version": "0.2.0", "created_at": "2025-02-01T00:00:00Z"},
|
||||
]
|
||||
result = sort_versions_by_date(versions)
|
||||
assert [v["version"] for v in result] == ["0.3.0", "0.2.0", "0.1.0"]
|
||||
|
||||
def test_sort_fallback_to_version(self) -> None:
|
||||
versions = [
|
||||
{"version": "0.1.0"},
|
||||
{"version": "0.3.0"},
|
||||
{"version": "0.2.0"},
|
||||
]
|
||||
result = sort_versions_by_date(versions)
|
||||
assert [v["version"] for v in result] == ["0.3.0", "0.2.0", "0.1.0"]
|
||||
|
||||
|
||||
class TestSelectForDeletion:
|
||||
def test_keep_2(self) -> None:
|
||||
versions = [
|
||||
{"version": "0.1.0", "created_at": "2025-01-01"},
|
||||
{"version": "0.2.0", "created_at": "2025-02-01"},
|
||||
{"version": "0.3.0", "created_at": "2025-03-01"},
|
||||
{"version": "0.4.0", "created_at": "2025-04-01"},
|
||||
]
|
||||
to_delete = select_for_deletion(versions, keep=2)
|
||||
assert len(to_delete) == 2
|
||||
assert {v["version"] for v in to_delete} == {"0.1.0", "0.2.0"}
|
||||
|
||||
def test_preserve_latest_tag(self) -> None:
|
||||
versions = [
|
||||
{"version": "latest", "created_at": "2025-01-01"},
|
||||
{"version": "0.2.0", "created_at": "2025-02-01"},
|
||||
{"version": "0.3.0", "created_at": "2025-03-01"},
|
||||
{"version": "0.4.0", "created_at": "2025-04-01"},
|
||||
]
|
||||
to_delete = select_for_deletion(versions, keep=2)
|
||||
deleted_versions = {v["version"] for v in to_delete}
|
||||
assert "latest" not in deleted_versions
|
||||
# latest is oldest by date but still preserved
|
||||
assert "0.2.0" in deleted_versions
|
||||
|
||||
def test_keep_all(self) -> None:
|
||||
versions = [
|
||||
{"version": "0.1.0", "created_at": "2025-01-01"},
|
||||
{"version": "0.2.0", "created_at": "2025-02-01"},
|
||||
]
|
||||
to_delete = select_for_deletion(versions, keep=2)
|
||||
assert len(to_delete) == 0
|
||||
|
||||
def test_keep_more_than_available(self) -> None:
|
||||
versions = [
|
||||
{"version": "0.1.0", "created_at": "2025-01-01"},
|
||||
]
|
||||
to_delete = select_for_deletion(versions, keep=5)
|
||||
assert len(to_delete) == 0
|
||||
|
||||
|
||||
class TestCleanImagesAPI:
|
||||
"""Tests for the clean_images module's API functions."""
|
||||
|
||||
def test_list_package_versions(self) -> None:
|
||||
from devx.tools.clean_images import list_package_versions
|
||||
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.json.return_value = [{"version": "0.1.0"}]
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
with patch("devx.tools.clean_images.requests.get", return_value=mock_resp) as mock_get:
|
||||
versions = list_package_versions(
|
||||
"https://git.example.com/api/v1",
|
||||
"oblachno-oss",
|
||||
"ci-base",
|
||||
"token",
|
||||
)
|
||||
assert versions == [{"version": "0.1.0"}]
|
||||
assert "page=1" in mock_get.call_args.args[0]
|
||||
|
||||
def test_list_package_versions_pagination(self) -> None:
|
||||
from devx.tools.clean_images import list_package_versions
|
||||
|
||||
# First page: 50 items, second page: 3 items, third page: empty
|
||||
page1 = [{"version": f"0.{i}.0"} for i in range(50)]
|
||||
page2 = [{"version": f"1.{i}.0"} for i in range(3)]
|
||||
responses = [
|
||||
MagicMock(json=MagicMock(return_value=page1), raise_for_status=MagicMock()),
|
||||
MagicMock(json=MagicMock(return_value=page2), raise_for_status=MagicMock()),
|
||||
MagicMock(json=MagicMock(return_value=[]), raise_for_status=MagicMock()),
|
||||
]
|
||||
with patch("devx.tools.clean_images.requests.get", side_effect=responses):
|
||||
versions = list_package_versions(
|
||||
"https://git.example.com/api/v1",
|
||||
"oblachno-oss",
|
||||
"ci-base",
|
||||
"token",
|
||||
)
|
||||
assert len(versions) == 53
|
||||
|
||||
def test_delete_package_version_success(self) -> None:
|
||||
from devx.tools.clean_images import delete_package_version
|
||||
|
||||
mock_resp = MagicMock(status_code=204)
|
||||
with patch("devx.tools.clean_images.requests.delete", return_value=mock_resp):
|
||||
assert (
|
||||
delete_package_version(
|
||||
"https://git.example.com/api/v1",
|
||||
"oblachno-oss",
|
||||
"ci-base",
|
||||
"0.1.0",
|
||||
"token",
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
def test_delete_package_version_failure(self) -> None:
|
||||
from devx.tools.clean_images import delete_package_version
|
||||
|
||||
mock_resp = MagicMock(status_code=404)
|
||||
with patch("devx.tools.clean_images.requests.delete", return_value=mock_resp):
|
||||
assert (
|
||||
delete_package_version(
|
||||
"https://git.example.com/api/v1",
|
||||
"oblachno-oss",
|
||||
"ci-base",
|
||||
"0.1.0",
|
||||
"token",
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
class TestCLIBuildImage:
|
||||
def test_single_image_build(self, tmp_path: Path) -> None:
|
||||
dockerfile = tmp_path / "Dockerfile"
|
||||
dockerfile.touch()
|
||||
runner = CliRunner()
|
||||
mock_result = MagicMock(returncode=0)
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
||||
result = runner.invoke(
|
||||
build_image.main,
|
||||
["--dockerfile", str(dockerfile), "--name", "ci-base", "--tag", "latest"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_manifest_build(self, tmp_path: Path) -> None:
|
||||
dockerfile = tmp_path / "Dockerfile"
|
||||
dockerfile.touch()
|
||||
manifest = tmp_path / "images.json"
|
||||
manifest.write_text(
|
||||
json.dumps(
|
||||
[
|
||||
{"name": "ci-base", "dockerfile": str(dockerfile)},
|
||||
]
|
||||
)
|
||||
)
|
||||
runner = CliRunner()
|
||||
mock_result = MagicMock(returncode=0)
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
||||
result = runner.invoke(
|
||||
build_image.main,
|
||||
["--manifest", str(manifest)],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_missing_dockerfile_and_manifest(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(build_image.main, [])
|
||||
assert result.exit_code != 0
|
||||
assert "manifest" in result.output.lower() or "dockerfile" in result.output.lower()
|
||||
|
||||
def test_push_without_registry(self, tmp_path: Path) -> None:
|
||||
dockerfile = tmp_path / "Dockerfile"
|
||||
dockerfile.touch()
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
build_image.main,
|
||||
["--dockerfile", str(dockerfile), "--name", "ci-base", "--push"],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "registry" in result.output.lower()
|
||||
|
||||
def test_push_without_credentials(self, tmp_path: Path) -> None:
|
||||
dockerfile = tmp_path / "Dockerfile"
|
||||
dockerfile.touch()
|
||||
runner = CliRunner()
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
result = runner.invoke(
|
||||
build_image.main,
|
||||
["--dockerfile", str(dockerfile), "--name", "ci-base", "--push", "--registry", "git.example.com"],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "credential" in result.output.lower() or "token" in result.output.lower()
|
||||
|
||||
def test_dry_run(self, tmp_path: Path) -> None:
|
||||
dockerfile = tmp_path / "Dockerfile"
|
||||
dockerfile.touch()
|
||||
runner = CliRunner()
|
||||
with patch("devx.tools.build_image.subprocess.run") as mock_run:
|
||||
result = runner.invoke(
|
||||
build_image.main,
|
||||
["--dockerfile", str(dockerfile), "--name", "ci-base", "--dry-run"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
mock_run.assert_not_called()
|
||||
assert "dry-run" in result.output
|
||||
|
||||
def test_build_failure_exits_with_error(self, tmp_path: Path) -> None:
|
||||
dockerfile = tmp_path / "Dockerfile"
|
||||
dockerfile.touch()
|
||||
runner = CliRunner()
|
||||
mock_result = MagicMock(returncode=1)
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
||||
result = runner.invoke(
|
||||
build_image.main,
|
||||
["--dockerfile", str(dockerfile), "--name", "ci-base"],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_push_login_failure(self, tmp_path: Path) -> None:
|
||||
dockerfile = tmp_path / "Dockerfile"
|
||||
dockerfile.touch()
|
||||
runner = CliRunner()
|
||||
login_result = MagicMock(returncode=1, stderr="auth failed", stdout="")
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "fake", "REGISTRY_USERNAME": "user"}):
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=login_result):
|
||||
result = runner.invoke(
|
||||
build_image.main,
|
||||
["--dockerfile", str(dockerfile), "--name", "ci-base", "--push", "--registry", "git.example.com"],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "login" in result.output.lower()
|
||||
|
||||
def test_push_image_failure(self, tmp_path: Path) -> None:
|
||||
dockerfile = tmp_path / "Dockerfile"
|
||||
dockerfile.touch()
|
||||
runner = CliRunner()
|
||||
build_result = MagicMock(returncode=0)
|
||||
login_result = MagicMock(returncode=0, stderr="", stdout="")
|
||||
push_result = MagicMock(returncode=1, stderr="push failed", stdout="")
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "fake", "REGISTRY_USERNAME": "user"}):
|
||||
with patch(
|
||||
"devx.tools.build_image.subprocess.run",
|
||||
side_effect=[login_result, build_result, push_result],
|
||||
):
|
||||
result = runner.invoke(
|
||||
build_image.main,
|
||||
["--dockerfile", str(dockerfile), "--name", "ci-base", "--push", "--registry", "git.example.com"],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
|
||||
|
||||
class TestCLICleanImages:
|
||||
def test_dry_run(self) -> None:
|
||||
from devx.tools.clean_images import main as clean_main
|
||||
|
||||
runner = CliRunner()
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.json.return_value = [
|
||||
{"version": "0.1.0", "created_at": "2025-01-01"},
|
||||
{"version": "0.2.0", "created_at": "2025-02-01"},
|
||||
{"version": "0.3.0", "created_at": "2025-03-01"},
|
||||
]
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
|
||||
with patch("devx.tools.clean_images.requests.get", return_value=mock_resp):
|
||||
result = runner.invoke(
|
||||
clean_main,
|
||||
["--owner", "oblachno-oss", "--name", "ci-base", "--keep", "1", "--dry-run"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "dry-run" in result.output
|
||||
assert "0.1.0" in result.output
|
||||
|
||||
def test_no_token(self) -> None:
|
||||
from devx.tools.clean_images import main as clean_main
|
||||
|
||||
runner = CliRunner()
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
result = runner.invoke(
|
||||
clean_main,
|
||||
["--owner", "oblachno-oss", "--name", "ci-base"],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "token" in result.output.lower()
|
||||
|
||||
def test_no_versions_found(self) -> None:
|
||||
from devx.tools.clean_images import main as clean_main
|
||||
|
||||
runner = CliRunner()
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.json.return_value = []
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
|
||||
with patch("devx.tools.clean_images.requests.get", return_value=mock_resp):
|
||||
result = runner.invoke(
|
||||
clean_main,
|
||||
["--owner", "oblachno-oss", "--name", "ci-base", "--dry-run"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "No versions" in result.output
|
||||
|
||||
def test_actual_delete(self) -> None:
|
||||
from devx.tools.clean_images import main as clean_main
|
||||
|
||||
runner = CliRunner()
|
||||
list_resp = MagicMock()
|
||||
list_resp.json.return_value = [
|
||||
{"version": "0.1.0", "created_at": "2025-01-01"},
|
||||
{"version": "0.2.0", "created_at": "2025-02-01"},
|
||||
{"version": "0.3.0", "created_at": "2025-03-01"},
|
||||
]
|
||||
list_resp.raise_for_status = MagicMock()
|
||||
delete_resp = MagicMock(status_code=204)
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
|
||||
with patch("devx.tools.clean_images.requests.get", return_value=list_resp):
|
||||
with patch("devx.tools.clean_images.requests.delete", return_value=delete_resp):
|
||||
result = runner.invoke(
|
||||
clean_main,
|
||||
["--owner", "oblachno-oss", "--name", "ci-base", "--keep", "2"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "Deleted" in result.output
|
||||
|
||||
def test_list_request_exception(self) -> None:
|
||||
import requests as req
|
||||
|
||||
from devx.tools.clean_images import main as clean_main
|
||||
|
||||
runner = CliRunner()
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
|
||||
with patch(
|
||||
"devx.tools.clean_images.requests.get",
|
||||
side_effect=req.ConnectionError("network down"),
|
||||
):
|
||||
result = runner.invoke(
|
||||
clean_main,
|
||||
["--owner", "oblachno-oss", "--name", "ci-base", "--dry-run"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "Failed to list" in result.output
|
||||
|
||||
def test_delete_failure_in_cli(self) -> None:
|
||||
from devx.tools.clean_images import main as clean_main
|
||||
|
||||
runner = CliRunner()
|
||||
list_resp = MagicMock()
|
||||
list_resp.json.return_value = [
|
||||
{"version": "0.1.0", "created_at": "2025-01-01"},
|
||||
{"version": "0.2.0", "created_at": "2025-02-01"},
|
||||
{"version": "0.3.0", "created_at": "2025-03-01"},
|
||||
]
|
||||
list_resp.raise_for_status = MagicMock()
|
||||
delete_resp = MagicMock(status_code=500)
|
||||
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
|
||||
with patch("devx.tools.clean_images.requests.get", return_value=list_resp):
|
||||
with patch("devx.tools.clean_images.requests.delete", return_value=delete_resp):
|
||||
result = runner.invoke(
|
||||
clean_main,
|
||||
["--owner", "oblachno-oss", "--name", "ci-base", "--keep", "2"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "FAILED" in result.output
|
||||
Reference in New Issue
Block a user