# AGENTS.md — Project Conventions for GRM ## Build & Test Commands ```bash make setup # Create venv, install deps, set up hooks make lint-all # ruff + pyright + bandit + ansible-lint + checkmake 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 ``` ## 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 - **CI Scripts** (`scripts/`) — Automation for 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) Configure the following branch protection rules for `master` in Gitea repo settings: - **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 the full diff (`git diff master...HEAD`) focusing on: - **Functional completeness**: Does the code do what it claims? Are all requirements met? - **Edge cases**: Are boundary conditions, empty inputs, error paths handled? - **Technical excellence**: - Architecture compliance and evolution - Single Responsibility Principle (SRP) - Deduplication (no copy-paste, single source of truth) - Code smells detection and removal - Best industry practices - Industry-grade code quality - Reusability - Clean code - Readability - Maintainability - Extensibility - **Performance**: No unnecessary allocations, O(n) vs O(n²), efficient data structures - **Security**: No secrets in logs/process list, input validation, no injection vectors - **User experience**: Clear error messages, intuitive CLI flags, helpful output - **Documentation**: Completeness and relevance of docs, CHANGELOG entries, AGENTS.md updates Post review comments using `scripts/ci/review_pr.py`: ```bash REPO_TOKEN= python3 scripts/ci/review_pr.py \ --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 comments are addressed: ```bash REPO_TOKEN= python3 scripts/ci/review_pr.py \ --event APPROVE \ --body "All comments addressed. LGTM." ``` Then add the `ready-to-merge` label. The auto-merge workflow will: 1. **Validate** PR title format and match against Vikunja task title 2. **Check** that at least one APPROVE review exists 3. Wait for all CI checks to pass 4. Squash-merge with title: `GRM-N ` (space-separated) 5. The post-merge workflow marks the Vikunja task as done 6. The release workflow automatically versions, tags, and publishes (see below) ### 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 `scripts/ci/discover_runners.py`. 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 release pipeline runs automatically: 1. **Release workflow** (`.gitea/workflows/release.yml`): - Triggers on push to master - Sets up full dev environment (`make setup`) so lint and tests can run - Runs `scripts/ci/release.py` which: - **Checks for user-facing changes** via `scripts/ci/classify_changes.py` — if only workflow/infrastructure files changed (`.gitea/`, `scripts/`, `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` prefix (cleaner than `chore(release):`) - 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 - On failure, creates a Gitea issue via `scripts/ci/notify_failure.py` ### 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 `scripts/ci/classify_changes.py`: **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. **Workflow-only paths** (infrastructure → no release needed): - `.gitea/**` — Gitea Actions workflows - `scripts/ci/**` — CI/CD automation scripts - `scripts/setup.sh`, `scripts/molecule_all.sh`, `scripts/__init__.py` — Shell scripts and package init - `docs/**` — Documentation - `tests/**` — Test files - `AGENTS.md`, `README.md`, `CHANGELOG.md`, `TROUBLESHOOTING.md` — Project docs - `Makefile`, `cliff.toml`, `.pre-commit-config.yaml`, `.ansible-lint` — Config - `.env.example`, `.gitignore`, `.ruff.toml` — Config - `hooks/**` — Git hooks **User-facing paths** (tool changes → release needed) — everything else: - `src/gitea_runner_manager/**` — Python CLI source - `ansible/**` — Ansible role - `pyproject.toml` — Package metadata - `scripts/check_test_speed.py`, `scripts/configure_repo.py`, `scripts/install_checkmake.py` — Dev tools - Any new file type not in the allowlist **Script directory structure:** - `scripts/` — Dev tools (run locally by developers): `check_test_speed.py`, `configure_repo.py`, `install_checkmake.py`, `setup.sh`, `molecule_all.sh` - `scripts/ci/` — CI/CD automation (run by workflows): `release.py`, `publish.py`, `auto_merge.py`, `classify_changes.py`, `doc_coverage.py`, `sync_wiki.py`, etc. **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.py` script enforces this automatically — no manual intervention needed - When adding a new CI script, place it in `scripts/ci/`. Dev tools go in `scripts/`. 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 `scripts/ci/notify_failure.py` ### 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` | ## 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 `scripts/ci/distribute_molecule.py` (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 `scripts/ci/sync_wiki.py` 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 - `scripts/ci/doc_coverage.py` checks that all CLI commands, Python modules, and CI scripts are documented - Runs as a CI step in the quality job - Goal: 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