# AGENTS.md — Project Conventions for GRM ## Build & Test Commands ```bash make setup # Create venv, install deps, set up hooks, install CI tools make install-tools # Install actionlint, git-cliff, act_runner to ~/.local/bin make lint-all # ruff + pyright + bandit + ansible-lint + checkmake + actionlint make pytest-cov # Unit tests with 100% coverage enforcement make test-unit # Unit tests without coverage make molecule # All 6 scenarios on Ubuntu 22.04 make molecule-all # All 6 scenarios on all 4 supported OSes make test-all # pytest-cov + molecule make workflow-lint # Static lint of .gitea/workflows/*.yml (actionlint) make workflow-dryrun # Dry-run all workflows in Docker (act_runner exec --dryrun) make workflow-check # workflow-lint + workflow-dryrun ``` `make setup` automatically installs all development tools: - **Python deps** via `devx.tools.setup` (pip install -e .[dev], ansible-galaxy, pre-commit hooks) - **devx package** via `make install-devx` (installs the devx package from git, providing all CI/CD tools) - **checkmake** via `devx.tools.install_checkmake` (Makefile linter) - **actionlint, git-cliff, act_runner, tea** via `devx.tools.install_tools` (CI/CD tools to ~/.local/bin) - **tea CLI login** via `devx.tools.setup` (configures `tea login` from `.env` `REPO_TOKEN`) ## Workflow Verification (Before Push) Workflow YAML files (`.gitea/workflows/*.yml`) are verified with two tools: 1. **actionlint** — Static linter that catches syntax errors, invalid expressions, unknown keys, type mismatches, and shellcheck issues. Config: `.gitea/actionlint.yaml` (registers custom `docker` runner label). Installed automatically by `make setup` via `devx.tools.install_tools`. 2. **act_runner exec --dryrun** — Gitea's own runner in dry-run mode. Validates job dependencies, step ordering, and Docker image selection without starting containers. Installed automatically by `make setup`. Both run via `make workflow-check` and are part of `make lint-all`. The pre-commit hook runs actionlint automatically when workflow files change. The CI `quality` job runs `make setup` (which installs all tools) then `make lint-all`. CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is not installed in the CI Docker image). ## Architecture - **Python CLI** (`src/gitea_runner_manager/`) — Click-based CLI that delegates to Ansible - **Ansible Role** (`ansible/roles/gitea-runner/`) — Idempotent role for rootless Docker runner setup - **devx package** (installed from git) — Reusable CI/CD tools: auto-merge, post-merge, release, publishing, molecule distribution, PR reviews, failure notifications - **Versioning** (`cliff.toml`) — git-cliff configuration for automated semver versioning from conventional commits ## PR Workflow (Mandatory) Every change to master goes through this workflow. No exceptions. ### Branch Protection (Required Gitea Settings) Branch protection and labels are automatically configured by `devx.tools.configure_repo` (run as `python -m devx.tools.configure_repo`), which runs as a `configure-repo` job in the post-merge workflow on every push to master. The following rules are enforced for `master`: - **Require pull request**: No direct pushes to master - **Require approval review**: At least 1 `APPROVE` review before merge - **Require status checks**: CI quality + molecule tests must pass - **Block force pushes**: No history rewriting on master The auto-merge workflow enforces the APPROVE review check programmatically as a defense-in-depth measure, but branch protection is the primary gate. ### 1. Create Vikunja Task Create a task in Vikunja project 6 to get a `GRM-N` identifier. ### 2. Create Branch ```bash git checkout master && git pull git checkout -b GRM-N-short-description ``` ### 3. Implement Changes - Write code following conventions below - Write/update tests (100% coverage required) - Update documentation (CHANGELOG, README, AGENTS.md as needed) ### 4. Commit (Conventional Commits) Branch commits use conventional commit format (no `GRM-N:` prefix): ``` feat: add new feature fix: resolve bug docs: update README ``` ### 5. Push and Create PR - **PR title format**: `GRM-N: ` (must match the Vikunja task title exactly) - PR body: summary of changes, `Closes GRM-N` - Add `ready-to-merge` label **only after review is complete** ### 6. Review the PR (Mandatory — Before Adding ready-to-merge Label) **Review checklist:** Every PR is reviewed against [REVIEW_CHECKLIST.md](REVIEW_CHECKLIST.md) — 13 categories covering architecture, code quality, security, i18n, testing, performance, UX, documentation, workflow compliance, maintainability, resource management, backwards compatibility, and logging. **Automated review (CI `pr-review` job):** Every PR triggers an automated review via `devx.ci.pr_review` (run as `python -m devx.ci.pr_review`). This job posts a review with `COMMENT` (no issues) or `REQUEST_CHANGES` (issues found) based on the **[auto]** items in the checklist: - 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) The automated review posts inline comments on specific lines and includes a link to the full checklist. The agent **must** address all `REQUEST_CHANGES` issues before proceeding. **Manual review (agent):** After the automated review passes, the agent must go through **every category** in `REVIEW_CHECKLIST.md` and verify the **[manual]** items by reviewing the full diff (`git diff master...HEAD`). Post review comments using `devx.ci.review_pr` (run as `python -m devx.ci.review_pr`): ```bash REPO_TOKEN= python -m devx.ci.review_pr \ --event REQUEST_CHANGES \ --body "Review summary" \ --comments-json comments.json ``` ### 7. Address Review Comments Fix each comment one by one, commit, and push. Re-review until satisfied. ### 8. Approve and Merge Once all checklist items are verified and comments are addressed, post an approval review with `--checklist-confirmed` and `--checklist-categories`: ```bash REPO_TOKEN= python -m devx.ci.review_pr \ --event APPROVE --checklist-confirmed \ --checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \ --body "All 13 REVIEW_CHECKLIST.md categories verified. Architecture: . Security: . Tests: . Docs: ." ``` The `--checklist-confirmed` flag is **required** for APPROVE events — it attests that the reviewer has gone through every checklist category. The `--checklist-categories` flag is also **required** — it must list at least 8 of the 13 category numbers, ensuring the reviewer actually checked each category rather than rubber-stamping. The review body must be substantive (> 50 characters) — trivial approvals like "LGTM" are rejected. Then add the `ready-to-merge` label. The auto-merge workflow will: 1. **Validate** PR title format (`GRM-N: `) and match against Vikunja task title 2. **Check** that at least one substantive APPROVE review exists (body > 20 chars or has inline comments) 3. Wait for all CI checks to pass (including the `pr-review` job) 4. Squash-merge with title: `GRM-N ` (space-separated, no colon after GRM-N) 5. The post-merge workflow marks the Vikunja task as done 6. The release workflow automatically versions, tags, and publishes (see below) > **IMPORTANT**: Never manually merge PRs via the API. Always use the auto-merge > workflow by adding the `ready-to-merge` label. Manual merges bypass the > `GRM-N ` format enforcement, producing incorrectly named commits. > The auto-merge script validates the PR title matches the Vikunja task ID > and conventional commit format before merging. ### CI Path Filtering The CI workflow includes a `detect-changes` job that checks whether any files under `ansible/` or `.ansible-lint` have changed. If no Ansible files are changed, molecule tests are skipped — this prevents non-Ansible changes (e.g., Python scripts, workflow YAML, docs) from being blocked by molecule test infrastructure flakiness. ### Dynamic Runner Discovery Molecule tests are distributed across available Gitea Actions runners dynamically via `devx.molecule.discover_runners`. The `discover-runners` job queries the Gitea API for runners at all levels (repo, org, instance) and generates a dynamic matrix. If the API can't see instance-level runners (no admin scope), it falls back to the `MOLECULE_RUNNERS` repo variable, then to a default of 3. **When adding/removing Gitea runners:** 1. Repo/org-level runners are auto-detected via the API 2. For instance-level runners, update the `MOLECULE_RUNNERS` repo variable 3. The workflow automatically scales the matrix to match available runners ### Automated Release Pipeline After a PR is merged to master, the **post-merge workflow** (`.gitea/workflows/post-merge.yml`) runs automatically. This single workflow consolidates release, wiki sync, badge generation, and Vikunja task updates: 1. **detect-type** — Checks if the commit is a regular merge or a release commit (`release: vX.Y.Z`). All subsequent jobs skip for release commits (the `[skip ci]` tag also prevents re-triggering). 2. **release** — Runs `devx.ci.release` which: - **Checks for user-facing changes** via `devx.ci.classify_changes` — if only workflow/infrastructure files changed (`.gitea/`, `docs/`, `tests/`, `AGENTS.md`, `Makefile`, etc.), the release is **skipped entirely** — no version bump, no tag, no publish. This prevents unnecessary releases for CI/docs-only changes. - Uses **git-cliff** to calculate the next semver version from conventional commits - Updates `__version__` in `src/gitea_runner_manager/__init__.py` (single source of truth) - Updates `CHANGELOG.md` with the new version section - **Runs `make lint-ruff` and `make pytest-cov`** to verify the release is healthy - If lint or tests fail, **aborts immediately** — no commit, no tag - Commits with `release: vX.Y.Z [skip ci]` prefix (the `[skip ci]` prevents re-triggering post-merge on the release commit) - Creates an annotated tag `vX.Y.Z` on the release commit - Pushes both the commit and tag to master - `--skip-tests` flag bypasses test verification (emergency use only, not recommended) - Loops are prevented by `has_unreleased_changes` — after a release commit is tagged, the next run finds no unreleased changes and exits 3. **sync-wiki** — Syncs documentation to the Gitea wiki. 4. **badges** — Generates and pushes quality badge SVGs to the `badges` branch. Runs **after** the release job (even if release fails or is skipped) so the version badge always reflects the latest state. The script fetches the latest master before generating badges to pick up any release commits. 5. **vikunja** — Marks the corresponding Vikunja task as done. The tag push triggers the **publish workflow** (`.gitea/workflows/publish.yml`) which builds and publishes the package to PyPI. ### Smart CI: User-Facing vs Workflow-Only Changes Not all changes require the full CI pipeline or a new release. The project classifies changes into two categories using `devx.ci.classify_changes`: **Classification strategy (safe-by-default):** Any file NOT in the explicit workflow-only allowlist is treated as user-facing. This prevents new file types from accidentally skipping releases. Classification is config-driven via `[tool.devx.classify]` in `pyproject.toml`. **Workflow-only paths** (infrastructure → no release needed): - `.gitea/**` — Gitea Actions workflows - `scripts/**` — Dev tools and CI/CD automation (not part of installed package) - `docs/**` — Documentation - `tests/**` — Test files - `AGENTS.md`, `README.md`, `CHANGELOG.md`, `TROUBLESHOOTING.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `REVIEW_CHECKLIST.md` — Project docs - `Makefile`, `cliff.toml`, `uv.lock` — Build tooling - `.pre-commit-config.yaml`, `.ruff.toml`, `.ansible-lint`, `.checkmake.ini`, `.editorconfig` — Lint config - `.env.example`, `.gitignore`, `.gitattributes` — Config - `.devin/**` — Agent/CI tooling config - `hooks/**` — Git hooks - `activate.sh`, `activate.fish`, `activate.zsh` — Generated venv scripts - `.taskid` — CI task tracking file **User-facing paths** (tool changes → release needed) — everything else: - `src/gitea_runner_manager/**` — Python CLI source (except `__init__.py` and `api_clients.py`) - `ansible/**` — Ansible role - `pyproject.toml` — Package metadata - Any new file type not in the allowlist **devx module structure** (installed from git, not in this repo): - `devx.ci.*` — CI/CD automation (run by workflows): release, publish, auto_merge, classify_changes, detect_release_commit, push_badges, doc_coverage, sync_wiki, distribute_molecule, molecule_ci_guard, discover_runners, notify_failure, post_merge, pr_review, review_pr, validate_commit_msg - `devx.tools.*` — Dev tools (run locally): check_test_speed, configure_repo, install_checkmake, install_tools, setup, generate_badges - `devx.molecule.*` — Molecule helpers: molecule_all, platforms, discover_runners, distribute_molecule, molecule_ci_guard - `devx.gitea_cli` — Tea CLI wrapper - `devx.i18n` — i18n translation system - `devx.config` — Shared configuration (DEVX_* env vars) - `devx.api_clients` — GiteaClient, VikunjaClient - `devx.exceptions` — APIError and other exceptions **CI behavior based on classification:** - **Molecule tests**: Only run when `ansible/` or `.ansible-lint` files change - **Release dry-run**: Only runs when user-facing files change - **Quality job** (lint, unit tests, coverage, doc-coverage): Always runs - **Release workflow**: Skips entirely when no user-facing files changed since last tag **AI agents must follow these rules:** - When working on workflow/CI/docs-only changes, use `ci:` or `docs:` commit prefixes - Do NOT bump the version or create tags for workflow-only changes - The `classify_changes` module enforces this automatically — no manual intervention needed ## Source Code Separation and devx Integration The codebase enforces strict separation between the GRM tool and the devx package: ### Directory Layout | Directory | Purpose | Release impact | |-----------|---------|----------------| | `src/gitea_runner_manager/` | User-facing GRM CLI tool | Changes trigger release | | `devx` package (installed from git) | Reusable CI/CD and dev tools | Not in this repo (no release impact) | | `ansible/` | Ansible role for runner setup | Changes trigger release | ### Import Rules 1. **`src/gitea_runner_manager/` NEVER imports from devx** — the GRM tool is self-contained 2. **devx MAY import from `gitea_runner_manager`** — one-way dependency (devx uses the tool's API clients, config, i18n) 3. **Cross-module imports within devx** are allowed (devx modules importing from other devx modules) and must be documented 4. **`devx.gitea_cli`** is a shared wrapper around the `tea` CLI — devx modules import from it for Gitea API operations (issues, labels, PRs, releases, reviews) ### tea CLI Integration The `tea` Gitea CLI tool is used for Gitea API interactions in devx. It is installed by `devx.tools.install_tools` and configured by `devx.tools.setup` (login profile from `.env` `REPO_TOKEN`). **`devx.gitea_cli`** — Python wrapper around `tea` CLI with JSON output parsing: - `TeaCLI.create_issue()` — Create issues with labels - `TeaCLI.list_labels()` / `TeaCLI.create_label()` / `TeaCLI.add_label()` — Label management - `TeaCLI.create_pr()` / `TeaCLI.merge_pr()` / `TeaCLI.review_pr()` — Pull request operations - `TeaCLI.create_release()` / `TeaCLI.list_releases()` — Release management - `TeaCLI.list_branches()` — Branch listing **Modules using tea (via `devx.gitea_cli`):** - `devx.ci.publish` — Creates Gitea releases via `tea releases create` - `devx.ci.notify_failure` — Creates issues via `tea issues create` (falls back to `GiteaClient` if tea not installed) - `devx.tools.configure_repo` — Creates labels via `tea labels create` (falls back to `GiteaClient` if tea fails; branch protection still uses `GiteaClient` since tea only supports basic protect/unprotect) **Operations still using `GiteaClient` (not supported by tea):** - PR reviews (`devx.ci.review_pr`) — tea v0.14.1 only supports interactive reviews - Wiki page management (`devx.ci.sync_wiki`) - Commit status checks (`devx.ci.auto_merge`) - Runner discovery (`devx.molecule.discover_runners`) - Branch protection with detailed config (`devx.tools.configure_repo`) - PR file/commit listing (`devx.ci.pr_review`) ### PYTHONPATH Configuration Since devx is installed as a package (via `pip install` from git), it is importable directly. Workflows only need `PYTHONPATH=src` when a devx module imports from `gitea_runner_manager`: | PYTHONPATH | When to use | Example modules | |------------|-------------|-----------------| | `src` | Module imports from `gitea_runner_manager` | `devx.ci.auto_merge`, `devx.ci.pr_review`, `devx.ci.review_pr`, `devx.ci.sync_wiki`, `devx.ci.post_merge`, `devx.ci.classify_changes`, `devx.molecule.discover_runners`, `devx.ci.doc_coverage` | | (none) | Module has no GRM imports | `devx.ci.detect_release_commit`, `devx.molecule.distribute_molecule`, `devx.molecule.molecule_ci_guard`, `devx.ci.push_badges`, `devx.ci.validate_commit_msg` | **In workflows**, always use `env:` blocks (not inline `PYTHONPATH=value`): ```yaml - name: Run module env: PYTHONPATH: src run: python -m devx.ci.example ``` **Locally**, devx is installed as a package, so only `PYTHONPATH=src` is needed if importing from `gitea_runner_manager`. ### Shared Constants `devx.molecule.platforms` is the single source of truth for the molecule platform matrix. Both `devx.molecule.distribute_molecule` (CI) and `devx.molecule.molecule_all` (dev tool) import `PLATFORMS` from it — this avoids dev tools importing directly from CI modules. 2. **Publish workflow** (`.gitea/workflows/publish.yml`): - Triggers on tag push (`v*`) - Validates `PYPI_TOKEN` is set (warns if missing) - Builds the Python package - Optionally publishes to PyPI (if `PYPI_TOKEN` is set) - Creates a Gitea release with git-cliff-generated release notes - On failure, creates a Gitea issue via `devx.ci.notify_failure` ### git-cliff Commit Preprocessing Merge commits on master have the format `GRM-N `. The `GRM-N ` prefix is not a valid conventional commit prefix, so `cliff.toml` includes a `commit_preprocessors` entry that strips it before parsing. This ensures all merged work appears in the changelog. ### Version Bumping Rules (git-cliff) | Commit type | Version bump | |-------------|-------------| | `feat:` | minor (0.X.0) | | `fix:` | patch (0.0.X) | | `feat!:` or `BREAKING CHANGE` | minor (pre-1.0: major would be 1.0.0) | | `chore:`, `ci:`, `docs:` | no bump (excluded by cliff.toml) | The version source is `__version__` in `src/gitea_runner_manager/__init__.py`, read by setuptools via `dynamic = ["version"]` in `pyproject.toml`. The release script only updates `__init__.py` — no need to touch `pyproject.toml`. `grm --version` reports this version. ### Title Format Summary | What | Format | Example | |------|--------|---------| | Branch name | `GRM-N-short-description` | `GRM-33-add-pr-review-step` | | Branch commits | `` | `feat: add review script` | | PR title | `GRM-N: ` | `GRM-33: Add mandatory PR review step` | | Merge commit | `GRM-N ` | `GRM-33 feat: add review script` | ### Configuration The devx package is configured via `DEVX_*` environment variables: - `DEVX_TASK_PREFIX=GRM` — Prefix for Vikunja task identifiers - `DEVX_VIKUNJA_PROJECT_ID=6` — Vikunja project ID for task tracking - `DEVX_VERSION_FILE=src/gitea_runner_manager/__init__.py` — Path to the version source file Change classification is config-driven via `[tool.devx.classify]` in `pyproject.toml`, which defines the workflow-only and user-facing path patterns. ## Key Conventions - Python 3.12+ required (ruff/pyright target `py312`) - 100% test coverage required (`--cov-fail-under=100`) - Conventional commits on feature branches (no `GRM-N:` prefix) - Branch names must include `GRM-N` task ID - Line length: 120 chars - Secrets are passed via temp JSON files, never on the command line (CWE-214) - CI triggers only on `opened` and `synchronize` PR events (not `labeled`) ## Ansible Role Structure ``` main.yml → systemd_check → user_setup → rootless_docker → install_runner → prune → integration_test ``` - `install_runner.yml` handles: download, config, validate, register, service - `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates) - `systemctl --user` tasks must be guarded by `docker_rootless_setup` - Template creation tasks are NOT guarded by `docker_rootless_setup` (they just create files) ## Molecule Scenarios 6 scenarios: `default`, `multi-instance`, `lifecycle`, `template-content`, `deregister`, `update` 4 platforms: `ubuntu-2204`, `ubuntu-2404`, `debian-12`, `archlinux` Platform list is defined in `devx.molecule.platforms` (single source of truth) ## Known Issues - `ansible-lint` may warn about `command-instead-of-module` for `systemctl --user` calls — this is expected (systemd module doesn't support user services) and skipped in `.ansible-lint` - Molecule Docker driver may print "Event loop is closed" warnings on interrupt — harmless ## Documentation-as-Code All documentation lives in `/docs/` and is synced to the Gitea wiki automatically. ### Structure ``` docs/ ├── index.md # Wiki homepage ├── mapping.json # File-to-wiki-page title mapping ├── user/ # User documentation │ ├── getting-started.md │ ├── installation.md │ ├── cli-commands.md │ ├── troubleshooting.md │ └── faq.md └── tech/ # Technical documentation ├── architecture.md ├── development-setup.md ├── ci-cd-workflow.md ├── testing-strategy.md ├── decision-log.md └── contributing.md ``` ### Wiki Sync - **On merge to master**: `sync-wiki.yml` workflow runs `devx.ci.sync_wiki` which pushes all `/docs/` content to the Gitea wiki via API - **On release tag**: Same sync runs, plus the wiki is tagged with the release version - `mapping.json` maps each file path to a wiki page title (e.g., `user/getting-started.md` → `Getting-Started`) - README.md is a lean entry point with links to the wiki — no detailed content ### Documentation Coverage - `devx.ci.doc_coverage` checks that all CLI commands, Python modules, and CI scripts are documented - Runs as a CI step in the quality job with `--fail-on-missing` (blocks CI if docs are missing) - Enforced: 100% coverage for public CLI commands and major architectural components ### Updating Documentation 1. Edit files in `/docs/` 2. If adding a new page, add it to `docs/mapping.json` 3. Commit and create a PR (standard PR workflow) 4. On merge, wiki is automatically synced