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
4 changes: 2 additions & 2 deletions .github/actions/just/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ This GitHub Action sets up [`just`](https://github.com/casey/just), authenticate
|--------------------|--------------------------------------------------|----------|--------------|
| `just_version` | Version of `just` to install | ❌ | `1.0.0` |
| `aws_region` | AWS region | ❌ | `eu-west-2` |
| `aws_oidc_role_arn`| ARN of the IAM role to assume via OIDC | ✅ | — |
| `aws_oidc_role_arn`| ARN of the IAM role to assume via OIDC (optional when AWS credentials are already configured in the job) | ❌ | `""` |
| `just_action` | The `just` recipe to execute | ✅ | — |
| `mask_result` | Use to mask value in CI | ❌ | `false` |

Expand Down Expand Up @@ -98,4 +98,4 @@ jobs:
- name: read output from script
run: |
echo "Script output will appear *** in CI logs: ${{ steps.get-secret.outputs.just_outputs }}"
```
```
4 changes: 2 additions & 2 deletions .github/actions/just/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ runs:
using: "composite"
steps:
- name: Install Just
uses: extractions/setup-just@v3
uses: extractions/setup-just@v4
with:
just-version: ${{ inputs.just_version }}

- name: Configure AWS OIDC Authentication
if: ${{ inputs.aws_oidc_role_arn != '' }}
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: ${{ inputs.aws_oidc_role_arn }}
aws-region: ${{ inputs.aws_region }}
Expand Down
31 changes: 17 additions & 14 deletions .github/actions/terragrunt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ This GitHub Action sets up **Terraform** and **Terragrunt**, authenticates to AW
- Installs pinned versions of Terraform and Terragrunt
- Authenticates to AWS using OIDC only when the selected action actually needs AWS access
- Optionally passes Terragrunt variables via JSON tfvars
- Supports `plan` mode with automatic GitHub artifact upload
- Supports `plan` mode for producing local saved plan files
- Supports `init` mode for outputs-only reads
- Uses the repo-local `./.github/actions/just` action with OIDC for saved plan artifact upload and download
- Exports Terragrunt outputs as compact JSON when state exists

## Inputs
Expand All @@ -19,8 +20,6 @@ This GitHub Action sets up **Terraform** and **Terragrunt**, authenticates to AW
| `tg_version` | Version of Terragrunt to install | No | `0.72.6` |
| `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 | `{}` |
| `plan_artifact_run_id` | Optional workflow run ID to download a plan artifact from in `apply_plan` mode | No | `""` |
| `github_token` | GitHub token used for cross-run plan artifact downloads | No | `""` |
| `aws_oidc_role_arn` | IAM role ARN to assume via OIDC | Yes | — |
| `tg_directory` | Directory containing the Terragrunt config | Yes | — |
| `tg_action` | Terragrunt action: `apply`, `plan`, `apply_plan`, `destroy`, or `init` | Yes | `apply` |
Expand All @@ -32,21 +31,28 @@ This GitHub Action sets up **Terraform** and **Terragrunt**, authenticates to AW
| Name | Description |
|---|---|
| `tg_outputs` | All Terraform outputs in compact JSON. If no state exists, returns `{}` |
| `plan_artifact_name` | Derived GitHub artifact name for a Terragrunt plan |

## Behavior

- `apply`
Runs `terragrunt apply -auto-approve`
- `plan`
Runs `terragrunt plan -detailed-exitcode -out=<absolute stack path>/terragrunt.tfplan`, renders a text view to `terragrunt.plan.txt`, writes `terragrunt.plan.meta.json` with `exit_code` and `has_changes`, and uploads all three files as a GitHub artifact via `actions/upload-artifact@v7`. The artifact name is derived from `tg_directory`.
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 same OIDC role.
- `apply_plan`
Downloads the derived plan artifact into the working directory via `actions/download-artifact@v8`, explicitly scoped to the current `github.repository` for cross-run recovery, fails if the artifact, binary plan file, or `terragrunt.plan.meta.json` is missing, reads `has_changes` from the saved metadata file, and skips both AWS authentication and apply with a GitHub Actions warning when the saved plan contains no mutating resource changes. Otherwise it configures AWS credentials and runs `terragrunt apply` against the downloaded absolute stack-path plan file. For separate workflow runs, pass `plan_artifact_run_id` and `github_token`.
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.
- `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`
- 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`

## Usage

### Minimal Apply
Expand Down Expand Up @@ -76,7 +82,7 @@ jobs:
echo '${{ steps.tg_action.outputs.tg_outputs }}' | jq .
```

### Plan And Upload Artifact
### Plan

```yaml
jobs:
Expand All @@ -89,20 +95,15 @@ jobs:
- uses: actions/checkout@v4

- name: Plan infrastructure
id: tg_plan
uses: your-org/your-action-repo@main
with:
aws_region: ${{ vars.AWS_REGION }}
aws_oidc_role_arn: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/${{ vars.PROJECT_NAME }}-dev-github-oidc-role
tg_directory: infra/live/dev/aws/network
tg_action: plan

- name: Show uploaded artifact name
run: |
echo "${{ steps.tg_plan.outputs.plan_artifact_name }}"
```

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

```yaml
jobs:
Expand All @@ -122,3 +123,5 @@ jobs:
tg_directory: infra/live/dev/aws/network
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`.
95 changes: 34 additions & 61 deletions .github/actions/terragrunt/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,6 @@ inputs:
description: "Override or additional Terragrunt variables in JSON format"
required: false
default: "{}"
plan_artifact_run_id:
description: "Optional workflow run ID to download a plan artifact from when using `apply_plan` across separate workflow runs"
required: false
default: ""
github_token:
description: "GitHub token used for cross-run plan artifact downloads"
required: false
default: ""
aws_oidc_role_arn:
description: "AWS iam role arn"
required: true
Expand All @@ -40,21 +32,18 @@ outputs:
tg_outputs:
description: "All Terraform outputs in JSON format"
value: ${{ steps.tg_outputs.outputs.terraform_json || steps.tg_outputs_skip.outputs.terraform_json }}
plan_artifact_name:
description: "Derived GitHub artifact name for a Terragrunt plan"
value: ${{ steps.plan_artifact.outputs.plan_artifact_name }}

runs:
using: "composite"
steps:
- name: Install Terraform
uses: hashicorp/setup-terraform@v3
uses: hashicorp/setup-terraform@v4
with:
terraform_version: ${{ inputs.tf_version }}
terraform_wrapper: false

- name: Install Terragrunt
uses: autero1/action-terragrunt@v1.3.2
uses: autero1/action-terragrunt@v3.0.2
with:
terragrunt_version: ${{ inputs.tg_version }}

Expand All @@ -66,42 +55,27 @@ runs:
run: |
echo "$OVERRIDE_TG_VARS" | jq -c . > ${{ inputs.tg_directory }}/override_tg_vars.tfvars.json

- name: Resolve plan artifact name
if: inputs.tg_action == 'plan' || inputs.tg_action == 'apply_plan'
id: plan_artifact
shell: bash
run: |
sanitized_dir="$(echo "${{ inputs.tg_directory }}" | tr '/.' '--')"
artifact_name="terragrunt-plan-${sanitized_dir}"
echo "plan_artifact_name=$artifact_name" >> "$GITHUB_OUTPUT"

- name: Download plan artifact from current run
if: inputs.tg_action == 'apply_plan' && inputs.plan_artifact_run_id == ''
uses: actions/download-artifact@v8
with:
name: ${{ steps.plan_artifact.outputs.plan_artifact_name }}
path: ${{ inputs.tg_directory }}

- name: Download plan artifact from prior run
if: inputs.tg_action == 'apply_plan' && inputs.plan_artifact_run_id != ''
uses: actions/download-artifact@v8
- name: Download saved plan artifacts
if: inputs.tg_action == 'apply_plan'
uses: ./.github/actions/just
env:
TG_DIRECTORY: ${{ inputs.tg_directory }}
with:
name: ${{ steps.plan_artifact.outputs.plan_artifact_name }}
path: ${{ inputs.tg_directory }}
repository: ${{ github.repository }}
github-token: ${{ inputs.github_token }}
run-id: ${{ inputs.plan_artifact_run_id }}
aws_oidc_role_arn: ${{ inputs.aws_oidc_role_arn }}
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 after artifact download." >&2
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 after artifact download." >&2
echo "Expected plan metadata file '${{ inputs.tg_directory }}/terragrunt.plan.meta.json' was not found before apply_plan." >&2
exit 1
}

Expand All @@ -123,7 +97,7 @@ runs:

- name: Configure AWS OIDC Authentication
if: inputs.tg_action != 'apply_plan' || steps.apply_plan_guard.outputs.should_apply == 'true'
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: ${{ inputs.aws_oidc_role_arn }}
aws-region: ${{ inputs.aws_region }}
Expand All @@ -137,8 +111,6 @@ runs:
working-directory: ${{ inputs.tg_directory }}
run: |
PLAN_PATH="$(pwd)/terragrunt.tfplan"
PLAN_TEXT_PATH="$(pwd)/terragrunt.plan.txt"
PLAN_META_PATH="$(pwd)/terragrunt.plan.meta.json"

case "${{ inputs.tg_action }}" in
apply)
Expand All @@ -154,19 +126,10 @@ runs:
exit 1
fi

terragrunt show -no-color "$PLAN_PATH" > "$PLAN_TEXT_PATH"
jq -n \
--arg tg_directory "${{ inputs.tg_directory }}" \
--argjson exit_code "$plan_exit_code" \
--argjson has_changes "$([ "$plan_exit_code" -eq 2 ] && echo true || echo false)" \
'{tg_directory: $tg_directory, exit_code: $exit_code, has_changes: $has_changes}' \
> "$PLAN_META_PATH"
echo "plan_exit_code=$plan_exit_code" >> "$GITHUB_OUTPUT"
echo "Terragrunt binary plan path: $PLAN_PATH"
ls -l "$PLAN_PATH"
echo "Terragrunt rendered plan path: $PLAN_TEXT_PATH"
cat "$PLAN_TEXT_PATH"
echo "Terragrunt plan metadata path: $PLAN_META_PATH"
cat "$PLAN_META_PATH"

;;
apply_plan)
terragrunt apply -auto-approve -compact-warnings "$PLAN_PATH"
Expand All @@ -184,16 +147,26 @@ runs:
;;
esac

- name: Upload plan artifact
- 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: actions/upload-artifact@v7
uses: ./.github/actions/just
env:
TG_DIRECTORY: ${{ inputs.tg_directory }}
with:
name: ${{ steps.plan_artifact.outputs.plan_artifact_name }}
path: |
${{ inputs.tg_directory }}/terragrunt.tfplan
${{ inputs.tg_directory }}/terragrunt.plan.txt
${{ inputs.tg_directory }}/terragrunt.plan.meta.json
if-no-files-found: error
aws_oidc_role_arn: ${{ inputs.aws_oidc_role_arn }}
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')
Expand Down
Loading
Loading