GRM-146: ci: consolidate CI and post-merge workflows
Post-merge / detect-and-configure (push) Waiting to run
Post-merge / release-and-maintain (push) Waiting to run

This commit was merged in pull request #216.
This commit is contained in:
2026-07-12 01:53:47 +00:00
parent dd9fc601fb
commit c4fe70979c
11 changed files with 271 additions and 471 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ If `.venv` doesn't exist, run `make setup` first.
**Always run `make pytest-cov` before pushing** — CI enforces 100%
coverage and will fail the PR if any lines are uncovered. This is the
most common cause of CI quality job failures after code changes. The
most common cause of CI validate job failures after code changes. The
pre-push git hook only validates Vikunja task existence, not tests.
### API Response Type Checking
+76 -138
View File
@@ -6,21 +6,38 @@ on:
workflow_dispatch:
env:
PIP_BREAK_SYSTEM_PACKAGES: "1"
PYTHONPATH: src
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
jobs:
quality:
# Single validation job that merges: quality, detect-changes,
# release-dry-run, pre-merge-check, pr-review, and discover-runners.
# Uses ci-full image (has git-cliff for release-dry-run).
# Saves ~5x checkout+setup overhead vs 6 separate jobs.
validate:
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest
timeout-minutes: 10
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 15
defaults:
run:
shell: bash
outputs:
ansible-changed: ${{ steps.detect.outputs.ansible-changed }}
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
runner-count: ${{ steps.discover-runners.outputs.runner-count }}
runner-indices: ${{ steps.discover-runners.outputs.runner-indices }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci,lint
# --- quality steps ---
- name: Lint all
run: |
. .venv/bin/activate 2>/dev/null || true
@@ -32,7 +49,6 @@ jobs:
make pytest-cov
- name: Documentation gate (coverage + stale refs + lint + version refs + prose)
env:
PYTHONPATH: src
DEVX_DOC_COVERAGE_STRICT: "1"
DEVX_DOC_VERSIONS_PKG: grm
DEVX_VALE_LEVEL: warning
@@ -45,8 +61,6 @@ jobs:
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.check_translations --translations src/grm/translations.json
- name: Check unit test speed
env:
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
@@ -67,52 +81,10 @@ jobs:
else
echo "act_runner not found — skipping workflow dry-run (static lint still passed)"
fi
release-dry-run:
needs: [quality, detect-changes]
if: needs.detect-changes.outputs.user-facing-changed == 'true'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci,lint
- name: Release dry-run validation
env:
PYTHONPATH: src
DEVX_VERSION_FILE: src/grm/__init__.py
DEVX_TASK_PREFIX: GRM
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release --dry-run
detect-changes:
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
outputs:
ansible-changed: ${{ steps.detect.outputs.ansible-changed }}
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
# --- detect-changes step ---
- name: Detect changed paths
id: detect
env:
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM
run: |
. .venv/bin/activate 2>/dev/null || true
@@ -120,22 +92,10 @@ jobs:
--base "origin/master" \
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
--github-output
pre-merge-check:
needs: [quality, detect-changes]
if: github.event_name == 'pull_request'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment
run: make setup-image EXTRAS=ci
# --- validate-pr + pr-review steps (PR only) ---
- name: Validate auto-merge preconditions
if: github.event_name == 'pull_request'
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
DEVX_TASK_PREFIX: GRM
DEVX_VIKUNJA_PROJECT_ID: 6
@@ -143,7 +103,6 @@ jobs:
PR_TITLE: ${{ github.event.pull_request.title }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.number }}
PYTHONPATH: ${{ env.PYTHONPATH }}
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.check_auto_merge_ready \
@@ -151,45 +110,58 @@ jobs:
--pr-title "$PR_TITLE" \
--repo "$REPOSITORY" \
--pr-number "$PR_NUMBER"
discover-runners:
needs: [detect-changes]
if: needs.detect-changes.outputs.ansible-changed == 'true'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
outputs:
runner-count: ${{ steps.discover.outputs.runner-count }}
runner-indices: ${{ steps.discover.outputs.runner-indices }}
steps:
- uses: actions/checkout@v4
- name: Set up environment
- name: Run automated PR review
if: github.event_name == 'pull_request'
run: |
. .venv/bin/activate 2>/dev/null || true
set -euo pipefail
python3 -m devx.ci.pr_review \
"${{ github.event.number }}" \
"${{ github.repository }}"
# --- release-dry-run step (conditional) ---
- name: Release dry-run validation
if: steps.detect.outputs.user-facing-changed == 'true'
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Discover available runners
id: discover
DEVX_VERSION_FILE: src/grm/__init__.py
DEVX_TASK_PREFIX: GRM
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release --dry-run
# --- discover-runners step (conditional on ansible-changed) ---
- name: Discover available molecule runners
id: discover-runners
if: steps.detect.outputs.ansible-changed == 'true'
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
MOLECULE_RUNNERS: ${{ vars.MOLECULE_RUNNERS }}
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.molecule.discover_runners \
--owner "${{ github.repository_owner }}" \
--repo "${{ github.event.repository.name }}" \
--github-output
- name: Notify on failure
if: failure()
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "ci/validate" \
--commit "${{ github.sha }}"
molecule-tests:
needs: [quality, detect-changes, discover-runners]
if: needs.detect-changes.outputs.ansible-changed == 'true'
needs: [validate]
if: needs.validate.outputs.ansible-changed == 'true'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 10
timeout-minutes: 15
strategy:
fail-fast: true
max-parallel: 3
max-parallel: 6
matrix:
runner-index: [1, 2, 3, 4, 5, 6]
steps:
@@ -206,8 +178,7 @@ jobs:
- name: Discover assigned test pairs
env:
RUNNER_INDEX: ${{ matrix.runner-index }}
MAX_RUNNERS: ${{ needs.discover-runners.outputs.runner-count }}
PYTHONPATH: src
MAX_RUNNERS: ${{ needs.validate.outputs.runner-count }}
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.molecule.distribute_molecule \
@@ -216,6 +187,16 @@ jobs:
--github-env --skip-if-excess
- name: Run molecule tests
if: env.SKIP != 'true'
env:
GITEA_URL: ${{ github.server_url }}
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
RUN_ID: ${{ github.run_id }}
ANSIBLE_INJECT_INVOCATION: "1"
JOB_NAME: ${{ github.job }}
MATRIX_INDEX: ${{ matrix.runner-index }}
GITEA_REPOSITORY: ${{ github.repository }}
DOCKER_HOST: unix:///var/run/docker.sock
run: |
. .venv/bin/activate 2>/dev/null || true
if [ -z "$TEST_PAIRS" ]; then exit 0; fi
@@ -228,58 +209,17 @@ jobs:
echo "$_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
# shellcheck disable=SC2086 # intentional word splitting for argument expansion
python3 -m devx.molecule.molecule_ci_guard $TEST_PAIRS
env:
GITEA_URL: ${{ github.server_url }}
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
RUN_ID: ${{ github.run_id }}
ANSIBLE_INJECT_INVOCATION: "1"
JOB_NAME: ${{ github.job }}
MATRIX_INDEX: ${{ matrix.runner-index }}
GITEA_REPOSITORY: ${{ github.repository }}
PYTHONPATH: src
DOCKER_HOST: unix:///var/run/docker.sock
pr-review:
if: github.event_name == 'pull_request'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Run automated PR review
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: |
set -euo pipefail
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.pr_review \
"${{ github.event.number }}" \
"${{ github.repository }}"
auto-merge:
# Auto-merge runs after all CI checks pass. It reads the task ID
# from the branch name, validates the PR title, and squash-merges.
# Auto-merge runs after validate + molecule-tests pass (or molecule is skipped).
# Uses always() so it evaluates even when molecule-tests is skipped
# (Gitea Actions skips dependent jobs of skipped jobs by default).
needs: [quality, detect-changes, pre-merge-check, pr-review, molecule-tests, release-dry-run]
needs: [validate, molecule-tests]
if: >-
always() &&
github.event_name == 'pull_request' &&
needs.quality.result == 'success' &&
needs.pre-merge-check.result == 'success' &&
needs.pr-review.result == 'success' &&
(needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped') &&
(needs.release-dry-run.result == 'success' || needs.release-dry-run.result == 'skipped')
needs.validate.result == 'success' &&
(needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped')
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
@@ -301,7 +241,6 @@ jobs:
REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
PR_NUMBER: ${{ github.event.number }}
REPOSITORY: ${{ github.repository }}
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.pr_review \
@@ -310,12 +249,11 @@ jobs:
--event APPROVE \
--checklist-confirmed \
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
--body "Auto-approved: all CI checks passed (quality, molecule, pr-review, pre-merge-check)."
--body "Auto-approved: all CI checks passed (validate, molecule-tests)."
- name: Squash merge with task ID
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM
DEVX_VIKUNJA_PROJECT_ID: 6
HEAD_REF: ${{ github.head_ref }}
+84 -216
View File
@@ -1,101 +1,121 @@
name: Post-merge
# Runs on every push to master. A single workflow with conditional jobs
# for release, publish, wiki sync, badges, and Vikunja task updates.
# Runs on every push to master (after CI workflow merges a PR).
# Consolidated into 2 jobs (from 7) to reduce runner overhead:
# detect-and-configure ──→ release-and-maintain
#
# Job dependency graph:
# Job 1: detect release commit, validate commit msg, configure repo
# (branch protection, labels).
# Job 2: release + publish + sync-wiki + vikunja + badges.
# Individual steps are conditional on job 1 outputs.
#
# detect-type ──┬── validate-commit-msg (skip if release commit)
# ├── release (skip if release commit)
# │ └── publish (needs release — builds & publishes to PyPI)
# ├── badges (ALWAYS runs — even on release commits)
# ├── configure-repo (independent — skip if release commit)
# ├── sync-wiki (skip if release commit — runs for ALL merges)
# └── vikunja (skip if release commit — runs for ALL merges)
#
# sync-wiki and vikunja run for ALL non-release commits, not just when
# release succeeds. This ensures the wiki and task tracker are updated
# even for infrastructure-only changes (docs, CI config, etc.).
#
# The badges job uses `if: always()` with no is-release condition so it
# runs on every push to master, including release commits. This ensures
# badges (tests, coverage, version, etc.) are always current.
# The badges step always runs (even on release commits) so version
# badge picks up the new __version__. It runs last so it sees the
# new version if release created one.
#
# When release creates a "release: vX.Y.Z" commit and tag, the publish
# job (which depends on release) builds and publishes the package to the
# Gitea PyPI registry. The release commit's post-merge run still updates
# badges (version badge picks up the new version). Other jobs skip.
# step builds and publishes the package to the Gitea PyPI registry.
# The release commit's post-merge run still updates badges. Other
# steps (sync-wiki, vikunja) skip on release commits.
on:
push:
branches: [master]
workflow_dispatch:
concurrency:
group: post-merge-${{ github.ref }}
cancel-in-progress: true
env:
PIP_BREAK_SYSTEM_PACKAGES: "1"
PYTHONPATH: src
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
jobs:
detect-type:
detect-and-configure:
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
defaults:
run:
shell: bash
outputs:
is-release: ${{ steps.check.outputs.is-release }}
is-automated: ${{ steps.check.outputs.is-automated }}
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
fetch-depth: 0
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Ensure branch protection and labels
env:
DEVX_REPO_NAME: grm
DEVX_REPO_OWNER: oblachno-oss
DEVX_STATUS_CHECKS: "CI / validate (pull_request),CI / molecule-tests (1) (pull_request),CI / molecule-tests (2) (pull_request),CI / molecule-tests (3) (pull_request)"
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.tools.configure_repo
- name: Check if this is a release commit
id: check
env:
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.detect_release_commit
validate-commit-msg:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Validate latest commit message
if: steps.check.outputs.is-automated == 'false'
env:
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM
run: |
. .venv/bin/activate 2>/dev/null || true
git log -1 --format=%B > commit-msg.txt
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
rm -f commit-msg.txt
- name: Detect changed paths
id: detect
if: steps.check.outputs.is-release == 'false'
env:
DEVX_TASK_PREFIX: GRM
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.classify_changes \
--base "HEAD~1" \
--head "HEAD" \
--github-output
- name: Notify on failure
if: failure()
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/detect-and-configure" \
--commit "${{ github.sha }}"
release:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
release-and-maintain:
needs: [detect-and-configure]
if: always() && needs.detect-and-configure.result == 'success'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 15
outputs:
tag: ${{ steps.release-tag.outputs.tag }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: master
token: ${{ secrets.CI_GITEA_API_TOKEN }}
- name: Set up environment
env:
@@ -106,10 +126,11 @@ jobs:
run: |
git config user.name "grm-ci-bot"
git config user.email "grm-ci-bot@oblachno.fyi"
# --- release + publish (only if not a release commit) ---
- name: Run release
id: release-tag
if: needs.detect-and-configure.outputs.is-release == 'false' && needs.detect-and-configure.outputs.user-facing-changed == 'true'
env:
PYTHONPATH: src
DEVX_VERSION_FILE: src/grm/__init__.py
DEVX_TASK_PREFIX: GRM
DEVX_VIKUNJA_PROJECT_ID: 6
@@ -117,207 +138,54 @@ jobs:
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release
- name: Notify on failure
if: failure()
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/release" \
--commit "${{ github.sha }}"
publish:
needs: [release]
if: needs.release.outputs.tag != ''
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.release.outputs.tag }}
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci,lint
- name: Build and publish release
if: steps.release-tag.outputs.tag != ''
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.publish \
"${{ needs.release.outputs.tag }}" \
"${{ github.repository }}" --auto-login
- name: Notify on failure
if: failure()
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/publish" \
--commit "${{ github.sha }}"
sync-wiki:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 15
concurrency:
group: sync-wiki-${{ github.repository }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
git fetch --tags
git checkout "${{ steps.release-tag.outputs.tag }}"
python3 -m devx.ci.publish "${{ steps.release-tag.outputs.tag }}" "${{ github.repository }}" --auto-login
# --- sync-wiki + vikunja (skip on automated/release commits) ---
- name: Sync documentation to wiki
if: needs.detect-and-configure.outputs.is-automated == 'false'
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --verify
- name: Notify on failure
if: failure()
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/sync-wiki" \
--commit "${{ github.sha }}"
badges:
needs: [detect-type]
if: always()
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: master
token: ${{ secrets.CI_GITEA_API_TOKEN }}
- name: Fetch latest master
run: |
git fetch origin master
git reset --hard origin/master
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=lint
- name: Generate and push badges
env:
PRE_COMMIT_ALLOW_NO_CONFIG: "1"
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.push_badges
- name: Notify on failure
if: failure()
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/badges" \
--commit "${{ github.sha }}"
vikunja:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Update Vikunja task
if: needs.detect-and-configure.outputs.is-automated == 'false'
env:
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM
DEVX_VIKUNJA_PROJECT_ID: 6
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
- name: Notify on failure
if: failure()
# --- badges (always run — even on release commits) ---
- name: Generate and push badges
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/vikunja" \
--commit "${{ github.sha }}"
configure-repo:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Ensure branch protection and labels
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
DEVX_REPO_NAME: grm
DEVX_REPO_OWNER: oblachno-oss
DEVX_STATUS_CHECKS: "CI / quality (pull_request),CI / molecule-tests (1) (pull_request),CI / molecule-tests (2) (pull_request),CI / molecule-tests (3) (pull_request)"
PRE_COMMIT_ALLOW_NO_CONFIG: "1"
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.tools.configure_repo
export PATH="$HOME/.local/bin:$PATH"
# Fetch latest master to pick up any release commit that was pushed
git fetch origin master
git reset --hard origin/master
python3 -m devx.ci.push_badges
- name: Notify on failure
if: failure()
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/configure-repo" \
--workflow "post-merge/release-and-maintain" \
--commit "${{ github.sha }}"
+47 -64
View File
@@ -51,7 +51,7 @@ Workflow YAML files (`.gitea/workflows/*.yml`) are verified with two tools:
Both run via `make workflow-check` and are part of `make lint-all`.
The pre-commit hook runs actionlint automatically when workflow files change.
The CI `quality` job runs `make setup` (which installs all tools) then `make lint-all`.
The CI `validate` job runs `make setup-image` (which installs all tools) then `make lint-all`.
CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is not installed in the CI Docker image).
## Architecture
@@ -69,13 +69,13 @@ Every change to master goes through this workflow. No exceptions.
Branch protection and labels are automatically configured by
`devx.tools.configure_repo` (run as `python -m devx.tools.configure_repo`),
which runs as a `configure-repo` job in
which runs as a step in the `detect-and-configure` job in
the post-merge workflow on every push to master.
The following rules are enforced for `master`:
- **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
- **Require status checks**: CI validate + molecule tests must pass
- **Block force pushes**: No history rewriting on master
The auto-merge workflow enforces the APPROVE review check programmatically
@@ -123,8 +123,9 @@ architecture, code quality, security, i18n, testing, performance,
UX, documentation, workflow compliance, maintainability, resource
management, backwards compatibility, and logging.
**Automated review (CI `pr-review` job):** Every PR triggers an automated
review via `devx.ci.pr_review` (run as `python -m devx.ci.pr_review`). This job posts a review with
**Automated review (CI `validate` job):** Every PR triggers an automated
review via `python -m devx.ci.pr_review` as a step in the `validate` job.
This posts a review with
`COMMENT` (no issues) or `REQUEST_CHANGES` (issues found) based on
the **[auto]** items in the checklist:
@@ -178,10 +179,10 @@ rejected.
Then add the `ready-to-merge` label. The auto-merge workflow will:
1. **Validate** PR title format (`GRM-N: <vikunja task title>`) and match against Vikunja task title
2. **Check** that at least one substantive APPROVE review exists (body > 20 chars or has inline comments)
3. Wait for all CI checks to pass (including the `pr-review` job)
3. Wait for all CI checks to pass (including the `validate` job)
4. Squash-merge with title: `GRM-N: <conventional commit message>`
5. The post-merge workflow marks the Vikunja task as done
6. The release workflow automatically versions, tags, and publishes (see below)
6. The release-and-maintain job automatically versions, tags, and publishes (see below)
**If the branch is behind master** (another PR merged first), auto-merge
automatically rebases the PR's head branch via the Gitea API. This triggers
@@ -197,21 +198,22 @@ No manual rebase needed. To rebase manually: `make rebase` (local) or
### CI Path Filtering
The CI workflow includes a `pre-merge-check` job (runs after quality +
detect-changes) that validates branch format, PR title, and Vikunja task
match. This fails fast before expensive molecule tests run.
The CI workflow's `validate` job includes a pre-merge validation step
that validates branch format, PR title, and Vikunja task match. This
fails fast before expensive molecule tests run.
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.
The `validate` job also includes a `detect-changes` step 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 `devx.molecule.discover_runners`. The `discover-runners`
job queries the Gitea API for runners at all levels (repo, org, instance)
dynamically via `devx.molecule.discover_runners`. The `validate` job
includes a `discover-runners` step (conditional on ansible-changed) that
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.
@@ -224,47 +226,27 @@ then to a default of 3.
### Automated Release Pipeline
After a PR is merged to master, the **post-merge workflow**
(`.gitea/workflows/post-merge.yml`) runs automatically. This single
workflow consolidates release, wiki sync, badge generation, and
Vikunja task updates:
(`.gitea/workflows/post-merge.yml`) runs automatically. Consolidated
into 2 jobs (from 7) to reduce runner overhead:
1. **detect-type** — Checks if the commit is a regular merge or a
release commit (`release: vX.Y.Z`). All subsequent jobs skip for
release commits (the `[skip ci]` tag also prevents re-triggering).
1. **detect-and-configure** — Configures repo (branch protection, labels),
detects release commit, validates commit message. Outputs `is-release`
and `is-automated` for the next job.
2. **release** — Runs `devx.ci.release` which:
- **Checks for user-facing changes** via `devx.ci.classify_changes` — if only
workflow/infrastructure files changed (`.gitea/`, `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/grm/__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 [skip ci]` prefix (the `[skip ci]` prevents
re-triggering post-merge on the release commit)
- 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
3. **sync-wiki** — Syncs documentation to the Gitea wiki. Runs for ALL
non-release commits (not only when release succeeds), so docs-only
changes still update the wiki.
4. **badges** — Generates and pushes quality badge SVGs to the `badges` branch.
Uses `if: always()` so it runs on every push, including release commits.
The script fetches the latest master before generating badges to pick up
any release commits.
5. **vikunja** — Marks the corresponding Vikunja task as done. Runs for ALL
non-release commits (not only when release succeeds), so infrastructure-only
changes still update the task tracker.
6. **publish** — Runs after release succeeds (needs: release). Builds and
publishes the package to the Gitea PyPI registry. Gets the tag from the
release job's `tag` output.
2. **release-and-maintain** — Runs all post-merge maintenance as
conditional steps:
- **release** (if not a release commit) — Runs `devx.ci.release` which
checks for user-facing changes via `classify_changes` (skips if only
workflow/infrastructure files changed), uses git-cliff for semver,
updates `__version__`, updates `CHANGELOG.md`, runs lint+tests, commits
with `release: vX.Y.Z [skip ci]`, creates annotated tag, pushes to master.
- **publish** (if release created a tag) — Builds and publishes the
package to the Gitea PyPI registry. Checks out the release tag
within the same job.
- **sync-wiki** (if not automated) — Syncs documentation to the Gitea wiki.
- **vikunja** (if not automated) — Marks the corresponding Vikunja task as done.
- **badges** (always) — Generates and pushes quality badge SVGs to the
`badges` branch. Fetches latest master first to pick up release commits.
### Smart CI: User-Facing vs Workflow-Only Changes
@@ -385,9 +367,9 @@ platform matrix. Both `devx.molecule.distribute_molecule` (CI) and
`devx.molecule.molecule_all` (dev tool) import `PLATFORMS` from it — this
avoids dev tools importing directly from CI modules.
2. **Publish job** (in `post-merge.yml`, needs: release):
- Runs after the release job creates a tag
- Gets the tag from `needs.release.outputs.tag`
2. **Publish step** (in the `release-and-maintain` job, runs after the release step creates a tag):
- Runs after the release step creates a tag
- Gets the tag from the release step's output
- Builds the Python package
- Publishes to the Gitea PyPI registry
- Creates a Gitea release with git-cliff-generated release notes
@@ -542,7 +524,7 @@ docs/
### Documentation Coverage
- `devx.ci.doc_coverage` checks that all CLI commands, Python modules, and CI scripts are documented
- Runs as a CI step in the quality job with `--fail-on-missing` (blocks CI if docs are missing)
- Runs as a CI step in the validate job with `--fail-on-missing` (blocks CI if docs are missing)
- Enforced: 100% coverage for public CLI commands and major architectural components
### Updating Documentation
@@ -572,7 +554,7 @@ the user should not need to specify which profile to use.
| Profile | Purpose |
|---------|---------|
| `ci-investigator` | Investigate CI failures (quality, molecule, release, publish, wiki sync) |
| `ci-investigator` | Investigate CI failures (validate, molecule-tests, release-and-maintain) |
| `molecule-runner` | Run 7 molecule scenarios across 4 platforms, report pass/fail |
| `dep-upgrader` | Python + Ansible dependency upgrades with molecule verification |
| `doc-sync-specialist` | Doc coverage, doc linting, wiki sync for grm docs |
@@ -582,7 +564,7 @@ the user should not need to specify which profile to use.
| Trigger | Profile | Mode |
|---------|---------|------|
| CI run failure (quality, molecule-tests, release, publish, sync-wiki) | `ci-investigator` | Background |
| CI run failure (validate, molecule-tests, release-and-maintain) | `ci-investigator` | Background |
| PR ready for review | `pr-reviewer` | Foreground |
| Molecule tests need to run | `molecule-runner` | Background |
| Dependency upgrade requested | `dep-upgrader` | Background |
@@ -608,8 +590,9 @@ tool, workflow, or process issues that warrant follow-up. These issues
use the `feedback` label plus a category label (`tooling`,
`ci-improvement`, `doc-improvement`, `workflow-improvement`).
Standard labels are created automatically by `configure_repo` (runs in
post-merge on every master push). If a label does not exist yet, the
Standard labels are created automatically by `configure_repo` (runs as
a step in `detect-and-configure` in post-merge on every master push).
If a label does not exist yet, the
subagent's issue creation will still succeed — labels can be added
afterwards.
+4 -3
View File
@@ -50,13 +50,14 @@ setup: $(VENV)/bin/activate .env activate-scripts configure-gitea-pypi
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install
# Lean setup for CI jobs that need pytest + lint tools + runtime deps
# (detect-changes, discover-runners, pr-review, sync-wiki, badges)
# badges job runs generate_badges.py which needs ruff, pyright, bandit
# (validate job steps: detect-changes, discover-runners, pr-review;
# release-and-maintain job steps: sync-wiki, badges)
# badges step runs generate_badges.py which needs ruff, pyright, bandit
setup-ci: $(VENV)/bin/activate .env configure-gitea-pypi
@$(PIP_INSTALL) install -e '.[ci,lint]'
@$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-ansible-collections --no-pre-commit --no-tea-login
# Setup for the quality job (lint + test deps, actionlint tool)
# Setup for the validate CI job (lint + test deps, actionlint tool)
setup-quality: $(VENV)/bin/activate .env configure-gitea-pypi
@$(PIP_INSTALL) install -e '.[ci,lint]'
@$(BIN)/python -m devx.tools.install_tools
+42 -33
View File
@@ -6,10 +6,9 @@ GRM uses a fully automated CI/CD pipeline built on Gitea Actions. Every change t
| Workflow | Trigger | Purpose |
|----------|---------|---------|
| `ci.yml` | PR opened/synchronized | Quality checks (lint, test, coverage) + molecule tests |
| `ci.yml` | PR opened/synchronized | Validate (lint, test, coverage, detect-changes, release-dry-run, pr-review, discover-runners) + molecule tests |
| `auto-merge.yml` | PR labeled `ready-to-merge` | Validates and squash-merges the PR |
| `post-merge.yml` | Push to `master` | Release, wiki sync, badges, Vikunja task update |
| `publish.yml` | Tag push (`v*`) | Build and publish package to PyPI, create Gitea release |
| `post-merge.yml` | Push to `master` | Detect-and-configure + release-and-maintain (release, publish, wiki sync, badges, Vikunja task update) |
Every change to master goes through a mandatory PR workflow. No exceptions.
@@ -101,14 +100,14 @@ Then add the `ready-to-merge` label. The auto-merge workflow will:
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
6. The release-and-maintain job 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 `devx.ci.post_merge` 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).
- The **post-merge workflow** (`.gitea/workflows/post-merge.yml`) triggers on push to `master`. The `detect-and-configure` job configures the repo and detects the commit type. The `release-and-maintain` job then runs the release, publish, sync-wiki, badges, and Vikunja steps as appropriate.
- The **release step** (in the `release-and-maintain` job) automatically versions, tags, and publishes (see below).
## Branch Protection (Required Gitea Settings)
@@ -116,42 +115,46 @@ Configure the following branch protection rules for `master` in Gitea repo setti
- **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
- **Require status checks**: CI validate + 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 CI workflow (`.gitea/workflows/ci.yml`) includes a `detect-changes` step in the `validate` 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:
The `detect-changes` step:
- 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'`.
The `molecule-tests` job depends on the `validate` job (which includes the `detect-changes` step), and only runs if `ansible-changed == 'true'`.
CI triggers only on `opened` and `synchronize` PR events (not `labeled`).
## CI Quality Job
## CI Validate Job
The `quality` job in `.gitea/workflows/ci.yml` runs:
The `validate` job in `.gitea/workflows/ci.yml` consolidates the former quality, detect-changes, release-dry-run, pre-merge-check, pr-review, and discover-runners jobs into a single job. It 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. `python -m devx.tools.check_test_speed --max-seconds 10` — verify unit tests run fast
5. `PYTHONPATH=src python -m devx.ci.release --dry-run` — release dry-run validation
5. `PYTHONPATH=src python -m devx.ci.release --dry-run` — release dry-run validation (release-dry-run step)
6. Pre-merge validation step — validates branch format, PR title, and Vikunja task match
7. `detect-changes` step — checks whether Ansible files changed (gates molecule tests)
8. `pr-review` step — automated PR review via `devx.ci.pr_review`
9. `discover-runners` step — dynamic runner discovery for molecule tests (conditional on ansible-changed)
## Automated Release Pipeline
After a PR is merged to master, the release pipeline runs automatically.
### Release Workflow (`.gitea/workflows/release.yml`)
### Release Step (in the release-and-maintain job)
- Triggers on push to `master`
- Runs as a conditional step in the `release-and-maintain` job (skipped for release commits)
- 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`
@@ -169,9 +172,10 @@ After a PR is merged to master, the release pipeline runs automatically.
- 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 `devx.ci.notify_failure`
### Publish Workflow (`.gitea/workflows/publish.yml`)
### Publish Step (in the release-and-maintain job)
- Triggers on tag push (`v*`)
- Runs as a conditional step in the `release-and-maintain` job (only if the release step created a tag)
- Checks out the release tag within the same job
- Installs git-cliff (version 2.13.0)
- Installs build tools (`build`, `twine`, `requests`, `python-dotenv`, `click`)
- Validates `PYPI_TOKEN` is set (warns if missing)
@@ -190,12 +194,17 @@ After a PR is merged to master, the release pipeline runs automatically.
### Post-Merge Workflow (`.gitea/workflows/post-merge.yml`)
- Triggers on push to `master`
- Consolidates release, wiki sync, badge generation, and Vikunja task updates into a single workflow
- **detect-type** — Runs `devx.ci.detect_release_commit` to check if the commit is a release commit (`release: vX.Y.Z`). All subsequent jobs skip for release commits (the `[skip ci]` tag also prevents re-triggering).
- **release** — Runs `devx.ci.release` (see Automated Release Pipeline below)
- **sync-wiki** — Syncs documentation to the Gitea wiki via `devx.ci.sync_wiki`
- **badges** — Generates and pushes quality badge SVGs to the `badges` branch via `devx.ci.push_badges`. Runs after the release job (even if release fails or is skipped) so the version badge always reflects the latest state.
- **vikunja** — Marks the corresponding Vikunja task as done via `devx.ci.post_merge`
- Consolidated from 7 jobs into 2 jobs to reduce runner overhead
- **detect-and-configure** — Configures repo (branch protection, labels), detects release commit, validates commit message. Outputs `is-release` and `is-automated` for the next job.
- **detect-type step** — Runs `devx.ci.detect_release_commit` to check if the commit is a release commit (`release: vX.Y.Z`). All subsequent steps skip for release commits (the `[skip ci]` tag also prevents re-triggering).
- **validate-commit-msg step** — Validates the commit message follows conventional commit format.
- **configure-repo step** — Runs `devx.tools.configure_repo` to set up branch protection and labels.
- **release-and-maintain** — Runs all post-merge maintenance as conditional steps:
- **release step** (if not a release commit) — Runs `devx.ci.release` (see Automated Release Pipeline below)
- **publish step** (if release created a tag) — Builds and publishes the package to the Gitea PyPI registry
- **sync-wiki step** (if not automated) — Syncs documentation to the Gitea wiki via `devx.ci.sync_wiki`
- **vikunja step** (if not automated) — Marks the corresponding Vikunja task as done via `devx.ci.post_merge`
- **badges step** (always) — Generates and pushes quality badge SVGs to the `badges` branch via `devx.ci.push_badges`. Runs even if release fails or is skipped so the version badge always reflects the latest state.
### Smart CI: User-Facing vs Workflow-Only Changes
@@ -218,16 +227,16 @@ from accidentally skipping releases. Classification is config-driven via
**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 (separate `release-dry-run` job)
- **Quality job** (lint, unit tests, coverage, doc-coverage): Always runs
- **Release workflow**: `release.py` calls `classify_changes` to check if any
- **Release dry-run**: Only runs when user-facing files change (release-dry-run step in the validate job)
- **Validate job** (lint, unit tests, coverage, doc-coverage): Always runs
- **Release step**: `release.py` calls `classify_changes` to check if any
user-facing files changed since the last tag. If not, the release is skipped
entirely — no version bump, no tag, no publish.
### Dynamic Runner Discovery
Molecule tests are distributed across available Gitea Actions runners
dynamically. The `discover-runners` job runs `devx.molecule.discover_runners` which queries the Gitea API for
dynamically. The `discover-runners` step in the `validate` job runs `devx.molecule.discover_runners` which queries the Gitea API for
registered runners at three levels (repo, org, instance) and generates
a matrix of runner indices. If the API query fails (e.g., no admin
access for instance-level runners), it falls back to the
@@ -263,15 +272,15 @@ feature branches.
### Release Commit Detection
The `detect-type` job in the post-merge workflow runs
The `detect-type` step in the `detect-and-configure` job (post-merge workflow) runs
`devx.ci.detect_release_commit` to check whether the latest commit
is a release commit (format: `release: vX.Y.Z`). When a release commit
is detected, all post-merge jobs (release, sync-wiki, badges, vikunja)
are skipped — the tag push triggers the publish workflow instead.
is detected, all subsequent steps in the `release-and-maintain` job (release, publish, sync-wiki, vikunja)
are skipped — the tag push triggers the publish step instead.
### Badge Generation and Push
The `badges` job in the post-merge workflow runs
The `badges` step in the `release-and-maintain` job (post-merge workflow) runs
`devx.ci.push_badges` which:
1. Fetches the latest master and hard-resets to it (picks up release commits)
2. Generates quality badge SVG files via `devx.tools.generate_badges`
@@ -279,8 +288,8 @@ The `badges` job in the post-merge workflow runs
4. Copies SVG files to the branch root
5. Force-pushes the branch to the remote
The badges job depends on the `release` job and uses `if: always()` so it
runs even if release fails or is skipped. This ensures the version badge
The badges step runs with `if: always()` so it
runs even if the release step fails or is skipped. This ensures the version badge
always reflects the actual state of the repository after any release
commits have been pushed.
+4 -4
View File
@@ -77,7 +77,7 @@ Every change to master goes through this workflow. No exceptions.
6. **Review** — review the full diff focusing on: functional completeness, edge cases, technical excellence (architecture, SRP, deduplication, code smells, best practices, code quality, reusability, clean code, readability, maintainability, extensibility), performance, security, UX, documentation completeness/relevance. Post review comments via `devx.ci.pr_review`.
7. **Address comments** — fix each comment, commit, push, re-review
8. **Approve** — post an `APPROVE` review via `devx.ci.pr_review`
9. **Add `ready-to-merge` label** — auto-merge workflow squash-merges with title `GRM-N <conventional commit message>`, post-merge workflow marks the Vikunja task as done, release workflow automatically versions and tags
9. **Add `ready-to-merge` label** — auto-merge workflow squash-merges with title `GRM-N <conventional commit message>`, post-merge workflow marks the Vikunja task as done, release-and-maintain job automatically versions and tags
### 1. Create Vikunja task
Create a task in Vikunja project 6 to get a `GRM-N` identifier.
@@ -165,17 +165,17 @@ Then add the `ready-to-merge` label. The auto-merge workflow will:
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
6. The release-and-maintain job automatically versions, tags, and publishes
> **IMPORTANT**: Never manually merge PRs via the API. Always use the auto-merge workflow by adding the `ready-to-merge` label. Manual merges bypass the `GRM-N <conventional>` format enforcement.
### Branch Protection (Required Gitea Settings)
Branch protection is automatically configured by `devx.tools.configure_repo` (runs as a `configure-repo` job in the post-merge workflow). The following rules are enforced for `master`:
Branch protection is automatically configured by the `configure-repo` step in the `detect-and-configure` job of the post-merge workflow. The following rules are enforced for `master`:
- **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
- **Require status checks**: CI validate + 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.
+6 -6
View File
@@ -44,7 +44,7 @@ Key technical decisions for the GRM project, extracted from `CHANGELOG.md` and `
**Date:** 2026-06-21 (v0.2.2)
**Decision:** The release workflow runs `make lint-ruff` and `make pytest-cov` before creating a release commit or tag. If lint or tests fail, the release aborts immediately — no commit, no tag.
**Decision:** The release step in the release-and-maintain job runs `make lint-ruff` and `make pytest-cov` before creating a release commit or tag. If lint or tests fail, the release aborts immediately — no commit, no tag.
**Rationale:** This ensures every tagged release is healthy. A `--skip-tests` flag exists for emergency use only but is not recommended. This decision was made as a bug fix after identifying that releases could be tagged without verifying test health. Loops are prevented by `has_unreleased_changes` — after a release commit is tagged, the next run finds no unreleased changes and exits.
@@ -58,7 +58,7 @@ Key technical decisions for the GRM project, extracted from `CHANGELOG.md` and `
**Decision:** Require branch protection on `master` (require pull request, require approval review, require status checks, block force pushes) and use an auto-merge workflow that programmatically enforces the APPROVE review check.
**Rationale:** Branch protection is the primary gate — no direct pushes to master, at least 1 APPROVE review before merge, CI quality + molecule tests must pass, and no history rewriting. The auto-merge workflow (`devx.ci.auto_merge`) enforces the APPROVE review check programmatically as a defense-in-depth measure. When the `ready-to-merge` label is added, the workflow validates PR title format, checks for APPROVE review, waits for CI, and squash-merges with title `GRM-N <conventional commit message>`. The post-merge workflow then marks the Vikunja task as done. Branch protection is automatically configured by `devx.tools.configure_repo`.
**Rationale:** Branch protection is the primary gate — no direct pushes to master, at least 1 APPROVE review before merge, CI validate + molecule tests must pass, and no history rewriting. The auto-merge workflow (`devx.ci.auto_merge`) enforces the APPROVE review check programmatically as a defense-in-depth measure. When the `ready-to-merge` label is added, the workflow validates PR title format, checks for APPROVE review, waits for CI, and squash-merges with title `GRM-N <conventional commit message>`. The post-merge workflow then marks the Vikunja task as done. Branch protection is automatically configured by the `configure-repo` step in the `detect-and-configure` job.
**Source:** `CHANGELOG.md` (Unreleased — Added: mandatory PR review step, auto_merge.py), `AGENTS.md` (Branch Protection, PR Workflow step 8)
@@ -68,11 +68,11 @@ Key technical decisions for the GRM project, extracted from `CHANGELOG.md` and `
**Date:** 2026-06-21 (v0.2.0 unreleased)
**Decision:** 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.
**Decision:** The CI workflow's `validate` job includes a `detect-changes` step that checks whether any files under `ansible/` or `.ansible-lint` have changed. If no Ansible files are changed, molecule tests are skipped.
**Rationale:** This prevents non-Ansible changes (e.g., Python scripts, workflow YAML, docs) from being blocked by molecule test infrastructure flakiness. Molecule tests are only relevant when Ansible files change. 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`) to avoid redundant runs.
**Rationale:** This prevents non-Ansible changes (e.g., Python scripts, workflow YAML, docs) from being blocked by molecule test infrastructure flakiness. Molecule tests are only relevant when Ansible files change. The `molecule-tests` job depends on the `validate` job (which includes the `detect-changes` step), and only runs if `ansible-changed == 'true'`. CI triggers only on `opened` and `synchronize` PR events (not `labeled`) to avoid redundant runs.
**Source:** `AGENTS.md` (CI Path Filtering), `.gitea/workflows/ci.yml` (detect-changes job)
**Source:** `AGENTS.md` (CI Path Filtering), `.gitea/workflows/ci.yml` (detect-changes step in the validate job)
---
@@ -120,4 +120,4 @@ Key technical decisions for the GRM project, extracted from `CHANGELOG.md` and `
**Rationale:** Hardcoding the number of CI runners would require manual updates when runners are added or removed. Dynamic discovery auto-detects repo/org-level runners via the API. For instance-level runners (which may not be visible without admin scope), it falls back to the `MOLECULE_RUNNERS` repo variable, then to a default of 3. The workflow automatically scales the matrix to match available runners, distributing test pairs evenly.
**Source:** `AGENTS.md` (Dynamic Runner Discovery), `.gitea/workflows/ci.yml` (discover-runners job)
**Source:** `AGENTS.md` (Dynamic Runner Discovery), `.gitea/workflows/ci.yml` (discover-runners step in the validate job)
+1 -1
View File
@@ -222,7 +222,7 @@ The pre-commit hook runs actionlint automatically when workflow files change.
|--------|-------------|
| `make setup` | Full setup: venv, deps, hooks, CI tools |
| `make setup-ci` | Lean setup for CI jobs (pytest + lint, no Ansible collections) |
| `make setup-quality` | Setup for the quality CI job (lint + test deps) |
| `make setup-quality` | Setup for the validate CI job (lint + test deps) |
| `make setup-molecule` | Full setup for molecule testing |
| `make setup-release` | Setup for release jobs (git-cliff, tea, lint tools) |
| `make install-tools` | Install actionlint, git-cliff, act_runner, tea to `~/.local/bin` |
+2 -2
View File
@@ -16,11 +16,11 @@ From the `Makefile`:
- `test-unit``pytest tests/unit/ -v --no-cov` (unit tests without coverage)
- `pytest-cov``pytest tests/ -v --cov=src/grm --cov-report=term-missing --cov-fail-under=100` (unit tests with 100% coverage enforcement)
The coverage requirement is `--cov-fail-under=100` — 100% test coverage is required for all code in `src/grm/`. The CI quality job runs `make pytest-cov` on every PR, and the release workflow runs it again before tagging a release.
The coverage requirement is `--cov-fail-under=100` — 100% test coverage is required for all code in `src/grm/`. The CI validate job runs `make pytest-cov` on every PR, and the release step in the release-and-maintain job runs it again before tagging a release.
### Test speed verification
The CI quality job also runs `python -m devx.tools.check_test_speed --max-seconds 10` to verify that unit tests run fast (under 10 seconds total). This catches performance regressions early.
The CI validate job also runs `python -m devx.tools.check_test_speed --max-seconds 10` to verify that unit tests run fast (under 10 seconds total). This catches performance regressions early.
## Integration Tests
+4 -3
View File
@@ -27,7 +27,8 @@ version = {attr = "grm.__version__"}
[project.optional-dependencies]
# Minimal deps for CI scripts that only need click/dotenv
# (detect-changes, discover-runners, pr-review, sync-wiki, badges, etc.)
# (validate job steps: detect-changes, discover-runners, pr-review;
# release-and-maintain job steps: sync-wiki, badges, etc.)
ci = [
"pytest==9.1.1",
"pytest-cov==7.1.0",
@@ -36,7 +37,7 @@ ci = [
# Reusable CI/CD and dev tools (auto-merge, pr-review, pre-push checks, etc.)
"devx==0.40.0",
]
# Lint and type-checking tools (quality job)
# Lint and type-checking tools (validate job)
lint = [
"ruff==0.15.20",
"pyright==1.1.411",
@@ -45,7 +46,7 @@ lint = [
"pre-commit==4.6.0",
"ansible-lint==26.4.0",
]
# Molecule testing (molecule-tests job)
# Molecule testing (molecule-tests job — stays as a separate CI job)
molecule = [
"molecule==26.6.0",
"molecule-docker==2.1.0",