Public Access
DEVX-1: fix: use python3 and venv python in workflows and Makefile
Post-merge / detect-type (push) Successful in 17s
Post-merge / validate-commit-msg (push) Successful in 16s
Post-merge / vikunja (push) Successful in 12s
Post-merge / configure-repo (push) Failing after 13s
Post-merge / release (push) Failing after 1m5s
Post-merge / sync-wiki (push) Successful in 1m2s
Post-merge / badges (push) Failing after 28s
Post-merge / detect-type (push) Successful in 17s
Post-merge / validate-commit-msg (push) Successful in 16s
Post-merge / vikunja (push) Successful in 12s
Post-merge / configure-repo (push) Failing after 13s
Post-merge / release (push) Failing after 1m5s
Post-merge / sync-wiki (push) Successful in 1m2s
Post-merge / badges (push) Failing after 28s
This commit was merged in pull request #1.
This commit is contained in:
@@ -27,19 +27,19 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python -m devx.tools.check_test_speed --max-seconds 10
|
||||
python3 -m devx.tools.check_test_speed --max-seconds 10
|
||||
- name: Documentation coverage check
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python -m devx.ci.doc_coverage --fail-on-missing
|
||||
python3 -m devx.ci.doc_coverage --fail-on-missing
|
||||
- name: Translation completeness check
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python -m devx.ci.check_translations
|
||||
python3 -m devx.ci.check_translations
|
||||
- name: Dependency security scan
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python -m devx.ci.classify_changes \
|
||||
python3 -m devx.ci.classify_changes \
|
||||
--base "origin/master" \
|
||||
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
|
||||
--github-output
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python -m devx.ci.release --dry-run || true
|
||||
python3 -m devx.ci.release --dry-run || true
|
||||
|
||||
pr-review:
|
||||
if: github.event_name == 'pull_request'
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
. .venv/bin/activate
|
||||
python -m devx.ci.pr_review \
|
||||
python3 -m devx.ci.pr_review \
|
||||
"${{ github.event.number }}" \
|
||||
"${{ github.repository }}"
|
||||
|
||||
@@ -139,13 +139,14 @@ jobs:
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||
DEVX_VIKUNJA_PROJECT_ID: "8"
|
||||
PYTHONPATH: src
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
run: |
|
||||
python -m devx.ci.auto_merge \
|
||||
python3 -m devx.ci.auto_merge \
|
||||
"$HEAD_REF" \
|
||||
"$PR_TITLE" \
|
||||
"$REPOSITORY" \
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
id: check
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
run: python -m devx.ci.detect_release_commit
|
||||
run: python3 -m devx.ci.detect_release_commit
|
||||
|
||||
validate-commit-msg:
|
||||
needs: [detect-type]
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
git log -1 --format=%B > commit-msg.txt
|
||||
python -m devx.ci.validate_commit_msg commit-msg.txt --branch master
|
||||
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
|
||||
rm -f commit-msg.txt
|
||||
|
||||
release:
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python -m devx.ci.release
|
||||
python3 -m devx.ci.release
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python -m devx.ci.notify_failure \
|
||||
python3 -m devx.ci.notify_failure \
|
||||
--repo "${{ github.repository }}" \
|
||||
--run-id "${{ github.run_id }}" \
|
||||
--workflow "post-merge/release" \
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python -m devx.ci.sync_wiki --repo "${{ github.repository }}" --strict
|
||||
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --strict
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python -m devx.ci.notify_failure \
|
||||
python3 -m devx.ci.notify_failure \
|
||||
--repo "${{ github.repository }}" \
|
||||
--run-id "${{ github.run_id }}" \
|
||||
--workflow "post-merge/sync-wiki" \
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
PRE_COMMIT_ALLOW_NO_CONFIG: "1"
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python -m devx.ci.push_badges
|
||||
python3 -m devx.ci.push_badges
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python -m devx.ci.notify_failure \
|
||||
python3 -m devx.ci.notify_failure \
|
||||
--repo "${{ github.repository }}" \
|
||||
--run-id "${{ github.run_id }}" \
|
||||
--workflow "post-merge/badges" \
|
||||
@@ -185,8 +185,9 @@ jobs:
|
||||
- name: Update Vikunja task
|
||||
env:
|
||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||
DEVX_VIKUNJA_PROJECT_ID: "8"
|
||||
PYTHONPATH: src
|
||||
run: python -m devx.ci.post_merge --git-sha "${{ github.sha }}"
|
||||
run: python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
@@ -194,10 +195,10 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python -m devx.tools.install_tools --tool tea
|
||||
python3 -m devx.tools.install_tools --tool tea
|
||||
tea login add --name devx --url "${{ github.server_url }}" --token "$REPO_TOKEN" || true
|
||||
tea login default devx || true
|
||||
python -m devx.ci.notify_failure \
|
||||
python3 -m devx.ci.notify_failure \
|
||||
--repo "${{ github.repository }}" \
|
||||
--run-id "${{ github.run_id }}" \
|
||||
--workflow "post-merge/vikunja" \
|
||||
@@ -218,7 +219,7 @@ jobs:
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: python -m devx.tools.configure_repo
|
||||
run: python3 -m devx.tools.configure_repo
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
@@ -226,10 +227,10 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python -m devx.tools.install_tools --tool tea
|
||||
python3 -m devx.tools.install_tools --tool tea
|
||||
tea login add --name devx --url "${{ github.server_url }}" --token "$REPO_TOKEN" || true
|
||||
tea login default devx || true
|
||||
python -m devx.ci.notify_failure \
|
||||
python3 -m devx.ci.notify_failure \
|
||||
--repo "${{ github.repository }}" \
|
||||
--run-id "${{ github.run_id }}" \
|
||||
--workflow "post-merge/configure-repo" \
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Install CI tools
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python -m devx.tools.install_tools --tool git-cliff --tool tea
|
||||
python3 -m devx.tools.install_tools --tool git-cliff --tool tea
|
||||
- name: Configure tea login
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python -m devx.ci.publish "${{ github.ref_name }}" "${{ github.repository }}"
|
||||
python3 -m devx.ci.publish "${{ github.ref_name }}" "${{ github.repository }}"
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python -m devx.ci.notify_failure \
|
||||
python3 -m devx.ci.notify_failure \
|
||||
--repo "${{ github.repository }}" \
|
||||
--run-id "${{ github.run_id }}" \
|
||||
--workflow "publish" \
|
||||
|
||||
@@ -3,7 +3,7 @@ repos:
|
||||
hooks:
|
||||
- id: validate-commit-msg
|
||||
name: validate commit message
|
||||
entry: env PYTHONPATH=src .venv/bin/python -m devx.ci.validate_commit_msg
|
||||
entry: env PYTHONPATH=src .venv/bin/python3 -m devx.ci.validate_commit_msg
|
||||
language: system
|
||||
stages: [commit-msg]
|
||||
pass_filenames: true
|
||||
@@ -59,7 +59,7 @@ repos:
|
||||
|
||||
- id: commit-msg
|
||||
name: validate commit message
|
||||
entry: env PYTHONPATH=src .venv/bin/python -m devx.ci.validate_commit_msg
|
||||
entry: env PYTHONPATH=src .venv/bin/python3 -m devx.ci.validate_commit_msg
|
||||
language: system
|
||||
stages: [commit-msg]
|
||||
pass_filenames: true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: all setup setup-ci setup-quality setup-release install update lint lint-ruff lint-format typecheck lint-bandit lint-deps lint-all test test-unit pytest-cov clean workflow-lint workflow-dryrun workflow-check install-tools
|
||||
.PHONY: all setup setup-ci setup-quality setup-release install update lint lint-ruff lint-format typecheck lint-bandit lint-deps lint-all test test-unit pytest-cov clean workflow-lint workflow-dryrun workflow-check install-tools install-hooks activate-scripts
|
||||
|
||||
PYTHON := python3
|
||||
VENV := .venv
|
||||
@@ -7,28 +7,28 @@ BIN := $(VENV)/bin
|
||||
all: setup
|
||||
|
||||
# Full setup for local development
|
||||
setup: $(VENV)/bin/activate .env install-tools
|
||||
setup: $(VENV)/bin/activate .env activate-scripts install-tools
|
||||
@$(BIN)/pip install -e '.[dev]' 2>/dev/null; \
|
||||
export PATH="$(HOME)/.local/bin:$$PATH"; \
|
||||
$(PYTHON) -m devx.tools.setup --bin "$(BIN)"
|
||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)"
|
||||
|
||||
# Lean setup for CI jobs (pytest + lint + runtime deps)
|
||||
setup-ci: $(VENV)/bin/activate .env
|
||||
@$(BIN)/pip install -e '.[ci,lint]' 2>/dev/null; \
|
||||
$(PYTHON) -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --no-pre-commit --no-tea-login
|
||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --no-pre-commit --no-tea-login
|
||||
|
||||
# Setup for quality job (lint + test deps, actionlint tool)
|
||||
setup-quality: $(VENV)/bin/activate .env install-tools
|
||||
@$(BIN)/pip install -e '.[ci,lint]' 2>/dev/null; \
|
||||
export PATH="$(HOME)/.local/bin:$$PATH"; \
|
||||
$(PYTHON) -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --no-pre-commit --no-tea-login
|
||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --no-pre-commit --no-tea-login
|
||||
|
||||
# Setup for release jobs (needs git-cliff, tea, lint tools)
|
||||
setup-release: $(VENV)/bin/activate .env
|
||||
@$(BIN)/pip install -e '.[ci,lint]' 2>/dev/null; \
|
||||
$(PYTHON) -m devx.tools.install_tools --tool git-cliff --tool tea; \
|
||||
$(BIN)/python -m devx.tools.install_tools --tool git-cliff --tool tea; \
|
||||
export PATH="$(HOME)/.local/bin:$$PATH"; \
|
||||
$(PYTHON) -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --no-pre-commit
|
||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --no-pre-commit
|
||||
|
||||
.env:
|
||||
@if [ ! -f .env ]; then cp .env.example .env; echo "Created .env from .env.example — please edit it."; fi
|
||||
@@ -38,9 +38,19 @@ $(VENV)/bin/activate:
|
||||
$(PYTHON) -m venv $(VENV)
|
||||
$(BIN)/pip install --upgrade pip setuptools wheel
|
||||
|
||||
install-tools:
|
||||
activate-scripts: $(VENV)/bin/activate
|
||||
@test -f activate.sh || (echo '#!/usr/bin/env bash' > activate.sh && echo 'source "$$(cd "$$(dirname "$${BASH_SOURCE[0]}")" && pwd)/.venv/bin/activate"' >> activate.sh && chmod +x activate.sh)
|
||||
@test -f activate.fish || (echo '#!/usr/bin/env fish' > activate.fish && echo 'set -l script_dir (dirname (status --current-filename))' >> activate.fish && echo 'source "$$script_dir/.venv/bin/activate.fish"' >> activate.fish && chmod +x activate.fish)
|
||||
@test -f activate.zsh || (echo '#!/usr/bin/env zsh' > activate.zsh && echo '0="$${ZERO:-$${0:#$$ZSH_ARGZERO}}"' >> activate.zsh && echo '0="$${$${(M)0:#/*}:-$$PWD/$$0}"' >> activate.zsh && echo 'source "$${0:A:h}/.venv/bin/activate"' >> activate.zsh && chmod +x activate.zsh)
|
||||
|
||||
install-hooks:
|
||||
@cp hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
|
||||
@cp hooks/pre-push .git/hooks/pre-push && chmod +x .git/hooks/pre-push
|
||||
@echo "Git hooks installed."
|
||||
|
||||
install-tools: $(VENV)/bin/activate
|
||||
@$(BIN)/pip install -e '.' 2>/dev/null; \
|
||||
$(PYTHON) -m devx.tools.install_tools
|
||||
$(BIN)/python -m devx.tools.install_tools
|
||||
|
||||
lint-ruff:
|
||||
$(BIN)/ruff check src/ tests/
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env fish
|
||||
set -l script_dir (dirname (status --current-filename))
|
||||
source "$script_dir/.venv/bin/activate.fish"
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.venv/bin/activate"
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env zsh
|
||||
0="${ZERO:-${0:#$ZSH_ARGZERO}}"
|
||||
0="${${(M)0:#/*}:-$PWD/$0}"
|
||||
source "${0:A:h}/.venv/bin/activate"
|
||||
+4
-1
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"index.md": "Home"
|
||||
"index.md": "Home",
|
||||
"user/cli-commands.md": "CLI-Commands",
|
||||
"tech/architecture.md": "Architecture",
|
||||
"tech/ci-cd-workflow.md": "CI-CD-Workflow"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Architecture
|
||||
|
||||
devx is a reusable Python package providing development and CI/CD tools for oblachno-oss projects.
|
||||
|
||||
## Package Structure
|
||||
|
||||
```
|
||||
src/devx/
|
||||
├── __init__.py # Version (single source of truth)
|
||||
├── cli.py # Click-based CLI entry point (devx command)
|
||||
├── config.py # Configuration system (DEVX_ env vars)
|
||||
├── api_clients.py # GiteaClient, VikunjaClient — HTTP API wrappers
|
||||
├── gitea_cli.py # TeaCLI — wrapper around tea CLI with JSON parsing
|
||||
├── i18n.py # Translation system (gettext-based, translations.json)
|
||||
├── exceptions.py # Custom exception types (DevxError, APIError)
|
||||
├── translations.json # Translation strings (en, bg, de, ru, zh)
|
||||
├── ci/ # CI/CD automation modules
|
||||
├── tools/ # Developer tooling modules
|
||||
└── molecule/ # Optional molecule testing helpers
|
||||
```
|
||||
|
||||
## Core Modules
|
||||
|
||||
### cli.py
|
||||
|
||||
Click-based CLI entry point. Provides three command groups: `devx ci`, `devx tools`, `devx molecule`. Each subcommand delegates to the corresponding module via `_run_module()`.
|
||||
|
||||
### i18n.py
|
||||
|
||||
Simple i18n system using a JSON translations file. Supports en, bg, de, ru, zh. Projects can extend translations by setting `DEVX_TRANSLATIONS_PATH` to a custom JSON file.
|
||||
|
||||
### exceptions.py
|
||||
|
||||
Custom exception hierarchy: `DevxError` (base), `APIError` (HTTP errors with status code and message).
|
||||
|
||||
### api_clients.py
|
||||
|
||||
HTTP API clients with connection pooling and retry logic:
|
||||
- `GiteaClient` — Gitea REST API (branch protection, labels, issues, PRs, releases, reviews)
|
||||
- `VikunjaClient` — Vikunja REST API (tasks, projects, comments)
|
||||
|
||||
Both clients retry on transient errors (429, 5xx, connection errors) with exponential backoff.
|
||||
|
||||
### config.py
|
||||
|
||||
Configuration constants with env-var overrides (`DEVX_` prefix). Includes API URLs, timeouts, retry settings, task prefix regex, and conventional commit regex.
|
||||
|
||||
### gitea_cli.py
|
||||
|
||||
Python wrapper around the `tea` Gitea CLI tool. Parses JSON output for structured data. Used by CI scripts for Gitea API operations (issues, labels, PRs, releases, reviews).
|
||||
@@ -0,0 +1,85 @@
|
||||
# CI/CD Workflow
|
||||
|
||||
devx uses Gitea Actions for CI/CD automation. The workflow replicates GRM's automated pipeline but without molecule tests.
|
||||
|
||||
## Workflows
|
||||
|
||||
### CI (`ci.yml`)
|
||||
|
||||
Runs on pull requests. Jobs:
|
||||
|
||||
1. **quality** — lint (ruff, pyright, bandit, actionlint), unit tests with 100% coverage, test speed check, doc coverage, translation check, dependency scan
|
||||
2. **detect-changes** — classify changes as user-facing or workflow-only
|
||||
3. **release-dry-run** — dry-run the release script (only if user-facing changes)
|
||||
4. **pr-review** — automated PR review
|
||||
5. **auto-merge** — squash-merge PR when all checks pass
|
||||
|
||||
### Post-merge (`post-merge.yml`)
|
||||
|
||||
Runs on every push to master. Jobs:
|
||||
|
||||
1. **detect-type** — check if commit is a release commit
|
||||
2. **validate-commit-msg** — validate conventional commit format
|
||||
3. **release** — calculate next version, update changelog, tag, push
|
||||
4. **sync-wiki** — sync docs to Gitea wiki
|
||||
5. **badges** — generate and push quality badges
|
||||
6. **vikunja** — mark Vikunja task as done
|
||||
7. **configure-repo** — ensure branch protection and labels
|
||||
|
||||
### Publish (`publish.yml`)
|
||||
|
||||
Runs on tag pushes (`v*`). Builds the package, publishes to Gitea PyPI registry, and creates a Gitea release.
|
||||
|
||||
## CI Scripts
|
||||
|
||||
### auto_merge.py
|
||||
|
||||
Auto-merge PR when all CI checks pass. Reads task ID from `.taskid`, validates PR title format, checks Vikunja task exists, squash-merges with `DEVX-N <conventional commit>` title.
|
||||
|
||||
### release.py
|
||||
|
||||
Automated release using git-cliff. Calculates next semver version from conventional commits, updates `__version__` in `__init__.py`, updates `CHANGELOG.md`, runs lint and tests, commits with `release: vX.Y.Z [skip ci]`, creates annotated tag, pushes.
|
||||
|
||||
### publish.py
|
||||
|
||||
Builds package with `python -m build`, publishes to Gitea PyPI registry via twine, creates Gitea release with git-cliff-generated notes.
|
||||
|
||||
### pr_review.py
|
||||
|
||||
Automated PR review. Checks architecture compliance, best practices, security, i18n, resource management, documentation, test coverage, and commit conventions. Posts inline comments and structured review.
|
||||
|
||||
### notify_failure.py
|
||||
|
||||
Creates a Gitea issue when a CI workflow fails. Uses tea CLI for issue creation with failure labels.
|
||||
|
||||
### post_merge.py
|
||||
|
||||
Updates Vikunja task after a merge to master. Extracts task ID from commit message, marks task as done, posts a comment with the merge SHA.
|
||||
|
||||
### classify_changes.py
|
||||
|
||||
Classifies git changes as user-facing or workflow-only. Used to skip releases for infrastructure-only changes. Patterns are configurable.
|
||||
|
||||
### discover_runners.py
|
||||
|
||||
Discovers available Gitea Actions runners at repo, org, and instance levels. Generates a dynamic matrix for parallel job distribution.
|
||||
|
||||
### detect_release_commit.py
|
||||
|
||||
Detects whether the latest git commit is a release commit. Writes `is-release=true` or `is-release=false` to GitHub output.
|
||||
|
||||
### push_badges.py
|
||||
|
||||
Generates SVG badge files from project metrics (tests, coverage, quality, version). Pushes to `badges` branch and updates README with cache-busting commit SHA URLs.
|
||||
|
||||
### distribute_molecule.py
|
||||
|
||||
Distributes molecule (scenario, platform) pairs across N parallel runners. Discovers scenarios under `ansible/roles/*/molecule/`.
|
||||
|
||||
### molecule_ci_guard.py
|
||||
|
||||
Runs molecule tests sequentially while polling Gitea for other runner failures. Aborts if another runner fails the same job.
|
||||
|
||||
### validate_commit_msg.py
|
||||
|
||||
Validates commit messages. On feature branches: conventional commits only (no `DEVX-N` prefix). On master: must have `DEVX-N` prefix from auto-merge.
|
||||
@@ -0,0 +1,105 @@
|
||||
# CLI Commands
|
||||
|
||||
devx provides a CLI with three command groups: `ci`, `tools`, and `molecule`.
|
||||
|
||||
## CI Commands
|
||||
|
||||
### `devx ci auto-merge`
|
||||
|
||||
Auto-merge a PR when all CI checks pass. Validates PR title, checks Vikunja task, squash-merges.
|
||||
|
||||
### `devx ci check-translations`
|
||||
|
||||
Check translation files for gaps, dead keys, and missing languages.
|
||||
|
||||
### `devx ci classify-changes`
|
||||
|
||||
Classify git changes as user-facing or workflow-only. Used to skip releases for infrastructure-only changes.
|
||||
|
||||
### `devx ci detect-release-commit`
|
||||
|
||||
Detect whether the latest git commit is a release commit (`release: vX.Y.Z [skip ci]`).
|
||||
|
||||
### `devx ci discover-runners`
|
||||
|
||||
Discover available Gitea Actions runners for dynamic job distribution.
|
||||
|
||||
### `devx ci doc-coverage`
|
||||
|
||||
Check documentation coverage for CLI commands and major modules.
|
||||
|
||||
### `devx ci notify-failure`
|
||||
|
||||
Create a Gitea issue when a CI workflow fails.
|
||||
|
||||
### `devx ci post-merge`
|
||||
|
||||
Update Vikunja task after a merge to master.
|
||||
|
||||
### `devx ci pr-review`
|
||||
|
||||
Run automated PR review: check architecture compliance, best practices, and quality.
|
||||
|
||||
### `devx ci publish`
|
||||
|
||||
Build package, publish to Gitea PyPI registry, and create Gitea release.
|
||||
|
||||
### `devx ci push-badges`
|
||||
|
||||
Generate badge SVG files and push them to the `badges` branch.
|
||||
|
||||
### `devx ci release`
|
||||
|
||||
Automated release: calculate next version, update files, tag, and push.
|
||||
|
||||
### `devx ci sync-wiki`
|
||||
|
||||
Sync documentation from `docs/` to the Gitea wiki.
|
||||
|
||||
### `devx ci validate-commit-msg`
|
||||
|
||||
Validate commit messages for conventional commit format.
|
||||
|
||||
## Tools Commands
|
||||
|
||||
### `devx tools check-test-speed`
|
||||
|
||||
Run unit tests and enforce a maximum execution-time budget.
|
||||
|
||||
### `devx tools configure-repo`
|
||||
|
||||
Configure repository: branch protection + labels via Gitea API.
|
||||
|
||||
### `devx tools generate-badges`
|
||||
|
||||
Generate self-contained SVG badge files from project metrics.
|
||||
|
||||
### `devx tools install-checkmake`
|
||||
|
||||
Install checkmake (Makefile linter) if not already present.
|
||||
|
||||
### `devx tools install-tools`
|
||||
|
||||
Install CI/CD development tools: actionlint, git-cliff, act_runner, tea.
|
||||
|
||||
### `devx tools setup`
|
||||
|
||||
Project setup: install Python deps and pre-commit hooks.
|
||||
|
||||
## Molecule Commands
|
||||
|
||||
### `devx molecule distribute`
|
||||
|
||||
Distribute molecule test pairs across parallel runners.
|
||||
|
||||
### `devx molecule discover-runners`
|
||||
|
||||
Discover available Gitea Actions runners for molecule tests.
|
||||
|
||||
### `devx molecule guard`
|
||||
|
||||
Run molecule tests sequentially with CI failure polling.
|
||||
|
||||
### `devx molecule all`
|
||||
|
||||
Run all molecule scenarios on all supported OS platforms.
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
# Aligned with CI timeout (ci.yml uses --max-seconds 10).
|
||||
set -e
|
||||
export PYTHONPATH=src
|
||||
python -m devx.tools.check_test_speed --max-seconds 10
|
||||
python3 -m devx.tools.check_test_speed --max-seconds 10
|
||||
|
||||
@@ -41,7 +41,7 @@ from devx.i18n import _
|
||||
TASKID_FILE = ".taskid"
|
||||
PR_TITLE_RE = re.compile(r"^DEVX-\d+:\s+.+")
|
||||
|
||||
load_dotenv(override=True)
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def run_cmd(args: list[str], check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
|
||||
@@ -25,7 +25,7 @@ from devx.config import GITEA_API_URL
|
||||
from devx.gitea_cli import TeaCLI, TeaCLIError
|
||||
from devx.i18n import _
|
||||
|
||||
load_dotenv(override=True)
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def _create_issue_via_tea(repo: str, title: str, body: str) -> int:
|
||||
|
||||
@@ -17,7 +17,7 @@ from devx.config import DEFAULT_PER_PAGE, TASK_ID_RE, VIKUNJA_API_URL, VIKUNJA_P
|
||||
from devx.exceptions import APIError
|
||||
from devx.i18n import _
|
||||
|
||||
load_dotenv(override=True)
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def _get_git_commit_message() -> str:
|
||||
|
||||
@@ -35,7 +35,7 @@ from devx.config import GITEA_API_URL
|
||||
from devx.exceptions import APIError
|
||||
from devx.i18n import _
|
||||
|
||||
load_dotenv(override=True)
|
||||
load_dotenv()
|
||||
|
||||
# Files that are exempt from certain checks
|
||||
WORKFLOW_ONLY_SUFFIXES = (".yml", ".yaml", ".md", ".json", ".toml", ".cfg", ".ini", ".txt")
|
||||
|
||||
@@ -31,7 +31,7 @@ from devx.config import GITEA_API_URL
|
||||
from devx.gitea_cli import TeaCLI, TeaCLIError
|
||||
from devx.i18n import _
|
||||
|
||||
load_dotenv(override=True)
|
||||
load_dotenv()
|
||||
|
||||
CLIFF_CONFIG = "cliff.toml"
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnk
|
||||
from devx.ci.classify_changes import has_user_facing_changes # cross-CI import, needs PYTHONPATH=.
|
||||
from devx.i18n import _
|
||||
|
||||
load_dotenv(override=True)
|
||||
load_dotenv()
|
||||
|
||||
INIT_FILE = os.getenv("DEVX_VERSION_FILE", "src/devx/__init__.py")
|
||||
CHANGELOG_FILE = "CHANGELOG.md"
|
||||
|
||||
@@ -32,7 +32,7 @@ from devx.config import GITEA_API_URL
|
||||
from devx.exceptions import APIError
|
||||
from devx.i18n import _
|
||||
|
||||
load_dotenv(override=True)
|
||||
load_dotenv()
|
||||
|
||||
DOCS_DIR = Path(__file__).resolve().parent.parent.parent.parent / "docs"
|
||||
MAPPING_FILE = DOCS_DIR / "mapping.json"
|
||||
|
||||
@@ -122,7 +122,9 @@ def install_git_cliff() -> bool:
|
||||
if _is_installed("git-cliff"):
|
||||
click.echo("git-cliff: already installed")
|
||||
return True
|
||||
arch = _arch()
|
||||
# git-cliff uses x86_64/arm64 in release asset names (not amd64)
|
||||
machine = platform.machine().lower()
|
||||
arch = "x86_64" if machine in {"x86_64", "amd64"} else "arm64"
|
||||
url = (
|
||||
f"https://github.com/orhun/git-cliff/releases/download/"
|
||||
f"v{GIT_CLIFF_VERSION}/git-cliff-{GIT_CLIFF_VERSION}-{arch}-unknown-linux-gnu.tar.gz"
|
||||
|
||||
+61
-6
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Project setup: install Python deps and pre-commit hooks.
|
||||
"""Project setup: install Python deps, pre-commit hooks, and tea CLI login.
|
||||
|
||||
Usage::
|
||||
|
||||
@@ -8,10 +8,15 @@ Usage::
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess # nosec B404
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def _run(cmd: list[str]) -> None:
|
||||
@@ -33,6 +38,55 @@ def _install_pre_commit_hooks(bin_dir: str) -> None:
|
||||
_run([pre_commit, "install", "--hook-type", hook_type])
|
||||
|
||||
|
||||
def _configure_tea_login() -> None:
|
||||
"""Configure tea CLI login from .env if REPO_TOKEN is set.
|
||||
|
||||
Idempotent: if a login with the same name already exists, it is not re-added.
|
||||
Skips if tea is not installed or REPO_TOKEN is not set.
|
||||
"""
|
||||
tea_bin = shutil.which("tea")
|
||||
if tea_bin is None:
|
||||
click.echo("tea: not installed — run 'make install-tools' to install it.")
|
||||
return
|
||||
|
||||
token = os.environ.get("REPO_TOKEN", "")
|
||||
if not token:
|
||||
click.echo("tea: REPO_TOKEN not set — skipping login configuration.")
|
||||
return
|
||||
|
||||
api_url = os.environ.get("DEVX_GITEA_API_URL", "https://git.oblachno.oblachno.fyi/api/v1")
|
||||
gitea_url = api_url.replace("/api/v1", "")
|
||||
login_name = "devx"
|
||||
|
||||
result = subprocess.run( # nosec B603
|
||||
[tea_bin, "login", "list", "--output", "simple"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0 and login_name in result.stdout:
|
||||
click.echo(f"tea: login '{login_name}' already configured.")
|
||||
return
|
||||
|
||||
click.echo(f"tea: configuring login '{login_name}' for {gitea_url}...")
|
||||
add_result = subprocess.run( # nosec B603
|
||||
[tea_bin, "login", "add", "--name", login_name, "--url", gitea_url, "--token", token],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if add_result.returncode != 0:
|
||||
click.echo(f"tea: login configuration failed: {add_result.stderr.strip()}", err=True)
|
||||
else:
|
||||
subprocess.run( # nosec B603
|
||||
[tea_bin, "login", "default", login_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
click.echo(f"tea: login '{login_name}' configured and set as default.")
|
||||
|
||||
|
||||
def _verify(bin_dir: str) -> None:
|
||||
"""Print versions of installed tools for verification."""
|
||||
devx = str(Path(bin_dir) / "devx")
|
||||
@@ -51,7 +105,7 @@ def _verify(bin_dir: str) -> None:
|
||||
@click.option(
|
||||
"--extras",
|
||||
default="dev",
|
||||
help="Dependency group to install: ci, lint, build, twine, or dev (default: dev).",
|
||||
help="Dependency group to install: ci, lint, or dev (default: dev).",
|
||||
)
|
||||
@click.option(
|
||||
"--no-pre-commit",
|
||||
@@ -63,7 +117,7 @@ def _verify(bin_dir: str) -> None:
|
||||
"--no-tea-login",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Skip tea CLI login configuration (no-op, kept for backwards compatibility).",
|
||||
help="Skip tea CLI login configuration.",
|
||||
)
|
||||
def main(
|
||||
bin_dir: str,
|
||||
@@ -71,7 +125,7 @@ def main(
|
||||
no_pre_commit: bool,
|
||||
no_tea_login: bool,
|
||||
) -> None:
|
||||
"""Install Python deps and pre-commit hooks."""
|
||||
"""Install Python deps, pre-commit hooks, and configure tea CLI."""
|
||||
if not Path(bin_dir).exists():
|
||||
raise click.ClickException(f"Bin directory not found: {bin_dir}. Run 'python3 -m venv .venv' first.")
|
||||
|
||||
@@ -82,8 +136,9 @@ def main(
|
||||
click.echo("Installing pre-commit hooks...")
|
||||
_install_pre_commit_hooks(bin_dir)
|
||||
|
||||
# --no-tea-login is a no-op (kept for backwards compatibility)
|
||||
_ = no_tea_login
|
||||
if not no_tea_login:
|
||||
click.echo("Configuring tea CLI login...")
|
||||
_configure_tea_login()
|
||||
|
||||
click.echo("")
|
||||
click.echo("Setup complete.")
|
||||
|
||||
@@ -110,6 +110,13 @@
|
||||
" FAIL: {title} — content mismatch or empty!": {
|
||||
"en": " FAIL: {title} — content mismatch or empty!"
|
||||
},
|
||||
" MISSING: devx {cmd}": {
|
||||
"en": " MISSING: devx {cmd}",
|
||||
"bg": " ЛИПСВА: devx {cmd}",
|
||||
"de": " FEHLT: devx {cmd}",
|
||||
"ru": " ОТСУТСТВУЕТ: devx {cmd}",
|
||||
"zh": " 缺失: devx {cmd}"
|
||||
},
|
||||
" MISSING: grm {cmd}": {
|
||||
"en": " MISSING: grm {cmd}"
|
||||
},
|
||||
@@ -119,6 +126,13 @@
|
||||
" MISSING: {script}": {
|
||||
"en": " MISSING: {script}"
|
||||
},
|
||||
" OK: devx {cmd}": {
|
||||
"en": " OK: devx {cmd}",
|
||||
"bg": " ОК: devx {cmd}",
|
||||
"de": " OK: devx {cmd}",
|
||||
"ru": " ОК: devx {cmd}",
|
||||
"zh": " 正常: devx {cmd}"
|
||||
},
|
||||
" OK: grm {cmd}": {
|
||||
"en": " OK: grm {cmd}"
|
||||
},
|
||||
@@ -204,6 +218,13 @@
|
||||
"ru": "ОШИБКА: VIKUNJA_TOKEN не задан.",
|
||||
"zh": "错误:未设置 VIKUNJA_TOKEN。"
|
||||
},
|
||||
"ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME.": {
|
||||
"en": "ERROR: Repository name not specified. Use --repo or set DEVX_REPO_NAME.",
|
||||
"bg": "ГРЕШКА: Името на хранилището не е указано. Използвайте --repo или задайте DEVX_REPO_NAME.",
|
||||
"de": "FEHLER: Repository-Name nicht angegeben. Verwenden Sie --repo oder setzen Sie DEVX_REPO_NAME.",
|
||||
"ru": "ОШИБКА: Имя репозитория не указано. Используйте --repo или задайте DEVX_REPO_NAME.",
|
||||
"zh": "错误:未指定仓库名称。请使用 --repo 或设置 DEVX_REPO_NAME。"
|
||||
},
|
||||
"ERROR: mapping.json not found at {path}": {
|
||||
"en": "ERROR: mapping.json not found at {path}"
|
||||
},
|
||||
@@ -239,6 +260,13 @@
|
||||
"Head branch is behind master. Pulling and rebasing...": {
|
||||
"en": "Head branch is behind master. Pulling and rebasing..."
|
||||
},
|
||||
"Infrastructure commit (no DEVX-N task ID), skipping Vikunja update: {msg}": {
|
||||
"en": "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}",
|
||||
"ru": "Инфраструктурный коммит (без ID задачи DEVX-N), пропуск обновления Vikunja: {msg}",
|
||||
"zh": "基础设施提交(无 DEVX-N 任务 ID),跳过 Vikunja 更新: {msg}"
|
||||
},
|
||||
"Infrastructure commit (no GRM-N task ID), skipping Vikunja update: {msg}": {
|
||||
"en": "Infrastructure commit (no GRM-N task ID), skipping Vikunja update: {msg}"
|
||||
},
|
||||
@@ -258,6 +286,13 @@
|
||||
"ru": "Слияние не удалось: HTTP {status}: {message}\nПроверьте, что PR готов и у вас есть права на слияние.",
|
||||
"zh": "合并失败: HTTP {status}: {message}\n请检查 PR 是否准备就绪且您具有合并权限。"
|
||||
},
|
||||
"Module {mod} has no main() function": {
|
||||
"en": "Module {mod} has no main() function",
|
||||
"bg": "Модул {mod} няма функция main()",
|
||||
"de": "Modul {mod} hat keine main()-Funktion",
|
||||
"ru": "Модуль {mod} не имеет функции main()",
|
||||
"zh": "模块 {mod} 没有 main() 函数"
|
||||
},
|
||||
"Molecule directory not found: {path}": {
|
||||
"en": "Molecule directory not found: {path}",
|
||||
"bg": "Директорията на molecule не е намерена: {path}",
|
||||
@@ -314,6 +349,13 @@
|
||||
"ru": "Ой! Сообщение коммита должно соответствовать формату conventional commit.\n Ожидается: <type>: <description>\n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE",
|
||||
"zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: <type>: <description>\n 实际: {subject}\n 允许的类型: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE"
|
||||
},
|
||||
"Oops! Do not include task ID (DEVX-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.": {
|
||||
"en": "Oops! Do not include task ID (DEVX-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.",
|
||||
"bg": "Опа! Не включвайте идентификатор на задача (DEVX-N) в commit-и от feature клонове.\n Идентификаторът ще бъде добавен автоматично при сливане чрез CI.",
|
||||
"de": "Ups! Keine Task-ID (DEVX-N) in Feature-Branch-Commits einfügen.\n Die Task-ID wird beim Merge automatisch über CI hinzugefügt.",
|
||||
"ru": "Ой! Не включайте ID задачи (DEVX-N) в коммиты feature-веток.\n ID задачи будет добавлен автоматически при слиянии через CI.",
|
||||
"zh": "哎呀!不要在 feature 分支的提交中包含任务 ID (DEVX-N)。\n 任务 ID 将在通过 CI 合并时自动添加。"
|
||||
},
|
||||
"Oops! Do not include task ID (GRM-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.": {
|
||||
"en": "Oops! Do not include task ID (GRM-N) in feature branch commits.\n The task ID will be added automatically on merge via CI.",
|
||||
"bg": "Опа! Не включвайте идентификатор на задача (GRM-N) в commit-и от feature клонове.\n Идентификаторът ще бъде добавен автоматично при сливане чрез CI.",
|
||||
@@ -321,6 +363,20 @@
|
||||
"ru": "Ой! Не включайте ID задачи (GRM-N) в коммиты feature-веток.\n ID задачи будет добавлен автоматически при слиянии через CI.",
|
||||
"zh": "哎呀!不要在 feature 分支的提交中包含任务 ID (GRM-N)。\n 任务 ID 将在通过 CI 合并时自动添加。"
|
||||
},
|
||||
"Oops! Gitea PyPI registry publish failed:\n{stderr}": {
|
||||
"en": "Oops! Gitea PyPI registry publish failed:\n{stderr}",
|
||||
"bg": "Опа! Публикуването в Gitea PyPI registry неуспешно:\n{stderr}",
|
||||
"de": "Ups! Veröffentlichung in der Gitea PyPI-Registry fehlgeschlagen:\n{stderr}",
|
||||
"ru": "Ой! Публикация в Gitea PyPI registry не удалась:\n{stderr}",
|
||||
"zh": "哎呀!Gitea PyPI registry 发布失败:\n{stderr}"
|
||||
},
|
||||
"Oops! Master branch commit must follow conventional format after task ID.\n Expected: DEVX-N: <type>: <description>\n Got: {subject}": {
|
||||
"en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: DEVX-N: <type>: <description>\n Got: {subject}",
|
||||
"bg": "Опа! Commit-ът в клона master трябва да следва конвенционален формат след идентификатора.\n Очаква се: DEVX-N: <type>: <description>\n Получено: {subject}",
|
||||
"de": "Ups! Master-Branch-Commit muss nach der Task-ID dem konventionellen Format folgen.\n Erwartet: DEVX-N: <type>: <description>\n Erhalten: {subject}",
|
||||
"ru": "Ой! Коммит в ветку master после ID задачи должен соответствовать conventional формату.\n Ожидается: DEVX-N: <type>: <description>\n Получено: {subject}",
|
||||
"zh": "哎呀!master 分支提交在任务 ID 后必须遵循 conventional commit 格式。\n 预期格式: DEVX-N: <type>: <description>\n 实际: {subject}"
|
||||
},
|
||||
"Oops! Master branch commit must follow conventional format after task ID.\n Expected: GRM-N: <type>: <description>\n Got: {subject}": {
|
||||
"en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: GRM-N: <type>: <description>\n Got: {subject}",
|
||||
"bg": "Опа! Commit-ът в клона master трябва да следва конвенционален формат след идентификатора.\n Очаква се: GRM-N: <type>: <description>\n Получено: {subject}",
|
||||
@@ -328,6 +384,13 @@
|
||||
"ru": "Ой! Коммит в ветку master после ID задачи должен соответствовать conventional формату.\n Ожидается: GRM-N: <type>: <description>\n Получено: {subject}",
|
||||
"zh": "哎呀!master 分支提交在任务 ID 后必须遵循 conventional commit 格式。\n 预期格式: GRM-N: <type>: <description>\n 实际: {subject}"
|
||||
},
|
||||
"Oops! Master branch commits must start with a task ID.\n Expected: DEVX-N: <conventional commit message>\n Got: {subject}": {
|
||||
"en": "Oops! Master branch commits must start with a task ID.\n Expected: DEVX-N: <conventional commit message>\n Got: {subject}",
|
||||
"bg": "Опа! Commit-ите в клона master трябва да започват с идентификатор на задача.\n Очаква се: DEVX-N: <conventional commit message>\n Получено: {subject}",
|
||||
"de": "Ups! Master-Branch-Commits müssen mit einer Task-ID beginnen.\n Erwartet: DEVX-N: <conventional commit message>\n Erhalten: {subject}",
|
||||
"ru": "Ой! Коммиты в ветку master должны начинаться с ID задачи.\n Ожидается: DEVX-N: <conventional commit message>\n Получено: {subject}",
|
||||
"zh": "哎呀!master 分支的提交必须以任务 ID 开头。\n 预期格式: DEVX-N: <conventional commit message>\n 实际: {subject}"
|
||||
},
|
||||
"Oops! Master branch commits must start with a task ID.\n Expected: GRM-N: <conventional commit message>\n Got: {subject}": {
|
||||
"en": "Oops! Master branch commits must start with a task ID.\n Expected: GRM-N: <conventional commit message>\n Got: {subject}",
|
||||
"bg": "Опа! Commit-ите в клона master трябва да започват с идентификатор на задача.\n Очаква се: GRM-N: <conventional commit message>\n Получено: {subject}",
|
||||
@@ -338,6 +401,13 @@
|
||||
"Oops! No task ID found in .taskid file or branch name '{branch}'.": {
|
||||
"en": "Oops! No task ID found in .taskid file or branch name '{branch}'."
|
||||
},
|
||||
"Oops! PR title must follow format 'DEVX-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}": {
|
||||
"en": "Oops! PR title must follow format 'DEVX-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}",
|
||||
"bg": "Опа! Заглавието на PR трябва да следва формата 'DEVX-N: <заглавие на задача>'.\n Очаква се: {task_id}: <заглавие на задача>\n Получено: {pr_title}",
|
||||
"de": "Ups! PR-Titel muss dem Format 'DEVX-N: <Task-Titel>' folgen.\n Erwartet: {task_id}: <Task-Titel>\n Erhalten: {pr_title}",
|
||||
"ru": "Ой! Заголовок PR должен соответствовать формату 'DEVX-N: <название задачи>'.\n Ожидается: {task_id}: <название задачи>\n Получено: {pr_title}",
|
||||
"zh": "哎呀!PR 标题必须遵循格式 'DEVX-N: <任务标题>'。\n 预期格式: {task_id}: <任务标题>\n 实际: {pr_title}"
|
||||
},
|
||||
"Oops! PR title must follow format 'GRM-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}": {
|
||||
"en": "Oops! PR title must follow format 'GRM-N: <task title>'.\n Expected: {task_id}: <task title>\n Got: {pr_title}"
|
||||
},
|
||||
@@ -364,6 +434,13 @@
|
||||
"PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}": {
|
||||
"en": "PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {pr_title}"
|
||||
},
|
||||
"PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.": {
|
||||
"en": "PYPI_TOKEN not set and no registry URL configured — skipping PyPI publish. No worries, we'll just create the Gitea release.",
|
||||
"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.",
|
||||
"ru": "PYPI_TOKEN не задан и URL registry не настроен — пропускаем публикацию в PyPI. Не беспокойтесь, мы просто создадим Gitea release.",
|
||||
"zh": "未设置 PYPI_TOKEN 且未配置 registry URL — 跳过 PyPI 发布。别担心,我们直接创建 Gitea release。"
|
||||
},
|
||||
"PYPI_TOKEN not set — skipping PyPI publish. No worries, we'll just create the Gitea release.": {
|
||||
"en": "PYPI_TOKEN not set — skipping PyPI publish. No worries, we'll just create the Gitea release.",
|
||||
"bg": "PYPI_TOKEN не е зададен — пропускаме публикуването в PyPI. Без притеснения, просто ще създадем Gitea release.",
|
||||
@@ -371,6 +448,13 @@
|
||||
"ru": "PYPI_TOKEN не задан — пропускаем публикацию в PyPI. Не беспокойтесь, мы просто создадим Gitea release.",
|
||||
"zh": "未设置 PYPI_TOKEN — 跳过 PyPI 发布。别担心,我们直接创建 Gitea release。"
|
||||
},
|
||||
"Published to Gitea PyPI registry.": {
|
||||
"en": "Published to Gitea PyPI registry.",
|
||||
"bg": "Публикувано в Gitea PyPI registry.",
|
||||
"de": "In der Gitea PyPI-Registry veröffentlicht.",
|
||||
"ru": "Опубликовано в Gitea PyPI registry.",
|
||||
"zh": "已发布到 Gitea PyPI registry。"
|
||||
},
|
||||
"Published to PyPI.": {
|
||||
"en": "Published to PyPI.",
|
||||
"bg": "Публикувано в PyPI.",
|
||||
@@ -455,6 +539,13 @@
|
||||
"WARNING: File {file} not found — skipping.": {
|
||||
"en": "WARNING: File {file} not found — skipping."
|
||||
},
|
||||
"Warning: No task ID (DEVX-N) found in commit message: {msg}. Skipping Vikunja update.": {
|
||||
"en": "Warning: No task ID (DEVX-N) found in commit message: {msg}. Skipping Vikunja update.",
|
||||
"bg": "Предупреждение: Не е намерен идентификатор на задача (DEVX-N) в съобщението за commit: {msg}. Пропускаме обновяването на Vikunja.",
|
||||
"de": "Warnung: Keine Task-ID (DEVX-N) in Commit-Nachricht gefunden: {msg}. Vikunja-Update wird übersprungen.",
|
||||
"ru": "Предупреждение: ID задачи (DEVX-N) не найден в сообщении коммита: {msg}. Пропуск обновления Vikunja.",
|
||||
"zh": "警告:提交消息中未找到任务 ID (DEVX-N): {msg}。跳过 Vikunja 更新。"
|
||||
},
|
||||
"Warning: No task ID (GRM-N) found in commit message: {msg}. Skipping Vikunja update.": {
|
||||
"en": "Warning: No task ID (GRM-N) found in commit message: {msg}. Skipping Vikunja update."
|
||||
},
|
||||
@@ -494,10 +585,59 @@
|
||||
"[dry-run] Would update {init}": {
|
||||
"en": "[dry-run] Would update {init}"
|
||||
},
|
||||
"active": {
|
||||
"en": "active",
|
||||
"bg": "активен",
|
||||
"de": "aktiv",
|
||||
"ru": "активен",
|
||||
"zh": "活跃"
|
||||
},
|
||||
"completed": {
|
||||
"en": "completed",
|
||||
"bg": "завършен",
|
||||
"de": "abgeschlossen",
|
||||
"ru": "завершён",
|
||||
"zh": "已完成"
|
||||
},
|
||||
"failed": {
|
||||
"en": "failed",
|
||||
"bg": "неуспешен",
|
||||
"de": "fehlgeschlagen",
|
||||
"ru": "неудачный",
|
||||
"zh": "失败"
|
||||
},
|
||||
"git command failed ({cmd}): {stderr}": {
|
||||
"en": "git command failed ({cmd}): {stderr}"
|
||||
},
|
||||
"git-cliff returned empty version.": {
|
||||
"en": "git-cliff returned empty version."
|
||||
},
|
||||
"inactive": {
|
||||
"en": "inactive",
|
||||
"bg": "неактивен",
|
||||
"de": "inaktiv",
|
||||
"ru": "неактивен",
|
||||
"zh": "未激活"
|
||||
},
|
||||
"in_progress": {
|
||||
"en": "in progress",
|
||||
"bg": "в процес",
|
||||
"de": "in Bearbeitung",
|
||||
"ru": "в процессе",
|
||||
"zh": "进行中"
|
||||
},
|
||||
"pending": {
|
||||
"en": "pending",
|
||||
"bg": "в очакване",
|
||||
"de": "ausstehend",
|
||||
"ru": "ожидает",
|
||||
"zh": "待处理"
|
||||
},
|
||||
"unknown": {
|
||||
"en": "unknown",
|
||||
"bg": "неизвестен",
|
||||
"de": "unbekannt",
|
||||
"ru": "неизвестно",
|
||||
"zh": "未知"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from devx.tools.setup import (
|
||||
_configure_tea_login,
|
||||
_install_pre_commit_hooks,
|
||||
_install_python_deps,
|
||||
_run,
|
||||
@@ -57,6 +58,67 @@ class TestInstallPreCommitHooks:
|
||||
assert "pre-push" in hook_types
|
||||
|
||||
|
||||
class TestConfigureTeaLogin:
|
||||
@patch("devx.tools.setup.shutil.which", return_value=None)
|
||||
def test_tea_not_installed(self, mock_which: MagicMock) -> None:
|
||||
_configure_tea_login()
|
||||
mock_which.assert_called_once_with("tea")
|
||||
|
||||
@patch("devx.tools.setup.shutil.which", return_value="/usr/local/bin/tea")
|
||||
@patch.dict("os.environ", {}, clear=True)
|
||||
def test_no_token(self, mock_which: MagicMock) -> None:
|
||||
_configure_tea_login()
|
||||
|
||||
@patch("devx.tools.setup.subprocess.run")
|
||||
@patch("devx.tools.setup.shutil.which", return_value="/usr/local/bin/tea")
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok123"}, clear=True)
|
||||
def test_login_already_exists(self, mock_which: MagicMock, mock_run: MagicMock) -> None:
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="devx\ngrm\n", stderr="")
|
||||
_configure_tea_login()
|
||||
# Should only call login list, not login add
|
||||
assert mock_run.call_count == 1
|
||||
assert "list" in mock_run.call_args[0][0]
|
||||
|
||||
@patch("devx.tools.setup.subprocess.run")
|
||||
@patch("devx.tools.setup.shutil.which", return_value="/usr/local/bin/tea")
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok123"}, clear=True)
|
||||
def test_login_add_success(self, mock_which: MagicMock, mock_run: MagicMock) -> None:
|
||||
list_result = MagicMock(returncode=0, stdout="", stderr="")
|
||||
add_result = MagicMock(returncode=0, stdout="", stderr="")
|
||||
default_result = MagicMock(returncode=0, stdout="", stderr="")
|
||||
mock_run.side_effect = [list_result, add_result, default_result]
|
||||
_configure_tea_login()
|
||||
assert mock_run.call_count == 3
|
||||
assert "add" in mock_run.call_args_list[1][0][0]
|
||||
assert "default" in mock_run.call_args_list[2][0][0]
|
||||
|
||||
@patch("devx.tools.setup.subprocess.run")
|
||||
@patch("devx.tools.setup.shutil.which", return_value="/usr/local/bin/tea")
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok123"}, clear=True)
|
||||
def test_login_add_failure(self, mock_which: MagicMock, mock_run: MagicMock) -> None:
|
||||
list_result = MagicMock(returncode=0, stdout="", stderr="")
|
||||
add_result = MagicMock(returncode=1, stdout="", stderr="auth failed")
|
||||
mock_run.side_effect = [list_result, add_result]
|
||||
_configure_tea_login()
|
||||
assert mock_run.call_count == 2
|
||||
|
||||
@patch("devx.tools.setup.subprocess.run")
|
||||
@patch("devx.tools.setup.shutil.which", return_value="/usr/local/bin/tea")
|
||||
@patch.dict(
|
||||
"os.environ",
|
||||
{"REPO_TOKEN": "tok123", "DEVX_GITEA_API_URL": "https://custom.example.com/api/v1"},
|
||||
clear=True,
|
||||
)
|
||||
def test_custom_gitea_url(self, mock_which: MagicMock, mock_run: MagicMock) -> None:
|
||||
list_result = MagicMock(returncode=0, stdout="", stderr="")
|
||||
add_result = MagicMock(returncode=0, stdout="", stderr="")
|
||||
default_result = MagicMock(returncode=0, stdout="", stderr="")
|
||||
mock_run.side_effect = [list_result, add_result, default_result]
|
||||
_configure_tea_login()
|
||||
add_call = mock_run.call_args_list[1][0][0]
|
||||
assert "https://custom.example.com" in add_call
|
||||
|
||||
|
||||
class TestVerify:
|
||||
@patch("devx.tools.setup.subprocess.run")
|
||||
def test_verify_runs_tools(self, mock_run: MagicMock) -> None:
|
||||
@@ -77,6 +139,7 @@ class TestVerify:
|
||||
|
||||
|
||||
class TestMain:
|
||||
@patch("devx.tools.setup._configure_tea_login")
|
||||
@patch("devx.tools.setup._verify")
|
||||
@patch("devx.tools.setup._install_pre_commit_hooks")
|
||||
@patch("devx.tools.setup._install_python_deps")
|
||||
@@ -85,6 +148,7 @@ class TestMain:
|
||||
mock_install_deps: MagicMock,
|
||||
mock_install_hooks: MagicMock,
|
||||
mock_verify: MagicMock,
|
||||
mock_tea: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
bin_dir = tmp_path / "bin"
|
||||
@@ -95,7 +159,9 @@ class TestMain:
|
||||
mock_install_deps.assert_called_once()
|
||||
mock_install_hooks.assert_called_once()
|
||||
mock_verify.assert_called_once()
|
||||
mock_tea.assert_called_once()
|
||||
|
||||
@patch("devx.tools.setup._configure_tea_login")
|
||||
@patch("devx.tools.setup._verify")
|
||||
@patch("devx.tools.setup._install_pre_commit_hooks")
|
||||
@patch("devx.tools.setup._install_python_deps")
|
||||
@@ -104,6 +170,7 @@ class TestMain:
|
||||
mock_install_deps: MagicMock,
|
||||
mock_install_hooks: MagicMock,
|
||||
mock_verify: MagicMock,
|
||||
mock_tea: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
bin_dir = tmp_path / "bin"
|
||||
@@ -114,6 +181,7 @@ class TestMain:
|
||||
mock_install_deps.assert_called_once()
|
||||
mock_install_hooks.assert_not_called()
|
||||
|
||||
@patch("devx.tools.setup._configure_tea_login")
|
||||
@patch("devx.tools.setup._verify")
|
||||
@patch("devx.tools.setup._install_pre_commit_hooks")
|
||||
@patch("devx.tools.setup._install_python_deps")
|
||||
@@ -122,6 +190,7 @@ class TestMain:
|
||||
mock_install_deps: MagicMock,
|
||||
mock_install_hooks: MagicMock,
|
||||
mock_verify: MagicMock,
|
||||
mock_tea: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
bin_dir = tmp_path / "bin"
|
||||
@@ -131,22 +200,24 @@ class TestMain:
|
||||
assert result.exit_code == 0
|
||||
mock_install_deps.assert_called_once_with(str(bin_dir), "ci")
|
||||
|
||||
@patch("devx.tools.setup._configure_tea_login")
|
||||
@patch("devx.tools.setup._verify")
|
||||
@patch("devx.tools.setup._install_pre_commit_hooks")
|
||||
@patch("devx.tools.setup._install_python_deps")
|
||||
def test_main_no_tea_login_noop(
|
||||
def test_main_no_tea_login(
|
||||
self,
|
||||
mock_install_deps: MagicMock,
|
||||
mock_install_hooks: MagicMock,
|
||||
mock_verify: MagicMock,
|
||||
mock_tea: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""--no-tea-login is a no-op (kept for backwards compatibility)."""
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--bin", str(bin_dir), "--no-tea-login"])
|
||||
assert result.exit_code == 0
|
||||
mock_tea.assert_not_called()
|
||||
|
||||
def test_main_missing_bin_dir(self) -> None:
|
||||
runner = CliRunner()
|
||||
@@ -162,8 +233,9 @@ def test_main_module_block(tmp_path: Path) -> None:
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
with patch("devx.tools.setup._install_python_deps") as mock_deps:
|
||||
with patch("devx.tools.setup._install_pre_commit_hooks"):
|
||||
with patch("devx.tools.setup._verify"):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--bin", str(bin_dir)])
|
||||
assert result.exit_code == 0
|
||||
mock_deps.assert_called_once()
|
||||
with patch("devx.tools.setup._configure_tea_login"):
|
||||
with patch("devx.tools.setup._verify"):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--bin", str(bin_dir)])
|
||||
assert result.exit_code == 0
|
||||
mock_deps.assert_called_once()
|
||||
|
||||
Reference in New Issue
Block a user