Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
a058f03
chore: only upload full artifacts when changes found
chrispsheehan May 19, 2026
ea8efa0
docs: note on script ownerships
chrispsheehan May 20, 2026
d6102f1
chore: graph deps
chrispsheehan May 20, 2026
3c837b3
chore: re add duped deps
chrispsheehan May 20, 2026
36c7ee3
chore: just tg-graph env
chrispsheehan May 20, 2026
cf77cf5
chore: just get dep array
chrispsheehan May 20, 2026
ad82fe2
feat: get list of dependencies
chrispsheehan May 20, 2026
a2527d6
chore: graph changed items
chrispsheehan May 20, 2026
3b51c86
debug: test
chrispsheehan May 20, 2026
2d9d875
chore: graph changes
chrispsheehan May 20, 2026
d3cd85a
fix: pass var
chrispsheehan May 20, 2026
84ed89f
debug: output test
chrispsheehan May 20, 2026
32a4454
fix: tg flags for graph
chrispsheehan May 20, 2026
768311f
feat: process graph output
chrispsheehan May 20, 2026
fb33c67
chore: just tg-all-module-dependencies
chrispsheehan May 20, 2026
8ccc12d
chore: output all deps json
chrispsheehan May 20, 2026
92cfa27
chore: just tg-graph-waves
chrispsheehan May 20, 2026
4889c54
chore: note
chrispsheehan May 20, 2026
470d45c
debug: test for modules
chrispsheehan May 21, 2026
dbba76b
chore: matrix test
chrispsheehan May 21, 2026
e2ff2ae
chore: add dependency on oidc
chrispsheehan May 21, 2026
938f56a
chore: mock code bucket
chrispsheehan May 21, 2026
5b6267e
chore: ecr deps
chrispsheehan May 21, 2026
591ead1
chore: no need for dev release setups
chrispsheehan May 21, 2026
9587c94
chore: same for plan
chrispsheehan May 21, 2026
3791b79
chore: rm unused inputs
chrispsheehan May 21, 2026
7bc935c
chore: shared get modules
chrispsheehan May 21, 2026
8d3fe8d
debug: use shared get modules
chrispsheehan May 21, 2026
cce0a15
chore: run a proper plan
chrispsheehan May 21, 2026
7bb65b4
chore: fix vars
chrispsheehan May 21, 2026
0d60273
fix: filter out tasks
chrispsheehan May 21, 2026
8cff8cc
chore: rn jobs
chrispsheehan May 21, 2026
ed3a466
chore: mv to shared plan wf
chrispsheehan May 21, 2026
d2719d1
chore: upload plans
chrispsheehan May 21, 2026
573405f
fix: plan vars + apply matrix
chrispsheehan May 21, 2026
9652fc9
fix: set plan action
chrispsheehan May 21, 2026
f1cbb08
chore: domain_name var in ci
chrispsheehan May 22, 2026
9ffe219
chore: pass in domain via global vars
chrispsheehan May 22, 2026
9d2b55c
fix: fmt
chrispsheehan May 22, 2026
a98ff2a
fix: TG_ENABLE_PLAN_ARTIFACTS: "true"
chrispsheehan May 22, 2026
2ce37bf
fix: set PLAN_ARTIFACT_RUN_ID
chrispsheehan May 22, 2026
cf48322
feat: try artifact upload/download
chrispsheehan May 22, 2026
96604f3
fix: fix use tg not tf
chrispsheehan May 22, 2026
dbaab91
debug: test pull artifacts
chrispsheehan May 22, 2026
dd4751b
debug: print out metadata
chrispsheehan May 22, 2026
5dae842
chore: mermaid for full flow
chrispsheehan May 22, 2026
8444716
chore: upload waves in plan
chrispsheehan May 22, 2026
33a866a
debug: echo out plan files
chrispsheehan May 22, 2026
89653b7
chore: cat plan-metadata.json
chrispsheehan May 22, 2026
638b8d7
chore: rn to no plan
chrispsheehan May 22, 2026
884865c
chore: rn to no_plan other ymls
chrispsheehan May 22, 2026
61ab74c
fix: amend waves to correct order
chrispsheehan May 22, 2026
2ab7f65
chore: plan mermaid update
chrispsheehan May 22, 2026
43ad9e1
fix: fmt
chrispsheehan May 22, 2026
210ece7
chore: better mermaid
chrispsheehan May 22, 2026
34f6036
chore: rm mermaid
chrispsheehan May 22, 2026
cd8e52b
debug: try apply from plan
chrispsheehan May 22, 2026
5902692
chore: waves in destroy
chrispsheehan May 22, 2026
d34992b
fix: destroy perms
chrispsheehan May 22, 2026
4729ff4
chore: ignore_shared_artifact_modules bool
chrispsheehan May 22, 2026
ec560cd
fix: mock_outputs_merge_strategy_with_state = "shallow"
chrispsheehan May 22, 2026
d3487fa
chore: update notes
chrispsheehan May 22, 2026
7fd22bc
chore: ignore_oidc_module true
chrispsheehan May 22, 2026
a0c8720
fix: more mock_outputs_merge_strategy_with_state = "shallow"
chrispsheehan May 22, 2026
ce8cd90
chore: more mock_outputs_merge_strategy_with_state = "shallow"
chrispsheehan May 22, 2026
c86db4f
chore: image destroy place holder
chrispsheehan May 22, 2026
455598e
chore: rm wave 3 from destroy
chrispsheehan May 22, 2026
3a0544a
fix: fmt
chrispsheehan May 22, 2026
026509c
chore: include has changes in metadata json
chrispsheehan May 22, 2026
118d0f2
chore: check for metadata json
chrispsheehan May 22, 2026
d8de37c
fix: --terragrunt-non-interactive
chrispsheehan May 22, 2026
edbc665
chore: cat out meta file
chrispsheehan May 22, 2026
de0ec05
fix: ignore_shared_artifact_modules: true
chrispsheehan May 22, 2026
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
28 changes: 16 additions & 12 deletions .github/actions/terragrunt/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Execute Terraform & Terragrunt

This GitHub Action sets up **Terraform** and **Terragrunt** and runs a specified `terragrunt` action: `apply`, `plan`, `apply_plan`, `destroy`, or `init`. When the action needs AWS, the workflow job should configure credentials first.
This GitHub Action sets up **Terraform** and **Terragrunt** and runs a specified `terragrunt` action: `apply`, `plan`, `apply_plan`, `destroy`, `init`, or `graph`. When the action needs AWS, the workflow job should configure credentials first.

## Features

Expand All @@ -10,7 +10,8 @@ This GitHub Action sets up **Terraform** and **Terragrunt** and runs a specified
- Optionally passes Terragrunt variables via JSON tfvars
- Supports `plan` mode for producing local saved plan files
- Supports `init` mode for outputs-only reads
- Relies on shared Terragrunt root hooks for per-stack saved plan artifact upload and download
- Supports `graph` mode for raw `terragrunt run-all graph-dependencies` output capture
- Writes saved plan files into the live stack directory so workflows can upload and download them with GitHub artifacts
- Exports Terragrunt outputs as compact JSON when state exists

The Terragrunt install step is kept in this repo-local action rather than hidden behind a third-party Terragrunt wrapper action so the repo can control the exact setup-action revision and react quickly to GitHub Actions runtime deprecations or nested dependency warnings.
Expand All @@ -24,7 +25,7 @@ The Terragrunt install step is kept in this repo-local action rather than hidden
| `aws_region` | AWS region to use | No | `eu-west-2` |
| `override_tg_vars` | Terragrunt variables in JSON, written to `override_tg_vars.tfvars.json` | No | `{}` |
| `tg_directory` | Directory containing the Terragrunt config | Yes | — |
| `tg_action` | Terragrunt action: `apply`, `plan`, `apply_plan`, `destroy`, or `init` | Yes | `apply` |
| `tg_action` | Terragrunt action: `apply`, `plan`, `apply_plan`, `destroy`, `init`, or `graph` | Yes | `apply` |

`override_tg_vars` is written for `apply`, `plan`, and `destroy`, but not for `init`.

Expand All @@ -33,28 +34,31 @@ The Terragrunt install step is kept in this repo-local action rather than hidden
| Name | Description |
|---|---|
| `tg_outputs` | All Terraform outputs in compact JSON. If no state exists, returns `{}` |
| `tg_graph_output` | Raw Terragrunt `run-all graph-dependencies` output. Set only for `tg_action: graph` |
## Behavior

- `apply`
Runs `terragrunt apply -auto-approve`
- `plan`
Runs `terragrunt plan -detailed-exitcode -out=terragrunt.tfplan`. The shared Terragrunt root `after_hook` then renders `terragrunt.plan.txt`, writes `terragrunt.plan.meta.json`, and uploads the per-stack plan bundle to the derived plan bucket when `TG_ENABLE_PLAN_ARTIFACTS=true` and `PLAN_ARTIFACT_RUN_ID` is set.
Runs `terragrunt plan -detailed-exitcode -out=<live stack>/terragrunt.tfplan`. The action writes `terragrunt.plan.meta.json` into the live stack directory for every plan run, including `has_changes` and `contains_mocked_outputs`, and writes `terragrunt.plan.txt` alongside the binary plan when the plan has changes.
- `apply_plan`
Runs `terragrunt apply terragrunt.tfplan`. The shared Terragrunt root `before_hook` downloads the saved plan bundle into the Terragrunt working directory when `TG_ENABLE_PLAN_ARTIFACTS=true` and `PLAN_ARTIFACT_RUN_ID` is set, and fails early if the saved metadata reports mocked dependency outputs.
Runs `terragrunt apply <live stack>/terragrunt.tfplan`. The calling workflow is expected to download that stack's saved plan artifact into the live stack directory before invoking `apply_plan`. The action also requires `terragrunt.plan.meta.json` to be present there. If that metadata file is missing, or if it says `contains_mocked_outputs: true`, the action fails before apply and tells the operator to regenerate the plan from real upstream outputs.
- `destroy`
Runs `terragrunt destroy -auto-approve`
- `init`
Runs `terragrunt init -input=false -reconfigure` and then captures outputs
- `graph`
Runs `terragrunt run-all graph-dependencies --terragrunt-non-interactive --terragrunt-include-external-dependencies --terragrunt-log-level error` and exposes the raw output as `tg_graph_output`

## Saved Plan Layout

- One run-level metadata file is stored separately by the shared infra wrapper as a GitHub Actions artifact:
- artifact name: `infra-plan-metadata`
- file: `plan-metadata.json`
- Each Terragrunt stack or module stores its own plan bundle at:
- `s3://<plan_bucket>/terragrunt_plan/<environment>/<plan_run_id>/terragrunt-plan-<sanitized-tg-directory>/terragrunt.tfplan`
- `s3://<plan_bucket>/terragrunt_plan/<environment>/<plan_run_id>/terragrunt-plan-<sanitized-tg-directory>/terragrunt.plan.txt`
- `s3://<plan_bucket>/terragrunt_plan/<environment>/<plan_run_id>/terragrunt-plan-<sanitized-tg-directory>/terragrunt.plan.meta.json`
- file: `plan-metadata.json` containing the frozen workflow inputs and derived `waves`
- Each Terragrunt stack or module stores its own plan bundle as a GitHub Actions artifact named `terragrunt-plan-<environment>-<module>`:
- `terragrunt.plan.meta.json`
- `terragrunt.tfplan` only when changes exist
- `terragrunt.plan.txt` only when changes exist

## AWS Credentials

Expand Down Expand Up @@ -139,7 +143,7 @@ jobs:
tg_action: plan
```

### Apply From Uploaded Plan In S3
### Apply From Downloaded GitHub Artifact

```yaml
jobs:
Expand All @@ -165,4 +169,4 @@ jobs:
tg_action: apply_plan
```

This action expects the workflow to set both `TG_ENABLE_PLAN_ARTIFACTS=true` and `PLAN_ARTIFACT_RUN_ID` when using cross-run saved plans so the shared Terragrunt root hooks can resolve the per-stack plan bundle location from the derived plan bucket and environment.
This action expects the workflow to download the matching per-stack plan artifact into the live stack directory before using `tg_action: apply_plan`.
89 changes: 69 additions & 20 deletions .github/actions/terragrunt/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ inputs:
description: "Module directory to perform action upon"
required: true
tg_action:
description: "Terragrunt action to perform (`apply`, `plan`, `apply_plan`, `destroy`, or `init` for outputs-only)"
description: "Terragrunt action to perform (`apply`, `plan`, `apply_plan`, `destroy`, `init` for outputs-only, or `graph` for raw run-all graph output)"
required: true
default: apply

outputs:
tg_outputs:
description: "All Terraform outputs in JSON format"
value: ${{ steps.tg_outputs.outputs.terraform_json }}
tg_graph_output:
description: "Raw Terragrunt run-all dependency graph output"
value: ${{ steps.tg_graph.outputs.graph_output }}

runs:
using: "composite"
Expand Down Expand Up @@ -70,38 +73,76 @@ runs:
TG_PLAN_LOG_ABS_PATH: ${{ github.workspace }}/${{ inputs.tg_directory }}/terragrunt.plan.log
working-directory: ${{ inputs.tg_directory }}
run: |
PLAN_PATH="terragrunt.tfplan"
PLAN_LOG_PATH="$(pwd)/terragrunt.plan.log"
PLAN_DIR="${{ github.workspace }}/${{ inputs.tg_directory }}"
PLAN_PATH="${PLAN_DIR}/terragrunt.tfplan"
PLAN_TEXT_PATH="${PLAN_DIR}/terragrunt.plan.txt"
PLAN_META_PATH="${PLAN_DIR}/terragrunt.plan.meta.json"
PLAN_JSON_PATH="${PLAN_DIR}/terragrunt.plan.json"
PLAN_LOG_PATH="${PLAN_DIR}/terragrunt.plan.log"

case "${{ inputs.tg_action }}" in
apply)
terragrunt apply -auto-approve -compact-warnings -var-file=override_tg_vars.tfvars.json
terragrunt --terragrunt-non-interactive apply -auto-approve -compact-warnings -var-file=override_tg_vars.tfvars.json
;;
plan)
set +e
terragrunt plan -input=false -lock=false -detailed-exitcode -compact-warnings -out="$PLAN_PATH" -var-file=override_tg_vars.tfvars.json 2>&1 | tee "$PLAN_LOG_PATH"
terragrunt --terragrunt-non-interactive plan -input=false -lock=false -detailed-exitcode -compact-warnings -out="$PLAN_PATH" -var-file=override_tg_vars.tfvars.json 2>&1 | tee "$PLAN_LOG_PATH"
plan_exit_code=${PIPESTATUS[0]}
set -e

if [ "$plan_exit_code" -eq 1 ]; then
exit 1
fi

plan_has_changes=false
if [ "$plan_exit_code" -eq 2 ]; then
plan_has_changes=true
fi

plan_contains_mocked_outputs=false
if grep -Fq "mock outputs provided and returning those in dependency output" "$PLAN_LOG_PATH"; then
plan_contains_mocked_outputs=true
echo "::warning title=Mock outputs used during plan::Terragrunt used dependency mock outputs while creating a saved plan. This plan artifact should not be used with apply_plan until a fresh plan is created from real upstream outputs."
fi

jq -n \
--arg tg_directory "${{ inputs.tg_directory }}" \
--argjson has_changes "$plan_has_changes" \
--argjson contains_mocked_outputs "$plan_contains_mocked_outputs" \
'{tg_directory: $tg_directory, has_changes: $has_changes, contains_mocked_outputs: $contains_mocked_outputs}' \
> "$PLAN_META_PATH"

echo "=== terragrunt.plan.meta.json ==="
cat "$PLAN_META_PATH"

terragrunt show -no-color "$PLAN_PATH" > "$PLAN_TEXT_PATH"

echo "plan_exit_code=$plan_exit_code" >> "$GITHUB_OUTPUT"
echo "plan_contains_mocked_outputs=$plan_contains_mocked_outputs" >> "$GITHUB_OUTPUT"
echo "Terragrunt binary plan path in cache: $PLAN_PATH"
echo "Terragrunt binary plan path: $PLAN_PATH"

;;
apply_plan)
if [ ! -f "$PLAN_PATH" ]; then
echo "::error title=Missing saved plan artifact::Expected '${PLAN_PATH}' to exist before apply_plan."
exit 1
fi

if [ ! -f "$PLAN_META_PATH" ]; then
echo "::error title=Missing saved plan metadata::Expected '${PLAN_META_PATH}' to exist before apply_plan."
exit 1
fi

plan_contains_mocked_outputs="$(jq -r '.contains_mocked_outputs // false' "$PLAN_META_PATH")"

if [ "$plan_contains_mocked_outputs" = "true" ]; then
echo "::error title=Saved plan contains mocked outputs::Saved plan metadata indicates mocked outputs were used. Regenerate it after upstream real outputs exist."
exit 1
fi

set +e
APPLY_LOG_PATH="$(pwd)/terragrunt.apply.log"
terragrunt apply -auto-approve "$PLAN_PATH" 2>&1 | tee "$APPLY_LOG_PATH"
terragrunt --terragrunt-non-interactive apply -auto-approve "$PLAN_PATH" 2>&1 | tee "$APPLY_LOG_PATH"
apply_exit_code=${PIPESTATUS[0]}
set -e

Expand All @@ -123,33 +164,41 @@ runs:
emit_error "Saved plan is stale" "Saved plan is stale"
fi

emit_error "Saved plan artifact missing" "Saved plan artifact not found for"
emit_error "Missing PLAN_ARTIFACT_RUN_ID" "PLAN_ARTIFACT_RUN_ID is required when TG_ENABLE_PLAN_ARTIFACTS=true"

if grep -Fq "Plan bucket" "$APPLY_LOG_PATH" && grep -Fq "does not exist" "$APPLY_LOG_PATH"; then
emit_error "Plan bucket missing" "Plan bucket"
fi

emit_error "Plan bucket creation declined" "Plan bucket creation declined."
emit_error "Plan bucket confirmation unavailable" "no interactive terminal is available for confirmation"
exit "$apply_exit_code"
fi
;;
destroy)
terragrunt destroy -auto-approve -compact-warnings -var-file=override_tg_vars.tfvars.json
terragrunt --terragrunt-non-interactive destroy -auto-approve -compact-warnings -var-file=override_tg_vars.tfvars.json
;;
init)
echo "Running init only (no infra changes)..."
terragrunt init -input=false -reconfigure
terragrunt --terragrunt-non-interactive init -input=false -reconfigure
;;
graph)
echo "Graph mode runs in the dedicated graph step."
;;
*)
echo "Unknown tg_action: '${{ inputs.tg_action }}' (expected: apply|plan|apply_plan|destroy|init)" >&2
echo "Unknown tg_action: '${{ inputs.tg_action }}' (expected: apply|plan|apply_plan|destroy|init|graph)" >&2
exit 2
;;
esac

- name: Capture Terragrunt graph JSON
if: inputs.tg_action == 'graph'
id: tg_graph
shell: bash
working-directory: ${{ inputs.tg_directory }}
run: |
echo "🕸️ Rendering Terragrunt run-all dependency graph..."
{
echo "graph_output<<EOF"
terragrunt run-all graph-dependencies --terragrunt-non-interactive --terragrunt-include-external-dependencies --terragrunt-log-level error
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo "✅ Terragrunt graph captured."

- name: Capture Terraform Outputs
if: inputs.tg_action != 'destroy'
if: inputs.tg_action != 'destroy' && inputs.tg_action != 'graph'
id: tg_outputs
shell: bash
working-directory: ${{ inputs.tg_directory }}
Expand Down
Loading