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

This commit was merged in pull request #1.
This commit is contained in:
2026-06-22 15:52:45 +00:00
parent 60fd11419c
commit 31bfd23fea
24 changed files with 591 additions and 58 deletions
+8 -7
View File
@@ -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" \
+15 -14
View File
@@ -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" \
+3 -3
View File
@@ -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" \
+2 -2
View File
@@ -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
+19 -9
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.venv/bin/activate"
Executable
+4
View File
@@ -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
View File
@@ -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"
}
+50
View File
@@ -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).
+85
View File
@@ -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.
+105
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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]:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+3 -1
View File
@@ -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
View File
@@ -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.")
+140
View File
@@ -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": "未知"
}
}
+79 -7
View File
@@ -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()