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.
| Field | Value |
|---|---|
| Status | RFC |
| Date | 2026-03-14 |
| Authors | Engineering |
| Extends | GitHub 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:
- 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.
- Multi-environment deploys have no standard architecture. Projects adopting
dev/pre/prodenvironments must invent their own workflow structure. Without guidance, teams may use GitHub Environments (which we have explicitly deferred), matrix strategies, or ad-hocworkflow_dispatchinputs — 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:
| Tier | Trust scope | Purpose | Used by |
|---|---|---|---|
| CI (read) | repo:ontopix/*:* | Read-only from any ref | ci.yaml |
| Deploy (write) | refs/heads/{master,pre,dev} | Write from deploy branches | deploy-*.yaml |
| Release (publish) | refs/tags/* | Publish from version tags | release.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_dispatchwith 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, seeon: workflow_dispatch— done.
3. Trigger Strategy
Recommended defaults for deploy triggers:
| Environment | Trigger | Rationale |
|---|---|---|
dev | on: push: branches: [dev] + workflow_dispatch | Continuous deployment — every merge to dev deploys automatically |
pre | on: workflow_dispatch | Deliberate promotion — pre is a staging gate |
prod | on: workflow_dispatch | Deliberate 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: falseensures 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.
| Pattern | Owns | Does NOT own |
|---|---|---|
| GitHub Actions Workflows | Composition, authentication, triggers, concurrency, multi-env architecture | What is built or deployed |
| Lambda Deploy | How Lambda code is packaged (zip vs S3), Terraform ignore_changes, bundle thresholds | Workflow triggers, OIDC roles, environment resolution |
| ECR + GitHub Actions | How container images are built and pushed, ECR lifecycle | Workflow triggers, OIDC roles, environment resolution |
| CodeArtifact + GitHub Actions | How private packages are published and consumed | Workflow triggers, OIDC roles, environment resolution |
| Taskfile Contract | What tasks exist and their interface | How/when tasks are invoked from CI |
| Infrastructure Layout | Where IaC code lives, state management | How 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.
Related
- Infra ADR-003 — GitHub Actions OIDC Trust Tier Model — defines the trust model this pattern consumes
- GitHub Actions Workflows pattern — the pattern this ADR extends
- ADR-0011 — Lambda Deploy Strategy — should reference this pattern for workflow architecture
- ECR + GitHub Actions pattern — should reference this pattern for workflow architecture
- CodeArtifact + GitHub Actions pattern — should reference this pattern for workflow architecture
ADR-0011: Lambda Deploy Strategy
Decision to use direct zip upload as the default Lambda code deployment method from CI/CD.
ADR-0013: Taskfile Contract v2 — Prescriptive Naming and Recipes
Decisions to extend the Taskfile contract with prescriptive task naming, CI namespace, developer lifecycle, environment handling, and CodeArtifact recipes.