Add /docs/ directory with user and technical documentation extracted from README, AGENTS.md, and source code. Add scripts/sync_wiki.py to sync docs to Gitea wiki via API. Add scripts/doc_coverage.py to check CLI commands, modules, and CI scripts are documented. Add sync-wiki.yml workflow for auto-sync on merge and release. Slim down README.md to lean entry point. 28 new unit tests, 100% coverage maintained. Closes GRM-36
10 KiB
AGENTS.md — Project Conventions for GRM
Build & Test Commands
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
APPROVEreview 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
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: <vikunja task title>(must match the Vikunja task title exactly) - PR body: summary of changes,
Closes GRM-N - Add
ready-to-mergelabel 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/review_pr.py:
REPO_TOKEN=<token> python3 scripts/review_pr.py <pr_number> <owner/repo> \
--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:
REPO_TOKEN=<token> python3 scripts/review_pr.py <pr_number> <owner/repo> \
--event APPROVE \
--body "All comments addressed. LGTM."
Then add the ready-to-merge label. The auto-merge workflow will:
- Validate PR title format and match against Vikunja task title
- Check that at least one APPROVE review exists
- Wait for all CI checks to pass
- Squash-merge with title:
GRM-N <conventional commit message>(space-separated) - The post-merge workflow marks the Vikunja task as done
- 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.
Automated Release Pipeline
After a PR is merged to master, the release pipeline runs automatically:
-
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/release.pywhich uses git-cliff to:- Calculate the next semver version from conventional commits since the last tag
- Update
__version__insrc/gitea_runner_manager/__init__.py(single source of truth) - Update
CHANGELOG.mdwith the new version section - Run
make lint-ruffandmake pytest-covto verify the release is healthy - If lint or tests fail, abort immediately — no commit, no tag
- Commit with
release: vX.Y.Zprefix (cleaner thanchore(release):) - Create an annotated tag
vX.Y.Zon the release commit - Push both the commit and tag to master
--skip-testsflag 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/notify_failure.py
-
Publish workflow (
.gitea/workflows/publish.yml):- Triggers on tag push (
v*) - Validates
PYPI_TOKENis set (warns if missing) - Builds the Python package
- Optionally publishes to PyPI (if
PYPI_TOKENis set) - Creates a Gitea release with git-cliff-generated release notes
- On failure, creates a Gitea issue via
scripts/notify_failure.py
- Triggers on tag push (
git-cliff Commit Preprocessing
Merge commits on master have the format GRM-N <conventional commit>. 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 | <conventional commit> |
feat: add review script |
| PR title | GRM-N: <vikunja task title> |
GRM-33: Add mandatory PR review step |
| Merge commit | GRM-N <conventional commit> |
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-Ntask ID - Line length: 120 chars
- Secrets are passed via temp JSON files, never on the command line (CWE-214)
- CI triggers only on
openedandsynchronizePR events (notlabeled)
Ansible Role Structure
main.yml → systemd_check → user_setup → rootless_docker → install_runner → prune → integration_test
install_runner.ymlhandles: download, config, validate, register, servicemain.ymlhandles: prune, integration_test (NOT install_runner — avoids duplicates)systemctl --usertasks must be guarded bydocker_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/distribute_molecule.py (single source of truth)
Known Issues
ansible-lintmay warn aboutcommand-instead-of-moduleforsystemctl --usercalls — 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.ymlworkflow runsscripts/sync_wiki.pywhich 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.jsonmaps 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/doc_coverage.pychecks 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
- Edit files in
/docs/ - If adding a new page, add it to
docs/mapping.json - Commit and create a PR (standard PR workflow)
- On merge, wiki is automatically synced