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:
-
Set up environment —
make setup-image(optionally withEXTRAS=). Appeared verbatim in 4 jobs acrossci.ymlandpost-merge.yml, each with the sameCI_GITEA_API_TOKENenv wiring. -
Notify on failure —
python3 -m devx.ci.notify_failure ... --auto-loginwith 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--workflowstring. -
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.ymlvalidate job, but the same sequence is needed bygrmandinfra(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:
grmandinfracould 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
runstep needs explicitshell:; composite actions cannot accesssecretsdirectly (onlyenv:). 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) — setsDEVX_DOC_VERSIONS_PKGfor doc version checks (for example,devx,grm).test-speed-max(default15) — total test seconds threshold.test-speed-max-single(default0.5) — per-test seconds threshold.translations-file(default empty) — path totranslations.jsonfor repos whose translations live outsidesrc/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.ymlvalidate job:setup-env+quality-checks+notify-failure.ci.ymlauto-merge job:setup-envonly (no quality checks, no notify-failure — auto-merge failure is surfaced by the validate job's notify-failure).post-merge.ymldetect-and-configure:setup-env+notify-failure.post-merge.ymlrelease-and-maintain:setup-env(withextras: "release") +notify-failure.build-images.ymlbuild-and-push:notify-failureonly. The setup steps usemake setup-releaseandmake setup-ci(notmake setup-image), sosetup-envdoes 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 sameaction.ymlfiles and adopt the sameuses:pattern. The quality-checks sequence is now portable. - Gitea 1.27 constraints centralized: the
shell: bashandenv:(notsecrets) patterns are encoded once per action, not re-derived per step. - No release triggered: changes to
.gitea/**are classified as workflow-only bydevx.ci.classify_changes. Phase 2a does not produce a new devx version.grm/infrabump 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, andinfraindependently. 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-levelenv:block must defineCI_GITEA_API_TOKENfor 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-checksis devx-shaped: thepackageandtranslations-fileinputs exist because consumer repos (for example,grm) have translations files outside the defaultsrc/devx/translations.jsonlocation 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: thenotify-failurecomposite action's step hasif: 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 || truedoes not fail if the venv is missing (pre-built image path) or already active. This matches the inline pattern's behavior.