Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7cb0a37
chore: inject dependency for init plan
chrispsheehan May 14, 2026
5a7cc12
chore: use mocked inputs for all dependencies
chrispsheehan May 14, 2026
d575747
docs: update readme
chrispsheehan May 14, 2026
613003b
chore: big bang convert to dependency + mock outputs
chrispsheehan May 14, 2026
2b19dde
chore: swap out more remote state datablocks
chrispsheehan May 14, 2026
205c0ea
chore: ask why terraform_remote_state used
chrispsheehan May 14, 2026
98c71c8
fix: rm no longer required init to get outputs
chrispsheehan May 14, 2026
a0c206a
docs: nots on plan artifacts with mocked inputs
chrispsheehan May 14, 2026
85d93c9
fix: don't allow mocked plan values
chrispsheehan May 14, 2026
305fdd8
Merge remote-tracking branch 'origin/main' into mock-inputs-for-plan
chrispsheehan May 14, 2026
c0f5238
fix: guard for mocked outputs in plan file
chrispsheehan May 14, 2026
31afbca
fix Deprecated Parameter warning
chrispsheehan May 14, 2026
4cf8b30
chore: fail on non-existant plan id
chrispsheehan May 15, 2026
7efb1f1
chore: mv upload/download to hcl
chrispsheehan May 15, 2026
a8487d5
chore: upload artifact fixes
chrispsheehan May 15, 2026
49a06d8
chore: error handling
chrispsheehan May 15, 2026
ae902b0
chore: ensure_lifecycle()
chrispsheehan May 15, 2026
563002f
fix: annoying python cli output
chrispsheehan May 15, 2026
d8b4e26
chore: TG_RESET_PLAN_ARTIFACT_BUCKET=true
chrispsheehan May 15, 2026
8d6578b
fix: tf plan path
chrispsheehan May 15, 2026
08ad19f
chore: proper erorr for gha
chrispsheehan May 17, 2026
786b278
chore: more error handling
chrispsheehan May 17, 2026
069282f
fix: '' escaping
chrispsheehan May 17, 2026
651155e
chore: use latest upload/download action versions
chrispsheehan May 17, 2026
2417ba1
fix: set dummy task arn var
chrispsheehan May 17, 2026
0d946d2
chore: rename deps files
chrispsheehan May 17, 2026
e29828b
chore: check for output/var check
chrispsheehan May 18, 2026
9c4a69c
chore: prefix database_ outs/vars
chrispsheehan May 18, 2026
3e3f753
chore: rename auth_
chrispsheehan May 18, 2026
25c3d58
chore: clearer dep naming
chrispsheehan May 18, 2026
d4565a8
fix: fmt
chrispsheehan May 18, 2026
d3512c8
chore: rn worker_messaging -> messaging
chrispsheehan May 18, 2026
6e3a68f
fix: output naming
chrispsheehan May 18, 2026
4b4dbf4
chore: un-auth health ep
chrispsheehan May 18, 2026
da3e7af
chore: fix renaming
chrispsheehan May 18, 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
21 changes: 11 additions & 10 deletions .github/actions/terragrunt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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
- Uses the repo-local `./.github/actions/just` action for saved plan artifact upload and download
- Relies on shared Terragrunt root hooks for per-stack saved plan artifact upload and download
- 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 Down Expand Up @@ -38,26 +38,27 @@ The Terragrunt install step is kept in this repo-local action rather than hidden
- `apply`
Runs `terragrunt apply -auto-approve`
- `plan`
Runs `terragrunt plan -detailed-exitcode -out=<absolute stack path>/terragrunt.tfplan`, then renders `terragrunt.plan.txt` and writes `terragrunt.plan.meta.json` via the repo `justfile.tg` recipe `terragrunt-plan-render`. It then uploads those files to S3 through the repo-local `./.github/actions/just` action using the AWS credentials already configured in the job.
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.
- `apply_plan`
Downloads the saved plan files into `tg_directory` via the repo-local `./.github/actions/just` action and `justfile.tg`, using the caller-provided `PLAN_ARTIFACT_S3_PREFIX` environment variable plus the stack-derived suffix from `tg_directory`. It then fails if the binary plan file or `terragrunt.plan.meta.json` is missing, reads `has_changes` from the saved metadata file, and skips apply with a GitHub Actions warning when the saved plan contains no mutating resource changes. Otherwise it runs `terragrunt apply` against the absolute stack-path plan file.
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.
- `destroy`
Runs `terragrunt destroy -auto-approve`
- `init`
Runs `terragrunt init -input=false -reconfigure` and then captures outputs

## Saved Plan Layout

- One run-level metadata file is stored separately by the shared infra wrapper at:
- `<plan_artifact_s3_prefix>/infra-plan-metadata/plan-metadata.json`
- 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:
- `<plan_artifact_s3_prefix>/terragrunt-plan-<sanitized-tg-directory>/terragrunt.tfplan`
- `<plan_artifact_s3_prefix>/terragrunt-plan-<sanitized-tg-directory>/terragrunt.plan.txt`
- `<plan_artifact_s3_prefix>/terragrunt-plan-<sanitized-tg-directory>/terragrunt.plan.meta.json`
- `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`

## AWS Credentials

Configure AWS credentials in the workflow job before calling this action. The action then reuses those ambient credentials for Terragrunt itself and for any saved-plan upload or download steps.
Configure AWS credentials in the workflow job before calling this action. The action then reuses those ambient credentials for Terragrunt itself and for any Terragrunt-hook-driven saved-plan upload or download steps.

## Usage

Expand Down Expand Up @@ -164,4 +165,4 @@ jobs:
tg_action: apply_plan
```

This action expects the workflow to download `terragrunt.tfplan`, `terragrunt.plan.txt`, and `terragrunt.plan.meta.json` into `tg_directory` before calling `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.
126 changes: 51 additions & 75 deletions .github/actions/terragrunt/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ inputs:
outputs:
tg_outputs:
description: "All Terraform outputs in JSON format"
value: ${{ steps.tg_outputs.outputs.terraform_json || steps.tg_outputs_skip.outputs.terraform_json }}
value: ${{ steps.tg_outputs.outputs.terraform_json }}

runs:
using: "composite"
Expand Down Expand Up @@ -61,76 +61,79 @@ runs:
run: |
echo "$OVERRIDE_TG_VARS" | jq -c . > ${{ inputs.tg_directory }}/override_tg_vars.tfvars.json

- name: Download saved plan artifacts
if: inputs.tg_action == 'apply_plan'
uses: ./.github/actions/just
env:
TG_DIRECTORY: ${{ inputs.tg_directory }}
with:
aws_region: ${{ inputs.aws_region }}
justfile_path: justfile.tg
just_action: terragrunt-plan-download

- name: Verify plan artifact files exist
if: inputs.tg_action == 'apply_plan'
shell: bash
run: |
test -f "${{ inputs.tg_directory }}/terragrunt.tfplan" || {
echo "Expected plan file '${{ inputs.tg_directory }}/terragrunt.tfplan' was not found before apply_plan." >&2
exit 1
}
test -f "${{ inputs.tg_directory }}/terragrunt.plan.meta.json" || {
echo "Expected plan metadata file '${{ inputs.tg_directory }}/terragrunt.plan.meta.json' was not found before apply_plan." >&2
exit 1
}

- name: Check saved plan for mutating changes
if: inputs.tg_action == 'apply_plan'
id: apply_plan_guard
shell: bash
working-directory: ${{ inputs.tg_directory }}
run: |
PLAN_PATH="$(pwd)/terragrunt.tfplan"
PLAN_META_PATH="$(pwd)/terragrunt.plan.meta.json"

if [ "$(jq -r '.has_changes' "$PLAN_META_PATH")" != "true" ]; then
echo "::warning title=Empty saved plan::Saved plan '$PLAN_PATH' contains no mutating resource changes. Skipping apply."
echo "should_apply=false" >> "$GITHUB_OUTPUT"
else
echo "should_apply=true" >> "$GITHUB_OUTPUT"
fi

- name: Action Terragrunt
if: inputs.tg_action != 'apply_plan' || steps.apply_plan_guard.outputs.should_apply == 'true'
id: terragrunt_action
shell: bash
env:
TF_IN_AUTOMATION: true
TG_PLAN_LOG_FILENAME: terragrunt.plan.log
TG_PLAN_LOG_ABS_PATH: ${{ github.workspace }}/${{ inputs.tg_directory }}/terragrunt.plan.log
working-directory: ${{ inputs.tg_directory }}
run: |
PLAN_PATH="$(pwd)/terragrunt.tfplan"
PLAN_PATH="terragrunt.tfplan"
PLAN_LOG_PATH="$(pwd)/terragrunt.plan.log"

case "${{ inputs.tg_action }}" in
apply)
terragrunt 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
plan_exit_code=$?
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"
plan_exit_code=${PIPESTATUS[0]}
set -e

if [ "$plan_exit_code" -eq 1 ]; then
exit 1
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

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

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

if [ "$apply_exit_code" -ne 0 ]; then
emit_error() {
local title="$1"
local pattern="$2"

if grep -Fq "$pattern" "$APPLY_LOG_PATH"; then
local err_line
err_line="$(grep -F "$pattern" "$APPLY_LOG_PATH" | head -n 1)"
echo "::error title=${title}::${err_line}"
fi
}

emit_error "Saved plan contains mocked outputs" "contains mocked outputs. Regenerate it after upstream real outputs exist."

if grep -Fq "Saved plan is stale" "$APPLY_LOG_PATH"; then
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
Expand All @@ -145,28 +148,8 @@ runs:
;;
esac

- name: Render plan sidecar artifacts
if: inputs.tg_action == 'plan'
uses: ./.github/actions/just
env:
TG_DIRECTORY: ${{ inputs.tg_directory }}
TG_PLAN_EXIT_CODE: ${{ steps.terragrunt_action.outputs.plan_exit_code }}
with:
justfile_path: justfile.tg
just_action: terragrunt-plan-render

- name: Upload saved plan artifacts
if: inputs.tg_action == 'plan'
uses: ./.github/actions/just
env:
TG_DIRECTORY: ${{ inputs.tg_directory }}
with:
aws_region: ${{ inputs.aws_region }}
justfile_path: justfile.tg
just_action: terragrunt-plan-upload

- name: Capture Terraform Outputs
if: inputs.tg_action != 'destroy' && (inputs.tg_action != 'apply_plan' || steps.apply_plan_guard.outputs.should_apply == 'true')
if: inputs.tg_action != 'destroy'
id: tg_outputs
shell: bash
working-directory: ${{ inputs.tg_directory }}
Expand All @@ -180,10 +163,3 @@ runs:
fi
echo "terraform_json=$TERRAGRUNT_OUTPUTS" >> "$GITHUB_OUTPUT"
echo "✅ Terraform outputs captured successfully."

- name: Capture Terraform Outputs For Skipped Apply Plan
if: inputs.tg_action == 'apply_plan' && steps.apply_plan_guard.outputs.should_apply != 'true'
id: tg_outputs_skip
shell: bash
run: |
echo "terraform_json={}" >> "$GITHUB_OUTPUT"
Loading
Loading