Files
grm/docs/tech/ci-cd-workflow.md
T
emil 5b05db4e6d GRM-36: feat: implement documentation-as-code with wiki sync and doc-coverage
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
2026-06-21 19:45:34 +00:00

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-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/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:

  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

9. Post-Merge Automation

After the squash-merge:

  • The post-merge workflow (.gitea/workflows/post-merge.yml) triggers on push to master and runs scripts/post_merge.py to 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 to master and 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 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.

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/master against the PR head SHA
  • For pushes to master: compares HEAD~1 against HEAD
  • Outputs ansible-changed as true or false

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:

  1. make setup — full environment setup
  2. make lint-all — ruff + pyright + bandit + ansible-lint + checkmake
  3. make pytest-cov — unit tests with 100% coverage enforcement
  4. python3 scripts/check_test_speed.py --max-seconds 10 — verify unit tests run fast
  5. PYTHONPATH=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.py which uses git-cliff to:
    • Calculate the next semver version from conventional commits since the last tag
    • Update __version__ in src/gitea_runner_manager/__init__.py (single source of truth)
    • Update CHANGELOG.md with the new version section
    • Run make lint-ruff and make pytest-cov to verify the release is healthy
    • If lint or tests fail, abort immediately — no commit, no tag
    • Commit with release: vX.Y.Z prefix (cleaner than chore(release):)
    • Create an annotated tag vX.Y.Z on the release commit
    • Push 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/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_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
  • Uses scripts/publish.py for 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_request labeled events
  • Runs scripts/auto_merge.py with 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.py with 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 format
  • filter_unconventional = true — skip non-conventional commits
  • render_always = true — always render the changelog
  • trim = 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 skipped
  • sort_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 = true
  • breaking_always_bump_major = false
  • initial_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