ADR-015: Agnostic local↔cloud environment & deploy model (single-trunk; deploy/IaC opt-in)
- Status: Accepted
- Date: 2026-06-19
- Implements: epic #316
- Supersedes: ADR-014 (branch-per-env promotion chains)
- Relates to: ADR-005 (PR & board lifecycle — single-trunk base), ADR-007 (enforcement layers), ADR-013 (distribution & governance — profile tiers)
Context
ADR-014 introduced opt-in branch-per-environment promotion chains for
scaffolded projects. Research across five tracks (human best practice, AI-agent
best practice, the PaaS landscape, self-managed/GitOps, and local↔cloud parity;
summarized in PLAN.md / PLAN-REVIEW-LOG.md, hardened over three Codex rounds)
found branch-per-env to be a deprecated pattern for both humans and AI agents:
environments differ by config and deploy target, not by branch. Conflating
them causes merge/config drift and — for an AI agent — forces it to reconstruct an
implicit branch→env model every session and hands it the deploy capability as a
destructive git capability.
The scaffolder is also language- and deploy-target-agnostic and must stay deterministic (pure file-ops, no network/LLM calls; ADR-001). It is consumed by a genuinely mixed, unknown population (libraries, deployed services, prototypes), so no single env model can be a fixed default — the model must be chosen by interview.
Decision
1. Single trunk is the only default
Every scaffolded project targets main. There are no environment branches. The
generic base_branch abstraction (main) is retained and consumed by ci.yml,
validate-pr.yml, and start_issue.sh.
2. A delivery-type question drives a coherent bundle
The wizard asks "How is this delivered?" → library / service-or-app /
prototype-or-none. Delivery is a template layer/variable, not a replacement
preset (it composes with preset × language).
- library → release/versioning bundle (publish workflow shipped disabled/TODO until registry + metadata + trusted-publishing are validated).
- service-or-app → the parity bundle (§3).
service-or-app+--language noneis rejected (no safe generic Dockerfile/test for an unknown runtime). - prototype-or-none → single trunk, nothing env-related.
3. The local↔cloud unifier is the container image + a one-command interface
Not Terraform (a cloud-only provisioning layer). The parity bundle is: a multi-stage
Dockerfile, a Compose-Spec compose.yaml (explicit backing-service selection;
optional compose watch), devcontainer.json, a deterministic non-interactive
justfile (up/test/lint/build), .env.example, and a CI that reuses the
same image with a pinned test-execution location. "Same image everywhere" holds;
"same behavior everywhere" does not (IAM, managed services, networking break — test
those against a real cloud dev project).
4. Deploy and IaC are opt-in per-target overlays (default OFF)
Managed PaaS usually owns the deploy last-mile better than scaffolded YAML, so the default stops at the cloud boundary. When opted in:
- Deploy builds once and promotes the same immutable artifact by digest, not
tag, through GitHub Environments. Three target classes: container-deploy,
registry-publish-only (publication, not deploy), and source/PaaS (routed to a
"platform owns deploy" path).
environments.yamldeclares the model in fixed shapes (the renderer is pure.tmplsubstitution with no loops). - Prod gate tiered by profile:
org= a true human gate (required reviewers + prevent-self-review + disallow-admin-bypass);individual/standalone= honestly labeled "delayed + advisory, not human-gated" (no second approver exists solo). The server-side GitHub Environment rule is the only boundary an agent cannot edit; in-repo hooks are advisory defense-in-depth. - IaC emits plain HCL defaulting to OpenTofu (license-safe vs BUSL/IBM Terraform), structural layer only, with apply manual/environment-gated by default (no apply-on-merge).
5. Cloud governance is a separate product
GCP (or any cloud) governance, monitoring, IAM, and landing-zone are out of this repo. The scaffolder emits only an integration seam (OIDC trust + env contract) that a separate platform/landing-zone product plugs into. Keeping a thin agnostic "paved road" separate from a heavyweight landing zone is the documented platform-engineering standard (CNCF, IDP reference model, Team Topologies, Backstage).
6. Determinism preserved
The Python scaffolder still only writes files and config. All git/network side
effects (branch protection, GitHub Environments, IaC apply) run post-clone via
operator-run scripts (setup_github.sh --protect, the deploy/IaC overlay setup),
never from the scaffolder core.
Consequences
- Branch-per-env is removed (not demoted); ADR-014 is a superseded tombstone.
- The reusable governance bits (squash-only merge policy, base-branch protection,
org rulesets) are centralized in
setup_github.sh --protect. - Child tickets implement the pieces under epic #316 (wizard delivery question, parity bundle, CI, guardrails, library path, deploy overlay, IaC overlay, integration seam, config schema + upgrade backfill, consistency sweep).
Out of scope
- Cloud governance/monitoring/landing-zone (separate product; seam only).
- Adopting env branches or containers in this repo (project-init stays a single-trunk Python scaffolder).
- Kubernetes/GitOps as a default; single-vendor abstraction tools (Score/Encore) as defaults.
Open questions
- Whether to later offer a first-class PaaS-native deploy sub-type per platform (Vercel/Render/Fly) beyond the generic "platform owns deploy" pointer.
- Whether
environments.yamlever needs dynamic N-environment generation (would require a deterministic, pure-file-ops code-gen render step).