Files
familienarchiv/docs/adr/029-composite-actions-for-cross-workflow-deploy-logic.md
Marcel 4757a174c9 docs(adr): add ADR-029 composite actions for cross-workflow deploy logic
Records the decision to extract the shared obs-deploy/reload-caddy/smoke-test
logic into three composite actions instead of a reusable workflow or shared
shell script. Numbered 029 (028 was taken by the pdf.js wasm ADR on main since
the issue was filed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:24:20 +02:00

3.4 KiB

ADR-029: Composite actions for cross-workflow deploy logic

Status

Accepted

Context

The nightly.yml (staging) and release.yml (production) workflows shared three blocks of deploy logic verbatim: the four observability-stack steps (deploy configs, validate, start, assert health), the Caddy reload step, and the public-surface smoke test. The only per-environment differences were secret names (STAGING_* vs PROD_*), the POSTGRES_HOST value, and the smoke-test hostname.

This duplication was held together by # Keep in sync with nightly.yml comments — an honour-system invariant. Any change (a new healthchecked service, a different rsync flag, a new secret) had to be applied in two places, and nothing enforced that it was. Issue #603 documents a real instance: the obs secret set had grown to five keys while a refactor draft listed only four.

Decision drivers

  1. Cross-workflow deploy logic must have a single definition, enforced — not a discipline-based "keep in sync" promise.
  2. Per-environment variation must be expressed as explicit, typed inputs, not by forking the whole step block.
  3. The mechanism must work on the existing single-tenant self-hosted Gitea runner with no new infrastructure.

Alternatives considered

A: Reusable workflow (workflow_call) — Gitea supports called workflows. Rejected for this case: reusable workflows run as a separate job with their own runner context, which breaks the in-job, sequential deploy → reload → smoke ordering these steps rely on and complicates passing the already-checked-out workspace. Composite actions run inline in the calling job, preserving step order and the workspace.

B: Shared shell script invoked from both workflows — e.g. scripts/deploy-obs.sh. Rejected: loses the typed-input contract and per-step CI log sections, and reintroduces manual argument threading that is as error-prone as the duplication it replaces.

C: Keep the # Keep in sync comments — status quo. Rejected: unenforced; issue #603 is direct evidence it fails.

Decision

Extract the shared logic into three single-responsibility Gitea composite actions under .gitea/actions/: deploy-obs (five inputs), reload-caddy (no inputs), and smoke-test (host input). Both workflows invoke each via a single uses: ./.gitea/actions/<name> call, passing per-environment values as with: inputs. This is the repository's first composite action and sets the convention; docs/infrastructure/ci-gitea.md documents it.

Consequences

Positive:

  • Shared deploy logic has one enforced definition; a change lands once and both environments get it. The # Keep in sync comments are deleted.
  • Per-environment variation is a typed input contract, not a forked block.
  • Runs inline on the existing runner — no reusable-workflow job context, no new infrastructure.

Negative / constraints:

  • Workflows now depend on a checked-out .gitea/actions/ tree: actions/checkout MUST run before the first uses: ./… (a local action does not exist on disk until checkout).
  • Secrets cannot be read from the secrets.* context inside a composite action; they must be passed as inputs and mapped to env:. The obs-secrets.env heredoc therefore uses an unquoted delimiter so $VAR expands at the shell layer.
  • The reload-caddy pinned alpine digest now lives in the action, not the workflow file — it must be added to Renovate's watch list so it does not go stale.