313 lines
14 KiB
Markdown
313 lines
14 KiB
Markdown
# 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: <vikunja task title>` (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=<token> python3 scripts/ci/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:
|
|
```bash
|
|
REPO_TOKEN=<token> python3 scripts/ci/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:
|
|
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 <conventional commit message>` (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 <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-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
|