Files
devx/docs/tech/architecture.md
T
emil b4350751f1
Post-merge / detect-type (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 7s
Post-merge / validate-commit-msg (push) Successful in 14s
Post-merge / release (push) Failing after 58s
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / badges (push) Successful in 45s
DEVX-47: feat: add Polish as officially supported language
2026-06-24 23:04:20 +00:00

593 lines
25 KiB
Markdown

# 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
```
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
│ ├── merge_junit.py # Merge JUnit XML reports from parallel runners
│ ├── distribute_files.py # Distribute files across parallel runners
│ ├── integration_guard.py # Run pytest with cross-runner fail-fast + JUnit
│ ├── 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
│ ├── 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_ci_guard.py # Run molecule with cross-runner fail-fast
├── 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 regex (e.g., `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 regex
### `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 five languages: `en`, `bg`, `de`, `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)
- 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 <conventional commit>`.
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 (e.g., `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/<sha>/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 `REPO_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 (admin). 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.
### `merge_junit.py`
Merges JUnit XML reports from parallel matrix runners into a single
consolidated report. Exit code is non-zero if any merged suite reports
failures, making it suitable as a CI gating step.
### `integration_guard.py`
Runs pytest with the same cross-runner failure detection mechanism used by
`molecule_ci_guard`. If any other integration-tests matrix runner reports
failure, the current pytest subprocess is killed and this runner exits early.
Generates JUnit XML via pytest's `--junitxml` flag.
## 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), 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
disable). Runs `make test-unit` with `PYTEST_ADDOPTS=--durations=0`.
### `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 / quality (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_ci_guard.py`
Runs molecule tests sequentially while polling the Gitea API for other runner
failures. If any other molecule matrix runner reports failure, the current
molecule subprocess is killed and this runner exits early. Generates JUnit
XML when `--junit-output` is provided. Supports both single-role (4-part) and
multi-role (5-part) pair encoding.
### `molecule_all.py`
Runs all molecule scenarios on all supported OS platforms sequentially.
Intended for local development; CI uses the parallel matrix instead.
### `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 (e.g., `release.py` imports from
`classify_changes.py`)
## Data flow
### PR lifecycle
```
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: <vikunja task title>")
CI workflow (ci.yml) triggers:
├── quality (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)
├── 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 <conventional commit>" title
└── push to master
Post-merge workflow triggers (see below)
```
### Post-merge flow
```
Push to master (squash-merge commit: "DEVX-N <conventional commit>")
Post-merge workflow (post-merge.yml) triggers:
├── detect-type (detect_release_commit.py)
│ └── is-release? → skip all jobs except badges
├── validate-commit-msg (validate_commit_msg.py --branch master)
├── release (release.py)
│ ├── 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
│ │
│ ▼
│ Tag push triggers publish workflow (see below)
├── sync-wiki (sync_wiki.py --strict)
│ └── sync docs/ to Gitea wiki with integrity check
├── 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
├── vikunja (post_merge.py)
│ ├── extract task ID from commit message
│ ├── mark Vikunja task as done
│ └── post comment with merge SHA
└── configure-repo (configure_repo.py)
└── ensure branch protection and labels
```
### Publish flow
```
Tag push (vX.Y.Z) triggers publish workflow (publish.yml):
├── install build, twine, git-cliff, tea
├── configure tea login
└── 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
```
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/<sha>/ 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` `REPO_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.