# Architecture devx is a reusable Python package providing development and CI/CD tools for oblachno-oss projects. It is self-contained under `src/devx/` and never imports from scripts outside the package. ## Package structure ```text src/devx/ ├── __init__.py # Version (single source of truth, read by setuptools) ├── cli.py # Click-based CLI entry point (devx command) ├── config.py # Configuration system (DEVX_ env vars, .env loading) ├── api_clients.py # GiteaClient, VikunjaClient — HTTP API wrappers ├── gitea_cli.py # TeaCLI — wrapper around tea CLI with JSON parsing ├── i18n.py # Translation system (JSON-based, translations.json) ├── exceptions.py # Custom exception types (DevxError, APIError) ├── opentofu.py # OpenTofu output helpers ├── translations.json # Translation strings (en, bg, de, ru, zh, pl) ├── ci/ # CI/CD automation modules (run by workflows) │ ├── __init__.py │ ├── _shared.py # Shared utilities (get_latest_tag) │ ├── release.py # Automated versioning, tagging, changelog │ ├── publish.py # Build and publish to Gitea PyPI registry │ ├── auto_merge.py # Squash-merge PRs with task ID validation │ ├── classify_changes.py # User-facing vs workflow-only change detection │ ├── detect_release_commit.py # Detect release commits on master │ ├── validate_commit_msg.py # Conventional commit validation │ ├── pr_review.py # Automated PR review │ ├── post_merge.py # Vikunja task updates after merge │ ├── sync_wiki.py # Sync documentation to Gitea wiki │ ├── push_badges.py # Generate and push quality badges │ ├── notify_failure.py # Create Gitea issues on CI failures │ ├── distribute_files.py # Distribute files across parallel runners │ ├── integration_guard.py # Run pytest with cross-runner fail-fast │ ├── discover_runners.py # Dynamic Gitea runner discovery │ ├── check_translations.py # Translation completeness check │ └── doc_coverage.py # Documentation coverage check ├── tools/ # Developer tooling modules (run locally or by CI) │ ├── __init__.py │ ├── setup.py # Environment setup (venv, deps, hooks, tea login) │ ├── install_tools.py # Install actionlint, git-cliff, act_runner, tea │ ├── check_test_speed.py # Measure unit test execution time │ ├── check_test_isolation.py # Pytest plugin: detect un-hermetic test patterns │ ├── configure_repo.py # Branch protection and label setup │ ├── generate_badges.py # Badge SVG generation │ ├── generate_cliff_config.py # Generate cliff.toml with correct prefix │ └── install_checkmake.py # Install checkmake (Makefile linter) └── molecule/ # Optional molecule testing helpers (Ansible projects) ├── __init__.py ├── discover_runners.py # Dynamic Gitea runner discovery ├── distribute_molecule.py # Distribute scenarios across runners ├── molecule_all.py # Run all molecule scenarios locally ├── start_docker.py # Ensure Docker is available for molecule └── platforms.py # Supported molecule platforms ``` ## Core modules ### `__init__.py` Contains only `__version__`, the single source of truth for the package version. Read by setuptools via `dynamic = ["version"]` in `pyproject.toml`. Updated automatically by `devx.ci.release` during the release process. Treated as infrastructure (not user-facing) by the change classifier since it is a release artifact, not user code. ### `cli.py` Click-based CLI entry point. Provides three command groups: `devx ci`, `devx tools`, and `devx molecule`. Each subcommand delegates to the corresponding module via `_run_module()`, which imports the module, sets `sys.argv`, and calls its `main()` function. This design keeps all logic in the modules themselves — `cli.py` is purely a router. The CLI is registered as a console script via `pyproject.toml`: ```toml [project.scripts] devx = "devx.cli:cli" ``` ### `config.py` Shared configuration constants for all devx modules. All defaults can be overridden via environment variables with the `DEVX_` prefix. Provides: - `GITEA_API_URL` / `VIKUNJA_API_URL` — API endpoints - `REPO_OWNER` — repository owner (must be set per-project) - `TASK_PREFIX` / `TASK_ID_RE` — task ID prefix and regular expression (for example, `DEVX-N`) - `VIKUNJA_PROJECT_ID` — Vikunja project for task tracking - `DEFAULT_TIMEOUT`, `DEFAULT_PER_PAGE` — HTTP client defaults - `MAX_RETRIES`, `RETRY_BACKOFF_BASE`, `RETRY_STATUS_CODES` — retry config - `CONVENTIONAL_RE` — conventional commit format regular expression ### `exceptions.py` Custom exception hierarchy: - `DevxError` — base exception for all devx errors - `APIError(DevxError)` — raised when a REST API call returns an HTTP error. Carries `status` (HTTP status code) and `message` (error message). ### `i18n.py` Simple i18n system using a JSON translations file (`translations.json`). Supports six languages: `en`, `bg`, `de`, `pl`, `ru`, `zh`. The `_()` function wraps user-facing strings for translation. Projects can extend translations by setting `DEVX_TRANSLATIONS_PATH` to a custom JSON file. Keys from the project's file are merged on top of devx's built-in translations, allowing projects to override, or add keys without modifying the package. ### `api_clients.py` Reusable HTTP API clients with connection pooling and retry logic. Both clients retry on transient errors (429, 5xx, connection errors) with exponential backoff (2s, 4s, 8s). **`GiteaClient`** — Gitea REST API wrapper: - Branch protection (get, create, update) - Labels (list, create, add to issues) - Issues (create, list) - Pull requests (get commits, merge, create review) - Releases (list, create idempotent) - Actions (list runs, list jobs, get job logs) - Actions variables (get, set idempotent) - Wiki pages (list, fetch, create, update, delete) **`VikunjaClient`** — Vikunja REST API wrapper: - Tasks (list project tasks, get, update, mark done) - Comments (create) ### `gitea_cli.py` Thin Python wrapper around the `tea` Gitea CLI tool. Parses JSON output for structured data. Used by CI scripts for Gitea API operations that tea handles well, avoiding hand-rolled HTTP requests. **`TeaCLI`** operations: - `create_issue()` — Create issues with labels - `list_labels()` / `create_label()` / `add_label()` — Label management - `create_pr()` / `merge_pr()` / `review_pr()` — Pull request operations - `create_release()` / `list_releases()` — Release management - `list_branches()` — Branch listing Operations NOT supported via tea (still use `GiteaClient`): - Wiki page management - Commit status checks - Runner discovery - PR file/commit listing (tea has limited support) - Branch protection with detailed config ### `opentofu.py` OpenTofu output helpers for CI/CD deployment scripts. Provides reusable functions for extracting values from `tofu output` in a structured way, eliminating duplicated `subprocess.run` boilerplate: - `get_tofu_output(output_name, cwd, env)` — Run `tofu output -json` and return parsed JSON - `get_tofu_vm_ip(output_name, vm_name, cwd, env)` — Extract a VM's IP address - `get_tofu_vm_field(output_name, vm_name, field, cwd, env)` — Extract a VM field ## CI/CD modules (`devx.ci`) Modules in this package are run by Gitea Actions workflows. They may import from `devx.api_clients`, `devx.config`, `devx.gitea_cli`, and `devx.i18n`. ### `release.py` Automated release using git-cliff. Calculates the next semver version from conventional commits since the last tag, updates `__version__` in `__init__.py` and `CHANGELOG.md`, runs lint, and tests to verify the release is healthy, commits with `release: vX.Y.Z [skip ci]`, creates an annotated tag, and pushes both to master. Idempotent: if there are no new conventional commits since the last tag, it exits without doing anything. If the tag already exists, it skips tag creation and only pushes. Includes a `--verify` mode that checks tag/version/changelog alignment without making changes. ### `publish.py` Builds the Python package with `python -m build`, publishes to a Gitea PyPI registry (or standard PyPI if `PYPI_TOKEN` is set), and creates a Gitea release with git-cliff-generated notes. Supports `--skip-build` for non-Python repos that only need a Gitea release. ### `auto_merge.py` Auto-merges a PR when all CI checks pass. Reads the task ID from the branch name, validates the PR title format against the Vikunja task title, extracts the conventional commit message from PR commits, and squash-merges with title `{PREFIX}-N `. If the head branch is behind master (HTTP 405), it automatically pulls master, rebases, force-pushes, and retries the merge. ### `classify_changes.py` Classifies git changes between two refs as user-facing or workflow-only. Uses a layered rule system configured in `pyproject.toml` under `[tool.devx.classify]`: 1. **User-facing overrides** (highest priority — safety override) 2. **Infrastructure overrides** (explicit per-file) 3. **Infrastructure patterns** (DEFAULT_INFRASTRUCTURE + project-specific) 4. **Default**: user-facing (safe default — any unknown file triggers release) Also supports custom tags (orthogonal to release impact) for CI conditional execution (for example, `ansible` tag to trigger molecule tests). ### `pr_review.py` Automated PR review. Fetches the PR diff via the Gitea API and runs a series of checks, posting a structured review with `COMMENT` (no issues) or `REQUEST_CHANGES` (issues found): - Architecture compliance (no subprocess in CLI, no hardcoded URLs) - Best practices (no `print()`, no bare `except`, no `TODO`/`FIXME`, no functions > 50 lines) - Security (no hardcoded secrets, no `shell=True`, no `eval`/`exec`) - i18n (no raw strings in `click.echo()` without `_()` wrapper) - Resource management (no `open()` without `with`, no `Popen()` without cleanup) - Documentation (source changes must include doc updates) - Test coverage (source changes must include test updates) - Commit conventions (conventional commit format on PR commits) ### `sync_wiki.py` Syncs documentation from `docs/` to the Gitea wiki via the API. Reads `docs/mapping.json` to map file paths to wiki page titles, then creates or updates pages. Supports `--dry-run`, `--verify` (check content), and `--strict` (full integrity check: page count, missing pages, stale pages, content match). ### `push_badges.py` Generates SVG badge files using `devx.tools.generate_badges`, pushes them to an orphan `badges` branch, and updates `README.md` and `docs/index.md` on master with cache-busting `raw/commit//badge.svg` URLs (Gitea caches `raw/branch/` URLs for 6 hours). Fetches latest master before generating badges so the version badge reflects the current state. Supports `--retries` for retrying on git push failures. ### `notify_failure.py` Creates a Gitea issue when a CI workflow fails. Uses the `tea` CLI for issue creation with failure labels. Supports `--auto-login` to configure the tea CLI login profile from `CI_GITEA_TOKEN` and `DEVX_GITEA_API_URL` before creating the issue. ### `post_merge.py` Updates the Vikunja task after a merge to master. Extracts the task ID from the commit message, marks the task as done, and posts a comment with the merge SHA. ### `validate_commit_msg.py` Validates commit messages. On feature branches: conventional commits only (no `{PREFIX}-N` prefix). On master: must have `{PREFIX}-N` prefix from auto-merge, followed by a conventional commit message. ### `detect_release_commit.py` Detects whether the latest git commit is a release commit (`release: vX.Y.Z [skip ci]`). Writes `is-release=true` or `is-release=false` to `$GITHUB_OUTPUT` for use in CI workflow conditionals. ### `check_translations.py` Validates translation files against the Python source code. Checks for missing keys (used in code but not in translations), dead keys (defined but not used), and missing languages (a key exists but is missing one of the five supported languages). Supports checking additional translation sets via `--translations`. ### `doc_coverage.py` Checks documentation coverage for CLI commands and major modules. Parses Click commands from `cli.py` and verifies each has documentation in `docs/user/cli-commands.md`. Checks that core modules are documented in `architecture.md` and CI scripts in `ci-cd-workflow.md`. Supports `--fail-on-missing` to enforce 100% coverage. ### `discover_runners.py` Discovers available Gitea Actions runners at three levels: repository, organization, and instance (administrator). Falls back to the `MOLECULE_RUNNERS` repo variable or `DEFAULT_MAX_RUNNERS` (3). Outputs runner count or a JSON index array for use as a dynamic matrix in Gitea Actions. ### `distribute_files.py` Distributes files matching a glob pattern across N parallel runners (round-robin). Writes the assigned file list for the current runner to `$GITHUB_ENV`. Used for splitting test suites across CI runners. ### `integration_guard.py` Runs pytest with cross-runner failure detection. A background thread polls the Gitea API. If any other integration-tests matrix runner reports failure, the current pytest subprocess is killed and this runner exits early. ## Developer tools (`devx.tools`) Modules in this package are run locally or by CI setup jobs. They may import from `devx.api_clients`, `devx.config`, and `devx.gitea_cli`. ### `setup.py` Project setup: installs Python dependencies (editable mode with extras), Ansible Galaxy collections (if `ansible/requirements.yml` exists in the target repo), pre-commit hooks (pre-commit, commit-msg, pre-push), and configures the `tea` CLI login profile from `.env`. Supports `--extras` to specify dependency groups, `--no-pre-commit` to skip hook installation, and `--no-tea-login` to skip tea configuration. ### `install_tools.py` Installs CI/CD development tools that are not Python packages: actionlint, git-cliff, act_runner, and tea. Each tool is installed to `~/.local/bin` if not already on PATH. Idempotent: skips tools that are already available. Supports `--tool` to install specific tools and `--list` to show status. ### `check_test_speed.py` Runs unit tests and enforces execution-time budgets. Two quality gates: total suite time must not exceed `--max-seconds` (default: 10s), and no individual test may exceed `--max-single-seconds` (default: 0.5s, 0 to off). Runs `make test-unit` with `PYTEST_ADDOPTS=--durations=0`. ### `check_test_isolation.py` Pytest plugin (auto-discovered via `pytest11` entry point) that statically analyzes test files for un-hermetic patterns causing slow or flaky tests: unpatched `subprocess.run`/`time.sleep` calls, known subprocess-spawning helpers called without `@patch`, and excessive loop iterations (>100). Also available as a standalone CLI for CI gates and pre-commit hooks. See ADR-0001 for design rationale. ### `configure_repo.py` Configures repository branch protection and labels via the Gitea REST API. Sets up master branch protection (required status checks, block on rejected reviews, block on outdated branch) and creates standard labels. Status check contexts are read from `DEVX_STATUS_CHECKS` or default to `CI / validate (pull_request)`. ### `generate_badges.py` Generates self-contained SVG badge files from project metrics. Runs pytest-cov, doc-coverage, lint checks, and version extraction, then writes SVG files that can be served as static files from the Gitea raw file API. Badges generated: coverage, tests, docs, quality, version, python. ### `generate_cliff_config.py` Generates a `cliff.toml` configuration file with the correct task ID prefix preprocessor. Eliminates the need to manually duplicate and maintain `cliff.toml` across repos that use devx. Supports `--prefix` to set the task ID prefix and `--force` to overwrite an existing file. ### `install_checkmake.py` Installs checkmake (Makefile linter) if not already present. Tries `go install` first if Go is available, otherwise downloads the latest pre-built Linux binary from the official GitHub releases. ## Molecule modules (`devx.molecule`) Optional modules for projects with Ansible roles. Requires the `molecule` extra (`pip install devx[molecule]`). ### `distribute_molecule.py` Distributes molecule (scenario, platform) pairs across N parallel runners. Discovers scenarios under `ansible/roles/*/molecule/` and crosses them with the supported OS platform matrix. Supports `--roles-root` for multi-role repositories, `--list` to list scenarios, and `--list-platforms` to list platforms. ### `molecule_all.py` Runs all molecule scenarios on all supported OS platforms sequentially. Intended for local development; CI uses the parallel matrix instead. ### `molecule/discover_runners.py` Discovers available Gitea Actions runners for molecule tests. Same logic as `devx.ci.discover_runners` but intended for molecule-specific workflows. ### `start_docker.py` Ensures Docker is available for molecule tests in CI. Verifies Docker is accessible and sets `DOCKER_HOST` explicitly. If the host socket is not available, tries the rootless socket, then starts a local `dockerd` with the vfs storage driver (requires privileged container). ### `platforms.py` Single source of truth for the supported OS platform matrix. Each entry maps a short name to (image, command). Uses the project's pre-built molecule-test-base image with `sleep infinity` (not systemd) to avoid cgroup v2 failures. Supports loading custom platforms from a JSON file. ## Design principles - **Self-contained package** — `src/devx/` never imports from scripts outside the package. This allows devx to be installed and used as a dependency without requiring a specific repo layout in the consumer. - **Module-based invocation** — All tools invoked via `python -m devx.ci.*`, `python -m devx.tools.*`, or `python -m devx.molecule.*`. The `devx` CLI is a thin router that delegates to module `main()` functions. - **PYTHONPATH: src** — Workflows set `PYTHONPATH: src` (not `.:src` since there are no scripts at repo root). The `src` directory is the sole import root. - **Config via env vars** — `DEVX_*` environment variables with `.env` file fallback. Projects override defaults via environment or `.env`, never by editing package code. - **100% test coverage** — enforced by `--cov-fail-under=100` in pytest. - **i18n by default** — all user-facing strings wrapped in `_()` for translation. Five languages supported out of the box. - **Safe-by-default classification** — any file that doesn't match an infrastructure pattern defaults to user-facing, triggering a release. This prevents new file types from accidentally skipping releases. - **Secrets via environment** — secrets are passed via environment variables, never on the command line. ## Import rules 1. **`src/devx/` is self-contained** — the package never imports from outside `src/` 2. **CI modules** (`devx.ci.*`) may import from `devx.api_clients`, `devx.config`, `devx.gitea_cli`, `devx.i18n` 3. **Tool modules** (`devx.tools.*`) may import from `devx.api_clients`, `devx.config`, `devx.gitea_cli` 4. **Cross-module imports** within `devx.ci.*` or `devx.tools.*` are allowed but must be documented (for example, `release.py` imports from `classify_changes.py`) ## Data flow ### PR lifecycle ```text Developer creates Vikunja task (DEVX-N) │ ▼ Developer creates branch (DEVX-N-short-description) │ ▼ Developer commits (conventional commits, no DEVX-N prefix) │ ▼ Developer pushes and creates PR (title: "DEVX-N: ") │ ▼ CI workflow (ci.yml) triggers: │ ├── validate (single job: quality + detect-changes + │ release-dry-run + pr-review + pre-merge validation) │ ├── quality steps (lint, tests, coverage, test speed, doc coverage, │ │ translation check, dependency scan, workflow dry-run) │ ├── detect-changes (classify_changes.py → user-facing or workflow-only) │ │ └── if user-facing → release-dry-run (release.py --dry-run) │ ├── pre-merge validation (check_auto_merge_ready.py) │ └── pr-review (pr_review.py → posts COMMENT or REQUEST_CHANGES) │ └── auto-merge (auto_merge.py) ├── validate PR title format ├── validate PR title matches Vikunja task title ├── extract conventional commit message from PR commits ├── squash-merge with "DEVX-N " title └── push to master │ ▼ Post-merge workflow triggers (see below) ``` ### Post-merge flow ```text Push to master (squash-merge commit: "DEVX-N ") │ ▼ Post-merge workflow (post-merge.yml) triggers: │ ├── detect-and-configure (single job) │ ├── configure-repo (configure_repo.py) │ ├── detect-type (detect_release_commit.py) │ │ └── is-release? → skip all steps except badges │ └── validate-commit-msg (validate_commit_msg.py --branch master) │ └── release-and-maintain (needs detect-and-configure) ├── release (release.py) [skip if release commit or workflow-only] │ ├── classify_changes.py → skip if workflow-only │ ├── git-cliff → calculate next version │ ├── update __version__ in __init__.py │ ├── update CHANGELOG.md │ ├── run make lint-ruff && make pytest-cov │ ├── commit "release: vX.Y.Z [skip ci]" │ ├── create annotated tag vX.Y.Z │ └── push commit + tag to master │ │ │ ▼ │ publish (publish.py) [if release created a tag] │ ├── build package (python -m build) │ ├── publish to Gitea PyPI registry (twine upload) │ │ OR publish to standard PyPI (if PYPI_TOKEN set) │ │ OR skip publish (if --skip-build) │ └── create Gitea release with git-cliff notes │ ├── sync-wiki (sync_wiki.py --strict) [skip if automated] │ └── sync docs/ to Gitea wiki with integrity check │ ├── vikunja (post_merge.py) [skip if automated] │ ├── extract task ID from commit message │ ├── mark Vikunja task as done │ └── post comment with merge SHA │ └── badges (push_badges.py) [ALWAYS runs, even on release commits] ├── fetch latest master ├── generate_badges.py → SVG files ├── push to orphan badges branch └── update README.md + docs/index.md with cache-busting URLs ``` ### Publish flow ```text Within release-and-maintain job (after release step creates a tag): │ ├── install build, twine, git-cliff, tea ├── configure tea login ├── checkout release tag │ └── publish (publish.py) ├── build package (python -m build) ├── publish to Gitea PyPI registry (twine upload) │ OR publish to standard PyPI (if PYPI_TOKEN set) │ OR skip publish (if --skip-build) └── create Gitea release with git-cliff notes ``` ### Badge generation flow ```text push_badges.py: │ ├── fetch_latest_master() → git fetch + reset --hard origin/master │ ├── generate_badges() → devx.tools.generate_badges │ ├── run pytest-cov → parse coverage % │ ├── run pytest → parse test count │ ├── run doc_coverage → parse doc coverage % │ ├── run lint → quality status │ ├── read __version__ from __init__.py │ └── write SVG files to .badges/ │ ├── push_to_badges_branch() │ ├── git checkout --orphan badges │ ├── git rm -rf . │ ├── copy SVG files to root │ ├── git commit "Update badges [skip ci]" │ ├── git push origin badges --force │ └── return commit SHA │ └── update_readme_with_badge_sha() ├── git checkout master ├── replace raw/branch/badges/ URLs with raw/commit// URLs ├── git commit "chore: update badge URLs [skip ci]" └── git push origin master ``` ## tea CLI integration The `tea` Gitea CLI tool is used for Gitea API interactions where tea provides reliable, official support. It is installed by `python -m devx.tools.install_tools` and configured by `python -m devx.tools.setup` (login profile from `.env` `CI_GITEA_TOKEN`). `devx.gitea_cli.TeaCLI` wraps tea with JSON output parsing. Operations that tea does not support (wiki management, commit status, runner discovery, detailed branch protection) fall back to `GiteaClient` (direct HTTP). ## Version source The version source is `__version__` in `src/devx/__init__.py`, read by setuptools via `dynamic = ["version"]` in `pyproject.toml`. The release script updates this file, commits it, and tags the commit. This ensures the package version, git tag, and changelog always stay aligned.