Files
devx/docs/decisions/0003-composite-actions.md
T

7.0 KiB

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 environmentmake 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 failurepython3 -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.