Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .github/workflows/terraform-discover.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ on:
default: linux-arm64
outputs:
stacks:
description: JSON array of {dir, role, environment, region}. Empty array when nothing is affected.
description: >
JSON array of {dir, role, environment, region}. Any other key on the matched accounts[]
entry is passed through, so a caller can carry its own fields (a secret name, a runner
label) per stack. Empty array when nothing is affected.
value: ${{ jobs.discover.outputs.stacks }}

permissions:
Expand Down Expand Up @@ -109,11 +112,9 @@ jobs:
[ .[]
| . as $dir
| ($acc | map(. as $e | select($dir | startswith($e.match))) | first) as $a
| { dir: $dir,
role: ($a.role // ""),
environment: ($a.environment // ""),
region: ($a.region // "eu-west-1"),
matched: ($a != null) } ]')
| ({ role: "", environment: "", region: "eu-west-1" }
+ (($a // {}) | del(.match))
+ { dir: $dir, matched: ($a != null) }) ]')

unmatched=$(printf '%s' "$stacks" | jq -r '[.[] | select(.matched == false) | .dir] | join(", ")')
if [ -n "$unmatched" ]; then
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/terraform-stack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ jobs:
if: inputs.command == 'plan' && steps.plan.outputs.failed != 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: tfplan-${{ steps.slug.outputs.value }}-${{ github.sha }}
name: tfplan-${{ steps.slug.outputs.value }}-${{ github.event.pull_request.head.sha || github.sha }}
path: ${{ inputs.stack }}/tfplan
retention-days: ${{ inputs.plan-retention-days }}
if-no-files-found: error
Expand Down
12 changes: 8 additions & 4 deletions docs/workflow-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1366,7 +1366,7 @@ terraform providers lock -platform=linux_arm64 -platform=darwin_arm64

### Apply runs the reviewed plan

Plan uploads `tfplan` as an artifact named `tfplan-<stack.path>-<sha>`. Apply downloads that artifact and runs `terraform apply tfplan` — it never re-plans, so what merges is what was reviewed. Apply fails loudly when no artifact matches, which is the intended behaviour for a direct push to `main`.
Plan uploads `tfplan` as an artifact named `tfplan-<stack.path>-<sha>`, where `<sha>` is the **pull request head** commit — not `github.sha`, which on a `pull_request` event is the ephemeral merge commit and never matches anything that lands on `main`. Apply resolves the head SHA back from the commit it is running on, downloads that artifact and runs `terraform apply tfplan` — it never re-plans, so what merges is what was reviewed. Apply fails loudly when no artifact matches, which is the intended behaviour for a direct push to `main`.

Combine with a **merge queue** and `merge_group` in the caller: the queue tests each PR against the queue head, so the plan attached to the landing commit already accounts for everything merging ahead of it.

Expand Down Expand Up @@ -1428,18 +1428,22 @@ jobs:
aws-region: ${{ matrix.stack.region }}

terraform-ok:
needs: terraform
needs: [discover, terraform]
if: always()
runs-on: ubuntu-latest
steps:
- run: '[[ "${{ needs.terraform.result }}" != "failure" ]]'
- run: |
[[ "${{ needs.discover.result }}" == "success" ]] || exit 1
[[ "${{ needs.terraform.result }}" != "failure" ]] || exit 1
[[ "${{ needs.terraform.result }}" != "cancelled" ]]
```

### Notes

- **`terraform-ok` is the only required status check.** The matrix is empty when no stack is affected, and a skipped job never reports a check — a required check on `terraform` itself would leave unrelated PRs blocked forever.
- **`terraform-ok` is the only required status check.** The matrix is empty when no stack is affected, and a skipped job never reports a check — a required check on `terraform` itself would leave unrelated PRs blocked forever. It must gate on `discover` as well: discovery hard-fails on an unmapped stack, which leaves `terraform` *skipped* rather than failed.
- **`fail-fast: false` is required**, otherwise one stack failing cancels sibling applies mid-apply.
- Locking is per stack, set inside `terraform-stack.yml`: plans of the same stack cancel each other per ref, applies queue and are never cancelled.
- `accounts[]` is matched by path prefix, first match wins — list more specific prefixes first. A stack matching no entry fails discovery rather than running without credentials.
- Any key on the matched `accounts[]` entry other than `match` is passed through to the matrix entry. A repo with one static key pair per account can carry the secret name that way and select it in the caller: `${{ secrets[format('TERRAFORM_CORE_{0}_AWS_ACCESS_KEY_ID', matrix.stack.secret_key)] }}`.
- Leave `aws-role` empty to fall back to the `aws-access-key-id` / `aws-secret-access-key` secrets during an OIDC migration.
- `tf-vars-json` writes a `ci.auto.tfvars.json` into the stack. It is a migration bridge: prefer reading secrets inside Terraform via `data "aws_secretsmanager_secret_version"` so CI holds nothing but the AWS role.
Loading