Decisions

ADR-0012: Workflow Architecture for Multi-Environment Deploys

Extend the GitHub Actions Workflows pattern to cover authentication, multi-environment architecture, and topic ownership across CI/CD patterns.

RFC
FieldValue
StatusRFC
Date2026-03-14
AuthorsEngineering
ExtendsGitHub Actions Workflows pattern

Context

The GitHub Actions Workflows pattern defines composition — workflows are thin wrappers around Taskfile tasks. This is well-established and universally applied.

However, the pattern is silent on architecture — how workflows are structured for multi-environment deploys, how they authenticate to AWS, and what triggers each environment. This gap has led to two problems:

  1. Service-specific patterns embed workflow advice. The ECR + GitHub Actions and CodeArtifact + GitHub Actions patterns each include their own workflow examples with OIDC roles, triggers, and environment handling. The proposed Lambda Deploy pattern (ADR-0011) does the same. Each pattern independently defines how workflows should look, risking contradiction as they evolve independently.
  2. Multi-environment deploys have no standard architecture. Projects adopting dev/pre/prod environments must invent their own workflow structure. Without guidance, teams may use GitHub Environments (which we have explicitly deferred), matrix strategies, or ad-hoc workflow_dispatch inputs — all of which work but create inconsistency.

The authentication model is already decided — Infra ADR-003 defines a three-tier OIDC trust model (CI, Deploy, Release) with ref-based branch constraints. What is missing is the engineering handbook pattern that tells projects how to consume those roles in a consistent workflow structure.


Decision

Extend the GitHub Actions Workflows pattern with four new sections covering workflow architecture. Service-specific patterns (ECR, Lambda, CodeArtifact) should reference this pattern for workflow structure and limit themselves to what is deployed, not how workflows are organized.

1. Authentication: OIDC Tiers

Workflows MUST use OIDC role assumption, never long-lived credentials. The trust model from Infra ADR-003 defines three tiers:

TierTrust scopePurposeUsed by
CI (read)repo:ontopix/*:*Read-only from any refci.yaml
Deploy (write)refs/heads/{master,pre,dev}Write from deploy branchesdeploy-*.yaml
Release (publish)refs/tags/*Publish from version tagsrelease.yaml

GitHub Environments are not used. Trust is enforced at the OIDC claim level (sub field matches ref constraints), not via GitHub Environment protection rules. This was evaluated and explicitly deferred in Infra ADR-003 — ref-based trust remains the sole model until org-level environment policies are in place.

2. Multi-Environment Architecture: Reusable + Thin Callers

Projects with multiple deploy environments SHOULD use a reusable workflow + thin caller pattern:

.github/workflows/
├── ci.yml                # Reusable — lint, test, build (called by ci-*.yml)
├── deploy.yml            # Reusable — all deploy logic, parameterized by environment
├── deploy-dev.yml        # Caller — passes environment: dev
├── deploy-pre.yml        # Caller — passes environment: pre
└── deploy-prod.yml       # Caller — passes environment: prod

The reusable workflow (deploy.yml) contains all deploy logic and accepts inputs.environment. The caller workflows (deploy-{env}.yml) are minimal files (~15 lines) that only define the trigger and pass the environment name.

Why not matrix or workflow_dispatch with environment input?

  • Matrix conflates trigger policy with execution — you cannot have dev auto-deploy on push while prod requires manual dispatch within the same workflow.
  • workflow_dispatch with an environment dropdown relies on humans selecting the right value and provides no branch-level constraint.
  • Thin callers make trigger policy auditable at a glance: open deploy-prod.yml, see on: workflow_dispatch — done.

3. Trigger Strategy

Recommended defaults for deploy triggers:

EnvironmentTriggerRationale
devon: push: branches: [dev] + workflow_dispatchContinuous deployment — every merge to dev deploys automatically
preon: workflow_dispatchDeliberate promotion — pre is a staging gate
prodon: workflow_dispatchDeliberate promotion — production requires human intent

These are recommended defaults, not hard requirements. Projects MAY adjust triggers to match their release cadence (e.g., a project with no pre-production environment simply omits deploy-pre.yml).

4. Concurrency Control

Deploy workflows MUST include concurrency settings scoped to the environment:

concurrency:
  group: deploy-${{ inputs.environment }}
  cancel-in-progress: false
  • Scoped by environment: A dev deploy does not block a prod deploy.
  • No cancellation: cancel-in-progress: false ensures a running deploy completes before the next one starts. Cancelling a mid-flight deploy can leave resources in an inconsistent state.

5. Topic Ownership

Each pattern owns a single concern. Workflow examples in service-specific patterns should be limited to the service-specific steps (build, push, deploy) and reference this pattern for the surrounding architecture.

PatternOwnsDoes NOT own
GitHub Actions WorkflowsComposition, authentication, triggers, concurrency, multi-env architectureWhat is built or deployed
Lambda DeployHow Lambda code is packaged (zip vs S3), Terraform ignore_changes, bundle thresholdsWorkflow triggers, OIDC roles, environment resolution
ECR + GitHub ActionsHow container images are built and pushed, ECR lifecycleWorkflow triggers, OIDC roles, environment resolution
CodeArtifact + GitHub ActionsHow private packages are published and consumedWorkflow triggers, OIDC roles, environment resolution
Taskfile ContractWhat tasks exist and their interfaceHow/when tasks are invoked from CI
Infrastructure LayoutWhere IaC code lives, state managementHow deploys are triggered

Rationale

Why extend the existing pattern instead of creating a new one?

The GitHub Actions pattern already owns "how CI/CD workflows should be composed and managed." Authentication, triggers, and multi-environment architecture are part of composition and management — not a separate concern. A new pattern would create ambiguity about where workflow decisions are documented.

Why not prescribe change detection (paths-filter, matrix)?

Change detection (e.g., dorny/paths-filter + dynamic matrix) is valuable in monorepos but irrelevant for single-service repositories. Including it as a universal prescription would over-constrain. Monorepos that need selective deploys should document their approach in their own .context/agents/patterns/, not in the org pattern.

Why is this an ADR and not just a pattern update?

The topic ownership table changes the scope of existing patterns (ECR, CodeArtifact, Lambda). That is an architectural decision that should be explicitly recorded, not silently applied.


Consequences

Positive

  • Single source of truth for workflow architecture — no more duplicated OIDC and trigger guidance across patterns.
  • Consistent multi-env deploys across projects — new projects follow the same structure.
  • Clearer pattern boundaries — each pattern answers one question, reducing contradiction risk as patterns evolve.
  • Auditable triggers — one file per environment, trigger policy visible without reading the reusable workflow.

Negative / Trade-offs

  • More workflow files — three thin callers instead of one parameterized workflow. Accepted because auditability and trigger isolation outweigh file count.
  • Service patterns must reference this pattern — adds a cross-reference dependency. Mitigated by keeping the reference to a single sentence.
  • Retroactive updates needed — existing ECR and CodeArtifact patterns contain workflow architecture advice that should eventually be refactored to reference this pattern. This is a follow-up, not a blocker.

Applies Principles

  • Ownership & Responsibility — each pattern owns one topic; topic ownership table makes this explicit.
  • Consistency Over Creativity — multi-env deploys follow one architecture, not ad-hoc per-project inventions.
  • Evidence Over Assumptions — the prescribed architecture is extracted from a working production implementation (maxcolchon), not theoretical.