# 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 `pip install -e .[dev]` (includes devx from Gitea PyPI registry, configured by `make configure-gitea-pypi`) - **Post-install setup** via `devx.tools.setup --skip-install` (ansible-galaxy, pre-commit hooks, tea CLI login) - **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) ## 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 via `make create-task -- --title "Task title" --description "

...

"` (requires `VIKUNJA_TOKEN` in `.env`). This prints the `GRM-N` identifier and next-step instructions. ### 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 - Push: `git push -u origin HEAD` (pre-push hook validates Vikunja task existence via `devx.tools.pre_push_check`) - Create PR: `make create-pr` (creates a PR with title `GRM-N: `, auto-derived from the branch name and Vikunja task) - Or both in one step: `make push-with-pr` - 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.pr_review` (run as `python -m devx.ci.pr_review`): ```bash CI_GITEA_TOKEN= python -m devx.ci.pr_review \ --event REQUEST_CHANGES \ --body "Review summary" ``` ### 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 CI_GITEA_TOKEN= python -m devx.ci.pr_review \ --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: ` 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. Runs for ALL non-release commits (not just when release succeeds), so docs-only changes still update the wiki. 4. **badges** — Generates and pushes quality badge SVGs to the `badges` branch. Uses `if: always()` so it runs on every push, including release commits. The script fetches the latest master before generating badges to pick up any release commits. 5. **vikunja** — Marks the corresponding Vikunja task as done. Runs for ALL non-release commits (not just when release succeeds), so infrastructure-only changes still update the task tracker. 6. **publish** — Runs after release succeeds (needs: release). Builds and publishes the package to the Gitea PyPI registry. Gets the tag from the release job's `tag` output. ### 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 **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, 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` `CI_GITEA_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.pr_review`) — 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.pr_review`, `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 job** (in `post-merge.yml`, needs: release): - Runs after the release job creates a tag - Gets the tag from `needs.release.outputs.tag` - Builds the Python package - Publishes to the Gitea PyPI registry - 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