diff --git a/.devin/skills/testing-and-debugging/SKILL.md b/.devin/skills/testing-and-debugging/SKILL.md index ff89c1a..555f0f4 100644 --- a/.devin/skills/testing-and-debugging/SKILL.md +++ b/.devin/skills/testing-and-debugging/SKILL.md @@ -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 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 756e0a6..865964b 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -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 }} diff --git a/.gitea/workflows/post-merge.yml b/.gitea/workflows/post-merge.yml index 36b6bdf..7db9d88 100644 --- a/.gitea/workflows/post-merge.yml +++ b/.gitea/workflows/post-merge.yml @@ -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 }}" diff --git a/AGENTS.md b/AGENTS.md index 5b86be6..9be2b8f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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: `) 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: ` 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. diff --git a/Makefile b/Makefile index 234d0f7..b5f9a38 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/docs/tech/ci-cd-workflow.md b/docs/tech/ci-cd-workflow.md index 2e3f26b..3e15107 100644 --- a/docs/tech/ci-cd-workflow.md +++ b/docs/tech/ci-cd-workflow.md @@ -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 ` (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. diff --git a/docs/tech/contributing.md b/docs/tech/contributing.md index faf258f..98f2265 100644 --- a/docs/tech/contributing.md +++ b/docs/tech/contributing.md @@ -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 `, 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 `, 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 ` (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 ` 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. diff --git a/docs/tech/decision-log.md b/docs/tech/decision-log.md index 5f65370..45e4332 100644 --- a/docs/tech/decision-log.md +++ b/docs/tech/decision-log.md @@ -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 `. 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 `. 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) diff --git a/docs/tech/development-setup.md b/docs/tech/development-setup.md index e9109cd..c799e94 100644 --- a/docs/tech/development-setup.md +++ b/docs/tech/development-setup.md @@ -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` | diff --git a/docs/tech/testing-strategy.md b/docs/tech/testing-strategy.md index 081f288..dc7cabb 100644 --- a/docs/tech/testing-strategy.md +++ b/docs/tech/testing-strategy.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 26d6d46..ed12225 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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",