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% **Always run `make pytest-cov` before pushing** — CI enforces 100%
coverage and will fail the PR if any lines are uncovered. This is the 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. pre-push git hook only validates Vikunja task existence, not tests.
### API Response Type Checking ### API Response Type Checking
+76 -138
View File
@@ -6,21 +6,38 @@ on:
workflow_dispatch: workflow_dispatch:
env: env:
PIP_BREAK_SYSTEM_PACKAGES: "1"
PYTHONPATH: src
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
jobs: 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 runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 10 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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment - name: Set up environment
env: env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci,lint run: make setup-image EXTRAS=ci,lint
# --- quality steps ---
- name: Lint all - name: Lint all
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
@@ -32,7 +49,6 @@ jobs:
make pytest-cov make pytest-cov
- name: Documentation gate (coverage + stale refs + lint + version refs + prose) - name: Documentation gate (coverage + stale refs + lint + version refs + prose)
env: env:
PYTHONPATH: src
DEVX_DOC_COVERAGE_STRICT: "1" DEVX_DOC_COVERAGE_STRICT: "1"
DEVX_DOC_VERSIONS_PKG: grm DEVX_DOC_VERSIONS_PKG: grm
DEVX_VALE_LEVEL: warning DEVX_VALE_LEVEL: warning
@@ -45,8 +61,6 @@ jobs:
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.check_translations --translations src/grm/translations.json python3 -m devx.ci.check_translations --translations src/grm/translations.json
- name: Check unit test speed - name: Check unit test speed
env:
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5 python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
@@ -67,52 +81,10 @@ jobs:
else else
echo "act_runner not found — skipping workflow dry-run (static lint still passed)" echo "act_runner not found — skipping workflow dry-run (static lint still passed)"
fi fi
# --- detect-changes step ---
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
- name: Detect changed paths - name: Detect changed paths
id: detect id: detect
env: env:
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM DEVX_TASK_PREFIX: GRM
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
@@ -120,22 +92,10 @@ jobs:
--base "origin/master" \ --base "origin/master" \
--head "${{ github.event.pull_request.head.sha || github.sha }}" \ --head "${{ github.event.pull_request.head.sha || github.sha }}" \
--github-output --github-output
# --- validate-pr + pr-review steps (PR only) ---
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
- name: Validate auto-merge preconditions - name: Validate auto-merge preconditions
if: github.event_name == 'pull_request'
env: env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }} VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
DEVX_TASK_PREFIX: GRM DEVX_TASK_PREFIX: GRM
DEVX_VIKUNJA_PROJECT_ID: 6 DEVX_VIKUNJA_PROJECT_ID: 6
@@ -143,7 +103,6 @@ jobs:
PR_TITLE: ${{ github.event.pull_request.title }} PR_TITLE: ${{ github.event.pull_request.title }}
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.number }} PR_NUMBER: ${{ github.event.number }}
PYTHONPATH: ${{ env.PYTHONPATH }}
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.check_auto_merge_ready \ python3 -m devx.ci.check_auto_merge_ready \
@@ -151,45 +110,58 @@ jobs:
--pr-title "$PR_TITLE" \ --pr-title "$PR_TITLE" \
--repo "$REPOSITORY" \ --repo "$REPOSITORY" \
--pr-number "$PR_NUMBER" --pr-number "$PR_NUMBER"
- name: Run automated PR review
discover-runners: if: github.event_name == 'pull_request'
needs: [detect-changes] run: |
if: needs.detect-changes.outputs.ansible-changed == 'true' . .venv/bin/activate 2>/dev/null || true
runs-on: docker set -euo pipefail
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest python3 -m devx.ci.pr_review \
timeout-minutes: 10 "${{ github.event.number }}" \
outputs: "${{ github.repository }}"
runner-count: ${{ steps.discover.outputs.runner-count }} # --- release-dry-run step (conditional) ---
runner-indices: ${{ steps.discover.outputs.runner-indices }} - name: Release dry-run validation
steps: if: steps.detect.outputs.user-facing-changed == 'true'
- uses: actions/checkout@v4
- name: Set up environment
env: env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} DEVX_VERSION_FILE: src/grm/__init__.py
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} DEVX_TASK_PREFIX: GRM
run: make setup-image EXTRAS=ci run: |
- name: Discover available runners . .venv/bin/activate 2>/dev/null || true
id: discover 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: env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
MOLECULE_RUNNERS: ${{ vars.MOLECULE_RUNNERS }} MOLECULE_RUNNERS: ${{ vars.MOLECULE_RUNNERS }}
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.molecule.discover_runners \ python3 -m devx.molecule.discover_runners \
--owner "${{ github.repository_owner }}" \ --owner "${{ github.repository_owner }}" \
--repo "${{ github.event.repository.name }}" \ --repo "${{ github.event.repository.name }}" \
--github-output --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: molecule-tests:
needs: [quality, detect-changes, discover-runners] needs: [validate]
if: needs.detect-changes.outputs.ansible-changed == 'true' if: needs.validate.outputs.ansible-changed == 'true'
runs-on: docker runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 10 timeout-minutes: 15
strategy: strategy:
fail-fast: true fail-fast: true
max-parallel: 3 max-parallel: 6
matrix: matrix:
runner-index: [1, 2, 3, 4, 5, 6] runner-index: [1, 2, 3, 4, 5, 6]
steps: steps:
@@ -206,8 +178,7 @@ jobs:
- name: Discover assigned test pairs - name: Discover assigned test pairs
env: env:
RUNNER_INDEX: ${{ matrix.runner-index }} RUNNER_INDEX: ${{ matrix.runner-index }}
MAX_RUNNERS: ${{ needs.discover-runners.outputs.runner-count }} MAX_RUNNERS: ${{ needs.validate.outputs.runner-count }}
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.molecule.distribute_molecule \ python3 -m devx.molecule.distribute_molecule \
@@ -216,6 +187,16 @@ jobs:
--github-env --skip-if-excess --github-env --skip-if-excess
- name: Run molecule tests - name: Run molecule tests
if: env.SKIP != 'true' 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: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
if [ -z "$TEST_PAIRS" ]; then exit 0; fi 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 echo "$_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
# shellcheck disable=SC2086 # intentional word splitting for argument expansion # shellcheck disable=SC2086 # intentional word splitting for argument expansion
python3 -m devx.molecule.molecule_ci_guard $TEST_PAIRS 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:
# Auto-merge runs after all CI checks pass. It reads the task ID # Auto-merge runs after validate + molecule-tests pass (or molecule is skipped).
# from the branch name, validates the PR title, and squash-merges.
# Uses always() so it evaluates even when molecule-tests is skipped # Uses always() so it evaluates even when molecule-tests is skipped
# (Gitea Actions skips dependent jobs of skipped jobs by default). # (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: >- if: >-
always() && always() &&
github.event_name == 'pull_request' && github.event_name == 'pull_request' &&
needs.quality.result == 'success' && needs.validate.result == 'success' &&
needs.pre-merge-check.result == 'success' && (needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped')
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')
runs-on: docker runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10 timeout-minutes: 10
@@ -301,7 +241,6 @@ jobs:
REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }} REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
PR_NUMBER: ${{ github.event.number }} PR_NUMBER: ${{ github.event.number }}
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.pr_review \ python3 -m devx.ci.pr_review \
@@ -310,12 +249,11 @@ jobs:
--event APPROVE \ --event APPROVE \
--checklist-confirmed \ --checklist-confirmed \
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \ --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 - name: Squash merge with task ID
env: env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }} VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM DEVX_TASK_PREFIX: GRM
DEVX_VIKUNJA_PROJECT_ID: 6 DEVX_VIKUNJA_PROJECT_ID: 6
HEAD_REF: ${{ github.head_ref }} HEAD_REF: ${{ github.head_ref }}
+84 -216
View File
@@ -1,101 +1,121 @@
name: Post-merge name: Post-merge
# Runs on every push to master. A single workflow with conditional jobs # Runs on every push to master (after CI workflow merges a PR).
# for release, publish, wiki sync, badges, and Vikunja task updates. # 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) # The badges step always runs (even on release commits) so version
# ├── release (skip if release commit) # badge picks up the new __version__. It runs last so it sees the
# │ └── publish (needs release — builds & publishes to PyPI) # new version if release created one.
# ├── 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.
# #
# When release creates a "release: vX.Y.Z" commit and tag, the publish # 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 # step builds and publishes the package to the Gitea PyPI registry.
# Gitea PyPI registry. The release commit's post-merge run still updates # The release commit's post-merge run still updates badges. Other
# badges (version badge picks up the new version). Other jobs skip. # steps (sync-wiki, vikunja) skip on release commits.
on: on:
push: push:
branches: [master] branches: [master]
workflow_dispatch: workflow_dispatch:
concurrency:
group: post-merge-${{ github.ref }}
cancel-in-progress: true
env: env:
PIP_BREAK_SYSTEM_PACKAGES: "1"
PYTHONPATH: src
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
jobs: jobs:
detect-type: detect-and-configure:
runs-on: docker runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10 timeout-minutes: 10
defaults:
run:
shell: bash
outputs: outputs:
is-release: ${{ steps.check.outputs.is-release }} is-release: ${{ steps.check.outputs.is-release }}
is-automated: ${{ steps.check.outputs.is-automated }}
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 0
- name: Set up environment - name: Set up environment
env: env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci 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 - name: Check if this is a release commit
id: check id: check
env:
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.detect_release_commit 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 - name: Validate latest commit message
if: steps.check.outputs.is-automated == 'false'
env: env:
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM DEVX_TASK_PREFIX: GRM
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
git log -1 --format=%B > commit-msg.txt git log -1 --format=%B > commit-msg.txt
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
rm -f commit-msg.txt 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: release-and-maintain:
needs: [detect-type] needs: [detect-and-configure]
if: needs.detect-type.outputs.is-release == 'false' if: always() && needs.detect-and-configure.result == 'success'
runs-on: docker runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 15 timeout-minutes: 15
outputs: outputs:
tag: ${{ steps.release-tag.outputs.tag }} tag: ${{ steps.release-tag.outputs.tag }}
defaults:
run:
shell: bash
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
ref: master
token: ${{ secrets.CI_GITEA_API_TOKEN }} token: ${{ secrets.CI_GITEA_API_TOKEN }}
- name: Set up environment - name: Set up environment
env: env:
@@ -106,10 +126,11 @@ jobs:
run: | run: |
git config user.name "grm-ci-bot" git config user.name "grm-ci-bot"
git config user.email "grm-ci-bot@oblachno.fyi" git config user.email "grm-ci-bot@oblachno.fyi"
# --- release + publish (only if not a release commit) ---
- name: Run release - name: Run release
id: release-tag id: release-tag
if: needs.detect-and-configure.outputs.is-release == 'false' && needs.detect-and-configure.outputs.user-facing-changed == 'true'
env: env:
PYTHONPATH: src
DEVX_VERSION_FILE: src/grm/__init__.py DEVX_VERSION_FILE: src/grm/__init__.py
DEVX_TASK_PREFIX: GRM DEVX_TASK_PREFIX: GRM
DEVX_VIKUNJA_PROJECT_ID: 6 DEVX_VIKUNJA_PROJECT_ID: 6
@@ -117,207 +138,54 @@ jobs:
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release 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 - name: Build and publish release
if: steps.release-tag.outputs.tag != ''
env: env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.publish \ git fetch --tags
"${{ needs.release.outputs.tag }}" \ git checkout "${{ steps.release-tag.outputs.tag }}"
"${{ github.repository }}" --auto-login python3 -m devx.ci.publish "${{ steps.release-tag.outputs.tag }}" "${{ github.repository }}" --auto-login
- name: Notify on failure # --- sync-wiki + vikunja (skip on automated/release commits) ---
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
- name: Sync documentation to wiki - name: Sync documentation to wiki
if: needs.detect-and-configure.outputs.is-automated == 'false'
env: env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --verify 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 - name: Update Vikunja task
if: needs.detect-and-configure.outputs.is-automated == 'false'
env: env:
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }} VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM DEVX_TASK_PREFIX: GRM
DEVX_VIKUNJA_PROJECT_ID: 6 DEVX_VIKUNJA_PROJECT_ID: 6
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}" python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
- name: Notify on failure # --- badges (always run — even on release commits) ---
if: failure() - name: Generate and push badges
env: env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PRE_COMMIT_ALLOW_NO_CONFIG: "1"
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)"
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .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 - name: Notify on failure
if: failure() if: failure()
env: env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure --auto-login \ python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \ --repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \ --run-id "${{ github.run_id }}" \
--workflow "post-merge/configure-repo" \ --workflow "post-merge/release-and-maintain" \
--commit "${{ github.sha }}" --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`. 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 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). CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is not installed in the CI Docker image).
## Architecture ## Architecture
@@ -69,13 +69,13 @@ Every change to master goes through this workflow. No exceptions.
Branch protection and labels are automatically configured by Branch protection and labels are automatically configured by
`devx.tools.configure_repo` (run as `python -m devx.tools.configure_repo`), `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 post-merge workflow on every push to master.
The following rules are enforced for `master`: The following rules are enforced for `master`:
- **Require pull request**: No direct pushes to master - **Require pull request**: No direct pushes to master
- **Require approval review**: At least 1 `APPROVE` review before merge - **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 - **Block force pushes**: No history rewriting on master
The auto-merge workflow enforces the APPROVE review check programmatically 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 UX, documentation, workflow compliance, maintainability, resource
management, backwards compatibility, and logging. management, backwards compatibility, and logging.
**Automated review (CI `pr-review` job):** Every PR triggers an automated **Automated review (CI `validate` 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 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 `COMMENT` (no issues) or `REQUEST_CHANGES` (issues found) based on
the **[auto]** items in the checklist: the **[auto]** items in the checklist:
@@ -178,10 +179,10 @@ rejected.
Then add the `ready-to-merge` label. The auto-merge workflow will: 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 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) 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>` 4. Squash-merge with title: `GRM-N: <conventional commit message>`
5. The post-merge workflow marks the Vikunja task as done 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 **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 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 ### CI Path Filtering
The CI workflow includes a `pre-merge-check` job (runs after quality + The CI workflow's `validate` job includes a pre-merge validation step
detect-changes) that validates branch format, PR title, and Vikunja task that validates branch format, PR title, and Vikunja task match. This
match. This fails fast before expensive molecule tests run. fails fast before expensive molecule tests run.
The CI workflow includes a `detect-changes` job that checks whether any files The `validate` job also includes a `detect-changes` step that checks
under `ansible/` or `.ansible-lint` have changed. If no Ansible files are whether any files under `ansible/` or `.ansible-lint` have changed. If
changed, molecule tests are skipped — this prevents non-Ansible changes no Ansible files are changed, molecule tests are skipped — this prevents
(e.g., Python scripts, workflow YAML, docs) from being blocked by molecule non-Ansible changes (e.g., Python scripts, workflow YAML, docs) from
test infrastructure flakiness. being blocked by molecule test infrastructure flakiness.
### Dynamic Runner Discovery ### Dynamic Runner Discovery
Molecule tests are distributed across available Gitea Actions runners Molecule tests are distributed across available Gitea Actions runners
dynamically via `devx.molecule.discover_runners`. The `discover-runners` dynamically via `devx.molecule.discover_runners`. The `validate` job
job queries the Gitea API for runners at all levels (repo, org, instance) 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 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, (no admin scope), it falls back to the `MOLECULE_RUNNERS` repo variable,
then to a default of 3. then to a default of 3.
@@ -224,47 +226,27 @@ then to a default of 3.
### Automated Release Pipeline ### Automated Release Pipeline
After a PR is merged to master, the **post-merge workflow** After a PR is merged to master, the **post-merge workflow**
(`.gitea/workflows/post-merge.yml`) runs automatically. This single (`.gitea/workflows/post-merge.yml`) runs automatically. Consolidated
workflow consolidates release, wiki sync, badge generation, and into 2 jobs (from 7) to reduce runner overhead:
Vikunja task updates:
1. **detect-type** — Checks if the commit is a regular merge or a 1. **detect-and-configure** — Configures repo (branch protection, labels),
release commit (`release: vX.Y.Z`). All subsequent jobs skip for detects release commit, validates commit message. Outputs `is-release`
release commits (the `[skip ci]` tag also prevents re-triggering). and `is-automated` for the next job.
2. **release** — Runs `devx.ci.release` which: 2. **release-and-maintain** — Runs all post-merge maintenance as
- **Checks for user-facing changes** via `devx.ci.classify_changes` — if only conditional steps:
workflow/infrastructure files changed (`.gitea/`, `docs/`, `tests/`, - **release** (if not a release commit) — Runs `devx.ci.release` which
`AGENTS.md`, `Makefile`, etc.), the release is **skipped entirely** — no version checks for user-facing changes via `classify_changes` (skips if only
bump, no tag, no publish. This prevents unnecessary releases for CI/docs-only changes. workflow/infrastructure files changed), uses git-cliff for semver,
- Uses **git-cliff** to calculate the next semver version from conventional commits updates `__version__`, updates `CHANGELOG.md`, runs lint+tests, commits
- Updates `__version__` in `src/grm/__init__.py` (single source of truth) with `release: vX.Y.Z [skip ci]`, creates annotated tag, pushes to master.
- Updates `CHANGELOG.md` with the new version section - **publish** (if release created a tag) — Builds and publishes the
- **Runs `make lint-ruff` and `make pytest-cov`** to verify the release is healthy package to the Gitea PyPI registry. Checks out the release tag
- If lint or tests fail, **aborts immediately** — no commit, no tag within the same job.
- Commits with `release: vX.Y.Z [skip ci]` prefix (the `[skip ci]` prevents - **sync-wiki** (if not automated) — Syncs documentation to the Gitea wiki.
re-triggering post-merge on the release commit) - **vikunja** (if not automated) — Marks the corresponding Vikunja task as done.
- Creates an annotated tag `vX.Y.Z` on the release commit - **badges** (always) — Generates and pushes quality badge SVGs to the
- Pushes both the commit and tag to master `badges` branch. Fetches latest master first to pick up release commits.
- `--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.
### Smart CI: User-Facing vs Workflow-Only Changes ### 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 `devx.molecule.molecule_all` (dev tool) import `PLATFORMS` from it — this
avoids dev tools importing directly from CI modules. avoids dev tools importing directly from CI modules.
2. **Publish job** (in `post-merge.yml`, needs: release): 2. **Publish step** (in the `release-and-maintain` job, runs after the release step creates a tag):
- Runs after the release job creates a tag - Runs after the release step creates a tag
- Gets the tag from `needs.release.outputs.tag` - Gets the tag from the release step's output
- Builds the Python package - Builds the Python package
- Publishes to the Gitea PyPI registry - Publishes to the Gitea PyPI registry
- Creates a Gitea release with git-cliff-generated release notes - Creates a Gitea release with git-cliff-generated release notes
@@ -542,7 +524,7 @@ docs/
### Documentation Coverage ### Documentation Coverage
- `devx.ci.doc_coverage` checks that all CLI commands, Python modules, and CI scripts are documented - `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 - Enforced: 100% coverage for public CLI commands and major architectural components
### Updating Documentation ### Updating Documentation
@@ -572,7 +554,7 @@ the user should not need to specify which profile to use.
| Profile | Purpose | | 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 | | `molecule-runner` | Run 7 molecule scenarios across 4 platforms, report pass/fail |
| `dep-upgrader` | Python + Ansible dependency upgrades with molecule verification | | `dep-upgrader` | Python + Ansible dependency upgrades with molecule verification |
| `doc-sync-specialist` | Doc coverage, doc linting, wiki sync for grm docs | | `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 | | 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 | | PR ready for review | `pr-reviewer` | Foreground |
| Molecule tests need to run | `molecule-runner` | Background | | Molecule tests need to run | `molecule-runner` | Background |
| Dependency upgrade requested | `dep-upgrader` | 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`, use the `feedback` label plus a category label (`tooling`,
`ci-improvement`, `doc-improvement`, `workflow-improvement`). `ci-improvement`, `doc-improvement`, `workflow-improvement`).
Standard labels are created automatically by `configure_repo` (runs in Standard labels are created automatically by `configure_repo` (runs as
post-merge on every master push). If a label does not exist yet, the 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 subagent's issue creation will still succeed — labels can be added
afterwards. 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 $(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install
# Lean setup for CI jobs that need pytest + lint tools + runtime deps # Lean setup for CI jobs that need pytest + lint tools + runtime deps
# (detect-changes, discover-runners, pr-review, sync-wiki, badges) # (validate job steps: detect-changes, discover-runners, pr-review;
# badges job runs generate_badges.py which needs ruff, pyright, bandit # 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 setup-ci: $(VENV)/bin/activate .env configure-gitea-pypi
@$(PIP_INSTALL) install -e '.[ci,lint]' @$(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 @$(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 setup-quality: $(VENV)/bin/activate .env configure-gitea-pypi
@$(PIP_INSTALL) install -e '.[ci,lint]' @$(PIP_INSTALL) install -e '.[ci,lint]'
@$(BIN)/python -m devx.tools.install_tools @$(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 | | 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 | | `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 | | `post-merge.yml` | Push to `master` | Detect-and-configure + release-and-maintain (release, publish, wiki sync, badges, Vikunja task update) |
| `publish.yml` | Tag push (`v*`) | Build and publish package to PyPI, create Gitea release |
Every change to master goes through a mandatory PR workflow. No exceptions. 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 3. Wait for all CI checks to pass
4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated) 4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated)
5. The post-merge workflow marks the Vikunja task as done 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 ### 9. Post-Merge Automation
After the squash-merge: 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 **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 workflow** (`.gitea/workflows/release.yml`) triggers on push to `master` and automatically versions, tags, and publishes (see below). - The **release step** (in the `release-and-maintain` job) automatically versions, tags, and publishes (see below).
## Branch Protection (Required Gitea Settings) ## 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 pull request**: No direct pushes to master
- **Require approval review**: At least 1 `APPROVE` review before merge - **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 - **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. 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 ## 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 pull requests: compares `origin/master` against the PR head SHA
- For pushes to master: compares `HEAD~1` against `HEAD` - For pushes to master: compares `HEAD~1` against `HEAD`
- Outputs `ansible-changed` as `true` or `false` - 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 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 1. `make setup` — full environment setup
2. `make lint-all` — ruff + pyright + bandit + ansible-lint + checkmake 2. `make lint-all` — ruff + pyright + bandit + ansible-lint + checkmake
3. `make pytest-cov` — unit tests with 100% coverage enforcement 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 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 ## Automated Release Pipeline
After a PR is merged to master, the release pipeline runs automatically. 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 - Sets up full dev environment (`make setup`) so lint and tests can run
- Installs git-cliff (version 2.13.0) - Installs git-cliff (version 2.13.0)
- Configures git as `grm-ci-bot` - 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 - 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` - 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 git-cliff (version 2.13.0)
- Installs build tools (`build`, `twine`, `requests`, `python-dotenv`, `click`) - Installs build tools (`build`, `twine`, `requests`, `python-dotenv`, `click`)
- Validates `PYPI_TOKEN` is set (warns if missing) - 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`) ### Post-Merge Workflow (`.gitea/workflows/post-merge.yml`)
- Triggers on push to `master` - Triggers on push to `master`
- Consolidates release, wiki sync, badge generation, and Vikunja task updates into a single workflow - Consolidated from 7 jobs into 2 jobs to reduce runner overhead
- **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). - **detect-and-configure** — Configures repo (branch protection, labels), detects release commit, validates commit message. Outputs `is-release` and `is-automated` for the next job.
- **release** — Runs `devx.ci.release` (see Automated Release Pipeline below) - **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).
- **sync-wiki** — Syncs documentation to the Gitea wiki via `devx.ci.sync_wiki` - **validate-commit-msg step** — Validates the commit message follows conventional commit format.
- **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. - **configure-repo step** — Runs `devx.tools.configure_repo` to set up branch protection and labels.
- **vikunja** — Marks the corresponding Vikunja task as done via `devx.ci.post_merge` - **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 ### 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:** **CI behavior based on classification:**
- **Molecule tests**: Only run when `ansible/` or `.ansible-lint` files change - **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) - **Release dry-run**: Only runs when user-facing files change (release-dry-run step in the validate job)
- **Quality job** (lint, unit tests, coverage, doc-coverage): Always runs - **Validate job** (lint, unit tests, coverage, doc-coverage): Always runs
- **Release workflow**: `release.py` calls `classify_changes` to check if any - **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 user-facing files changed since the last tag. If not, the release is skipped
entirely — no version bump, no tag, no publish. entirely — no version bump, no tag, no publish.
### Dynamic Runner Discovery ### Dynamic Runner Discovery
Molecule tests are distributed across available Gitea Actions runners 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 registered runners at three levels (repo, org, instance) and generates
a matrix of runner indices. If the API query fails (e.g., no admin a matrix of runner indices. If the API query fails (e.g., no admin
access for instance-level runners), it falls back to the access for instance-level runners), it falls back to the
@@ -263,15 +272,15 @@ feature branches.
### Release Commit Detection ### 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 `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 a release commit (format: `release: vX.Y.Z`). When a release commit
is detected, all post-merge jobs (release, sync-wiki, badges, vikunja) is detected, all subsequent steps in the `release-and-maintain` job (release, publish, sync-wiki, vikunja)
are skipped — the tag push triggers the publish workflow instead. are skipped — the tag push triggers the publish step instead.
### Badge Generation and Push ### 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: `devx.ci.push_badges` which:
1. Fetches the latest master and hard-resets to it (picks up release commits) 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` 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 4. Copies SVG files to the branch root
5. Force-pushes the branch to the remote 5. Force-pushes the branch to the remote
The badges job depends on the `release` job and uses `if: always()` so it The badges step runs with `if: always()` so it
runs even if release fails or is skipped. This ensures the version badge 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 always reflects the actual state of the repository after any release
commits have been pushed. 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`. 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 7. **Address comments** — fix each comment, commit, push, re-review
8. **Approve** — post an `APPROVE` review via `devx.ci.pr_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 ### 1. Create Vikunja task
Create a task in Vikunja project 6 to get a `GRM-N` identifier. 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 3. Wait for all CI checks to pass
4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated) 4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated)
5. The post-merge workflow marks the Vikunja task as done 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. > **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 (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 pull request**: No direct pushes to master
- **Require approval review**: At least 1 `APPROVE` review before merge - **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 - **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. 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) **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. **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. **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) **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) **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. **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` | Full setup: venv, deps, hooks, CI tools |
| `make setup-ci` | Lean setup for CI jobs (pytest + lint, no Ansible collections) | | `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-molecule` | Full setup for molecule testing |
| `make setup-release` | Setup for release jobs (git-cliff, tea, lint tools) | | `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` | | `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) - `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) - `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 ### 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 ## Integration Tests
+4 -3
View File
@@ -27,7 +27,8 @@ version = {attr = "grm.__version__"}
[project.optional-dependencies] [project.optional-dependencies]
# Minimal deps for CI scripts that only need click/dotenv # 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 = [ ci = [
"pytest==9.1.1", "pytest==9.1.1",
"pytest-cov==7.1.0", "pytest-cov==7.1.0",
@@ -36,7 +37,7 @@ ci = [
# Reusable CI/CD and dev tools (auto-merge, pr-review, pre-push checks, etc.) # Reusable CI/CD and dev tools (auto-merge, pr-review, pre-push checks, etc.)
"devx==0.40.0", "devx==0.40.0",
] ]
# Lint and type-checking tools (quality job) # Lint and type-checking tools (validate job)
lint = [ lint = [
"ruff==0.15.20", "ruff==0.15.20",
"pyright==1.1.411", "pyright==1.1.411",
@@ -45,7 +46,7 @@ lint = [
"pre-commit==4.6.0", "pre-commit==4.6.0",
"ansible-lint==26.4.0", "ansible-lint==26.4.0",
] ]
# Molecule testing (molecule-tests job) # Molecule testing (molecule-tests job — stays as a separate CI job)
molecule = [ molecule = [
"molecule==26.6.0", "molecule==26.6.0",
"molecule-docker==2.1.0", "molecule-docker==2.1.0",