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
9.3 KiB
CI/CD Workflow
Every change to master goes through a mandatory PR workflow. No exceptions.
PR Workflow
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
- 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
9. Post-Merge Automation
After the squash-merge:
- The post-merge workflow (
.gitea/workflows/post-merge.yml) triggers on push tomasterand runsscripts/post_merge.pyto mark the Vikunja task as done, extracting the task ID from the merge commit message. - The release workflow (
.gitea/workflows/release.yml) triggers on push tomasterand automatically versions, tags, and publishes (see below).
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.
CI Path Filtering
The CI workflow (.gitea/workflows/ci.yml) 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.
The detect-changes job:
- For pull requests: compares
origin/masteragainst the PR head SHA - For pushes to master: compares
HEAD~1againstHEAD - Outputs
ansible-changedastrueorfalse
The molecule-tests job depends on both quality and detect-changes, and only runs if ansible-changed == 'true'.
CI triggers only on opened and synchronize PR events (not labeled).
CI Quality Job
The quality job in .gitea/workflows/ci.yml runs:
make setup— full environment setupmake lint-all— ruff + pyright + bandit + ansible-lint + checkmakemake pytest-cov— unit tests with 100% coverage enforcementpython3 scripts/check_test_speed.py --max-seconds 10— verify unit tests run fastPYTHONPATH=src python3 scripts/release.py --dry-run— release dry-run validation
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 - Installs git-cliff (version 2.13.0)
- Configures git as
grm-ci-bot - 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*) - Installs git-cliff (version 2.13.0)
- Installs build tools (
build,twine,requests,python-dotenv,click) - 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
- Uses
scripts/publish.pyfor build and publish orchestration - On failure, creates a Gitea issue via
scripts/notify_failure.py
Auto-Merge Workflow (.gitea/workflows/auto-merge.yml)
- Triggers on
pull_requestlabeled events - Runs
scripts/auto_merge.pywith the branch name, PR title, repository, PR number, and label name - Validates PR title format, checks for APPROVE review, waits for CI, and squash-merges
Post-Merge Workflow (.gitea/workflows/post-merge.yml)
- Triggers on push to
master - Runs
scripts/post_merge.pywith the latest commit message and commit SHA - Marks the corresponding Vikunja task as done
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:
commit_preprocessors = [
# Strip GRM-N task ID prefix from merge commits so git-cliff sees conventional commits
{ pattern = "^GRM-\\d+\\s+", replace = "" },
]
This ensures all merged work appears in the changelog.
git-cliff Configuration Highlights (cliff.toml)
conventional_commits = true— parse conventional commit formatfilter_unconventional = true— skip non-conventional commitsrender_always = true— always render the changelogtrim = true— trim whitespace- Commit parsers group commits into: Features, Bug Fixes, Documentation, Performance, Refactor, Styling, Testing, Miscellaneous Tasks, Security, Revert, Other
chore(release): prepare for,chore(deps.*),chore(pr),chore(pull)commits are skippedsort_commits = "oldest"— oldest commits first
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) |
From cliff.toml [bump] section:
features_always_bump_minor = truebreaking_always_bump_major = falseinitial_tag = "0.1.0"
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 |