From f5d3b72a38ab0fa85b4e3b8caee1bd64e1305372 Mon Sep 17 00:00:00 2001 From: emil User Date: Wed, 12 Aug 2026 19:32:28 +0000 Subject: [PATCH] DEVX-157: docs: add ADR-0003 and update docs for composite actions --- .gitea/actions/notify-failure/action.yml | 47 +++++++ .gitea/actions/quality-checks/action.yml | 89 +++++++++++++ .gitea/actions/setup-env/action.yml | 37 ++++++ .gitea/workflows/build-images.yml | 31 ++--- .gitea/workflows/ci.yml | 72 ++--------- .gitea/workflows/post-merge.yml | 52 ++------ AGENTS.md | 68 ++++++++++ CHANGELOG.md | 7 + README.md | 11 ++ docs/decisions/0003-composite-actions.md | 155 +++++++++++++++++++++++ docs/tech/architecture.md | 18 +++ docs/tech/ci-cd-workflow.md | 50 +++++--- 12 files changed, 504 insertions(+), 133 deletions(-) create mode 100644 .gitea/actions/notify-failure/action.yml create mode 100644 .gitea/actions/quality-checks/action.yml create mode 100644 .gitea/actions/setup-env/action.yml create mode 100644 docs/decisions/0003-composite-actions.md diff --git a/.gitea/actions/notify-failure/action.yml b/.gitea/actions/notify-failure/action.yml new file mode 100644 index 0000000..ad6c888 --- /dev/null +++ b/.gitea/actions/notify-failure/action.yml @@ -0,0 +1,47 @@ +name: 'Notify on failure' +description: 'Create a Gitea issue when a CI workflow fails (calls devx.ci.notify_failure)' + +# Composite action for the common "Notify on failure" step pattern. +# Replaces the repeated inline: +# - 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 \ +# --repo "${{ github.repository }}" \ +# --run-id "${{ github.run_id }}" \ +# --workflow "ci/validate" \ +# --commit "${{ github.sha }}" \ +# --auto-login +# +# Gitea 1.27 notes: +# - `if: failure()` is evaluated in the calling workflow's context and +# propagates correctly to composite action steps. +# - `secrets` are not accessible here; the calling workflow's top-level +# `env:` CI_GITEA_API_TOKEN is used via `${{ env.* }}`. + +inputs: + workflow: + description: 'Workflow/job name used in the Gitea issue title (e.g., ci/validate)' + required: true + +runs: + using: 'composite' + steps: + - name: Notify on failure + if: failure() + shell: bash + env: + CI_GITEA_API_TOKEN: ${{ env.CI_GITEA_API_TOKEN }} + run: | + . .venv/bin/activate 2>/dev/null || true + export PATH="$HOME/.local/bin:$PATH" + python3 -m devx.ci.notify_failure \ + --repo "${{ github.repository }}" \ + --run-id "${{ github.run_id }}" \ + --workflow "${{ inputs.workflow }}" \ + --commit "${{ github.sha }}" \ + --auto-login diff --git a/.gitea/actions/quality-checks/action.yml b/.gitea/actions/quality-checks/action.yml new file mode 100644 index 0000000..81c726c --- /dev/null +++ b/.gitea/actions/quality-checks/action.yml @@ -0,0 +1,89 @@ +name: 'Quality checks' +description: 'Run lint, unit tests with coverage, test speed, docs, translations, and security scan' + +# Composite action for the 6-step quality check sequence used by the +# devx validate job. Replaces the inline block: +# - Lint all +# - Unit tests with 100% coverage +# - Check unit test speed +# - Documentation gate (coverage + stale refs + lint + version refs + prose) +# - Translation completeness check +# - Dependency security scan +# +# Each step activates the venv defensively (`. .venv/bin/activate 2>/dev/null +# || true`) so the action works whether or not the setup step created a +# venv at the repo root (pre-built CI images symlink /opt/venv to .venv). +# +# Gitea 1.27 notes: +# - Every `run` step needs explicit `shell:`. +# - Inputs are string-typed; numeric thresholds are passed through as +# strings to `devx.tools.check_test_speed`. + +inputs: + package: + description: 'Package name for doc version checks (e.g., devx, grm). Empty = no DEVX_DOC_VERSIONS_PKG override.' + required: false + default: '' + test-speed-max: + description: 'Max total test seconds (passed to check_test_speed --max-seconds)' + required: false + default: '15' + test-speed-max-single: + description: 'Max single test seconds (passed to check_test_speed --max-single-seconds)' + required: false + default: '0.5' + translations-file: + description: 'Path to translations.json (empty = default location src/devx/translations.json)' + required: false + default: '' + +runs: + using: 'composite' + steps: + - name: Lint all + shell: bash + run: | + . .venv/bin/activate 2>/dev/null || true + export PATH="$HOME/.local/bin:$PATH" + make lint-all + - name: Unit tests with 100% coverage + shell: bash + run: | + . .venv/bin/activate 2>/dev/null || true + make pytest-cov + - name: Check unit test speed + shell: bash + run: | + . .venv/bin/activate 2>/dev/null || true + python3 -m devx.tools.check_test_speed \ + --max-seconds "${{ inputs.test-speed-max }}" \ + --max-single-seconds "${{ inputs.test-speed-max-single }}" + - name: Documentation gate (coverage + stale refs + lint + version refs + prose) + shell: bash + env: + DEVX_DOC_COVERAGE_STRICT: "1" + DEVX_VALE_LEVEL: warning + run: | + . .venv/bin/activate 2>/dev/null || true + export PATH="$HOME/.local/bin:$PATH" + if [ -n "${{ inputs.package }}" ]; then + export DEVX_DOC_VERSIONS_PKG="${{ inputs.package }}" + fi + make devx-docs-check + - name: Translation completeness check + shell: bash + run: | + . .venv/bin/activate 2>/dev/null || true + if [ -n "${{ inputs.translations-file }}" ]; then + python3 -m devx.ci.check_translations --translations "${{ inputs.translations-file }}" + else + python3 -m devx.ci.check_translations + fi + - name: Dependency security scan + shell: bash + 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 diff --git a/.gitea/actions/setup-env/action.yml b/.gitea/actions/setup-env/action.yml new file mode 100644 index 0000000..b41a5c2 --- /dev/null +++ b/.gitea/actions/setup-env/action.yml @@ -0,0 +1,37 @@ +name: 'Set up environment' +description: 'Set up CI environment with venv and PATH (calls make setup-image)' + +# Composite action for the common "Set up environment" step pattern. +# Replaces the repeated inline: +# - name: Set up environment +# env: +# CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} +# run: make setup-image +# +# Gitea 1.27 notes: +# - Every `run` step needs explicit `shell:`. +# - Composite actions cannot access `secrets` directly; they read from +# the `env:` context which the calling workflow must populate. +# - The calling workflow's top-level `env:` block (CI_GITEA_API_TOKEN, +# CI_GITEA_USERNAME) is visible here via `${{ env.* }}`. + +inputs: + extras: + description: 'Extra pip install groups passed to make setup-image (e.g., ci,lint,release)' + required: false + default: '' + +runs: + using: 'composite' + steps: + - name: Set up environment + shell: bash + env: + CI_GITEA_API_TOKEN: ${{ env.CI_GITEA_API_TOKEN }} + CI_GITEA_USERNAME: ${{ env.CI_GITEA_USERNAME }} + run: | + if [ -n "${{ inputs.extras }}" ]; then + make setup-image EXTRAS="${{ inputs.extras }}" + else + make setup-image + fi diff --git a/.gitea/workflows/build-images.yml b/.gitea/workflows/build-images.yml index 6de5a87..1f0e750 100644 --- a/.gitea/workflows/build-images.yml +++ b/.gitea/workflows/build-images.yml @@ -29,14 +29,20 @@ concurrency: group: build-images cancel-in-progress: false +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: build-and-push: runs-on: docker container: image: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest credentials: - username: ${{ vars.CI_GITEA_USERNAME }} - password: ${{ secrets.CI_GITEA_API_TOKEN }} + username: ${{ env.CI_GITEA_USERNAME }} + password: ${{ env.CI_GITEA_API_TOKEN }} timeout-minutes: 30 outputs: is-release: ${{ steps.check.outputs.is-release }} @@ -101,20 +107,9 @@ jobs: --tag latest \ --registry git.oblachno.oblachno.fyi \ --push - - 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 \ - --repo "${{ github.repository }}" \ - --run-id "${{ github.run_id }}" \ - --workflow "build-images/build-and-push" \ - --commit "${{ github.sha }}" \ - --auto-login + - uses: ./.gitea/actions/notify-failure + with: + workflow: "build-images/build-and-push" cleanup: needs: [build-and-push] @@ -123,8 +118,8 @@ jobs: container: image: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest credentials: - username: ${{ vars.CI_GITEA_USERNAME }} - password: ${{ secrets.CI_GITEA_API_TOKEN }} + username: ${{ env.CI_GITEA_USERNAME }} + password: ${{ env.CI_GITEA_API_TOKEN }} timeout-minutes: 10 steps: - uses: actions/checkout@v4 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index e1eee70..ed77c32 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -21,8 +21,8 @@ jobs: container: image: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest credentials: - username: ${{ vars.CI_GITEA_USERNAME }} - password: ${{ secrets.CI_GITEA_API_TOKEN }} + username: ${{ env.CI_GITEA_USERNAME }} + password: ${{ env.CI_GITEA_API_TOKEN }} timeout-minutes: 15 defaults: run: @@ -33,43 +33,12 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up environment - env: - CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} - run: make setup-image - # --- quality steps --- - - 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: Check unit test speed - run: | - . .venv/bin/activate 2>/dev/null || true - python3 -m devx.tools.check_test_speed --max-seconds 15 --max-single-seconds 0.5 - - name: Documentation gate (coverage + stale refs + lint + version refs + prose) - env: - DEVX_DOC_COVERAGE_STRICT: "1" - 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 - - 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 + - uses: ./.gitea/actions/setup-env + - uses: ./.gitea/actions/quality-checks + with: + package: devx + test-speed-max: "15" + test-speed-max-single: "0.5" - name: Workflow dry-run validation run: | . .venv/bin/activate 2>/dev/null || true @@ -121,19 +90,9 @@ jobs: . .venv/bin/activate 2>/dev/null || true export PATH="$HOME/.local/bin:$PATH" python3 -m devx.ci.release --dry-run - - 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 \ - --repo "${{ github.repository }}" \ - --run-id "${{ github.run_id }}" \ - --workflow "ci/validate" \ - --commit "${{ github.sha }}" \ - --auto-login + - uses: ./.gitea/actions/notify-failure + with: + workflow: "ci/validate" auto-merge: # Auto-merge runs after validate passes. It reads the task ID @@ -147,8 +106,8 @@ jobs: container: image: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest credentials: - username: ${{ vars.CI_GITEA_USERNAME }} - password: ${{ secrets.CI_GITEA_API_TOKEN }} + username: ${{ env.CI_GITEA_USERNAME }} + password: ${{ env.CI_GITEA_API_TOKEN }} timeout-minutes: 10 defaults: run: @@ -158,10 +117,7 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.CI_GITEA_API_TOKEN }} - - name: Set up environment - env: - CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} - run: make setup-image + - uses: ./.gitea/actions/setup-env - name: Post approval review env: REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }} diff --git a/.gitea/workflows/post-merge.yml b/.gitea/workflows/post-merge.yml index 916ef1d..2bad4ab 100644 --- a/.gitea/workflows/post-merge.yml +++ b/.gitea/workflows/post-merge.yml @@ -38,8 +38,8 @@ jobs: container: image: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest credentials: - username: ${{ vars.CI_GITEA_USERNAME }} - password: ${{ secrets.CI_GITEA_API_TOKEN }} + username: ${{ env.CI_GITEA_USERNAME }} + password: ${{ env.CI_GITEA_API_TOKEN }} timeout-minutes: 10 defaults: run: @@ -52,10 +52,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up environment - env: - CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} - run: make setup-image + - uses: ./.gitea/actions/setup-env - name: Ensure branch protection and labels env: DEVX_REPO_NAME: devx @@ -85,19 +82,9 @@ jobs: --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 \ - --repo "${{ github.repository }}" \ - --run-id "${{ github.run_id }}" \ - --workflow "post-merge/detect-and-configure" \ - --commit "${{ github.sha }}" \ - --auto-login + - uses: ./.gitea/actions/notify-failure + with: + workflow: "post-merge/detect-and-configure" release-and-maintain: needs: [detect-and-configure] @@ -106,8 +93,8 @@ jobs: container: image: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest credentials: - username: ${{ vars.CI_GITEA_USERNAME }} - password: ${{ secrets.CI_GITEA_API_TOKEN }} + username: ${{ env.CI_GITEA_USERNAME }} + password: ${{ env.CI_GITEA_API_TOKEN }} timeout-minutes: 15 outputs: tag: ${{ steps.release-tag.outputs.tag }} @@ -120,10 +107,9 @@ jobs: fetch-depth: 0 ref: master token: ${{ secrets.CI_GITEA_API_TOKEN }} - - name: Set up environment - env: - CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} - run: make setup-image EXTRAS=release + - uses: ./.gitea/actions/setup-env + with: + extras: "release" - name: Configure git env: CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} @@ -179,16 +165,6 @@ jobs: 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 }} - run: | - . .venv/bin/activate 2>/dev/null || true - export PATH="$HOME/.local/bin:$PATH" - python3 -m devx.ci.notify_failure \ - --repo "${{ github.repository }}" \ - --run-id "${{ github.run_id }}" \ - --workflow "post-merge/release-and-maintain" \ - --commit "${{ github.sha }}" \ - --auto-login + - uses: ./.gitea/actions/notify-failure + with: + workflow: "post-merge/release-and-maintain" diff --git a/AGENTS.md b/AGENTS.md index 0aa5dde..f5bdba2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,74 @@ The pre-commit hook runs actionlint automatically when workflow files change. The CI `validate` job runs `make setup-image` 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). +## Composite Actions (`.gitea/actions/`) + +Reusable Gitea composite actions eliminate repeated multi-step sequences +across workflows. Each action lives in its own directory under +`.gitea/actions//action.yml` and is referenced via +`uses: ./.gitea/actions/`. + +### Available Composite Actions + +| Action | Purpose | Inputs | +|--------|---------|--------| +| `setup-env` | Run `make setup-image` (with optional `EXTRAS=`) | `extras` (default: `""`) | +| `notify-failure` | Create a Gitea issue on job failure via `devx.ci.notify_failure` | `workflow` (required) | +| `quality-checks` | 6-step quality sequence: lint, tests, speed, docs, translations, security | `package`, `test-speed-max`, `test-speed-max-single`, `translations-file` | + +### Gitea 1.27 Constraints + +- Every `run` step in a composite action MUST have explicit `shell:`. +- Composite actions CANNOT access `secrets` directly. They read from + the calling workflow's `env:` context (for example, `${{ env.CI_GITEA_API_TOKEN }}`). + The calling workflow's top-level `env:` block must define the required + env vars. +- `if: failure()` in a composite action step is evaluated in the + calling workflow's job-status context. + +### Usage Pattern + +```yaml +jobs: + validate: + env: + CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }} + CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} + steps: + - uses: actions/checkout@v4 + - uses: ./.gitea/actions/setup-env + - uses: ./.gitea/actions/quality-checks + with: + package: devx + - uses: ./.gitea/actions/notify-failure + with: + workflow: "ci/validate" +``` + +### Same-Repo Copies (No Cross-Repo References) + +Each repo (`devx`, `grm`, `infra`) gets its own copy of the composite +actions under its `.gitea/actions/` directory. There is no +`uses: oblachno-oss/devx/.gitea/actions/...@vX.Y.Z` reference. This +avoids a single-point-of-failure where a bad devx master commit would +break all repos' CI simultaneously. See ADR-0003 for the full +rationale. + +### When NOT to Use Composite Actions + +- **`make setup-release` / `make setup-ci`**: the `setup-env` action + only wraps `make setup-image`. Workflows that use other setup targets + (for example, `build-images.yml` uses `make setup-release`) keep the inline + setup step. +- **Deploy-specific setup**: infra deploy workflows have additional + steps (`install-collections`, `setup-vault`, `setup_ssh_key`) that + are NOT part of the common setup. The `setup-env` action only + replaces the `make setup-image` step; deploy-specific steps stay + inline. +- **Custom notification**: `security-scan.yml` uses a Mattermost + webhook, not `devx.ci.notify_failure`. The `notify-failure` action + does not apply. + ## Architecture devx is a reusable Python package providing development and CI/CD tools for oblachno-oss projects. diff --git a/CHANGELOG.md b/CHANGELOG.md index 5444b1f..bb0b9a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +### Ci + +- Add composite actions (setup-env, notify-failure, quality-checks) in `.gitea/actions/` +- Convert ci.yml, post-merge.yml, build-images.yml to use composite actions + ## [0.50.4] - 2026-08-12 ### Bug Fixes diff --git a/README.md b/README.md index d038d50..7250ef2 100644 --- a/README.md +++ b/README.md @@ -445,6 +445,17 @@ src/devx/ └── molecule/ # Optional molecule testing helpers (for Ansible projects) ``` +### Composite Actions (`.gitea/actions/`) + +Reusable Gitea composite actions for CI workflow steps: + +- `setup-env` — runs `make setup-image` (with optional `EXTRAS=`) +- `notify-failure` — creates a Gitea issue on job failure +- `quality-checks` — 6-step quality gate (lint, tests, speed, docs, translations, security) + +Each consumer repo gets its own copy (no cross-repo references). See +ADR-0003 for the design rationale. + ### Design principles - **Self-contained package** — `src/devx/` never imports from scripts outside the package diff --git a/docs/decisions/0003-composite-actions.md b/docs/decisions/0003-composite-actions.md new file mode 100644 index 0000000..b38c198 --- /dev/null +++ b/docs/decisions/0003-composite-actions.md @@ -0,0 +1,155 @@ +# ADR-0003: Composite Actions for CI Workflow Reuse + +Date: 2026-08-12 +Status: Accepted + +## Context + +The devx repository's Gitea Actions workflows (`.gitea/workflows/ci.yml`, +`post-merge.yml`, `build-images.yml`) repeated multi-step +sequences across jobs: + +1. **Set up environment** — `make setup-image` (optionally with `EXTRAS=`). + Appeared verbatim in 4 jobs across `ci.yml` and `post-merge.yml`, each + with the same `CI_GITEA_API_TOKEN` env wiring. + +2. **Notify on failure** — `python3 -m devx.ci.notify_failure ... --auto-login` + with venv activation, PATH export, and 5 fixed CLI args. Appeared in + 4 jobs (`ci/validate`, `post-merge/detect-and-configure`, + `post-merge/release-and-maintain`, `build-images/build-and-push`), + each differing only in the `--workflow` string. + +3. **Quality checks** — a 6-step sequence (lint-all, pytest-cov, + check-test-speed, devx-docs-check, check-translations, pip-audit) + with venv activation boilerplate on every step. Appeared once in + `ci.yml` validate job, but the same sequence is needed by `grm` and + `infra` (Phase 2b/2c of the cross-repo refactoring plan). + +This duplication had the following costs: + +- **Drift risk**: a fix to notify-failure (for example, new flag, + different env var) had to be applied to 4 places; missing one caused + inconsistent failure notifications. +- **Workflow YAML noise**: the 6-step quality block obscured the + validate job's actual structure (detect-changes, pr-review, + release-dry-run). +- **Cross-repo reuse blocked**: `grm` and `infra` could not adopt the + same quality-checks sequence without copy-pasting the inline steps, + which would amplify the drift problem across 3 repos. +- **Gitea 1.27 constraints**: every `run` step needs explicit `shell:`; + composite actions cannot access `secrets` directly (only `env:`). + These constraints had to be re-discovered and re-applied per step. + +## Decision + +Introduce three Gitea composite actions in `.gitea/actions/`: + +### 1. `setup-env/action.yml` + +Wraps the `make setup-image` call. Single input `extras` (default empty) +forwarded to `make setup-image EXTRAS=`. Reads `CI_GITEA_API_TOKEN` and +`CI_GITEA_USERNAME` from the calling workflow's `env:` context. + +### 2. `notify-failure/action.yml` + +Wraps the `devx.ci.notify_failure` invocation. Single required input +`workflow` (the workflow/job name for the Gitea issue title). Step is +gated by `if: failure()` so it only runs on job failure. Reads +`CI_GITEA_API_TOKEN` from the calling workflow's `env:` context. + +### 3. `quality-checks/action.yml` + +Wraps the 6-step quality sequence. Inputs: + +- `package` (default empty) — sets `DEVX_DOC_VERSIONS_PKG` for doc + version checks (for example, `devx`, `grm`). +- `test-speed-max` (default `15`) — total test seconds threshold. +- `test-speed-max-single` (default `0.5`) — per-test seconds threshold. +- `translations-file` (default empty) — path to `translations.json` + for repos whose translations live outside `src/devx/`. + +Each step activates the venv defensively +(`. .venv/bin/activate 2>/dev/null || true`) so the action works with +both pre-built CI images (which symlink `/opt/venv` to `.venv`) and +fresh `make setup-image` runs. + +### Adoption Scope + +- **`ci.yml` validate job**: `setup-env` + `quality-checks` + + `notify-failure`. +- **`ci.yml` auto-merge job**: `setup-env` only (no quality checks, + no notify-failure — auto-merge failure is surfaced by the validate + job's notify-failure). +- **`post-merge.yml` detect-and-configure**: `setup-env` + + `notify-failure`. +- **`post-merge.yml` release-and-maintain**: `setup-env` (with + `extras: "release"`) + `notify-failure`. +- **`build-images.yml` build-and-push**: `notify-failure` only. The + setup steps use `make setup-release` and `make setup-ci` (not + `make setup-image`), so `setup-env` does not apply. The cleanup job + has no notify-failure step (it only runs on build-and-push success). + +### Same-Repo Copies (No Cross-Repo References) + +Each consumer repo (`devx`, `grm`, `infra`) gets its own copy of the +composite actions under its `.gitea/actions/` directory. There is no +`uses: oblachno-oss/devx/.gitea/actions/...@vX.Y.Z` reference. + +This avoids a single-point-of-failure where a bad `devx` master commit +would break all three repos' CI simultaneously. The cost is three +copies of ~30 lines of YAML each, updated manually when a composite +action changes. Given the stability of these patterns (the inline +versions were unchanged for months), this cost is acceptable. + +## Consequences + +### Positive + +- **Workflow YAML is shorter and clearer**: the validate job's + quality block collapses from 32 lines to 5 lines. The intent + (`uses: ./.gitea/actions/quality-checks`) is more legible than + 6 individually wrapped steps. +- **Drift eliminated**: a change to notify-failure (new flag, different + env var) is applied in one file. All 4 calling sites pick it up. +- **Cross-repo reuse enabled**: Phase 2b (`grm`) and Phase 2c (`infra`) + copy the same `action.yml` files and adopt the same `uses:` pattern. + The quality-checks sequence is now portable. +- **Gitea 1.27 constraints centralized**: the `shell: bash` and + `env:` (not `secrets`) patterns are encoded once per action, not + re-derived per step. +- **No release triggered**: changes to `.gitea/**` are classified as + workflow-only by `devx.ci.classify_changes`. Phase 2a does not + produce a new devx version. `grm`/`infra` bump to the Phase 1 + release (v0.50.4), not a Phase 2a version. + +### Negative + +- **Three copies of each action**: when a composite action changes, + the change must be applied to `devx`, `grm`, and `infra` + independently. This is intentional (see Same-Repo Copies preceding) + but is a maintenance cost. +- **Composite action debugging is harder**: Gitea's log output for + composite action steps is nested under the action name. Finding the + failing step requires reading one more level of indentation. +- **`env:` propagation is implicit**: the calling workflow's top-level + `env:` block must define `CI_GITEA_API_TOKEN` for the composite + action to read it. A workflow that omits this will see an empty + token at runtime, not at lint time. actionlint does not catch this. +- **`quality-checks` is devx-shaped**: the `package` and + `translations-file` inputs exist because consumer repos (for example, + `grm`) have translations files outside the default + `src/devx/translations.json` location and need doc version checks + targeting their own package name. + A repo with a different translations path or package layout would need + a new input or a different action. This is acceptable for the current + 3-repo scope. + +### Neutral + +- **`if: failure()` is preserved**: the `notify-failure` composite + action's step has `if: failure()`, which is evaluated in the + calling workflow's job-status context. This is the standard Gitea + Actions pattern for post-failure notification. +- **Venv activation is defensive**: `. .venv/bin/activate 2>/dev/null + || true` does not fail if the venv is missing (pre-built image path) + or already active. This matches the inline pattern's behavior. diff --git a/docs/tech/architecture.md b/docs/tech/architecture.md index 6a469f2..2df9d49 100644 --- a/docs/tech/architecture.md +++ b/docs/tech/architecture.md @@ -453,6 +453,24 @@ v2 failures. Supports loading custom platforms from a JSON file. - **Secrets via environment** — secrets are passed via environment variables, never on the command line. +## Composite Actions (`.gitea/actions/`) + +Reusable Gitea composite actions eliminate repeated multi-step sequences +across workflows. Each action lives in `.gitea/actions//action.yml` +and is referenced via `uses: ./.gitea/actions/`. + +| Action | Purpose | +|--------|---------| +| `setup-env` | Run `make setup-image` (with optional `EXTRAS=`) | +| `notify-failure` | Create a Gitea issue on job failure via `devx.ci.notify_failure` | +| `quality-checks` | 6-step quality sequence: lint, tests, speed, docs, translations, security | + +Each consumer repo (`devx`, `grm`, `infra`) gets its own copy — there are +no cross-repo composite action references. This avoids a single-point-of-failure +where a bad devx master commit would break all repos' CI simultaneously. + +See ADR-0003 for the full design rationale and Gitea 1.27 constraints. + ## Import rules 1. **`src/devx/` is self-contained** — the package never imports from outside `src/` diff --git a/docs/tech/ci-cd-workflow.md b/docs/tech/ci-cd-workflow.md index 094dbbc..4f3b578 100644 --- a/docs/tech/ci-cd-workflow.md +++ b/docs/tech/ci-cd-workflow.md @@ -38,22 +38,33 @@ The single validation job. Consolidates the former `quality`, `detect-changes`, `release-dry-run`, `pr-review`, and `pre-merge-check` jobs into one job to save checkout+setup overhead. Runs on every PR. -**Quality steps** +**Setup and quality steps** (composite actions) -The main quality gate: +The validate job uses three composite actions from `.gitea/actions/`: -1. **Lint all** — ruff check, ruff format check, pyright, bandit, actionlint - (via `make lint-all`) -2. **Unit tests with 100% coverage** — `make pytest-cov` -3. **Check unit test speed** — `python -m devx.tools.check_test_speed - --max-seconds 4 --max-single-seconds 0.5` -4. **Documentation coverage check** — `python -m devx.ci.doc_coverage - --fail-on-missing` -5. **Translation completeness check** — `python -m devx.ci.check_translations` -6. **Dependency security scan** — `pip-audit --desc --skip-editable` - (best-effort, non-blocking) -7. **Workflow dry-run validation** — `make workflow-dryrun` via act_runner - (best-effort, skipped if act_runner is not installed) +1. **`setup-env`** — runs `make setup-image` to link the pre-built venv + and install the project (no-deps mode) +2. **`quality-checks`** — runs the 6-step quality gate: + - **Lint all** — ruff check, ruff format check, pyright, bandit, + actionlint (via `make lint-all`) + - **Unit tests with 100% coverage** — `make pytest-cov` + - **Check unit test speed** — `python -m devx.tools.check_test_speed + --max-seconds 15 --max-single-seconds 0.5` + - **Documentation gate** — `make devx-docs-check` (coverage + stale + refs + lint + version refs + prose) + - **Translation completeness check** — `python -m devx.ci.check_translations` + - **Dependency security scan** — `pip-audit --desc --skip-editable` + (best-effort, non-blocking) +3. **`notify-failure`** — creates a Gitea issue if any step fails + +The quality-checks action accepts inputs (`package`, `test-speed-max`, +`test-speed-max-single`, `translations-file`) for cross-repo reuse. +See ADR-0003 for the composite action design rationale. + +**Workflow dry-run validation** (inline step, not part of composite action) + +`make workflow-dryrun` via act_runner (best-effort, skipped if +act_runner is not installed). **`detect-changes` step** @@ -574,8 +585,9 @@ picks up the new version number). This prevents infinite loops. ## Failure handling -Every job in the CI and post-merge workflows has a `notify_failure` step -that runs `if: failure()`. This creates a Gitea issue with the workflow name, -run ID, and commit SHA, ensuring failures that would otherwise go unnoticed -in the Actions tab are surfaced as issues. The issue is created via the tea -CLI with a `bug` label if available. +Every job in the CI, post-merge, and build-images workflows uses the +`notify-failure` composite action (`.gitea/actions/notify-failure`), +which runs `if: failure()`. This creates a Gitea issue with the workflow +name, run ID, and commit SHA, ensuring failures that would otherwise go +unnoticed in the Actions tab are surfaced as issues. The issue is created +via the tea CLI with a `bug` label if available.