name: CI on: pull_request: types: [opened, synchronize] workflow_dispatch: env: CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} jobs: quality: runs-on: docker container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Set up environment env: CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} run: make setup-image EXTRAS=ci,lint - name: Lint all run: | . .venv/bin/activate 2>/dev/null || true export PATH="$HOME/.local/bin:$PATH" make lint-all - name: Unit tests with 100% coverage run: | . .venv/bin/activate 2>/dev/null || true 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 run: | . .venv/bin/activate 2>/dev/null || true export PATH="$HOME/.local/bin:$PATH" make devx-docs-check - name: Translation completeness check run: | . .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 - name: Dependency security scan run: | . .venv/bin/activate 2>/dev/null || true # Install pip in venv if missing (needed by pip-audit) .venv/bin/python -m ensurepip 2>/dev/null || true PIPAPI_PYTHON_LOCATION=$PWD/.venv/bin/python \ pip-audit --desc --skip-editable 2>&1 || true - name: Workflow dry-run validation run: | . .venv/bin/activate 2>/dev/null || true export PATH="$HOME/.local/bin:$PATH" # Best-effort: only runs if act_runner is installed if command -v act_runner >/dev/null 2>&1; then make workflow-dryrun 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_TOKEN: ${{ secrets.CI_GITEA_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_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} run: make setup-image EXTRAS=ci - name: Detect changed paths id: detect env: PYTHONPATH: src DEVX_TASK_PREFIX: GRM run: | . .venv/bin/activate 2>/dev/null || true python3 -m devx.ci.classify_changes \ --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 - name: Validate auto-merge preconditions env: CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }} DEVX_TASK_PREFIX: GRM DEVX_VIKUNJA_PROJECT_ID: 6 HEAD_REF: ${{ github.head_ref }} 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 \ --branch "$HEAD_REF" \ --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 env: CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} run: make setup-image EXTRAS=ci - name: Discover available runners id: discover env: CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_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 molecule-tests: needs: [quality, detect-changes, discover-runners] if: needs.detect-changes.outputs.ansible-changed == 'true' runs-on: docker container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest timeout-minutes: 10 strategy: fail-fast: true max-parallel: 3 matrix: runner-index: [1, 2, 3, 4, 5, 6] steps: - uses: actions/checkout@v4 - name: Set up environment env: CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} run: make setup-image EXTRAS=ci,molecule - name: Install Ansible collections run: | . .venv/bin/activate 2>/dev/null || true python3 -m devx.tools.setup --skip-install --no-pre-commit --no-tea-login - name: Discover assigned test pairs env: RUNNER_INDEX: ${{ matrix.runner-index }} MAX_RUNNERS: ${{ needs.discover-runners.outputs.runner-count }} PYTHONPATH: src run: | . .venv/bin/activate 2>/dev/null || true python3 -m devx.molecule.distribute_molecule \ --runner-index "$RUNNER_INDEX" \ --max-runners "$MAX_RUNNERS" \ --github-env --skip-if-excess - name: Run molecule tests if: env.SKIP != 'true' run: | . .venv/bin/activate 2>/dev/null || true if [ -z "$TEST_PAIRS" ]; then exit 0; fi if ! python3 -c "import docker; docker.from_env().ping()" 2>/dev/null; then echo "Docker not available in CI container — skipping molecule tests" exit 0 fi echo "$CI_GITEA_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_TOKEN: ${{ secrets.CI_GITEA_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_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} run: make setup-image EXTRAS=ci - name: Run automated PR review env: CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_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. # 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] 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') 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 with: fetch-depth: 0 token: ${{ secrets.CI_GITEA_TOKEN }} - name: Set up environment env: CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} run: make setup-image EXTRAS=ci - name: Post approval review env: CI_GITEA_TOKEN: ${{ secrets.REVIEW_GITEA_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 \ "$PR_NUMBER" \ "$REPOSITORY" \ --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)." - name: Squash merge with task ID env: CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }} PYTHONPATH: src DEVX_TASK_PREFIX: GRM DEVX_VIKUNJA_PROJECT_ID: 6 HEAD_REF: ${{ github.head_ref }} PR_TITLE: ${{ github.event.pull_request.title }} REPOSITORY: ${{ github.repository }} PR_NUMBER: ${{ github.event.number }} run: | . .venv/bin/activate 2>/dev/null || true python3 -m devx.ci.auto_merge \ "$HEAD_REF" \ "$PR_TITLE" \ "$REPOSITORY" \ "$PR_NUMBER"