From 31bfd23fea929867f764d1b25eb12b908587c015 Mon Sep 17 00:00:00 2001 From: emil Date: Mon, 22 Jun 2026 15:52:45 +0000 Subject: [PATCH] DEVX-1: fix: use python3 and venv python in workflows and Makefile --- .gitea/workflows/ci.yml | 15 ++-- .gitea/workflows/post-merge.yml | 29 +++---- .gitea/workflows/publish.yml | 6 +- .pre-commit-config.yaml | 4 +- Makefile | 28 +++++-- activate.fish | 3 + activate.sh | 2 + activate.zsh | 4 + docs/mapping.json | 5 +- docs/tech/architecture.md | 50 ++++++++++++ docs/tech/ci-cd-workflow.md | 85 +++++++++++++++++++ docs/user/cli-commands.md | 105 ++++++++++++++++++++++++ hooks/pre-commit | 2 +- src/devx/ci/auto_merge.py | 2 +- src/devx/ci/notify_failure.py | 2 +- src/devx/ci/post_merge.py | 2 +- src/devx/ci/pr_review.py | 2 +- src/devx/ci/publish.py | 2 +- src/devx/ci/release.py | 2 +- src/devx/ci/sync_wiki.py | 2 +- src/devx/tools/install_tools.py | 4 +- src/devx/tools/setup.py | 67 +++++++++++++-- src/devx/translations.json | 140 ++++++++++++++++++++++++++++++++ tests/unit/test_setup.py | 86 ++++++++++++++++++-- 24 files changed, 591 insertions(+), 58 deletions(-) create mode 100755 activate.fish create mode 100755 activate.sh create mode 100755 activate.zsh create mode 100644 docs/tech/architecture.md create mode 100644 docs/tech/ci-cd-workflow.md create mode 100644 docs/user/cli-commands.md diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 2618a80..f344384 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -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" \ diff --git a/.gitea/workflows/post-merge.yml b/.gitea/workflows/post-merge.yml index 23d0d44..4d61351 100644 --- a/.gitea/workflows/post-merge.yml +++ b/.gitea/workflows/post-merge.yml @@ -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" \ diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml index 318e20f..6875bd7 100644 --- a/.gitea/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -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" \ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 881d820..df7c1e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/Makefile b/Makefile index c96e4f0..4bd629e 100644 --- a/Makefile +++ b/Makefile @@ -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/ diff --git a/activate.fish b/activate.fish new file mode 100755 index 0000000..c2ba640 --- /dev/null +++ b/activate.fish @@ -0,0 +1,3 @@ +#!/usr/bin/env fish +set -l script_dir (dirname (status --current-filename)) +source "$script_dir/.venv/bin/activate.fish" diff --git a/activate.sh b/activate.sh new file mode 100755 index 0000000..dadce24 --- /dev/null +++ b/activate.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.venv/bin/activate" diff --git a/activate.zsh b/activate.zsh new file mode 100755 index 0000000..32ef0c8 --- /dev/null +++ b/activate.zsh @@ -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" diff --git a/docs/mapping.json b/docs/mapping.json index a36690e..bcb35f4 100644 --- a/docs/mapping.json +++ b/docs/mapping.json @@ -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" } diff --git a/docs/tech/architecture.md b/docs/tech/architecture.md new file mode 100644 index 0000000..5a9b64d --- /dev/null +++ b/docs/tech/architecture.md @@ -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). diff --git a/docs/tech/ci-cd-workflow.md b/docs/tech/ci-cd-workflow.md new file mode 100644 index 0000000..c5a7132 --- /dev/null +++ b/docs/tech/ci-cd-workflow.md @@ -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 ` 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. diff --git a/docs/user/cli-commands.md b/docs/user/cli-commands.md new file mode 100644 index 0000000..4e781db --- /dev/null +++ b/docs/user/cli-commands.md @@ -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. diff --git a/hooks/pre-commit b/hooks/pre-commit index 5b8661f..bc952ea 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -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 diff --git a/src/devx/ci/auto_merge.py b/src/devx/ci/auto_merge.py index 8104b0f..5cb549f 100644 --- a/src/devx/ci/auto_merge.py +++ b/src/devx/ci/auto_merge.py @@ -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]: diff --git a/src/devx/ci/notify_failure.py b/src/devx/ci/notify_failure.py index 4c524b6..746b0aa 100644 --- a/src/devx/ci/notify_failure.py +++ b/src/devx/ci/notify_failure.py @@ -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: diff --git a/src/devx/ci/post_merge.py b/src/devx/ci/post_merge.py index d5525c0..58d8b09 100644 --- a/src/devx/ci/post_merge.py +++ b/src/devx/ci/post_merge.py @@ -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: diff --git a/src/devx/ci/pr_review.py b/src/devx/ci/pr_review.py index 1caa652..15eae60 100644 --- a/src/devx/ci/pr_review.py +++ b/src/devx/ci/pr_review.py @@ -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") diff --git a/src/devx/ci/publish.py b/src/devx/ci/publish.py index d3eb195..0f8e875 100644 --- a/src/devx/ci/publish.py +++ b/src/devx/ci/publish.py @@ -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" diff --git a/src/devx/ci/release.py b/src/devx/ci/release.py index 2263dd3..83dbd97 100644 --- a/src/devx/ci/release.py +++ b/src/devx/ci/release.py @@ -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" diff --git a/src/devx/ci/sync_wiki.py b/src/devx/ci/sync_wiki.py index e697cb1..276cfe8 100644 --- a/src/devx/ci/sync_wiki.py +++ b/src/devx/ci/sync_wiki.py @@ -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" diff --git a/src/devx/tools/install_tools.py b/src/devx/tools/install_tools.py index 9175abc..66d0816 100644 --- a/src/devx/tools/install_tools.py +++ b/src/devx/tools/install_tools.py @@ -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" diff --git a/src/devx/tools/setup.py b/src/devx/tools/setup.py index 13f2c1c..5b68c84 100644 --- a/src/devx/tools/setup.py +++ b/src/devx/tools/setup.py @@ -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.") diff --git a/src/devx/translations.json b/src/devx/translations.json index 022f1df..bef52bf 100644 --- a/src/devx/translations.json +++ b/src/devx/translations.json @@ -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 Ожидается: : \n Получено: {subject}\n Допустимые типы: feat, fix, chore, docs, style, refactor,\n perf, test, ci, build, revert, BREAKING CHANGE", "zh": "哎呀!提交消息必须遵循 conventional commit 格式。\n 预期格式: : \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: : \n Got: {subject}": { + "en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: DEVX-N: : \n Got: {subject}", + "bg": "Опа! Commit-ът в клона master трябва да следва конвенционален формат след идентификатора.\n Очаква се: DEVX-N: : \n Получено: {subject}", + "de": "Ups! Master-Branch-Commit muss nach der Task-ID dem konventionellen Format folgen.\n Erwartet: DEVX-N: : \n Erhalten: {subject}", + "ru": "Ой! Коммит в ветку master после ID задачи должен соответствовать conventional формату.\n Ожидается: DEVX-N: : \n Получено: {subject}", + "zh": "哎呀!master 分支提交在任务 ID 后必须遵循 conventional commit 格式。\n 预期格式: DEVX-N: : \n 实际: {subject}" + }, "Oops! Master branch commit must follow conventional format after task ID.\n Expected: GRM-N: : \n Got: {subject}": { "en": "Oops! Master branch commit must follow conventional format after task ID.\n Expected: GRM-N: : \n Got: {subject}", "bg": "Опа! Commit-ът в клона master трябва да следва конвенционален формат след идентификатора.\n Очаква се: GRM-N: : \n Получено: {subject}", @@ -328,6 +384,13 @@ "ru": "Ой! Коммит в ветку master после ID задачи должен соответствовать conventional формату.\n Ожидается: GRM-N: : \n Получено: {subject}", "zh": "哎呀!master 分支提交在任务 ID 后必须遵循 conventional commit 格式。\n 预期格式: GRM-N: : \n 实际: {subject}" }, + "Oops! Master branch commits must start with a task ID.\n Expected: DEVX-N: \n Got: {subject}": { + "en": "Oops! Master branch commits must start with a task ID.\n Expected: DEVX-N: \n Got: {subject}", + "bg": "Опа! Commit-ите в клона master трябва да започват с идентификатор на задача.\n Очаква се: DEVX-N: \n Получено: {subject}", + "de": "Ups! Master-Branch-Commits müssen mit einer Task-ID beginnen.\n Erwartet: DEVX-N: \n Erhalten: {subject}", + "ru": "Ой! Коммиты в ветку master должны начинаться с ID задачи.\n Ожидается: DEVX-N: \n Получено: {subject}", + "zh": "哎呀!master 分支的提交必须以任务 ID 开头。\n 预期格式: DEVX-N: \n 实际: {subject}" + }, "Oops! Master branch commits must start with a task ID.\n Expected: GRM-N: \n Got: {subject}": { "en": "Oops! Master branch commits must start with a task ID.\n Expected: GRM-N: \n Got: {subject}", "bg": "Опа! Commit-ите в клона master трябва да започват с идентификатор на задача.\n Очаква се: GRM-N: \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: '.\n Expected: {task_id}: \n Got: {pr_title}": { + "en": "Oops! PR title must follow format 'DEVX-N: '.\n Expected: {task_id}: \n Got: {pr_title}", + "bg": "Опа! Заглавието на PR трябва да следва формата 'DEVX-N: <заглавие на задача>'.\n Очаква се: {task_id}: <заглавие на задача>\n Получено: {pr_title}", + "de": "Ups! PR-Titel muss dem Format 'DEVX-N: ' folgen.\n Erwartet: {task_id}: \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: '.\n Expected: {task_id}: \n Got: {pr_title}": { "en": "Oops! PR title must follow format 'GRM-N: '.\n Expected: {task_id}: \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": "未知" } } diff --git a/tests/unit/test_setup.py b/tests/unit/test_setup.py index d36401b..7265576 100644 --- a/tests/unit/test_setup.py +++ b/tests/unit/test_setup.py @@ -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()