Skip to content

Commit 11ec111

Browse files
Merge branch 'main' into docs-72-stacked-pull-request-workflow
2 parents eaee7e0 + 010f688 commit 11ec111

28 files changed

Lines changed: 920 additions & 63 deletions

.github/linters/.markdown-lint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ MD029: false # Ordered list item prefix
2121
MD033: false # Allow inline HTML
2222
MD036: false # Emphasis used instead of a heading
2323
MD041: false # First line in file should be a top level heading, PULL_REQUEST_TEMPLATE.md is an exception
24+
MD051: false # Link fragments: docs use python-markdown attr_list anchors ({ #fr1 }) that markdownlint can't resolve; Test-DocumentationLink.ps1 validates fragments instead
2425
MD060: false # Table column style — content mixes compact and aligned tables
2526

2627
#################
File renamed without changes.
File renamed without changes.

src/docs/Frameworks/Agentic-Development/memory-template.md renamed to src/docs/Capabilities/agentic-development/memory-template.md

File renamed without changes.
File renamed without changes.
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
---
2+
title: Deploying Azure from GitHub
3+
description: How the deployment spec is delivered with Azure as the service provider and GitHub as the CI/CD platform, using Terraform to compute and apply the approved effect.
4+
---
5+
6+
# Deploying Azure from GitHub
7+
8+
This design delivers the [Deployment spec](../spec.md) for one combination:
9+
**Azure** (including Microsoft Entra ID) as the service provider and **GitHub**
10+
as the CI/CD platform, with **Terraform (open source)** as the change
11+
engine. The computed effect the spec speaks of is a **saved Terraform plan**;
12+
the record of state is a remote Terraform state per environment.
13+
14+
Another design covers another combination — deploying Azure from Azure DevOps,
15+
deploying AWS from GitHub, deploying GitHub from GitHub — without changing the
16+
spec.
17+
18+
## Approach
19+
20+
A change is proposed as a pull request. On the pull request the process computes
21+
the effect for **every environment in the promotion path** and shows them all;
22+
approving and merging the pull request is the single approval of the code change
23+
together with all of those effects ([FR2](../spec.md#fr2), [FR3](../spec.md#fr3)).
24+
The merge then runs the promotion, deploying to each environment in order using
25+
the **exact plans that were approved** ([FR4](../spec.md#fr4)), running tests
26+
between environments ([FR7](../spec.md#fr7)) and recording every action
27+
([FR9](../spec.md#fr9)).
28+
29+
```mermaid
30+
flowchart TB
31+
subgraph Review [Pull request — one approval]
32+
C[Change as code] --> P[Plan per environment\ndev · test · preprod · prod]
33+
P --> R[All plans + tests shown in the PR]
34+
end
35+
R -->|team approves code + all effects, merges| P0
36+
subgraph Promote [Merge — promotion using approved plans]
37+
P0[Apply approved dev plan] --> T0[Automated tests] --> P1[Apply approved test plan]
38+
P1 --> T1[Automated tests] --> P2[Apply approved preprod plan]
39+
P2 --> T2[Automated tests] --> P3[Apply approved prod plan]
40+
end
41+
P3 --> Rec[Every action recorded]
42+
P0 -.effect no longer valid.-> Stop[Stop · recompute · re-approve]
43+
```
44+
45+
### Why this shape
46+
47+
- **One approval for the whole path.** Because the reviewer sees the effect on
48+
every environment, a single approval is sufficient; the design adds no
49+
per-environment reviewer gate, matching the spec's non-goal.
50+
- **Approved plans, not re-derived ones.** The saved plans reviewed on the pull
51+
request are the artifacts the promotion applies. This trades the convenience of
52+
re-planning at deploy time for the guarantee the spec requires — the deployed
53+
effect equals the approved effect.
54+
55+
### Alternatives considered
56+
57+
- **Re-plan at deploy.** Rejected: the deploy would compute its own effect, which
58+
can differ from the approved one — it fails [FR4](../spec.md#fr4).
59+
- **Approve only the source diff, plan at deploy.** Rejected: the reviewer never
60+
sees the effect, failing [FR2](../spec.md#fr2)[FR3](../spec.md#fr3).
61+
- **A required-reviewer gate on each environment.** Rejected as redundant: the
62+
effects for all environments are already approved together; extra gates add
63+
friction without adding a decision.
64+
65+
## Components
66+
67+
| Component | Role |
68+
| --- | --- |
69+
| **Reusable workflow** (`.github/workflows/terraform.yml`, `workflow_call`) | The provider-agnostic core: init, format, validate, plan, render, policy scan, test, apply. |
70+
| **Plan (pull-request) workflow** | For every environment in the path, computes `plan -out`, renders it into the pull request, scans it, and uploads it as the approved artifact. |
71+
| **Promotion (merge) workflow** | Walks the environments in order, applying each approved plan and running tests between them. |
72+
| **Azure identity step** | `azure/login` federated sign-in; sets `ARM_USE_OIDC=true` so the `azurerm` and `azuread` providers use the same token. |
73+
| **State backend** | Azure Storage per environment, with blob-lease locking and encryption at rest — the record of state. |
74+
| **Environments** | GitHub Environments per environment name — the per-environment **identity boundary** and deployment branch policy (not an approval gate). |
75+
| **Break-glass workflow** (`workflow_dispatch`) | The emergency path. |
76+
| **Drift job** (`schedule`) | Periodic `plan -detailed-exitcode` that opens an issue on drift. |
77+
78+
## The effect is a saved plan
79+
80+
Terraform's saved plan is what makes "approve the effect, then apply exactly it"
81+
concrete:
82+
83+
1. `terraform plan -out=tfplan.<env>` computes the effect against that
84+
environment's state and writes it to a file that pins provider versions and
85+
the state's **serial** and **lineage**.
86+
2. `terraform show` renders it for humans (pull-request comment) and as JSON for
87+
policy scanning; `sha256(tfplan.<env>)` is recorded.
88+
3. `terraform apply tfplan.<env>` carries out **exactly** that effect, and
89+
refuses with `Saved plan is stale` if the state advanced — the native backstop
90+
for [FR5](../spec.md#fr5).
91+
92+
## Review and approval
93+
94+
The pull-request workflow computes a plan for **each** environment in the
95+
promotion path and makes all of them visible in the pull request, alongside
96+
format, validation, policy, and test results. These are **required checks**, so a
97+
change cannot merge unless every environment's effect computed successfully. A
98+
branch ruleset on `main` requires review (CODEOWNERS for `**/*.tf`), dismisses
99+
stale approvals on new commits, and requires branches to be up to date — so the
100+
shown effects reflect current `main`. Approving and merging is the single
101+
approval the spec requires ([FR3](../spec.md#fr3)); the approved plan artifacts
102+
are retained for the promotion.
103+
104+
## Promotion using the approved plans
105+
106+
On merge, the promotion workflow retrieves the plan artifacts approved on the
107+
pull request (keyed to the merge commit) and, for each environment in order:
108+
109+
1. **Verifies** `sha256(tfplan.<env>)` against the value recorded at plan time,
110+
defeating artifact swap.
111+
2. **Applies** `terraform apply tfplan.<env>` — exactly the approved effect
112+
([FR4](../spec.md#fr4)). If the plan is stale, the apply fails closed and the
113+
promotion stops; a fresh plan is produced and re-approved
114+
([FR5](../spec.md#fr5)).
115+
3. **Tests** — runs the environment's automated tests; a failing required test
116+
stops the promotion before the next environment ([FR7](../spec.md#fr7)).
117+
118+
Deployments against the same state are **serialised** by a concurrency group
119+
keyed per state with `cancel-in-progress: false`, backed by the state lock, and
120+
an in-flight apply is never cancelled ([FR6](../spec.md#fr6)).
121+
122+
## Identity and access (OIDC)
123+
124+
- The workflow requests the GitHub OIDC token with `permissions: id-token: write`;
125+
`azure/login` federates on it using the non-secret `client-id`, `tenant-id`,
126+
and `subscription-id` — no client secret exists ([NFR1](../spec.md#nfr1)).
127+
- Azure federated credentials pin the subject claim
128+
`repo:MSXOrg/Platform:environment:<env>`, binding identity to the environment
129+
([NFR3](../spec.md#nfr3)).
130+
- Two identities per environment: **read-only** for computing the effect and
131+
**write** for applying it, the write identity reachable only from that
132+
environment ([NFR2](../spec.md#nfr2)).
133+
- Entra ID resources use the `azuread` provider on the same federated token —
134+
Entra rides the Azure identity context, no separate secret. Managing GitHub
135+
itself uses the `github` provider with a GitHub App installation token, never a
136+
personal access token.
137+
138+
## Break-glass path
139+
140+
The emergency path ([FR8](../spec.md#fr8)) is a `workflow_dispatch` "emergency
141+
deploy" with required inputs `reason`, `incident_id`, target environment, and a
142+
typed confirmation, restricted to authorised operators. It still runs
143+
`plan -out` then `apply tfplan`, so the effect is shown before it deploys; it
144+
records the inputs and logs as an immutable run record, and opens a follow-up
145+
issue to bring the change back through the standard pull-request path. There is
146+
no flag on the standard workflow that skips review.
147+
148+
## Records and drift
149+
150+
Every run's rendered plans, apply logs, and test results are retained as run
151+
artifacts and logs, and each apply is recorded in the environment's deployment
152+
history — the audit trail for [FR9](../spec.md#fr9)/[NFR4](../spec.md#nfr4). A
153+
scheduled `plan -detailed-exitcode` job detects drift and opens an issue
154+
([FR10](../spec.md#fr10)).
155+
156+
## Interface
157+
158+
```yaml
159+
# .github/workflows/terraform.yml (reusable — workflow_call)
160+
on:
161+
workflow_call:
162+
inputs:
163+
environment: { type: string, required: true } # dev | test | preprod | prod
164+
working-directory: { type: string, required: true }
165+
action: { type: string, default: plan } # plan | apply
166+
terraform-version: { type: string, default: "1.9.x" }
167+
```
168+
169+
The core is provider-agnostic; the Azure sign-in is the single service-provider
170+
step. A different service provider (AWS) or CI/CD platform (Azure DevOps)
171+
reuses this design's shape with its own identity step and is documented as its
172+
own design under [Deployment](../index.md).
173+
174+
## Security boundaries and threats
175+
176+
- **Credential exposure** — OIDC only; no static cloud secret exists, and the
177+
default token is read-only.
178+
- **Plan artifact confidentiality** — a saved plan can contain sensitive values
179+
in plaintext. Retention is short, artifacts are restricted, sensitive variables
180+
and outputs are marked, and pull-request comments carry scrubbed renders; on a
181+
public repository plan artifacts are never exposed unscrubbed.
182+
- **Artifact swap** — each apply verifies `sha256(tfplan.<env>)` before applying.
183+
- **Cross-environment escalation** — federated subjects are pinned per
184+
environment; the write identity is reachable only from its environment.
185+
- **Stale apply** — Terraform's serial/lineage guard, plus serialisation and
186+
branch-freshness rules.
187+
188+
## Testing and operability
189+
190+
- **Dry run** the full path on `dev`: pull-request plans for every environment,
191+
review, merge, promotion with tests.
192+
- **Exercise the stale path** by mutating state between approval and deploy and
193+
confirming the apply fails closed.
194+
- **Exercise break-glass** end-to-end and confirm the reconciliation follow-up is
195+
opened and attribution is recorded.
196+
197+
## Repository hardening
198+
199+
Third-party actions are pinned to commit SHAs; allowed actions are restricted;
200+
secret scanning with push protection is enabled as defence in depth even under an
201+
OIDC model.
202+
203+
## Decisions
204+
205+
One-way-door decisions are recorded here beside the spec, per
206+
[Decision before change](../../../Ways-of-Working/Principles/AI-First-Development.md#decision-before-change).
207+
208+
- **Approve every environment's effect on the pull request; apply those exact
209+
saved plans on merge.** Chosen over re-plan-at-deploy (does not apply the
210+
approved effect) and per-environment gates (redundant given the effects are
211+
already approved together). Revisit only if a saved plan can no longer be the
212+
review-to-deploy contract.
213+
214+
## References
215+
216+
- GitHub — security hardening with OpenID Connect: <https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect>
217+
- GitHub — configuring OIDC in Azure: <https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-azure>
218+
- GitHub — managing environments: <https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments>
219+
- GitHub — reusing workflows: <https://docs.github.com/en/actions/using-workflows/reusing-workflows>
220+
- GitHub — control concurrency: <https://docs.github.com/en/actions/using-jobs/using-concurrency>
221+
- HashiCorp — automate Terraform with GitHub Actions: <https://developer.hashicorp.com/terraform/tutorials/automation/github-actions>
222+
- HashiCorp — `terraform plan` / `terraform apply` (saved plan): <https://developer.hashicorp.com/terraform/cli/commands/plan> · <https://developer.hashicorp.com/terraform/cli/commands/apply>
223+
- Terraform AzureRM provider — OIDC: <https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_oidc>
224+
- Azure — `azure/login`: <https://github.com/Azure/login>
225+
- Microsoft — GitHub Actions + Terraform OIDC on Azure sample: <https://learn.microsoft.com/en-us/samples/azure-samples/github-terraform-oidc-ci-cd/github-terraform-oidc-ci-cd/>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
title: Deployment
3+
description: How a change to managed resources is approved together with its effect and deployed exactly as approved — one spec, and one design for each combination of deploying a service provider from a CI/CD platform.
4+
---
5+
6+
# Deployment
7+
8+
How a change to the resources Platform manages moves from a proposal to live: its
9+
**effect on the resources** is computed and shown, the team approves the code
10+
change together with that effect across every environment it will pass through,
11+
and the approved effect is exactly what is deployed — with every action recorded.
12+
13+
The **spec** is the durable contract and is deliberately free of any technology.
14+
Each **design** delivers that contract for one combination, named **"deploying
15+
&lt;service provider&gt; from &lt;CI/CD platform&gt;"** so the two parts are
16+
clear: the **service provider** is where the resources live (Azure, AWS,
17+
GitHub), and the **CI/CD platform** is what runs the deployment (GitHub, Azure
18+
DevOps) — for example deploying Azure from GitHub, deploying Azure from Azure
19+
DevOps, deploying AWS from GitHub, or deploying GitHub from GitHub. Adding a
20+
combination adds a design; it never changes the spec.
21+
22+
## Spec
23+
24+
| Page | Description |
25+
| --- | --- |
26+
| [Spec](spec.md) | The why and what — a change is approved together with its effect, and the approved effect is exactly what deploys. |
27+
28+
## Designs
29+
30+
| Design | Service provider | CI/CD platform | Description |
31+
| --- | --- | --- | --- |
32+
| [Deploying Azure from GitHub](designs/azure-from-github.md) | Azure | GitHub | GitHub Actions and Terraform deploy Azure and Entra resources with passwordless identity, approving the code change together with its per-environment effect. |

0 commit comments

Comments
 (0)