From 021f88a0fea72b4618530b3f83367462788ad0ea Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Fri, 24 Apr 2026 16:58:05 +0100 Subject: [PATCH 01/23] fix: rm git-token input --- .github/workflows/dev_infra_apply_from_plan.yml | 1 - .github/workflows/dev_infra_plan_and_apply.yml | 1 - .github/workflows/prod_infra_apply_from_plan.yml | 1 - 3 files changed, 3 deletions(-) diff --git a/.github/workflows/dev_infra_apply_from_plan.yml b/.github/workflows/dev_infra_apply_from_plan.yml index a0a82d0c..5c18b61a 100644 --- a/.github/workflows/dev_infra_apply_from_plan.yml +++ b/.github/workflows/dev_infra_apply_from_plan.yml @@ -20,4 +20,3 @@ jobs: with: environment: dev plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ github.token }} diff --git a/.github/workflows/dev_infra_plan_and_apply.yml b/.github/workflows/dev_infra_plan_and_apply.yml index 73890ea3..645a2721 100644 --- a/.github/workflows/dev_infra_plan_and_apply.yml +++ b/.github/workflows/dev_infra_plan_and_apply.yml @@ -42,4 +42,3 @@ jobs: with: environment: dev plan_artifact_run_id: ${{ github.run_id }} - github_token: ${{ github.token }} diff --git a/.github/workflows/prod_infra_apply_from_plan.yml b/.github/workflows/prod_infra_apply_from_plan.yml index 1eff5d1d..403a89b8 100644 --- a/.github/workflows/prod_infra_apply_from_plan.yml +++ b/.github/workflows/prod_infra_apply_from_plan.yml @@ -20,4 +20,3 @@ jobs: with: environment: prod plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ github.token }} From 78ce1e60121c09c6b65885a818e2ba6e451bc209 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Fri, 24 Apr 2026 16:58:27 +0100 Subject: [PATCH 02/23] fix: rm git-token --- infra/root.hcl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/infra/root.hcl b/infra/root.hcl index ce19e3e2..981ea3fd 100644 --- a/infra/root.hcl +++ b/infra/root.hcl @@ -1,6 +1,4 @@ locals { - github_token = get_env("GITHUB_TOKEN", "not_set") - git_remote = run_cmd("--terragrunt-quiet", "git", "remote", "get-url", "origin") github_repo = regex("[/:]([-0-9_A-Za-z]*/[-0-9_A-Za-z]*)[^/]*$", local.git_remote)[0] repo_owner = split("/", local.github_repo)[0] @@ -105,7 +103,7 @@ inputs = merge( deploy_role_arn = local.deploy_role_arn state_bucket = local.state_bucket state_lock_table = local.state_lock_table - code_bucket = local.code_bucket - ecr_repository_name = local.ecr_repository_name + code_bucket = local.code_bucket + ecr_repository_name = local.ecr_repository_name } ) From 191e70f74c3477ddc3526d55167963f25c1b3d4e Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Fri, 24 Apr 2026 17:19:52 +0100 Subject: [PATCH 03/23] chore: breadcrumb --- .github/actions/terragrunt/README.md | 21 +- .github/actions/terragrunt/action.yml | 62 +--- .github/docs/README.md | 13 +- .github/workflows/shared_infra.yml | 272 +++++++++++++++--- .../shared_infra_apply_from_plan.yml | 28 +- .github/workflows/shared_infra_plan.yml | 21 +- README.md | 1 + infra/README.md | 3 +- .../modules/aws/_shared/code_bucket/README.md | 1 + justfile.ci | 139 +++++++++ 10 files changed, 434 insertions(+), 127 deletions(-) diff --git a/.github/actions/terragrunt/README.md b/.github/actions/terragrunt/README.md index e9587c9e..dbd9ef5a 100644 --- a/.github/actions/terragrunt/README.md +++ b/.github/actions/terragrunt/README.md @@ -7,7 +7,7 @@ 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 - Exports Terragrunt outputs as compact JSON when state exists @@ -19,8 +19,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` | @@ -32,16 +30,14 @@ 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=/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=/terragrunt.tfplan`, renders a text view to `terragrunt.plan.txt`, and writes `terragrunt.plan.meta.json` with `exit_code` and `has_changes`. - `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`. + Expects the saved plan files to already exist in `tg_directory`, 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 downloaded absolute stack-path plan file. - `destroy` Runs `terragrunt destroy -auto-approve` - `init` @@ -76,7 +72,7 @@ jobs: echo '${{ steps.tg_action.outputs.tg_outputs }}' | jq . ``` -### Plan And Upload Artifact +### Plan ```yaml jobs: @@ -89,20 +85,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: @@ -122,3 +113,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`. diff --git a/.github/actions/terragrunt/action.yml b/.github/actions/terragrunt/action.yml index c553e20b..68abea3c 100644 --- a/.github/actions/terragrunt/action.yml +++ b/.github/actions/terragrunt/action.yml @@ -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 @@ -40,9 +32,6 @@ 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" @@ -66,42 +55,22 @@ 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: Configure AWS OIDC Authentication + uses: aws-actions/configure-aws-credentials@v4 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 }} + role-to-assume: ${{ inputs.aws_oidc_role_arn }} + aws-region: ${{ inputs.aws_region }} - 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 } @@ -121,13 +90,6 @@ runs: echo "should_apply=true" >> "$GITHUB_OUTPUT" fi - - 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 - with: - role-to-assume: ${{ inputs.aws_oidc_role_arn }} - aws-region: ${{ inputs.aws_region }} - - name: Action Terragrunt if: inputs.tg_action != 'apply_plan' || steps.apply_plan_guard.outputs.should_apply == 'true' id: terragrunt_action @@ -167,6 +129,7 @@ runs: 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" @@ -184,17 +147,6 @@ runs: ;; esac - - name: Upload plan artifact - if: inputs.tg_action == 'plan' - uses: actions/upload-artifact@v7 - 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 - - name: Capture Terraform Outputs if: inputs.tg_action != 'destroy' && (inputs.tg_action != 'apply_plan' || steps.apply_plan_guard.outputs.should_apply == 'true') id: tg_outputs diff --git a/.github/docs/README.md b/.github/docs/README.md index a3a9033d..072e4e31 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -82,14 +82,14 @@ flowchart LR ### Infra And Code Rollout - `shared_infra_plan.yml` - Plan wrapper around `shared_infra.yml`. It takes resolved workflow inputs directly, writes and uploads `infra-plan-metadata`, and then calls `shared_infra.yml` with `tg_action: plan`. After the plan completes, it prints the current workflow `github.run_id` into both the logs and the GitHub Actions step summary as `plan_artifact_run_id`, and exposes that value as a reusable-workflow output. + Plan wrapper around `shared_infra.yml`. It takes resolved workflow inputs directly, writes `infra-plan-metadata`, uploads it to the shared code bucket under `terragrunt_plan///infra-plan-metadata/` via `justfile.ci`, and then calls `shared_infra.yml` with `tg_action: plan`. After the plan completes, it prints the current workflow `github.run_id` into both the logs and the GitHub Actions step summary as `plan_artifact_run_id`, and exposes that value as a reusable-workflow output. - `shared_infra_apply.yml` Direct-input apply wrapper around `shared_infra.yml`. It takes resolved workflow inputs directly and calls `shared_infra.yml` with `tg_action: apply`. - `shared_infra_apply_from_plan.yml` - Apply-from-plan wrapper around `shared_infra.yml`. It takes `plan_artifact_run_id` plus a `github_token` input, downloads `infra-plan-metadata`, reads the frozen graph inputs back out, and then calls `shared_infra.yml` with `tg_action: apply_plan`. + Apply-from-plan wrapper around `shared_infra.yml`. It takes `plan_artifact_run_id`, downloads `infra-plan-metadata` from the shared code bucket via `justfile.ci`, reads the frozen graph inputs back out, and then calls `shared_infra.yml` with `tg_action: apply_plan`. - `shared_infra.yml` - Pure ordered infra graph executor. It applies shared stacks first, then runtime stacks, then frontend infrastructure. Shared stacks now include the CloudWatch observability dashboard. It accepts `tg_action` so the same graph can run a normal apply, upload derived per-stack plan artifacts, or apply from previously uploaded plan artifacts. Its visible step labels now follow `tg_action` too, so plan and apply-from-plan runs no longer show everything as `Deploy ...`. The `security -> network` edge is a real bootstrap dependency because `network` reads security outputs like `vpc_endpoint_sg` from remote state; if those outputs do not exist yet, `network` fails with an upstream `Unsupported attribute` error rather than a networking-specific error. -- The shared infra wrappers must forward the permissions required by the nested reusable call chain. In practice that means `id-token: write` everywhere the Terragrunt action may assume AWS OIDC, `contents: read` for checkout, and `actions: read` anywhere cross-run artifact recovery can happen. That includes `shared_infra_apply_from_plan.yml` and the nested `shared_infra.yml` jobs, because both the shared metadata recovery and the per-stack `apply_plan` downloads use artifact actions across workflow runs in that chain. + Pure ordered infra graph executor. It applies shared stacks first, then runtime stacks, then frontend infrastructure. Shared stacks now include the CloudWatch observability dashboard. It accepts `tg_action` so the same graph can run a normal apply, upload derived per-stack plan artifacts to the shared code bucket under `terragrunt_plan/`, or apply from previously uploaded plan artifacts. In `apply_plan` mode, each stack job first downloads its own saved plan files via `justfile.ci`, then the Terragrunt action validates and applies those local files. Its visible step labels now follow `tg_action` too, so plan and apply-from-plan runs no longer show everything as `Deploy ...`. The `security -> network` edge is a real bootstrap dependency because `network` reads security outputs like `vpc_endpoint_sg` from remote state; if those outputs do not exist yet, `network` fails with an upstream `Unsupported attribute` error rather than a networking-specific error. +- The shared infra wrappers must forward the permissions required by the nested reusable call chain. In practice that means `id-token: write` everywhere the Terragrunt action may assume AWS OIDC and `contents: read` for checkout. The shared plan/apply wrappers now rely on AWS access to the shared code bucket rather than GitHub artifact permissions for cross-run recovery. - `shared_deploy.yml` Rolls out Lambda code, optional migrations, optional reconciliation Lambdas, ECS task and service updates, and optional frontend deploys. The reusable workflow renders its Lambda and ECS CodeDeploy AppSpec files from the shared templates under `config/deploy/`, and its mutating `just` steps should target `justfile.deploy` rather than the repo-root `justfile`. @@ -141,11 +141,10 @@ Run these checks on every CI, workflow, or deploy-contract change. - compare every caller `with:` block against the callee `workflow_call.inputs` - compare expected outputs against actual `jobs..outputs.*` - verify optional inputs are intentionally omitted, not accidentally missing -- the repo-local `./.github/actions/terragrunt` action now supports `tg_action: plan` plus automatic artifact upload of the binary plan, rendered text plan, and a `terragrunt.plan.meta.json` sidecar with `exit_code` and `has_changes`; keep that contract in mind before inventing a second plan-storage mechanism +- the repo-local `./.github/actions/terragrunt` action now supports `tg_action: plan` for producing the binary plan, rendered text plan, and `terragrunt.plan.meta.json` sidecar locally; `shared_infra.yml` is responsible for uploading those files into the shared code bucket under `terragrunt_plan/`, and `apply_plan` expects them to have been downloaded into the stack directory by the workflow first - `./.github/actions/terragrunt` now skips `apply_plan` with a warning when the saved `terragrunt.plan.meta.json` reports `has_changes: false`, and in that no-op case it also skips `aws-actions/configure-aws-credentials` - `./.github/actions/terragrunt` derives its plan artifact name from `tg_directory`, so callers do not need to pass artifact naming inputs -- if `apply_plan` is used across separate workflow runs, pass the earlier workflow `run_id` through `plan_artifact_run_id` and pass the GitHub token as a reusable-workflow secret with `actions: read` so both the shared metadata artifact and the plan artifacts can be downloaded -- for cross-run artifact recovery, scope `actions/download-artifact` explicitly to `repository: ${{ github.repository }}` rather than relying on implicit repository resolution through the reusable-workflow chain +- if `apply_plan` is used across separate workflow runs, pass the earlier workflow `run_id` through `plan_artifact_run_id`; the shared wrappers recover both metadata and per-stack plan files from the shared code bucket under `terragrunt_plan///...` - if a cross-run apply should not ask the operator to re-enter versions or recompute artifact resolution, store both the input versions and the resolved reusable-workflow outputs in a metadata artifact during plan and recover them in the apply wrapper from the earlier `run_id` - keep `shared_infra.yml` as the pure graph executor and prefer handling metadata creation/recovery in the dedicated plan/apply wrappers - when using `./.github/actions/just`, check whether the caller needs the repo-root `justfile` or an explicit `justfile_path` diff --git a/.github/workflows/shared_infra.yml b/.github/workflows/shared_infra.yml index 3baff81b..6d1a4208 100644 --- a/.github/workflows/shared_infra.yml +++ b/.github/workflows/shared_infra.yml @@ -38,11 +38,6 @@ on: required: false type: string default: "" - github_token: - description: "Optional GitHub token used for cross-run artifact download in apply-from-plan mode" - required: false - type: string - default: "" concurrency: # only run one instance of workflow at any one time @@ -62,62 +57,132 @@ env: jobs: oidc: runs-on: ubuntu-latest + env: + BUCKET_NAME: ${{ inputs.code_bucket }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} + TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/oidc steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.infra_version }} + - name: Download planned oidc artifact + if: inputs.tg_action == 'apply_plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-download + - name: ${{ env.TG_ACTION_LABEL }} oidc role infra uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/oidc tg_action: ${{ inputs.tg_action }} - plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ inputs.github_token }} + + - name: Upload planned oidc artifact + if: inputs.tg_action == 'plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-upload worker_messaging: needs: oidc runs-on: ubuntu-latest + env: + BUCKET_NAME: ${{ inputs.code_bucket }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} + TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/worker_messaging steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.infra_version }} + - name: Download planned worker messaging artifact + if: inputs.tg_action == 'apply_plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-download + - name: ${{ env.TG_ACTION_LABEL }} worker messaging infra uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/worker_messaging tg_action: ${{ inputs.tg_action }} - plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ inputs.github_token }} + + - name: Upload planned worker messaging artifact + if: inputs.tg_action == 'plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-upload observability: needs: oidc runs-on: ubuntu-latest + env: + BUCKET_NAME: ${{ inputs.code_bucket }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} + TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/observability steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.infra_version }} + - name: Download planned observability artifact + if: inputs.tg_action == 'apply_plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-download + - name: ${{ env.TG_ACTION_LABEL }} observability infra uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/observability tg_action: ${{ inputs.tg_action }} - plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ inputs.github_token }} + + - name: Upload planned observability artifact + if: inputs.tg_action == 'plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-upload cognito: needs: oidc runs-on: ubuntu-latest + env: + BUCKET_NAME: ${{ inputs.code_bucket }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} + TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/cognito steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.infra_version }} + - name: Download planned cognito artifact + if: inputs.tg_action == 'apply_plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-download + - name: ${{ env.TG_ACTION_LABEL }} cognito infra uses: ./.github/actions/terragrunt env: @@ -126,20 +191,32 @@ jobs: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/cognito tg_action: ${{ inputs.tg_action }} - plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ inputs.github_token }} + + - name: Upload planned cognito artifact + if: inputs.tg_action == 'plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-upload frontend: needs: - network - cognito runs-on: ubuntu-latest + env: + BUCKET_NAME: ${{ inputs.code_bucket }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} + TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/frontend steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.infra_version }} - name: Get network infra + if: inputs.tg_action != 'apply_plan' uses: ./.github/actions/terragrunt id: get-network with: @@ -148,6 +225,7 @@ jobs: tg_action: init - name: Get cognito infra + if: inputs.tg_action != 'apply_plan' uses: ./.github/actions/terragrunt id: get-cognito env: @@ -158,6 +236,7 @@ jobs: tg_action: init - name: Get api invoke url + if: inputs.tg_action != 'apply_plan' id: get_api_vars env: TG_OUTPUTS: ${{ steps.get-network.outputs.tg_outputs }} @@ -165,6 +244,7 @@ jobs: echo "invoke_url=$(echo $TG_OUTPUTS | jq -r '.api_invoke_url.value')" >> $GITHUB_OUTPUT - name: Get cognito values + if: inputs.tg_action != 'apply_plan' id: get_cognito_vars env: TG_OUTPUTS: ${{ steps.get-cognito.outputs.tg_outputs }} @@ -174,42 +254,80 @@ jobs: echo "hosted_ui_url=$(echo "$TG_OUTPUTS" | jq -r '.hosted_ui_url.value')" >> "$GITHUB_OUTPUT" echo "readonly_group_name=$(echo "$TG_OUTPUTS" | jq -r '.readonly_group_name.value')" >> "$GITHUB_OUTPUT" + - name: Download planned frontend artifact + if: inputs.tg_action == 'apply_plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-download + - name: ${{ env.TG_ACTION_LABEL }} frontend infra uses: ./.github/actions/terragrunt env: - TF_VAR_api_invoke_url: ${{ steps.get_api_vars.outputs.invoke_url }} + TF_VAR_api_invoke_url: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_api_vars.outputs.invoke_url }} TF_VAR_domain_name: ${{ env.DOMAIN_NAME }} - TF_VAR_auth_user_pool_id: ${{ steps.get_cognito_vars.outputs.user_pool_id }} - TF_VAR_auth_user_pool_client_id: ${{ steps.get_cognito_vars.outputs.user_pool_client_id }} - TF_VAR_auth_hosted_ui_url: ${{ steps.get_cognito_vars.outputs.hosted_ui_url }} - TF_VAR_auth_readonly_group_name: ${{ steps.get_cognito_vars.outputs.readonly_group_name }} + TF_VAR_auth_user_pool_id: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.user_pool_id }} + TF_VAR_auth_user_pool_client_id: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.user_pool_client_id }} + TF_VAR_auth_hosted_ui_url: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.hosted_ui_url }} + TF_VAR_auth_readonly_group_name: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.readonly_group_name }} with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/frontend tg_action: ${{ inputs.tg_action }} - plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ inputs.github_token }} + + - name: Upload planned frontend artifact + if: inputs.tg_action == 'plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-upload cluster: needs: oidc runs-on: ubuntu-latest + env: + BUCKET_NAME: ${{ inputs.code_bucket }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} + TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/cluster steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.infra_version }} + - name: Download planned cluster artifact + if: inputs.tg_action == 'apply_plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-download + - name: ${{ env.TG_ACTION_LABEL }} cluster infra uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/cluster tg_action: ${{ inputs.tg_action }} - plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ inputs.github_token }} + + - name: Upload planned cluster artifact + if: inputs.tg_action == 'plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-upload security: needs: oidc runs-on: ubuntu-latest + env: + BUCKET_NAME: ${{ inputs.code_bucket }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} + TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/security outputs: postgres_sg: ${{ steps.get_security_outputs.outputs.postgres_sg }} steps: @@ -217,6 +335,14 @@ jobs: with: ref: ${{ inputs.infra_version }} + - name: Download planned security artifact + if: inputs.tg_action == 'apply_plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-download + - name: ${{ env.TG_ACTION_LABEL }} security infra id: deploy-security uses: ./.github/actions/terragrunt @@ -224,8 +350,14 @@ jobs: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/security tg_action: ${{ inputs.tg_action }} - plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ inputs.github_token }} + + - name: Upload planned security artifact + if: inputs.tg_action == 'plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-upload - name: Get security outputs id: get_security_outputs @@ -240,11 +372,24 @@ jobs: - oidc - security runs-on: ubuntu-latest + env: + BUCKET_NAME: ${{ inputs.code_bucket }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} + TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/database steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.infra_version }} + - name: Download planned database artifact + if: inputs.tg_action == 'apply_plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-download + - name: ${{ env.TG_ACTION_LABEL }} database infra uses: ./.github/actions/terragrunt env: @@ -253,27 +398,52 @@ jobs: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/database tg_action: ${{ inputs.tg_action }} - plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ inputs.github_token }} + + - name: Upload planned database artifact + if: inputs.tg_action == 'plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-upload network: needs: - security - cognito runs-on: ubuntu-latest + env: + BUCKET_NAME: ${{ inputs.code_bucket }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} + TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/network steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.infra_version }} + - name: Download planned network artifact + if: inputs.tg_action == 'apply_plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-download + - name: ${{ env.TG_ACTION_LABEL }} network infra uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/network tg_action: ${{ inputs.tg_action }} - plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ inputs.github_token }} + + - name: Upload planned network artifact + if: inputs.tg_action == 'plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-upload lambdas: needs: @@ -287,19 +457,38 @@ jobs: fail-fast: false # this is to prevent terraform lock issues matrix: value: ${{ fromJson(inputs.lambda_matrix) }} + env: + BUCKET_NAME: ${{ inputs.code_bucket }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} + TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.infra_version }} + - name: Download planned ${{ matrix.value }} artifact + if: inputs.tg_action == 'apply_plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-download + - name: ${{ env.TG_ACTION_LABEL }} ${{ matrix.value }} infra uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} tg_action: ${{ inputs.tg_action }} - plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ inputs.github_token }} + + - name: Upload planned ${{ matrix.value }} artifact + if: inputs.tg_action == 'plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-upload services: needs: @@ -314,11 +503,24 @@ jobs: fail-fast: false matrix: value: ${{ fromJson(inputs.service_matrix) }} + env: + BUCKET_NAME: ${{ inputs.code_bucket }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} + TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.infra_version }} + - name: Download planned ${{ matrix.value }} artifact + if: inputs.tg_action == 'apply_plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-download + - name: ${{ env.TG_ACTION_LABEL }} ${{ matrix.value }} bootstrap service infra uses: ./.github/actions/terragrunt env: @@ -328,5 +530,11 @@ jobs: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} tg_action: ${{ inputs.tg_action }} - plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ inputs.github_token }} + + - name: Upload planned ${{ matrix.value }} artifact + if: inputs.tg_action == 'plan' + uses: ./.github/actions/just + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: terragrunt-plan-upload diff --git a/.github/workflows/shared_infra_apply_from_plan.yml b/.github/workflows/shared_infra_apply_from_plan.yml index 7fb6abe8..f53f55d7 100644 --- a/.github/workflows/shared_infra_apply_from_plan.yml +++ b/.github/workflows/shared_infra_apply_from_plan.yml @@ -11,16 +11,15 @@ on: description: "Workflow run ID used to recover metadata and plan artifacts" required: true type: string - github_token: - description: "GitHub token used for cross-run artifact download" - required: true - type: string permissions: id-token: write contents: read actions: read +env: + AWS_OIDC_ROLE_ARN: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/${{ vars.PROJECT_NAME }}-${{ inputs.environment }}-github-oidc-role + jobs: metadata: runs-on: ubuntu-latest @@ -31,20 +30,24 @@ jobs: bootstrap_image_uri: ${{ steps.read_metadata.outputs.bootstrap_image_uri }} service_matrix: ${{ steps.read_metadata.outputs.service_matrix }} steps: - - name: Download plan metadata artifact - uses: actions/download-artifact@v8 + - uses: actions/checkout@v6 + + - name: Download plan metadata from S3 + uses: ./.github/actions/just + env: + BUCKET_NAME: ${{ vars.AWS_ACCOUNT_ID }}-${{ vars.AWS_REGION }}-${{ vars.PROJECT_NAME }}-${{ inputs.environment }}-code + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ inputs.plan_artifact_run_id }} with: - name: infra-plan-metadata - path: . - repository: ${{ github.repository }} - github-token: ${{ inputs.github_token }} - run-id: ${{ inputs.plan_artifact_run_id }} + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: infra-plan-metadata-download - name: Check plan metadata artifact shell: bash run: | if [ ! -f plan-metadata.json ]; then - echo "::error title=Missing plan metadata artifact::Expected 'plan-metadata.json' from artifact 'infra-plan-metadata' in run '${{ inputs.plan_artifact_run_id }}', but it was not downloaded." + echo "::error title=Missing plan metadata artifact::Expected 'plan-metadata.json' at s3://${{ vars.AWS_ACCOUNT_ID }}-${{ vars.AWS_REGION }}-${{ vars.PROJECT_NAME }}-${{ inputs.environment }}-code/terragrunt_plan/${{ inputs.environment }}/${{ inputs.plan_artifact_run_id }}/infra-plan-metadata/plan-metadata.json, but it was not downloaded." exit 1 fi @@ -71,4 +74,3 @@ jobs: service_matrix: ${{ needs.metadata.outputs.service_matrix }} tg_action: apply_plan plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} - github_token: ${{ inputs.github_token }} diff --git a/.github/workflows/shared_infra_plan.yml b/.github/workflows/shared_infra_plan.yml index 733147b4..57ab2c0c 100644 --- a/.github/workflows/shared_infra_plan.yml +++ b/.github/workflows/shared_infra_plan.yml @@ -38,10 +38,17 @@ permissions: contents: read actions: read +env: + AWS_OIDC_ROLE_ARN: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/${{ vars.PROJECT_NAME }}-${{ inputs.environment }}-github-oidc-role + jobs: metadata: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.infra_version }} + - name: Write plan metadata from workflow inputs shell: bash run: | @@ -55,12 +62,16 @@ jobs: } EOF - - name: Upload plan metadata artifact - uses: actions/upload-artifact@v7 + - name: Upload plan metadata to S3 + uses: ./.github/actions/just + env: + BUCKET_NAME: ${{ inputs.code_bucket }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ github.run_id }} with: - name: infra-plan-metadata - path: plan-metadata.json - if-no-files-found: error + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + justfile_path: justfile.ci + just_action: infra-plan-metadata-upload infra: needs: diff --git a/README.md b/README.md index d3d1bce9..68b14802 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,7 @@ Infrastructure apply and feature-code rollout are intentionally decoupled in thi - `*_infra` workflows apply infrastructure only - `*_code` workflows deploy feature code only - code deploy workflows publish the real Lambda versions and ECS task revisions into that pre-created deploy surface +- saved infra plans are stored in the shared S3 code bucket under `terragrunt_plan///...` - rerunning infrastructure apply does not roll out new feature code - the shared Lambda and ECS module READMEs are the canonical source for bootstrap, rollout, and rollback details for each runtime shape - detailed workflow contracts, reusable-workflow inputs, repo-local action behavior, and `justfile_path` rules live in [.github/docs/README.md](.github/docs/README.md) diff --git a/infra/README.md b/infra/README.md index 6cd8ed32..536d2d88 100644 --- a/infra/README.md +++ b/infra/README.md @@ -32,7 +32,7 @@ Shared artifact names also follow environment-aware conventions from `infra/root - shared artifact base: `dev -> ...-dev`, otherwise `...-ci` - code bucket: `-code` -- ECS ECR repository: `-ecs-worker` +- ECS ECR repository: `-ecr` So a stack at: @@ -119,6 +119,7 @@ That `containers/lib` directory is helper code only and is not treated as a depl - build workflows produce Lambda zips and container images - `*_infra` wrappers need the inputs required to apply infra safely, such as directory-derived stack matrices and any artifact-derived bootstrap references - in `prod`, the `*_infra` wrappers read shared artifact resources from `ci` but only apply service and task stacks in `prod` +- saved `plan` / `apply_plan` artifacts live in the shared code bucket under `terragrunt_plan///...` - deploy workflows: - publish Lambda versions and use Lambda CodeDeploy - optionally invoke the `migrations` Lambda when it is part of the Lambda deploy matrix diff --git a/infra/modules/aws/_shared/code_bucket/README.md b/infra/modules/aws/_shared/code_bucket/README.md index c131c038..dd2a38d9 100644 --- a/infra/modules/aws/_shared/code_bucket/README.md +++ b/infra/modules/aws/_shared/code_bucket/README.md @@ -7,6 +7,7 @@ Shared S3 bucket for deployable artifacts. - Lambda zip storage - frontend bundle storage - ECS AppSpec storage for CodeDeploy +- Terragrunt saved plan artifacts under the `terragrunt_plan/` prefix ## Key outputs diff --git a/justfile.ci b/justfile.ci index 81599f1f..624bb6dd 100644 --- a/justfile.ci +++ b/justfile.ci @@ -105,6 +105,145 @@ get-version-file-keys: | jq -s -c . +# Download saved Terragrunt plan files for a stack from the shared code bucket. +terragrunt-plan-download: + #!/usr/bin/env bash + set -euo pipefail + + if [[ -z "${BUCKET_NAME:-}" ]]; then + echo "❌ BUCKET_NAME environment variable is not set." + exit 1 + fi + + if [[ -z "${ENVIRONMENT:-}" ]]; then + echo "❌ ENVIRONMENT environment variable is not set." + exit 1 + fi + + if [[ -z "${RUN_ID:-}" ]]; then + echo "❌ RUN_ID environment variable is not set." + exit 1 + fi + + if [[ -z "${TG_DIRECTORY:-}" ]]; then + echo "❌ TG_DIRECTORY environment variable is not set." + exit 1 + fi + + artifact_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.ci" terragrunt-plan-prefix)" + + mkdir -p "$TG_DIRECTORY" + + aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/terragrunt.tfplan" "${TG_DIRECTORY}/terragrunt.tfplan" + aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/terragrunt.plan.txt" "${TG_DIRECTORY}/terragrunt.plan.txt" + aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/terragrunt.plan.meta.json" "${TG_DIRECTORY}/terragrunt.plan.meta.json" + + +# Upload saved Terragrunt plan files for a stack to the shared code bucket. +terragrunt-plan-upload: + #!/usr/bin/env bash + set -euo pipefail + + if [[ -z "${BUCKET_NAME:-}" ]]; then + echo "❌ BUCKET_NAME environment variable is not set." + exit 1 + fi + + if [[ -z "${ENVIRONMENT:-}" ]]; then + echo "❌ ENVIRONMENT environment variable is not set." + exit 1 + fi + + if [[ -z "${RUN_ID:-}" ]]; then + echo "❌ RUN_ID environment variable is not set." + exit 1 + fi + + if [[ -z "${TG_DIRECTORY:-}" ]]; then + echo "❌ TG_DIRECTORY environment variable is not set." + exit 1 + fi + + artifact_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.ci" terragrunt-plan-prefix)" + + aws s3 cp "${TG_DIRECTORY}/terragrunt.tfplan" "s3://${BUCKET_NAME}/${artifact_prefix}/terragrunt.tfplan" + aws s3 cp "${TG_DIRECTORY}/terragrunt.plan.txt" "s3://${BUCKET_NAME}/${artifact_prefix}/terragrunt.plan.txt" + aws s3 cp "${TG_DIRECTORY}/terragrunt.plan.meta.json" "s3://${BUCKET_NAME}/${artifact_prefix}/terragrunt.plan.meta.json" + + +# Derive the shared S3 prefix for Terragrunt plan artifacts. +terragrunt-plan-prefix: + #!/usr/bin/env bash + set -euo pipefail + + if [[ -z "${ENVIRONMENT:-}" ]]; then + echo "❌ ENVIRONMENT environment variable is not set." + exit 1 + fi + + if [[ -z "${RUN_ID:-}" ]]; then + echo "❌ RUN_ID environment variable is not set." + exit 1 + fi + + if [[ -z "${TG_DIRECTORY:-}" ]]; then + echo "❌ TG_DIRECTORY environment variable is not set." + exit 1 + fi + + sanitized_dir="$(echo "$TG_DIRECTORY" | tr '/.' '--')" + artifact_name="terragrunt-plan-${sanitized_dir}" + artifact_prefix="terragrunt_plan/${ENVIRONMENT}/${RUN_ID}/${artifact_name}" + + echo "$artifact_prefix" + + +# Upload shared infra plan metadata to the shared code bucket. +infra-plan-metadata-upload: + #!/usr/bin/env bash + set -euo pipefail + + if [[ -z "${BUCKET_NAME:-}" ]]; then + echo "❌ BUCKET_NAME environment variable is not set." + exit 1 + fi + + if [[ -z "${ENVIRONMENT:-}" ]]; then + echo "❌ ENVIRONMENT environment variable is not set." + exit 1 + fi + + if [[ -z "${RUN_ID:-}" ]]; then + echo "❌ RUN_ID environment variable is not set." + exit 1 + fi + + aws s3 cp "plan-metadata.json" "s3://${BUCKET_NAME}/terragrunt_plan/${ENVIRONMENT}/${RUN_ID}/infra-plan-metadata/plan-metadata.json" + + +# Download shared infra plan metadata from the shared code bucket. +infra-plan-metadata-download: + #!/usr/bin/env bash + set -euo pipefail + + if [[ -z "${BUCKET_NAME:-}" ]]; then + echo "❌ BUCKET_NAME environment variable is not set." + exit 1 + fi + + if [[ -z "${ENVIRONMENT:-}" ]]; then + echo "❌ ENVIRONMENT environment variable is not set." + exit 1 + fi + + if [[ -z "${RUN_ID:-}" ]]; then + echo "❌ RUN_ID environment variable is not set." + exit 1 + fi + + aws s3 cp "s3://${BUCKET_NAME}/terragrunt_plan/${ENVIRONMENT}/${RUN_ID}/infra-plan-metadata/plan-metadata.json" "plan-metadata.json" + + # List ECR image tag prefixes for a published version. get-ecr-version-images: #!/usr/bin/env bash From a4aefce7d4228895e173f55425bc93b609f70430 Mon Sep 17 00:00:00 2001 From: Chris Sheehan Date: Fri, 24 Apr 2026 20:53:15 +0100 Subject: [PATCH 04/23] chore: nfra_plan_artifact bucket --- .github/docs/README.md | 4 ++-- .github/workflows/destroy.yml | 13 +++++++++++++ .github/workflows/shared_infra_releases.yml | 13 +++++++++++++ .../dev/aws/infra_artifact_bucket/terragrunt.hcl | 7 +++++++ infra/live/global_vars.hcl | 12 +++++++----- .../prod/aws/infra_artifact_bucket/terragrunt.hcl | 7 +++++++ infra/modules/aws/infra_artifact_bucket/main.tf | 6 ++++++ .../modules/aws/infra_artifact_bucket/variables.tf | 12 ++++++++++++ infra/modules/aws/infra_artifact_bucket/versions.tf | 11 +++++++++++ infra/root.hcl | 7 +++++-- 10 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 infra/live/dev/aws/infra_artifact_bucket/terragrunt.hcl create mode 100644 infra/live/prod/aws/infra_artifact_bucket/terragrunt.hcl create mode 100644 infra/modules/aws/infra_artifact_bucket/main.tf create mode 100644 infra/modules/aws/infra_artifact_bucket/variables.tf create mode 100644 infra/modules/aws/infra_artifact_bucket/versions.tf diff --git a/.github/docs/README.md b/.github/docs/README.md index 072e4e31..f8172837 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -66,7 +66,7 @@ flowchart LR ### Shared Artifact Prep And Build - `shared_infra_releases.yml` - Prepares or reads shared CI-side artifact infrastructure such as ECR and the code bucket. + Prepares or reads shared CI-side artifact infrastructure such as ECR, the code bucket, and the infra plan artifact bucket. - `shared_build.yml` Builds and publishes frontend, Lambda, and ECS artifacts. - `shared_build_get.yml` @@ -128,7 +128,7 @@ flowchart LR ### Cleanup And Discovery - `destroy.yml` - Tears down app layers before shared dependencies, including the shared observability dashboard. + Tears down app layers before shared dependencies, including the shared observability dashboard and the infra plan artifact bucket stack. - `shared_directories_get.yml` Derives the directory-based matrices used by wrapper workflows and PR action-test discovery. diff --git a/.github/workflows/destroy.yml b/.github/workflows/destroy.yml index 0b696c31..18d6bb5b 100644 --- a/.github/workflows/destroy.yml +++ b/.github/workflows/destroy.yml @@ -263,3 +263,16 @@ jobs: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/cluster tg_action: destroy + + infra-artifact-bucket: + name: Infra Artifact Bucket + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Destroy infra artifact bucket + uses: ./.github/actions/terragrunt + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + tg_directory: infra/live/${{ inputs.environment }}/aws/infra_artifact_bucket + tg_action: destroy diff --git a/.github/workflows/shared_infra_releases.yml b/.github/workflows/shared_infra_releases.yml index 86ac280f..0a286644 100644 --- a/.github/workflows/shared_infra_releases.yml +++ b/.github/workflows/shared_infra_releases.yml @@ -34,6 +34,19 @@ env: AWS_OIDC_ROLE_ARN: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/${{ vars.PROJECT_NAME }}-${{ inputs.environment }}-github-oidc-role jobs: + infra-artifact-bucket: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.infra_version }} + + - name: Deploy infra plan artifact bucket + uses: ./.github/actions/terragrunt + with: + aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + tg_directory: infra/live/${{ inputs.environment }}/aws/infra_artifact_bucket + ecr: runs-on: ubuntu-latest outputs: diff --git a/infra/live/dev/aws/infra_artifact_bucket/terragrunt.hcl b/infra/live/dev/aws/infra_artifact_bucket/terragrunt.hcl new file mode 100644 index 00000000..007c828f --- /dev/null +++ b/infra/live/dev/aws/infra_artifact_bucket/terragrunt.hcl @@ -0,0 +1,7 @@ +include "root" { + path = find_in_parent_folders("root.hcl") +} + +terraform { + source = "../../../../modules//aws//infra_artifact_bucket" +} diff --git a/infra/live/global_vars.hcl b/infra/live/global_vars.hcl index 71a42684..65eeb462 100644 --- a/infra/live/global_vars.hcl +++ b/infra/live/global_vars.hcl @@ -27,12 +27,14 @@ locals { "route53:*", "cognito-idp:*", ] - container_port = 80 + container_port = 80 + infra_plan_artifact_days = 30 } inputs = { - vpc_name = local.vpc_name - aws_region = local.aws_region - allowed_role_actions = local.allowed_role_actions - container_port = local.container_port + vpc_name = local.vpc_name + aws_region = local.aws_region + allowed_role_actions = local.allowed_role_actions + container_port = local.container_port + infra_plan_artifact_days = local.infra_plan_artifact_days } diff --git a/infra/live/prod/aws/infra_artifact_bucket/terragrunt.hcl b/infra/live/prod/aws/infra_artifact_bucket/terragrunt.hcl new file mode 100644 index 00000000..007c828f --- /dev/null +++ b/infra/live/prod/aws/infra_artifact_bucket/terragrunt.hcl @@ -0,0 +1,7 @@ +include "root" { + path = find_in_parent_folders("root.hcl") +} + +terraform { + source = "../../../../modules//aws//infra_artifact_bucket" +} diff --git a/infra/modules/aws/infra_artifact_bucket/main.tf b/infra/modules/aws/infra_artifact_bucket/main.tf new file mode 100644 index 00000000..f0726f27 --- /dev/null +++ b/infra/modules/aws/infra_artifact_bucket/main.tf @@ -0,0 +1,6 @@ +module "infra_arrifact_bucket" { + source = "../_shared/code_bucket" + + code_bucket = var.infra_plan_artifact_bucket + s3_expiration_days = var.infra_plan_artifact_days +} \ No newline at end of file diff --git a/infra/modules/aws/infra_artifact_bucket/variables.tf b/infra/modules/aws/infra_artifact_bucket/variables.tf new file mode 100644 index 00000000..47562acf --- /dev/null +++ b/infra/modules/aws/infra_artifact_bucket/variables.tf @@ -0,0 +1,12 @@ +### start of static vars set in root.hcl ### +variable "infra_plan_artifact_bucket" { + description = "S3 bucket to host build artifacts" + type = string +} +### end of static vars set in root.hcl ### + +variable "infra_plan_artifact_days" { + description = "Number of days before objects are deleted (set to 0 to disable)" + type = number + default = 0 +} diff --git a/infra/modules/aws/infra_artifact_bucket/versions.tf b/infra/modules/aws/infra_artifact_bucket/versions.tf new file mode 100644 index 00000000..10b8fcb5 --- /dev/null +++ b/infra/modules/aws/infra_artifact_bucket/versions.tf @@ -0,0 +1,11 @@ +terraform { + required_version = ">= 1.0.8" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "= 6.40.0" + configuration_aliases = [aws.domain_aws_region] + } + } +} diff --git a/infra/root.hcl b/infra/root.hcl index 981ea3fd..ac301280 100644 --- a/infra/root.hcl +++ b/infra/root.hcl @@ -23,6 +23,8 @@ locals { state_key = "${local.environment}/${local.provider}/${local.module}/terraform.tfstate" state_lock_table = "${local.project_name}-tf-lockid" + infra_plan_artifact_bucket = "${local.aws_account_id}-${local.project_name}-${local.environment}-infra-plan" + # separate shared artifact resources when dev, otherwise ci artifact_base = local.environment == "dev" ? "${local.base_reference}-${local.environment}" : "${local.base_reference}-ci" code_bucket = "${local.artifact_base}-code" @@ -103,7 +105,8 @@ inputs = merge( deploy_role_arn = local.deploy_role_arn state_bucket = local.state_bucket state_lock_table = local.state_lock_table - code_bucket = local.code_bucket - ecr_repository_name = local.ecr_repository_name + infra_plan_artifact_bucket = local.infra_plan_artifact_bucket + code_bucket = local.code_bucket + ecr_repository_name = local.ecr_repository_name } ) From ceb8ede10a13ac576b7e4b9eae068cd23ad632bd Mon Sep 17 00:00:00 2001 From: Chris Sheehan Date: Fri, 24 Apr 2026 21:13:08 +0100 Subject: [PATCH 05/23] chore: output infra bucket --- .github/docs/README.md | 2 +- .github/workflows/shared_infra_releases.yml | 13 +++++++++++++ infra/modules/aws/infra_artifact_bucket/outputs.tf | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 infra/modules/aws/infra_artifact_bucket/outputs.tf diff --git a/.github/docs/README.md b/.github/docs/README.md index f8172837..fc64801f 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -66,7 +66,7 @@ flowchart LR ### Shared Artifact Prep And Build - `shared_infra_releases.yml` - Prepares or reads shared CI-side artifact infrastructure such as ECR, the code bucket, and the infra plan artifact bucket. + Prepares or reads shared CI-side artifact infrastructure such as ECR, the code bucket, and the infra plan artifact bucket, and exposes those bucket/repository values as reusable-workflow outputs. - `shared_build.yml` Builds and publishes frontend, Lambda, and ECS artifacts. - `shared_build_get.yml` diff --git a/.github/workflows/shared_infra_releases.yml b/.github/workflows/shared_infra_releases.yml index 0a286644..9ce2a821 100644 --- a/.github/workflows/shared_infra_releases.yml +++ b/.github/workflows/shared_infra_releases.yml @@ -15,6 +15,9 @@ on: code_bucket: description: "Bucket containing build artifacts" value: ${{ jobs.bucket.outputs.bucket_name }} + infra_artifact_bucket: + description: "Bucket containing infra plan artifacts" + value: ${{ jobs.infra-artifact-bucket.outputs.bucket_name }} repository_url: description: "ECR repository url" value: ${{ jobs.ecr.outputs.repository_url }} @@ -36,17 +39,27 @@ env: jobs: infra-artifact-bucket: runs-on: ubuntu-latest + outputs: + bucket_name: ${{ steps.get_bucket_name.outputs.bucket }} steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.infra_version }} - name: Deploy infra plan artifact bucket + id: deploy_bucket uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/infra_artifact_bucket + - name: Get infra artifact bucket name + id: get_bucket_name + env: + TG_OUTPUTS: ${{ steps.deploy_bucket.outputs.tg_outputs }} + run: | + echo "bucket=$(echo "$TG_OUTPUTS" | jq -r '.bucket.value')" >> "$GITHUB_OUTPUT" + ecr: runs-on: ubuntu-latest outputs: diff --git a/infra/modules/aws/infra_artifact_bucket/outputs.tf b/infra/modules/aws/infra_artifact_bucket/outputs.tf new file mode 100644 index 00000000..5b66ec1b --- /dev/null +++ b/infra/modules/aws/infra_artifact_bucket/outputs.tf @@ -0,0 +1,4 @@ +output "bucket" { + description = "S3 bucket that stores infra plan artifacts" + value = module.infra_arrifact_bucket.bucket +} From 98cd77ae434659c65a76d0537a1b4d2cf06105b7 Mon Sep 17 00:00:00 2001 From: Chris Sheehan Date: Sat, 25 Apr 2026 14:02:39 +0100 Subject: [PATCH 06/23] chore: breadcrumb --- .github/actions/just/README.md | 4 +- .github/actions/terragrunt/README.md | 8 +- .github/actions/terragrunt/action.yml | 77 +++++++-- .github/docs/README.md | 6 +- .github/workflows/shared_infra.yml | 220 ++++++-------------------- justfile | 2 + justfile.ci | 94 +---------- justfile.tg | 148 +++++++++++++++++ 8 files changed, 275 insertions(+), 284 deletions(-) create mode 100644 justfile.tg diff --git a/.github/actions/just/README.md b/.github/actions/just/README.md index c4cda353..4ea57051 100644 --- a/.github/actions/just/README.md +++ b/.github/actions/just/README.md @@ -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` | @@ -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 }}" -``` \ No newline at end of file +``` diff --git a/.github/actions/terragrunt/README.md b/.github/actions/terragrunt/README.md index dbd9ef5a..c59b1aef 100644 --- a/.github/actions/terragrunt/README.md +++ b/.github/actions/terragrunt/README.md @@ -20,6 +20,10 @@ This GitHub Action sets up **Terraform** and **Terragrunt**, authenticates to AW | `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 | `{}` | | `aws_oidc_role_arn` | IAM role ARN to assume via OIDC | Yes | — | +| `manage_plan_artifacts` | When `true`, downloads saved plan artifacts for `apply_plan` and uploads plan artifacts for `plan` via `justfile.tg` | No | `false` | +| `plan_bucket_name` | S3 bucket used for saved plan artifacts (required when `manage_plan_artifacts: true`) | No | `""` | +| `plan_environment` | Environment name used in the plan artifact prefix (required when `manage_plan_artifacts: true`) | No | `""` | +| `plan_run_id` | Run ID used in the plan artifact prefix (required when `manage_plan_artifacts: true`) | No | `""` | | `tg_directory` | Directory containing the Terragrunt config | Yes | — | | `tg_action` | Terragrunt action: `apply`, `plan`, `apply_plan`, `destroy`, or `init` | Yes | `apply` | @@ -35,9 +39,9 @@ This GitHub Action sets up **Terraform** and **Terragrunt**, authenticates to AW - `apply` Runs `terragrunt apply -auto-approve` - `plan` - Runs `terragrunt plan -detailed-exitcode -out=/terragrunt.tfplan`, renders a text view to `terragrunt.plan.txt`, and writes `terragrunt.plan.meta.json` with `exit_code` and `has_changes`. + Runs `terragrunt plan -detailed-exitcode -out=/terragrunt.tfplan`, then renders `terragrunt.plan.txt` and writes `terragrunt.plan.meta.json` via the repo `justfile.tg` recipe `terragrunt-plan-render`. If `manage_plan_artifacts: true`, it also uploads those files to S3 via `justfile.tg`. - `apply_plan` - Expects the saved plan files to already exist in `tg_directory`, 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 downloaded absolute stack-path plan file. + If `manage_plan_artifacts: true`, downloads the saved plan files into `tg_directory` via `justfile.tg`. Otherwise it expects the saved plan files to already exist in `tg_directory`. In both cases it 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` diff --git a/.github/actions/terragrunt/action.yml b/.github/actions/terragrunt/action.yml index 68abea3c..d4862114 100644 --- a/.github/actions/terragrunt/action.yml +++ b/.github/actions/terragrunt/action.yml @@ -20,6 +20,22 @@ inputs: aws_oidc_role_arn: description: "AWS iam role arn" required: true + manage_plan_artifacts: + description: "When true, downloads saved plan artifacts for apply_plan and uploads plan artifacts for plan" + required: false + default: "false" + plan_bucket_name: + description: "S3 bucket used for saved plan artifacts (required when manage_plan_artifacts is true)" + required: false + default: "" + plan_environment: + description: "Environment name used for saved plan artifact prefixes (required when manage_plan_artifacts is true)" + required: false + default: "" + plan_run_id: + description: "Run ID used for saved plan artifact prefixes (required when manage_plan_artifacts is true)" + required: false + default: "" tg_directory: description: "Module directory to perform action upon" required: true @@ -55,12 +71,26 @@ runs: run: | echo "$OVERRIDE_TG_VARS" | jq -c . > ${{ inputs.tg_directory }}/override_tg_vars.tfvars.json - - name: Configure AWS OIDC Authentication + - name: Configure AWS OIDC Authentication (plan artifacts) + if: inputs.tg_action == 'apply_plan' && inputs.manage_plan_artifacts == 'true' uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ inputs.aws_oidc_role_arn }} aws-region: ${{ inputs.aws_region }} + - name: Download saved plan artifacts + if: inputs.tg_action == 'apply_plan' && inputs.manage_plan_artifacts == 'true' + uses: ./.github/actions/just + env: + BUCKET_NAME: ${{ inputs.plan_bucket_name }} + ENVIRONMENT: ${{ inputs.plan_environment }} + RUN_ID: ${{ inputs.plan_run_id }} + TG_DIRECTORY: ${{ inputs.tg_directory }} + with: + justfile_path: justfile.tg + just_action: terragrunt-plan-download + mask_result: "false" + - name: Verify plan artifact files exist if: inputs.tg_action == 'apply_plan' shell: bash @@ -90,6 +120,13 @@ runs: echo "should_apply=true" >> "$GITHUB_OUTPUT" fi + - name: Configure AWS OIDC Authentication + if: inputs.tg_action != 'apply_plan' || (inputs.manage_plan_artifacts != 'true' && steps.apply_plan_guard.outputs.should_apply == 'true') + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ inputs.aws_oidc_role_arn }} + aws-region: ${{ inputs.aws_region }} + - name: Action Terragrunt if: inputs.tg_action != 'apply_plan' || steps.apply_plan_guard.outputs.should_apply == 'true' id: terragrunt_action @@ -99,8 +136,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) @@ -116,19 +151,9 @@ 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) @@ -147,6 +172,30 @@ 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 + mask_result: "false" + + - name: Upload saved plan artifacts + if: inputs.tg_action == 'plan' && inputs.manage_plan_artifacts == 'true' + uses: ./.github/actions/just + env: + BUCKET_NAME: ${{ inputs.plan_bucket_name }} + ENVIRONMENT: ${{ inputs.plan_environment }} + RUN_ID: ${{ inputs.plan_run_id }} + TG_DIRECTORY: ${{ inputs.tg_directory }} + with: + justfile_path: justfile.tg + just_action: terragrunt-plan-upload + mask_result: "false" + - name: Capture Terraform Outputs if: inputs.tg_action != 'destroy' && (inputs.tg_action != 'apply_plan' || steps.apply_plan_guard.outputs.should_apply == 'true') id: tg_outputs diff --git a/.github/docs/README.md b/.github/docs/README.md index fc64801f..dfbebd31 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -141,8 +141,9 @@ Run these checks on every CI, workflow, or deploy-contract change. - compare every caller `with:` block against the callee `workflow_call.inputs` - compare expected outputs against actual `jobs..outputs.*` - verify optional inputs are intentionally omitted, not accidentally missing -- the repo-local `./.github/actions/terragrunt` action now supports `tg_action: plan` for producing the binary plan, rendered text plan, and `terragrunt.plan.meta.json` sidecar locally; `shared_infra.yml` is responsible for uploading those files into the shared code bucket under `terragrunt_plan/`, and `apply_plan` expects them to have been downloaded into the stack directory by the workflow first -- `./.github/actions/terragrunt` now skips `apply_plan` with a warning when the saved `terragrunt.plan.meta.json` reports `has_changes: false`, and in that no-op case it also skips `aws-actions/configure-aws-credentials` +- the repo-local `./.github/actions/terragrunt` action supports `tg_action: plan` for producing the binary plan locally; it renders `terragrunt.plan.txt` and writes `terragrunt.plan.meta.json` via `justfile.tg` (`terragrunt-plan-render`) +- when `manage_plan_artifacts: true`, `./.github/actions/terragrunt` also uploads (on `plan`) and downloads (on `apply_plan`) the per-stack plan artifacts to/from the shared code bucket under `terragrunt_plan/`, so graph executors like `shared_infra.yml` do not need separate `./.github/actions/just` steps for those transfers +- `./.github/actions/terragrunt` skips `apply_plan` with a warning when the saved `terragrunt.plan.meta.json` reports `has_changes: false`; when it does not need to download plan artifacts (`manage_plan_artifacts: false`), it also skips `aws-actions/configure-aws-credentials` in that no-op case - `./.github/actions/terragrunt` derives its plan artifact name from `tg_directory`, so callers do not need to pass artifact naming inputs - if `apply_plan` is used across separate workflow runs, pass the earlier workflow `run_id` through `plan_artifact_run_id`; the shared wrappers recover both metadata and per-stack plan files from the shared code bucket under `terragrunt_plan///...` - if a cross-run apply should not ask the operator to re-enter versions or recompute artifact resolution, store both the input versions and the resolved reusable-workflow outputs in a metadata artifact during plan and recover them in the apply wrapper from the earlier `run_id` @@ -152,6 +153,7 @@ Run these checks on every CI, workflow, or deploy-contract change. - keep the split `just` ownership clear: - repo-root `justfile` for local/developer commands - `justfile.ci` for read-only CI helpers + - `justfile.tg` for Terragrunt plan artifact helpers (render/upload/download) - `justfile.deploy` for mutating CI build and deploy steps ### Release Tagging Checks diff --git a/.github/workflows/shared_infra.yml b/.github/workflows/shared_infra.yml index 6d1a4208..0e80f801 100644 --- a/.github/workflows/shared_infra.yml +++ b/.github/workflows/shared_infra.yml @@ -67,29 +67,17 @@ jobs: with: ref: ${{ inputs.infra_version }} - - name: Download planned oidc artifact - if: inputs.tg_action == 'apply_plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-download - - name: ${{ env.TG_ACTION_LABEL }} oidc role infra uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + manage_plan_artifacts: "true" + plan_bucket_name: ${{ env.BUCKET_NAME }} + plan_environment: ${{ env.ENVIRONMENT }} + plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/oidc tg_action: ${{ inputs.tg_action }} - - name: Upload planned oidc artifact - if: inputs.tg_action == 'plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-upload - worker_messaging: needs: oidc runs-on: ubuntu-latest @@ -103,29 +91,17 @@ jobs: with: ref: ${{ inputs.infra_version }} - - name: Download planned worker messaging artifact - if: inputs.tg_action == 'apply_plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-download - - name: ${{ env.TG_ACTION_LABEL }} worker messaging infra uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + manage_plan_artifacts: "true" + plan_bucket_name: ${{ env.BUCKET_NAME }} + plan_environment: ${{ env.ENVIRONMENT }} + plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/worker_messaging tg_action: ${{ inputs.tg_action }} - - name: Upload planned worker messaging artifact - if: inputs.tg_action == 'plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-upload - observability: needs: oidc runs-on: ubuntu-latest @@ -139,29 +115,17 @@ jobs: with: ref: ${{ inputs.infra_version }} - - name: Download planned observability artifact - if: inputs.tg_action == 'apply_plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-download - - name: ${{ env.TG_ACTION_LABEL }} observability infra uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + manage_plan_artifacts: "true" + plan_bucket_name: ${{ env.BUCKET_NAME }} + plan_environment: ${{ env.ENVIRONMENT }} + plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/observability tg_action: ${{ inputs.tg_action }} - - name: Upload planned observability artifact - if: inputs.tg_action == 'plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-upload - cognito: needs: oidc runs-on: ubuntu-latest @@ -175,31 +139,19 @@ jobs: with: ref: ${{ inputs.infra_version }} - - name: Download planned cognito artifact - if: inputs.tg_action == 'apply_plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-download - - name: ${{ env.TG_ACTION_LABEL }} cognito infra uses: ./.github/actions/terragrunt env: TF_VAR_domain_name: ${{ env.DOMAIN_NAME }} with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + manage_plan_artifacts: "true" + plan_bucket_name: ${{ env.BUCKET_NAME }} + plan_environment: ${{ env.ENVIRONMENT }} + plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/cognito tg_action: ${{ inputs.tg_action }} - - name: Upload planned cognito artifact - if: inputs.tg_action == 'plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-upload - frontend: needs: - network @@ -254,14 +206,6 @@ jobs: echo "hosted_ui_url=$(echo "$TG_OUTPUTS" | jq -r '.hosted_ui_url.value')" >> "$GITHUB_OUTPUT" echo "readonly_group_name=$(echo "$TG_OUTPUTS" | jq -r '.readonly_group_name.value')" >> "$GITHUB_OUTPUT" - - name: Download planned frontend artifact - if: inputs.tg_action == 'apply_plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-download - - name: ${{ env.TG_ACTION_LABEL }} frontend infra uses: ./.github/actions/terragrunt env: @@ -273,17 +217,13 @@ jobs: TF_VAR_auth_readonly_group_name: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.readonly_group_name }} with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + manage_plan_artifacts: "true" + plan_bucket_name: ${{ env.BUCKET_NAME }} + plan_environment: ${{ env.ENVIRONMENT }} + plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/frontend tg_action: ${{ inputs.tg_action }} - - name: Upload planned frontend artifact - if: inputs.tg_action == 'plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-upload - cluster: needs: oidc runs-on: ubuntu-latest @@ -297,29 +237,17 @@ jobs: with: ref: ${{ inputs.infra_version }} - - name: Download planned cluster artifact - if: inputs.tg_action == 'apply_plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-download - - name: ${{ env.TG_ACTION_LABEL }} cluster infra uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + manage_plan_artifacts: "true" + plan_bucket_name: ${{ env.BUCKET_NAME }} + plan_environment: ${{ env.ENVIRONMENT }} + plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/cluster tg_action: ${{ inputs.tg_action }} - - name: Upload planned cluster artifact - if: inputs.tg_action == 'plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-upload - security: needs: oidc runs-on: ubuntu-latest @@ -335,30 +263,18 @@ jobs: with: ref: ${{ inputs.infra_version }} - - name: Download planned security artifact - if: inputs.tg_action == 'apply_plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-download - - name: ${{ env.TG_ACTION_LABEL }} security infra id: deploy-security uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + manage_plan_artifacts: "true" + plan_bucket_name: ${{ env.BUCKET_NAME }} + plan_environment: ${{ env.ENVIRONMENT }} + plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/security tg_action: ${{ inputs.tg_action }} - - name: Upload planned security artifact - if: inputs.tg_action == 'plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-upload - - name: Get security outputs id: get_security_outputs if: inputs.tg_action != 'destroy' @@ -382,31 +298,19 @@ jobs: with: ref: ${{ inputs.infra_version }} - - name: Download planned database artifact - if: inputs.tg_action == 'apply_plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-download - - name: ${{ env.TG_ACTION_LABEL }} database infra uses: ./.github/actions/terragrunt env: TF_VAR_database_security_group_id: ${{ needs.security.outputs.postgres_sg }} with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + manage_plan_artifacts: "true" + plan_bucket_name: ${{ env.BUCKET_NAME }} + plan_environment: ${{ env.ENVIRONMENT }} + plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/database tg_action: ${{ inputs.tg_action }} - - name: Upload planned database artifact - if: inputs.tg_action == 'plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-upload - network: needs: - security @@ -422,29 +326,17 @@ jobs: with: ref: ${{ inputs.infra_version }} - - name: Download planned network artifact - if: inputs.tg_action == 'apply_plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-download - - name: ${{ env.TG_ACTION_LABEL }} network infra uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + manage_plan_artifacts: "true" + plan_bucket_name: ${{ env.BUCKET_NAME }} + plan_environment: ${{ env.ENVIRONMENT }} + plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/network tg_action: ${{ inputs.tg_action }} - - name: Upload planned network artifact - if: inputs.tg_action == 'plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-upload - lambdas: needs: - oidc @@ -467,29 +359,17 @@ jobs: with: ref: ${{ inputs.infra_version }} - - name: Download planned ${{ matrix.value }} artifact - if: inputs.tg_action == 'apply_plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-download - - name: ${{ env.TG_ACTION_LABEL }} ${{ matrix.value }} infra uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + manage_plan_artifacts: "true" + plan_bucket_name: ${{ env.BUCKET_NAME }} + plan_environment: ${{ env.ENVIRONMENT }} + plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} tg_action: ${{ inputs.tg_action }} - - name: Upload planned ${{ matrix.value }} artifact - if: inputs.tg_action == 'plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-upload - services: needs: - oidc @@ -513,14 +393,6 @@ jobs: with: ref: ${{ inputs.infra_version }} - - name: Download planned ${{ matrix.value }} artifact - if: inputs.tg_action == 'apply_plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-download - - name: ${{ env.TG_ACTION_LABEL }} ${{ matrix.value }} bootstrap service infra uses: ./.github/actions/terragrunt env: @@ -528,13 +400,9 @@ jobs: TF_VAR_bootstrap_image_uri: ${{ inputs.bootstrap_image_uri }} with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + manage_plan_artifacts: "true" + plan_bucket_name: ${{ env.BUCKET_NAME }} + plan_environment: ${{ env.ENVIRONMENT }} + plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} tg_action: ${{ inputs.tg_action }} - - - name: Upload planned ${{ matrix.value }} artifact - if: inputs.tg_action == 'plan' - uses: ./.github/actions/just - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - justfile_path: justfile.ci - just_action: terragrunt-plan-upload diff --git a/justfile b/justfile index 1fd396e6..66cee043 100644 --- a/justfile +++ b/justfile @@ -3,6 +3,8 @@ _default: @just --list @printf '\nCI recipes (`just --justfile justfile.ci --list`):\n' @just --justfile justfile.ci --list + @printf '\nTerragrunt recipes (`just --justfile justfile.tg --list`):\n' + @just --justfile justfile.tg --list @printf '\nDeploy recipes (`just --justfile justfile.deploy --list`):\n' @just --justfile justfile.deploy --list diff --git a/justfile.ci b/justfile.ci index 624bb6dd..782bb8c3 100644 --- a/justfile.ci +++ b/justfile.ci @@ -105,97 +105,15 @@ get-version-file-keys: | jq -s -c . -# Download saved Terragrunt plan files for a stack from the shared code bucket. -terragrunt-plan-download: - #!/usr/bin/env bash - set -euo pipefail - - if [[ -z "${BUCKET_NAME:-}" ]]; then - echo "❌ BUCKET_NAME environment variable is not set." - exit 1 - fi - - if [[ -z "${ENVIRONMENT:-}" ]]; then - echo "❌ ENVIRONMENT environment variable is not set." - exit 1 - fi - - if [[ -z "${RUN_ID:-}" ]]; then - echo "❌ RUN_ID environment variable is not set." - exit 1 - fi - - if [[ -z "${TG_DIRECTORY:-}" ]]; then - echo "❌ TG_DIRECTORY environment variable is not set." - exit 1 - fi - - artifact_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.ci" terragrunt-plan-prefix)" - - mkdir -p "$TG_DIRECTORY" - - aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/terragrunt.tfplan" "${TG_DIRECTORY}/terragrunt.tfplan" - aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/terragrunt.plan.txt" "${TG_DIRECTORY}/terragrunt.plan.txt" - aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/terragrunt.plan.meta.json" "${TG_DIRECTORY}/terragrunt.plan.meta.json" - - -# Upload saved Terragrunt plan files for a stack to the shared code bucket. -terragrunt-plan-upload: - #!/usr/bin/env bash - set -euo pipefail - - if [[ -z "${BUCKET_NAME:-}" ]]; then - echo "❌ BUCKET_NAME environment variable is not set." - exit 1 - fi - - if [[ -z "${ENVIRONMENT:-}" ]]; then - echo "❌ ENVIRONMENT environment variable is not set." - exit 1 - fi - - if [[ -z "${RUN_ID:-}" ]]; then - echo "❌ RUN_ID environment variable is not set." - exit 1 - fi - - if [[ -z "${TG_DIRECTORY:-}" ]]; then - echo "❌ TG_DIRECTORY environment variable is not set." - exit 1 - fi - - artifact_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.ci" terragrunt-plan-prefix)" - - aws s3 cp "${TG_DIRECTORY}/terragrunt.tfplan" "s3://${BUCKET_NAME}/${artifact_prefix}/terragrunt.tfplan" - aws s3 cp "${TG_DIRECTORY}/terragrunt.plan.txt" "s3://${BUCKET_NAME}/${artifact_prefix}/terragrunt.plan.txt" - aws s3 cp "${TG_DIRECTORY}/terragrunt.plan.meta.json" "s3://${BUCKET_NAME}/${artifact_prefix}/terragrunt.plan.meta.json" - - -# Derive the shared S3 prefix for Terragrunt plan artifacts. +# Back-compat wrappers: Terragrunt plan helpers live in `justfile.tg`. terragrunt-plan-prefix: - #!/usr/bin/env bash - set -euo pipefail + @just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-prefix - if [[ -z "${ENVIRONMENT:-}" ]]; then - echo "❌ ENVIRONMENT environment variable is not set." - exit 1 - fi - - if [[ -z "${RUN_ID:-}" ]]; then - echo "❌ RUN_ID environment variable is not set." - exit 1 - fi - - if [[ -z "${TG_DIRECTORY:-}" ]]; then - echo "❌ TG_DIRECTORY environment variable is not set." - exit 1 - fi - - sanitized_dir="$(echo "$TG_DIRECTORY" | tr '/.' '--')" - artifact_name="terragrunt-plan-${sanitized_dir}" - artifact_prefix="terragrunt_plan/${ENVIRONMENT}/${RUN_ID}/${artifact_name}" +terragrunt-plan-download: + @just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-download - echo "$artifact_prefix" +terragrunt-plan-upload: + @just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-upload # Upload shared infra plan metadata to the shared code bucket. diff --git a/justfile.tg b/justfile.tg new file mode 100644 index 00000000..1817163f --- /dev/null +++ b/justfile.tg @@ -0,0 +1,148 @@ +# Terragrunt plan artifact helpers. +# This file is for producing, downloading, and uploading saved Terragrunt plan files. + +PROJECT_DIR := justfile_directory() + +PLAN_FILE := "terragrunt.tfplan" +PLAN_TEXT_FILE := "terragrunt.plan.txt" +PLAN_META_FILE := "terragrunt.plan.meta.json" + + +# Render Terragrunt plan sidecars (plan text + metadata) for an existing binary plan. +# +# Expected environment variables: +# - TG_DIRECTORY: directory containing the saved plan file +# - TG_PLAN_EXIT_CODE: detailed-exitcode from `terragrunt plan` (0 or 2) +terragrunt-plan-render: + #!/usr/bin/env bash + set -euo pipefail + + if [[ -z "${TG_DIRECTORY:-}" ]]; then + echo "❌ TG_DIRECTORY environment variable is not set." + exit 1 + fi + + if [[ -z "${TG_PLAN_EXIT_CODE:-}" ]]; then + echo "❌ TG_PLAN_EXIT_CODE environment variable is not set." + exit 1 + fi + + cd "$TG_DIRECTORY" + + PLAN_PATH="$(pwd)/{{PLAN_FILE}}" + PLAN_TEXT_PATH="$(pwd)/{{PLAN_TEXT_FILE}}" + PLAN_META_PATH="$(pwd)/{{PLAN_META_FILE}}" + + if [[ ! -f "$PLAN_PATH" ]]; then + echo "❌ Expected plan file '$PLAN_PATH' was not found." + exit 1 + fi + + terragrunt show -no-color "$PLAN_PATH" > "$PLAN_TEXT_PATH" + + jq -n \ + --arg tg_directory "$TG_DIRECTORY" \ + --argjson exit_code "$TG_PLAN_EXIT_CODE" \ + --argjson has_changes "$([ "$TG_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 "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" + + +# Derive the shared S3 prefix for Terragrunt plan artifacts. +terragrunt-plan-prefix: + #!/usr/bin/env bash + set -euo pipefail + + if [[ -z "${ENVIRONMENT:-}" ]]; then + echo "❌ ENVIRONMENT environment variable is not set." + exit 1 + fi + + if [[ -z "${RUN_ID:-}" ]]; then + echo "❌ RUN_ID environment variable is not set." + exit 1 + fi + + if [[ -z "${TG_DIRECTORY:-}" ]]; then + echo "❌ TG_DIRECTORY environment variable is not set." + exit 1 + fi + + sanitized_dir="$(echo "$TG_DIRECTORY" | tr '/.' '--')" + artifact_name="terragrunt-plan-${sanitized_dir}" + artifact_prefix="terragrunt_plan/${ENVIRONMENT}/${RUN_ID}/${artifact_name}" + + echo "$artifact_prefix" + + +# Download saved Terragrunt plan files for a stack from the shared code bucket. +terragrunt-plan-download: + #!/usr/bin/env bash + set -euo pipefail + + if [[ -z "${BUCKET_NAME:-}" ]]; then + echo "❌ BUCKET_NAME environment variable is not set." + exit 1 + fi + + if [[ -z "${ENVIRONMENT:-}" ]]; then + echo "❌ ENVIRONMENT environment variable is not set." + exit 1 + fi + + if [[ -z "${RUN_ID:-}" ]]; then + echo "❌ RUN_ID environment variable is not set." + exit 1 + fi + + if [[ -z "${TG_DIRECTORY:-}" ]]; then + echo "❌ TG_DIRECTORY environment variable is not set." + exit 1 + fi + + artifact_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-prefix)" + + mkdir -p "$TG_DIRECTORY" + + aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/{{PLAN_FILE}}" "${TG_DIRECTORY}/{{PLAN_FILE}}" + aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/{{PLAN_TEXT_FILE}}" "${TG_DIRECTORY}/{{PLAN_TEXT_FILE}}" + aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/{{PLAN_META_FILE}}" "${TG_DIRECTORY}/{{PLAN_META_FILE}}" + + +# Upload saved Terragrunt plan files for a stack to the shared code bucket. +terragrunt-plan-upload: + #!/usr/bin/env bash + set -euo pipefail + + if [[ -z "${BUCKET_NAME:-}" ]]; then + echo "❌ BUCKET_NAME environment variable is not set." + exit 1 + fi + + if [[ -z "${ENVIRONMENT:-}" ]]; then + echo "❌ ENVIRONMENT environment variable is not set." + exit 1 + fi + + if [[ -z "${RUN_ID:-}" ]]; then + echo "❌ RUN_ID environment variable is not set." + exit 1 + fi + + if [[ -z "${TG_DIRECTORY:-}" ]]; then + echo "❌ TG_DIRECTORY environment variable is not set." + exit 1 + fi + + artifact_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-prefix)" + + aws s3 cp "${TG_DIRECTORY}/{{PLAN_FILE}}" "s3://${BUCKET_NAME}/${artifact_prefix}/{{PLAN_FILE}}" + aws s3 cp "${TG_DIRECTORY}/{{PLAN_TEXT_FILE}}" "s3://${BUCKET_NAME}/${artifact_prefix}/{{PLAN_TEXT_FILE}}" + aws s3 cp "${TG_DIRECTORY}/{{PLAN_META_FILE}}" "s3://${BUCKET_NAME}/${artifact_prefix}/{{PLAN_META_FILE}}" From 11b29d3464f654d5e04ff0fd7d6c6defb8e1732b Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 10:29:16 +0100 Subject: [PATCH 07/23] chore: rm just.ci backwards compat --- justfile.ci | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/justfile.ci b/justfile.ci index 782bb8c3..40c3843c 100644 --- a/justfile.ci +++ b/justfile.ci @@ -105,17 +105,6 @@ get-version-file-keys: | jq -s -c . -# Back-compat wrappers: Terragrunt plan helpers live in `justfile.tg`. -terragrunt-plan-prefix: - @just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-prefix - -terragrunt-plan-download: - @just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-download - -terragrunt-plan-upload: - @just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-upload - - # Upload shared infra plan metadata to the shared code bucket. infra-plan-metadata-upload: #!/usr/bin/env bash From 066984ef82f17f13dcf72255b12fea8e1f62f639 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 12:01:14 +0100 Subject: [PATCH 08/23] chore: rm separate infra bucket --- .github/docs/README.md | 11 ++++--- .github/workflows/destroy.yml | 13 -------- .../shared_infra_apply_from_plan.yml | 31 +++++++++++++++++-- .github/workflows/shared_infra_releases.yml | 26 ---------------- README.md | 2 +- infra/README.md | 3 +- .../aws/infra_artifact_bucket/terragrunt.hcl | 7 ----- infra/live/global_vars.hcl | 2 -- .../aws/infra_artifact_bucket/terragrunt.hcl | 7 ----- .../modules/aws/_shared/code_bucket/README.md | 5 +++ .../modules/aws/infra_artifact_bucket/main.tf | 6 ---- .../aws/infra_artifact_bucket/outputs.tf | 4 --- .../aws/infra_artifact_bucket/variables.tf | 12 ------- .../aws/infra_artifact_bucket/versions.tf | 11 ------- infra/root.hcl | 4 --- 15 files changed, 42 insertions(+), 102 deletions(-) delete mode 100644 infra/live/dev/aws/infra_artifact_bucket/terragrunt.hcl delete mode 100644 infra/live/prod/aws/infra_artifact_bucket/terragrunt.hcl delete mode 100644 infra/modules/aws/infra_artifact_bucket/main.tf delete mode 100644 infra/modules/aws/infra_artifact_bucket/outputs.tf delete mode 100644 infra/modules/aws/infra_artifact_bucket/variables.tf delete mode 100644 infra/modules/aws/infra_artifact_bucket/versions.tf diff --git a/.github/docs/README.md b/.github/docs/README.md index dfbebd31..215278e2 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -66,7 +66,7 @@ flowchart LR ### Shared Artifact Prep And Build - `shared_infra_releases.yml` - Prepares or reads shared CI-side artifact infrastructure such as ECR, the code bucket, and the infra plan artifact bucket, and exposes those bucket/repository values as reusable-workflow outputs. + Prepares or reads shared artifact infrastructure such as ECR and the code bucket, and exposes those bucket/repository values as reusable-workflow outputs. - `shared_build.yml` Builds and publishes frontend, Lambda, and ECS artifacts. - `shared_build_get.yml` @@ -82,13 +82,13 @@ flowchart LR ### Infra And Code Rollout - `shared_infra_plan.yml` - Plan wrapper around `shared_infra.yml`. It takes resolved workflow inputs directly, writes `infra-plan-metadata`, uploads it to the shared code bucket under `terragrunt_plan///infra-plan-metadata/` via `justfile.ci`, and then calls `shared_infra.yml` with `tg_action: plan`. After the plan completes, it prints the current workflow `github.run_id` into both the logs and the GitHub Actions step summary as `plan_artifact_run_id`, and exposes that value as a reusable-workflow output. + Plan wrapper around `shared_infra.yml`. It takes resolved workflow inputs directly, writes `infra-plan-metadata`, uploads it to the resolved code bucket under `terragrunt_plan///infra-plan-metadata/` via `justfile.ci`, and then calls `shared_infra.yml` with `tg_action: plan`. The bucket resolution follows the same artifact split as ECR and build outputs: `dev` uses the `dev` code bucket, while non-`dev` environments reuse the `ci` code bucket. After the plan completes, it prints the current workflow `github.run_id` into both the logs and the GitHub Actions step summary as `plan_artifact_run_id`, and exposes that value as a reusable-workflow output. - `shared_infra_apply.yml` Direct-input apply wrapper around `shared_infra.yml`. It takes resolved workflow inputs directly and calls `shared_infra.yml` with `tg_action: apply`. - `shared_infra_apply_from_plan.yml` - Apply-from-plan wrapper around `shared_infra.yml`. It takes `plan_artifact_run_id`, downloads `infra-plan-metadata` from the shared code bucket via `justfile.ci`, reads the frozen graph inputs back out, and then calls `shared_infra.yml` with `tg_action: apply_plan`. + Apply-from-plan wrapper around `shared_infra.yml`. It takes `plan_artifact_run_id`, resolves the same artifact bucket split used by release artifacts (`dev` stays on `dev`, non-`dev` uses `ci`), downloads `infra-plan-metadata` from that code bucket via `justfile.ci`, reads the frozen graph inputs back out, and then calls `shared_infra.yml` with `tg_action: apply_plan`. - `shared_infra.yml` - Pure ordered infra graph executor. It applies shared stacks first, then runtime stacks, then frontend infrastructure. Shared stacks now include the CloudWatch observability dashboard. It accepts `tg_action` so the same graph can run a normal apply, upload derived per-stack plan artifacts to the shared code bucket under `terragrunt_plan/`, or apply from previously uploaded plan artifacts. In `apply_plan` mode, each stack job first downloads its own saved plan files via `justfile.ci`, then the Terragrunt action validates and applies those local files. Its visible step labels now follow `tg_action` too, so plan and apply-from-plan runs no longer show everything as `Deploy ...`. The `security -> network` edge is a real bootstrap dependency because `network` reads security outputs like `vpc_endpoint_sg` from remote state; if those outputs do not exist yet, `network` fails with an upstream `Unsupported attribute` error rather than a networking-specific error. + Pure ordered infra graph executor. It applies shared stacks first, then runtime stacks, then frontend infrastructure. Shared stacks now include the CloudWatch observability dashboard. It accepts `tg_action` so the same graph can run a normal apply, upload derived per-stack plan artifacts to the resolved code bucket under `terragrunt_plan/`, or apply from previously uploaded plan artifacts. In `apply_plan` mode, each stack job first downloads its own saved plan files via `justfile.tg` through the Terragrunt action, then the Terragrunt action validates and applies those local files. Its visible step labels now follow `tg_action` too, so plan and apply-from-plan runs no longer show everything as `Deploy ...`. The `security -> network` edge is a real bootstrap dependency because `network` reads security outputs like `vpc_endpoint_sg` from remote state; if those outputs do not exist yet, `network` fails with an upstream `Unsupported attribute` error rather than a networking-specific error. - The shared infra wrappers must forward the permissions required by the nested reusable call chain. In practice that means `id-token: write` everywhere the Terragrunt action may assume AWS OIDC and `contents: read` for checkout. The shared plan/apply wrappers now rely on AWS access to the shared code bucket rather than GitHub artifact permissions for cross-run recovery. - `shared_deploy.yml` Rolls out Lambda code, optional migrations, optional reconciliation Lambdas, ECS task and service updates, and optional frontend deploys. The reusable workflow renders its Lambda and ECS CodeDeploy AppSpec files from the shared templates under `config/deploy/`, and its mutating `just` steps should target `justfile.deploy` rather than the repo-root `justfile`. @@ -128,7 +128,7 @@ flowchart LR ### Cleanup And Discovery - `destroy.yml` - Tears down app layers before shared dependencies, including the shared observability dashboard and the infra plan artifact bucket stack. + Tears down app layers before shared dependencies, including the shared observability dashboard and any environment-owned shared artifact stacks such as the `dev` code bucket. - `shared_directories_get.yml` Derives the directory-based matrices used by wrapper workflows and PR action-test discovery. @@ -143,6 +143,7 @@ Run these checks on every CI, workflow, or deploy-contract change. - verify optional inputs are intentionally omitted, not accidentally missing - the repo-local `./.github/actions/terragrunt` action supports `tg_action: plan` for producing the binary plan locally; it renders `terragrunt.plan.txt` and writes `terragrunt.plan.meta.json` via `justfile.tg` (`terragrunt-plan-render`) - when `manage_plan_artifacts: true`, `./.github/actions/terragrunt` also uploads (on `plan`) and downloads (on `apply_plan`) the per-stack plan artifacts to/from the shared code bucket under `terragrunt_plan/`, so graph executors like `shared_infra.yml` do not need separate `./.github/actions/just` steps for those transfers +- plan artifact storage follows the same artifact environment split as ECR and build outputs: `dev` uses the `dev` code bucket, while non-`dev` environments read and write `terragrunt_plan/` in the shared `ci` code bucket - `./.github/actions/terragrunt` skips `apply_plan` with a warning when the saved `terragrunt.plan.meta.json` reports `has_changes: false`; when it does not need to download plan artifacts (`manage_plan_artifacts: false`), it also skips `aws-actions/configure-aws-credentials` in that no-op case - `./.github/actions/terragrunt` derives its plan artifact name from `tg_directory`, so callers do not need to pass artifact naming inputs - if `apply_plan` is used across separate workflow runs, pass the earlier workflow `run_id` through `plan_artifact_run_id`; the shared wrappers recover both metadata and per-stack plan files from the shared code bucket under `terragrunt_plan///...` diff --git a/.github/workflows/destroy.yml b/.github/workflows/destroy.yml index 18d6bb5b..0b696c31 100644 --- a/.github/workflows/destroy.yml +++ b/.github/workflows/destroy.yml @@ -263,16 +263,3 @@ jobs: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/cluster tg_action: destroy - - infra-artifact-bucket: - name: Infra Artifact Bucket - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Destroy infra artifact bucket - uses: ./.github/actions/terragrunt - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - tg_directory: infra/live/${{ inputs.environment }}/aws/infra_artifact_bucket - tg_action: destroy diff --git a/.github/workflows/shared_infra_apply_from_plan.yml b/.github/workflows/shared_infra_apply_from_plan.yml index f53f55d7..b5655300 100644 --- a/.github/workflows/shared_infra_apply_from_plan.yml +++ b/.github/workflows/shared_infra_apply_from_plan.yml @@ -19,9 +19,34 @@ permissions: env: AWS_OIDC_ROLE_ARN: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/${{ vars.PROJECT_NAME }}-${{ inputs.environment }}-github-oidc-role + ARTIFACT_ENVIRONMENT: ${{ inputs.environment == 'dev' && 'dev' || 'ci' }} + ARTIFACT_AWS_OIDC_ROLE_ARN: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/${{ vars.PROJECT_NAME }}-${{ inputs.environment == 'dev' && 'dev' || 'ci' }}-github-oidc-role jobs: + artifact_bucket: + runs-on: ubuntu-latest + outputs: + code_bucket_name: ${{ steps.get_bucket_name.outputs.bucket }} + steps: + - uses: actions/checkout@v6 + + - name: Get shared code bucket outputs + uses: ./.github/actions/terragrunt + id: code_action + with: + aws_oidc_role_arn: ${{ env.ARTIFACT_AWS_OIDC_ROLE_ARN }} + tg_directory: infra/live/${{ env.ARTIFACT_ENVIRONMENT }}/aws/code_bucket + tg_action: init + + - name: Get bucket name + id: get_bucket_name + env: + TG_OUTPUTS: ${{ steps.code_action.outputs.tg_outputs }} + run: | + echo "bucket=$(echo "$TG_OUTPUTS" | jq -r '.bucket.value // empty')" >> "$GITHUB_OUTPUT" + metadata: + needs: artifact_bucket runs-on: ubuntu-latest outputs: infra_version: ${{ steps.read_metadata.outputs.infra_version }} @@ -35,11 +60,11 @@ jobs: - name: Download plan metadata from S3 uses: ./.github/actions/just env: - BUCKET_NAME: ${{ vars.AWS_ACCOUNT_ID }}-${{ vars.AWS_REGION }}-${{ vars.PROJECT_NAME }}-${{ inputs.environment }}-code + BUCKET_NAME: ${{ needs.artifact_bucket.outputs.code_bucket_name }} ENVIRONMENT: ${{ inputs.environment }} RUN_ID: ${{ inputs.plan_artifact_run_id }} with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + aws_oidc_role_arn: ${{ env.ARTIFACT_AWS_OIDC_ROLE_ARN }} justfile_path: justfile.ci just_action: infra-plan-metadata-download @@ -47,7 +72,7 @@ jobs: shell: bash run: | if [ ! -f plan-metadata.json ]; then - echo "::error title=Missing plan metadata artifact::Expected 'plan-metadata.json' at s3://${{ vars.AWS_ACCOUNT_ID }}-${{ vars.AWS_REGION }}-${{ vars.PROJECT_NAME }}-${{ inputs.environment }}-code/terragrunt_plan/${{ inputs.environment }}/${{ inputs.plan_artifact_run_id }}/infra-plan-metadata/plan-metadata.json, but it was not downloaded." + echo "::error title=Missing plan metadata artifact::Expected 'plan-metadata.json' at s3://${{ needs.artifact_bucket.outputs.code_bucket_name }}/terragrunt_plan/${{ inputs.environment }}/${{ inputs.plan_artifact_run_id }}/infra-plan-metadata/plan-metadata.json, but it was not downloaded." exit 1 fi diff --git a/.github/workflows/shared_infra_releases.yml b/.github/workflows/shared_infra_releases.yml index 9ce2a821..86ac280f 100644 --- a/.github/workflows/shared_infra_releases.yml +++ b/.github/workflows/shared_infra_releases.yml @@ -15,9 +15,6 @@ on: code_bucket: description: "Bucket containing build artifacts" value: ${{ jobs.bucket.outputs.bucket_name }} - infra_artifact_bucket: - description: "Bucket containing infra plan artifacts" - value: ${{ jobs.infra-artifact-bucket.outputs.bucket_name }} repository_url: description: "ECR repository url" value: ${{ jobs.ecr.outputs.repository_url }} @@ -37,29 +34,6 @@ env: AWS_OIDC_ROLE_ARN: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/${{ vars.PROJECT_NAME }}-${{ inputs.environment }}-github-oidc-role jobs: - infra-artifact-bucket: - runs-on: ubuntu-latest - outputs: - bucket_name: ${{ steps.get_bucket_name.outputs.bucket }} - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.infra_version }} - - - name: Deploy infra plan artifact bucket - id: deploy_bucket - uses: ./.github/actions/terragrunt - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - tg_directory: infra/live/${{ inputs.environment }}/aws/infra_artifact_bucket - - - name: Get infra artifact bucket name - id: get_bucket_name - env: - TG_OUTPUTS: ${{ steps.deploy_bucket.outputs.tg_outputs }} - run: | - echo "bucket=$(echo "$TG_OUTPUTS" | jq -r '.bucket.value')" >> "$GITHUB_OUTPUT" - ecr: runs-on: ubuntu-latest outputs: diff --git a/README.md b/README.md index 68b14802..aaa9c136 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ Infrastructure apply and feature-code rollout are intentionally decoupled in thi - `*_infra` workflows apply infrastructure only - `*_code` workflows deploy feature code only - code deploy workflows publish the real Lambda versions and ECS task revisions into that pre-created deploy surface -- saved infra plans are stored in the shared S3 code bucket under `terragrunt_plan///...` +- saved infra plans are stored in the shared S3 code bucket under `terragrunt_plan///...`, using the same artifact split as build outputs: `dev` writes to the `dev` code bucket and non-`dev` environments reuse the `ci` code bucket - rerunning infrastructure apply does not roll out new feature code - the shared Lambda and ECS module READMEs are the canonical source for bootstrap, rollout, and rollback details for each runtime shape - detailed workflow contracts, reusable-workflow inputs, repo-local action behavior, and `justfile_path` rules live in [.github/docs/README.md](.github/docs/README.md) diff --git a/infra/README.md b/infra/README.md index 536d2d88..e1da71ac 100644 --- a/infra/README.md +++ b/infra/README.md @@ -33,6 +33,7 @@ Shared artifact names also follow environment-aware conventions from `infra/root - shared artifact base: `dev -> ...-dev`, otherwise `...-ci` - code bucket: `-code` - ECS ECR repository: `-ecr` +- saved Terragrunt plan artifacts: `s3:///terragrunt_plan///...` So a stack at: @@ -119,7 +120,7 @@ That `containers/lib` directory is helper code only and is not treated as a depl - build workflows produce Lambda zips and container images - `*_infra` wrappers need the inputs required to apply infra safely, such as directory-derived stack matrices and any artifact-derived bootstrap references - in `prod`, the `*_infra` wrappers read shared artifact resources from `ci` but only apply service and task stacks in `prod` -- saved `plan` / `apply_plan` artifacts live in the shared code bucket under `terragrunt_plan///...` +- saved `plan` / `apply_plan` artifacts live in the shared code bucket under `terragrunt_plan///...`; `dev` uses the `dev` code bucket, while non-`dev` environments reuse the shared `ci` code bucket - deploy workflows: - publish Lambda versions and use Lambda CodeDeploy - optionally invoke the `migrations` Lambda when it is part of the Lambda deploy matrix diff --git a/infra/live/dev/aws/infra_artifact_bucket/terragrunt.hcl b/infra/live/dev/aws/infra_artifact_bucket/terragrunt.hcl deleted file mode 100644 index 007c828f..00000000 --- a/infra/live/dev/aws/infra_artifact_bucket/terragrunt.hcl +++ /dev/null @@ -1,7 +0,0 @@ -include "root" { - path = find_in_parent_folders("root.hcl") -} - -terraform { - source = "../../../../modules//aws//infra_artifact_bucket" -} diff --git a/infra/live/global_vars.hcl b/infra/live/global_vars.hcl index 65eeb462..a3f9f3b4 100644 --- a/infra/live/global_vars.hcl +++ b/infra/live/global_vars.hcl @@ -28,7 +28,6 @@ locals { "cognito-idp:*", ] container_port = 80 - infra_plan_artifact_days = 30 } inputs = { @@ -36,5 +35,4 @@ inputs = { aws_region = local.aws_region allowed_role_actions = local.allowed_role_actions container_port = local.container_port - infra_plan_artifact_days = local.infra_plan_artifact_days } diff --git a/infra/live/prod/aws/infra_artifact_bucket/terragrunt.hcl b/infra/live/prod/aws/infra_artifact_bucket/terragrunt.hcl deleted file mode 100644 index 007c828f..00000000 --- a/infra/live/prod/aws/infra_artifact_bucket/terragrunt.hcl +++ /dev/null @@ -1,7 +0,0 @@ -include "root" { - path = find_in_parent_folders("root.hcl") -} - -terraform { - source = "../../../../modules//aws//infra_artifact_bucket" -} diff --git a/infra/modules/aws/_shared/code_bucket/README.md b/infra/modules/aws/_shared/code_bucket/README.md index dd2a38d9..220eab1f 100644 --- a/infra/modules/aws/_shared/code_bucket/README.md +++ b/infra/modules/aws/_shared/code_bucket/README.md @@ -9,6 +9,11 @@ Shared S3 bucket for deployable artifacts. - ECS AppSpec storage for CodeDeploy - Terragrunt saved plan artifacts under the `terragrunt_plan/` prefix +## Decision Rules + +- `dev` keeps its own code bucket and stores saved Terragrunt plans there +- non-`dev` environments reuse the shared `ci` code bucket for both release artifacts and saved Terragrunt plans + ## Key outputs - artifact bucket name diff --git a/infra/modules/aws/infra_artifact_bucket/main.tf b/infra/modules/aws/infra_artifact_bucket/main.tf deleted file mode 100644 index f0726f27..00000000 --- a/infra/modules/aws/infra_artifact_bucket/main.tf +++ /dev/null @@ -1,6 +0,0 @@ -module "infra_arrifact_bucket" { - source = "../_shared/code_bucket" - - code_bucket = var.infra_plan_artifact_bucket - s3_expiration_days = var.infra_plan_artifact_days -} \ No newline at end of file diff --git a/infra/modules/aws/infra_artifact_bucket/outputs.tf b/infra/modules/aws/infra_artifact_bucket/outputs.tf deleted file mode 100644 index 5b66ec1b..00000000 --- a/infra/modules/aws/infra_artifact_bucket/outputs.tf +++ /dev/null @@ -1,4 +0,0 @@ -output "bucket" { - description = "S3 bucket that stores infra plan artifacts" - value = module.infra_arrifact_bucket.bucket -} diff --git a/infra/modules/aws/infra_artifact_bucket/variables.tf b/infra/modules/aws/infra_artifact_bucket/variables.tf deleted file mode 100644 index 47562acf..00000000 --- a/infra/modules/aws/infra_artifact_bucket/variables.tf +++ /dev/null @@ -1,12 +0,0 @@ -### start of static vars set in root.hcl ### -variable "infra_plan_artifact_bucket" { - description = "S3 bucket to host build artifacts" - type = string -} -### end of static vars set in root.hcl ### - -variable "infra_plan_artifact_days" { - description = "Number of days before objects are deleted (set to 0 to disable)" - type = number - default = 0 -} diff --git a/infra/modules/aws/infra_artifact_bucket/versions.tf b/infra/modules/aws/infra_artifact_bucket/versions.tf deleted file mode 100644 index 10b8fcb5..00000000 --- a/infra/modules/aws/infra_artifact_bucket/versions.tf +++ /dev/null @@ -1,11 +0,0 @@ -terraform { - required_version = ">= 1.0.8" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "= 6.40.0" - configuration_aliases = [aws.domain_aws_region] - } - } -} diff --git a/infra/root.hcl b/infra/root.hcl index ac301280..6e713ae9 100644 --- a/infra/root.hcl +++ b/infra/root.hcl @@ -22,9 +22,6 @@ locals { state_bucket = "${local.base_reference}-tfstate" state_key = "${local.environment}/${local.provider}/${local.module}/terraform.tfstate" state_lock_table = "${local.project_name}-tf-lockid" - - infra_plan_artifact_bucket = "${local.aws_account_id}-${local.project_name}-${local.environment}-infra-plan" - # separate shared artifact resources when dev, otherwise ci artifact_base = local.environment == "dev" ? "${local.base_reference}-${local.environment}" : "${local.base_reference}-ci" code_bucket = "${local.artifact_base}-code" @@ -105,7 +102,6 @@ inputs = merge( deploy_role_arn = local.deploy_role_arn state_bucket = local.state_bucket state_lock_table = local.state_lock_table - infra_plan_artifact_bucket = local.infra_plan_artifact_bucket code_bucket = local.code_bucket ecr_repository_name = local.ecr_repository_name } From 026a758d630e4a050342fc7e8960e5e99e25c2cc Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 12:14:44 +0100 Subject: [PATCH 09/23] chore: lifecycle rules --- README.md | 1 + infra/README.md | 1 + infra/live/global_vars.hcl | 14 ++++-- .../modules/aws/_shared/code_bucket/README.md | 6 +++ infra/modules/aws/_shared/code_bucket/main.tf | 49 ++++++++++++++++--- .../aws/_shared/code_bucket/variables.tf | 10 +++- justfile | 1 + justfile.ci | 9 +++- justfile.tg | 16 ++++-- 9 files changed, 88 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index aaa9c136..3a064f1d 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,7 @@ Infrastructure apply and feature-code rollout are intentionally decoupled in thi - `*_code` workflows deploy feature code only - code deploy workflows publish the real Lambda versions and ECS task revisions into that pre-created deploy surface - saved infra plans are stored in the shared S3 code bucket under `terragrunt_plan///...`, using the same artifact split as build outputs: `dev` writes to the `dev` code bucket and non-`dev` environments reuse the `ci` code bucket +- Code artifact retention and infra-plan retention are configured separately in the shared code bucket module - rerunning infrastructure apply does not roll out new feature code - the shared Lambda and ECS module READMEs are the canonical source for bootstrap, rollout, and rollback details for each runtime shape - detailed workflow contracts, reusable-workflow inputs, repo-local action behavior, and `justfile_path` rules live in [.github/docs/README.md](.github/docs/README.md) diff --git a/infra/README.md b/infra/README.md index e1da71ac..a7f407e9 100644 --- a/infra/README.md +++ b/infra/README.md @@ -34,6 +34,7 @@ Shared artifact names also follow environment-aware conventions from `infra/root - code bucket: `-code` - ECS ECR repository: `-ecr` - saved Terragrunt plan artifacts: `s3:///terragrunt_plan///...` +- code-bucket lifecycle inputs: `code_artifact_expiration_days` for deployable code artifacts and `infra_plan_artifact_expiration_days` for `terragrunt_plan/` So a stack at: diff --git a/infra/live/global_vars.hcl b/infra/live/global_vars.hcl index a3f9f3b4..d0a2d62c 100644 --- a/infra/live/global_vars.hcl +++ b/infra/live/global_vars.hcl @@ -27,12 +27,16 @@ locals { "route53:*", "cognito-idp:*", ] - container_port = 80 + container_port = 80 + code_artifact_expiration_days = 0 + infra_plan_artifact_expiration_days = 30 } inputs = { - vpc_name = local.vpc_name - aws_region = local.aws_region - allowed_role_actions = local.allowed_role_actions - container_port = local.container_port + vpc_name = local.vpc_name + aws_region = local.aws_region + allowed_role_actions = local.allowed_role_actions + container_port = local.container_port + code_artifact_expiration_days = local.code_artifact_expiration_days + infra_plan_artifact_expiration_days = local.infra_plan_artifact_expiration_days } diff --git a/infra/modules/aws/_shared/code_bucket/README.md b/infra/modules/aws/_shared/code_bucket/README.md index 220eab1f..00f9bf5f 100644 --- a/infra/modules/aws/_shared/code_bucket/README.md +++ b/infra/modules/aws/_shared/code_bucket/README.md @@ -9,10 +9,16 @@ Shared S3 bucket for deployable artifacts. - ECS AppSpec storage for CodeDeploy - Terragrunt saved plan artifacts under the `terragrunt_plan/` prefix +## Inputs That Change Behavior + +- `code_artifact_expiration_days` +- `infra_plan_artifact_expiration_days` + ## Decision Rules - `dev` keeps its own code bucket and stores saved Terragrunt plans there - non-`dev` environments reuse the shared `ci` code bucket for both release artifacts and saved Terragrunt plans +- lifecycle retention is prefix-scoped: code artifact cleanup applies to `lambdas/`, `frontend/`, and `appspec/`, while saved plan cleanup applies only to `terragrunt_plan/` ## Key outputs diff --git a/infra/modules/aws/_shared/code_bucket/main.tf b/infra/modules/aws/_shared/code_bucket/main.tf index ad16b9b8..c662d1c5 100644 --- a/infra/modules/aws/_shared/code_bucket/main.tf +++ b/infra/modules/aws/_shared/code_bucket/main.tf @@ -11,17 +11,52 @@ resource "aws_s3_bucket_ownership_controls" "code" { } } -resource "aws_s3_bucket_lifecycle_configuration" "delete_old_files" { - count = var.s3_expiration_days > 0 ? 1 : 0 +locals { + lifecycle_rules = { + code_artifacts_lambdas = { + days = var.code_artifact_expiration_days + prefix = "lambdas/" + } + code_artifacts_frontend = { + days = var.code_artifact_expiration_days + prefix = "frontend/" + } + code_artifacts_appspec = { + days = var.code_artifact_expiration_days + prefix = "appspec/" + } + terragrunt_plans = { + days = var.infra_plan_artifact_expiration_days + prefix = "terragrunt_plan/" + } + } +} + +resource "aws_s3_bucket_lifecycle_configuration" "managed_artifact_retention" { + count = length({ + for name, rule in local.lifecycle_rules : name => rule + if rule.days > 0 + }) > 0 ? 1 : 0 bucket = aws_s3_bucket.code.id - rule { - id = "delete-expired-objects" - status = "Enabled" + dynamic "rule" { + for_each = { + for name, lifecycle_rule in local.lifecycle_rules : name => lifecycle_rule + if lifecycle_rule.days > 0 + } + + content { + id = "delete-${rule.key}" + status = "Enabled" + + filter { + prefix = rule.value.prefix + } - expiration { - days = var.s3_expiration_days + expiration { + days = rule.value.days + } } } } diff --git a/infra/modules/aws/_shared/code_bucket/variables.tf b/infra/modules/aws/_shared/code_bucket/variables.tf index 7ad8beb8..0999cb6b 100644 --- a/infra/modules/aws/_shared/code_bucket/variables.tf +++ b/infra/modules/aws/_shared/code_bucket/variables.tf @@ -5,8 +5,14 @@ variable "code_bucket" { } ### end of static vars set in root.hcl ### -variable "s3_expiration_days" { - description = "Number of days before objects are deleted (set to 0 to disable)" +variable "code_artifact_expiration_days" { + description = "Number of days before deployable code artifacts under lambdas/, frontend/, and appspec/ are deleted (set to 0 to disable)" + type = number + default = 0 +} + +variable "infra_plan_artifact_expiration_days" { + description = "Number of days before saved Terragrunt plan artifacts under terragrunt_plan/ are deleted (set to 0 to disable)" type = number default = 0 } diff --git a/justfile b/justfile index 66cee043..c50e38c2 100644 --- a/justfile +++ b/justfile @@ -12,6 +12,7 @@ _default: PROJECT_DIR := justfile_directory() LAMBDA_DIR := "lambdas" FRONTEND_DIR := "frontend" +INFRA_PLAN_DIR := "terragrunt_plan" # Delete local git branches whose upstream refs have gone away. diff --git a/justfile.ci b/justfile.ci index 40c3843c..c641f06d 100644 --- a/justfile.ci +++ b/justfile.ci @@ -5,6 +5,7 @@ PROJECT_DIR := justfile_directory() LAMBDA_DIR := "lambdas" CONTAINERS_DIR := "containers" +INFRA_PLAN_DIR := "terragrunt_plan" EXTRA_CONTAINER_DIRECTORIES := "[\"debug\",\"otel_collector\"]" NON_SERVICE_CONTAINER_DIRECTORIES := "[\"lib\",\"_shared\"]" @@ -125,7 +126,9 @@ infra-plan-metadata-upload: exit 1 fi - aws s3 cp "plan-metadata.json" "s3://${BUCKET_NAME}/terragrunt_plan/${ENVIRONMENT}/${RUN_ID}/infra-plan-metadata/plan-metadata.json" + artifact_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-base-prefix)" + + aws s3 cp "plan-metadata.json" "s3://${BUCKET_NAME}/${artifact_prefix}/infra-plan-metadata/plan-metadata.json" # Download shared infra plan metadata from the shared code bucket. @@ -148,7 +151,9 @@ infra-plan-metadata-download: exit 1 fi - aws s3 cp "s3://${BUCKET_NAME}/terragrunt_plan/${ENVIRONMENT}/${RUN_ID}/infra-plan-metadata/plan-metadata.json" "plan-metadata.json" + artifact_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-base-prefix)" + + aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/infra-plan-metadata/plan-metadata.json" "plan-metadata.json" # List ECR image tag prefixes for a published version. diff --git a/justfile.tg b/justfile.tg index 1817163f..a9bd10d5 100644 --- a/justfile.tg +++ b/justfile.tg @@ -2,6 +2,7 @@ # This file is for producing, downloading, and uploading saved Terragrunt plan files. PROJECT_DIR := justfile_directory() +INFRA_PLAN_DIR := "terragrunt_plan" PLAN_FILE := "terragrunt.tfplan" PLAN_TEXT_FILE := "terragrunt.plan.txt" @@ -55,8 +56,8 @@ terragrunt-plan-render: cat "$PLAN_META_PATH" -# Derive the shared S3 prefix for Terragrunt plan artifacts. -terragrunt-plan-prefix: +# Derive the shared S3 base prefix for Terragrunt plan artifacts. +terragrunt-plan-base-prefix: #!/usr/bin/env bash set -euo pipefail @@ -70,14 +71,23 @@ terragrunt-plan-prefix: exit 1 fi + echo "{{INFRA_PLAN_DIR}}/${ENVIRONMENT}/${RUN_ID}" + + +# Derive the shared S3 prefix for Terragrunt plan artifacts. +terragrunt-plan-prefix: + #!/usr/bin/env bash + set -euo pipefail + if [[ -z "${TG_DIRECTORY:-}" ]]; then echo "❌ TG_DIRECTORY environment variable is not set." exit 1 fi + artifact_base_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-base-prefix)" sanitized_dir="$(echo "$TG_DIRECTORY" | tr '/.' '--')" artifact_name="terragrunt-plan-${sanitized_dir}" - artifact_prefix="terragrunt_plan/${ENVIRONMENT}/${RUN_ID}/${artifact_name}" + artifact_prefix="${artifact_base_prefix}/${artifact_name}" echo "$artifact_prefix" From 858d61c59748929136473fedfdcd64d575a036c4 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 12:31:57 +0100 Subject: [PATCH 10/23] chore: single source of truth for paths --- .github/docs/README.md | 2 +- .github/workflows/shared_infra_releases.yml | 33 ++++++++++++++++++ .../modules/aws/_shared/code_bucket/README.md | 6 +++- infra/modules/aws/_shared/code_bucket/main.tf | 8 ++--- .../aws/_shared/code_bucket/variables.tf | 24 +++++++++++++ justfile | 32 +++++++++++++++++ justfile.ci | 34 +++++++++++++++---- justfile.deploy | 7 ++-- justfile.tg | 8 +++-- 9 files changed, 136 insertions(+), 18 deletions(-) diff --git a/.github/docs/README.md b/.github/docs/README.md index 215278e2..2054b368 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -66,7 +66,7 @@ flowchart LR ### Shared Artifact Prep And Build - `shared_infra_releases.yml` - Prepares or reads shared artifact infrastructure such as ECR and the code bucket, and exposes those bucket/repository values as reusable-workflow outputs. + Prepares or reads shared artifact infrastructure such as ECR and the code bucket, and exposes those bucket/repository values as reusable-workflow outputs. The code-bucket job reads the Lambda, frontend, AppSpec, and infra-plan S3 prefix names from string-returning `justfile.ci` recipes and forwards them as `TF_VAR_*`, so the workflow does not duplicate those key names inline. - `shared_build.yml` Builds and publishes frontend, Lambda, and ECS artifacts. - `shared_build_get.yml` diff --git a/.github/workflows/shared_infra_releases.yml b/.github/workflows/shared_infra_releases.yml index 86ac280f..25f0db4f 100644 --- a/.github/workflows/shared_infra_releases.yml +++ b/.github/workflows/shared_infra_releases.yml @@ -84,9 +84,42 @@ jobs: with: ref: ${{ inputs.infra_version }} + - name: Read Lambda artifact dir + id: lambda_artifact_dir + uses: ./.github/actions/just + with: + justfile_path: justfile.ci + just_action: code-bucket-get-lambda-artifact-dir + + - name: Read frontend artifact dir + id: frontend_artifact_dir + uses: ./.github/actions/just + with: + justfile_path: justfile.ci + just_action: code-bucket-get-frontend-artifact-dir + + - name: Read AppSpec artifact dir + id: appspec_artifact_dir + uses: ./.github/actions/just + with: + justfile_path: justfile.ci + just_action: code-bucket-get-appspec-artifact-dir + + - name: Read infra plan dir + id: infra_plan_dir + uses: ./.github/actions/just + with: + justfile_path: justfile.ci + just_action: code-bucket-get-infra-plan-dir + - name: Deploy build artifact bucket id: deploy_bucket uses: ./.github/actions/terragrunt + env: + TF_VAR_lambda_artifact_dir: ${{ steps.lambda_artifact_dir.outputs.just_outputs }} + TF_VAR_frontend_artifact_dir: ${{ steps.frontend_artifact_dir.outputs.just_outputs }} + TF_VAR_appspec_artifact_dir: ${{ steps.appspec_artifact_dir.outputs.just_outputs }} + TF_VAR_infra_plan_dir: ${{ steps.infra_plan_dir.outputs.just_outputs }} with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} tg_directory: infra/live/${{ inputs.environment }}/aws/code_bucket diff --git a/infra/modules/aws/_shared/code_bucket/README.md b/infra/modules/aws/_shared/code_bucket/README.md index 00f9bf5f..e5ee176a 100644 --- a/infra/modules/aws/_shared/code_bucket/README.md +++ b/infra/modules/aws/_shared/code_bucket/README.md @@ -11,6 +11,10 @@ Shared S3 bucket for deployable artifacts. ## Inputs That Change Behavior +- `lambda_artifact_dir` +- `frontend_artifact_dir` +- `appspec_artifact_dir` +- `infra_plan_dir` - `code_artifact_expiration_days` - `infra_plan_artifact_expiration_days` @@ -18,7 +22,7 @@ Shared S3 bucket for deployable artifacts. - `dev` keeps its own code bucket and stores saved Terragrunt plans there - non-`dev` environments reuse the shared `ci` code bucket for both release artifacts and saved Terragrunt plans -- lifecycle retention is prefix-scoped: code artifact cleanup applies to `lambdas/`, `frontend/`, and `appspec/`, while saved plan cleanup applies only to `terragrunt_plan/` +- lifecycle retention is prefix-scoped: code artifact cleanup applies to `lambda_artifact_dir/`, `frontend_artifact_dir/`, and `appspec_artifact_dir/`, while saved plan cleanup applies only to `infra_plan_dir/` ## Key outputs diff --git a/infra/modules/aws/_shared/code_bucket/main.tf b/infra/modules/aws/_shared/code_bucket/main.tf index c662d1c5..eeae6df7 100644 --- a/infra/modules/aws/_shared/code_bucket/main.tf +++ b/infra/modules/aws/_shared/code_bucket/main.tf @@ -15,19 +15,19 @@ locals { lifecycle_rules = { code_artifacts_lambdas = { days = var.code_artifact_expiration_days - prefix = "lambdas/" + prefix = "${var.lambda_artifact_dir}/" } code_artifacts_frontend = { days = var.code_artifact_expiration_days - prefix = "frontend/" + prefix = "${var.frontend_artifact_dir}/" } code_artifacts_appspec = { days = var.code_artifact_expiration_days - prefix = "appspec/" + prefix = "${var.appspec_artifact_dir}/" } terragrunt_plans = { days = var.infra_plan_artifact_expiration_days - prefix = "terragrunt_plan/" + prefix = "${var.infra_plan_dir}/" } } } diff --git a/infra/modules/aws/_shared/code_bucket/variables.tf b/infra/modules/aws/_shared/code_bucket/variables.tf index 0999cb6b..69ad48fd 100644 --- a/infra/modules/aws/_shared/code_bucket/variables.tf +++ b/infra/modules/aws/_shared/code_bucket/variables.tf @@ -5,6 +5,30 @@ variable "code_bucket" { } ### end of static vars set in root.hcl ### +variable "lambda_artifact_dir" { + description = "Top-level S3 prefix used for Lambda zip artifacts" + type = string + default = "lambdas" +} + +variable "frontend_artifact_dir" { + description = "Top-level S3 prefix used for frontend build artifacts" + type = string + default = "frontend" +} + +variable "appspec_artifact_dir" { + description = "Top-level S3 prefix used for AppSpec deployment artifacts" + type = string + default = "appspec" +} + +variable "infra_plan_dir" { + description = "Top-level S3 prefix used for saved Terragrunt plan artifacts" + type = string + default = "terragrunt_plan" +} + variable "code_artifact_expiration_days" { description = "Number of days before deployable code artifacts under lambdas/, frontend/, and appspec/ are deleted (set to 0 to disable)" type = number diff --git a/justfile b/justfile index c50e38c2..17c11ac3 100644 --- a/justfile +++ b/justfile @@ -12,7 +12,31 @@ _default: PROJECT_DIR := justfile_directory() LAMBDA_DIR := "lambdas" FRONTEND_DIR := "frontend" +CONTAINERS_DIR := "containers" +APPSPEC_DIR := "appspec" INFRA_PLAN_DIR := "terragrunt_plan" +EXTRA_CONTAINER_DIRECTORIES := "[\"debug\",\"otel_collector\"]" +NON_SERVICE_CONTAINER_DIRECTORIES := "[\"lib\",\"_shared\"]" + + +# Return the Lambda artifact directory name. +code-bucket-get-lambda-artifact-dir: + @echo {{LAMBDA_DIR}} + + +# Return the frontend artifact directory name. +code-bucket-get-frontend-artifact-dir: + @echo {{FRONTEND_DIR}} + + +# Return the infra plan artifact directory name. +code-bucket-get-infra-plan-dir: + @echo {{INFRA_PLAN_DIR}} + + +# Return the AppSpec artifact directory name. +code-bucket-get-appspec-artifact-dir: + @echo {{APPSPEC_DIR}} # Delete local git branches whose upstream refs have gone away. @@ -60,12 +84,20 @@ format: # Run a Terragrunt operation for one environment/module pair. tg env module op: #!/usr/bin/env bash + export TF_VAR_lambda_artifact_dir="{{LAMBDA_DIR}}" + export TF_VAR_frontend_artifact_dir="{{FRONTEND_DIR}}" + export TF_VAR_appspec_artifact_dir="{{APPSPEC_DIR}}" + export TF_VAR_infra_plan_dir="{{INFRA_PLAN_DIR}}" cd {{justfile_directory()}}/infra/live/{{env}}/{{module}} ; terragrunt {{op}} # Run a Terragrunt operation across all live stacks. tg-all op: #!/usr/bin/env bash + export TF_VAR_lambda_artifact_dir="{{LAMBDA_DIR}}" + export TF_VAR_frontend_artifact_dir="{{FRONTEND_DIR}}" + export TF_VAR_appspec_artifact_dir="{{APPSPEC_DIR}}" + export TF_VAR_infra_plan_dir="{{INFRA_PLAN_DIR}}" cd {{justfile_directory()}}/infra/live terragrunt run-all {{op}} diff --git a/justfile.ci b/justfile.ci index c641f06d..9766e491 100644 --- a/justfile.ci +++ b/justfile.ci @@ -2,12 +2,14 @@ # This file is for discovery, validation, artifact existence checks, and matrix shaping. # Do not put AWS-mutating rollout or deploy commands here. -PROJECT_DIR := justfile_directory() -LAMBDA_DIR := "lambdas" -CONTAINERS_DIR := "containers" -INFRA_PLAN_DIR := "terragrunt_plan" -EXTRA_CONTAINER_DIRECTORIES := "[\"debug\",\"otel_collector\"]" -NON_SERVICE_CONTAINER_DIRECTORIES := "[\"lib\",\"_shared\"]" +PROJECT_DIR := `just --justfile justfile --evaluate PROJECT_DIR` +LAMBDA_DIR := `just --justfile justfile --evaluate LAMBDA_DIR` +FRONTEND_DIR := `just --justfile justfile --evaluate FRONTEND_DIR` +APPSPEC_DIR := `just --justfile justfile --evaluate APPSPEC_DIR` +CONTAINERS_DIR := `just --justfile justfile --evaluate CONTAINERS_DIR` +INFRA_PLAN_DIR := `just --justfile justfile --evaluate INFRA_PLAN_DIR` +EXTRA_CONTAINER_DIRECTORIES := `just --justfile justfile --evaluate EXTRA_CONTAINER_DIRECTORIES` +NON_SERVICE_CONTAINER_DIRECTORIES := `just --justfile justfile --evaluate NON_SERVICE_CONTAINER_DIRECTORIES` # Run `tflint` across Terraform module directories. @@ -156,6 +158,26 @@ infra-plan-metadata-download: aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/infra-plan-metadata/plan-metadata.json" "plan-metadata.json" +# Return the Lambda artifact directory name from the repo-root justfile. +code-bucket-get-lambda-artifact-dir: + @just --justfile "{{PROJECT_DIR}}/justfile" code-bucket-get-lambda-artifact-dir + + +# Return the frontend artifact directory name from the repo-root justfile. +code-bucket-get-frontend-artifact-dir: + @just --justfile "{{PROJECT_DIR}}/justfile" code-bucket-get-frontend-artifact-dir + + +# Return the infra plan artifact directory name from the repo-root justfile. +code-bucket-get-infra-plan-dir: + @just --justfile "{{PROJECT_DIR}}/justfile" code-bucket-get-infra-plan-dir + + +# Return the AppSpec artifact directory name from the repo-root justfile. +code-bucket-get-appspec-artifact-dir: + @just --justfile "{{PROJECT_DIR}}/justfile" code-bucket-get-appspec-artifact-dir + + # List ECR image tag prefixes for a published version. get-ecr-version-images: #!/usr/bin/env bash diff --git a/justfile.deploy b/justfile.deploy index 47d169c0..1f989600 100644 --- a/justfile.deploy +++ b/justfile.deploy @@ -2,9 +2,10 @@ # This file is for build/publish/deploy/invalidate steps that change remote state. # Keep read-only CI helpers in `justfile.ci`. -PROJECT_DIR := justfile_directory() -LAMBDA_DIR := "lambdas" -FRONTEND_DIR := "frontend" +PROJECT_DIR := `just --justfile justfile --evaluate PROJECT_DIR` +LAMBDA_DIR := `just --justfile justfile --evaluate LAMBDA_DIR` +FRONTEND_DIR := `just --justfile justfile --evaluate FRONTEND_DIR` +APPSPEC_DIR := `just --justfile justfile --evaluate APPSPEC_DIR` # Invoke a deployed Lambda and fail if it does not return HTTP 200. diff --git a/justfile.tg b/justfile.tg index a9bd10d5..2eb2c27c 100644 --- a/justfile.tg +++ b/justfile.tg @@ -1,8 +1,8 @@ # Terragrunt plan artifact helpers. # This file is for producing, downloading, and uploading saved Terragrunt plan files. -PROJECT_DIR := justfile_directory() -INFRA_PLAN_DIR := "terragrunt_plan" +PROJECT_DIR := `just --justfile justfile --evaluate PROJECT_DIR` +INFRA_PLAN_DIR := `just --justfile justfile --evaluate INFRA_PLAN_DIR` PLAN_FILE := "terragrunt.tfplan" PLAN_TEXT_FILE := "terragrunt.plan.txt" @@ -71,7 +71,9 @@ terragrunt-plan-base-prefix: exit 1 fi - echo "{{INFRA_PLAN_DIR}}/${ENVIRONMENT}/${RUN_ID}" + infra_plan_dir="${TF_VAR_infra_plan_dir:-{{INFRA_PLAN_DIR}}}" + + echo "${infra_plan_dir}/${ENVIRONMENT}/${RUN_ID}" # Derive the shared S3 prefix for Terragrunt plan artifacts. From c8d3d6b22031a7b94ac1654c29f9f8577b44c558 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 12:36:52 +0100 Subject: [PATCH 11/23] chore: move container port to default --- infra/live/global_vars.hcl | 2 -- infra/modules/aws/network/variables.tf | 3 ++- infra/modules/aws/security/variables.tf | 3 ++- infra/modules/aws/service_worker/variables.tf | 3 ++- infra/modules/aws/task_worker/variables.tf | 3 ++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/infra/live/global_vars.hcl b/infra/live/global_vars.hcl index d0a2d62c..e24023d0 100644 --- a/infra/live/global_vars.hcl +++ b/infra/live/global_vars.hcl @@ -27,7 +27,6 @@ locals { "route53:*", "cognito-idp:*", ] - container_port = 80 code_artifact_expiration_days = 0 infra_plan_artifact_expiration_days = 30 } @@ -36,7 +35,6 @@ inputs = { vpc_name = local.vpc_name aws_region = local.aws_region allowed_role_actions = local.allowed_role_actions - container_port = local.container_port code_artifact_expiration_days = local.code_artifact_expiration_days infra_plan_artifact_expiration_days = local.infra_plan_artifact_expiration_days } diff --git a/infra/modules/aws/network/variables.tf b/infra/modules/aws/network/variables.tf index aa66a8ce..6242bad8 100644 --- a/infra/modules/aws/network/variables.tf +++ b/infra/modules/aws/network/variables.tf @@ -21,7 +21,8 @@ variable "vpc_name" { } variable "container_port" { - type = number + type = number + default = 80 } variable "local_tunnel" { diff --git a/infra/modules/aws/security/variables.tf b/infra/modules/aws/security/variables.tf index 0bea6c1a..b7f8dd5e 100644 --- a/infra/modules/aws/security/variables.tf +++ b/infra/modules/aws/security/variables.tf @@ -13,7 +13,8 @@ variable "vpc_name" { } variable "container_port" { - type = number + type = number + default = 80 } variable "additional_listener_port" { diff --git a/infra/modules/aws/service_worker/variables.tf b/infra/modules/aws/service_worker/variables.tf index 75980e2f..4b065255 100644 --- a/infra/modules/aws/service_worker/variables.tf +++ b/infra/modules/aws/service_worker/variables.tf @@ -26,7 +26,8 @@ variable "vpc_name" { } variable "container_port" { - type = number + type = number + default = 80 } variable "root_path" { diff --git a/infra/modules/aws/task_worker/variables.tf b/infra/modules/aws/task_worker/variables.tf index 086d02c8..c4bd09a5 100644 --- a/infra/modules/aws/task_worker/variables.tf +++ b/infra/modules/aws/task_worker/variables.tf @@ -21,7 +21,8 @@ variable "ecr_repository_name" { ### end of static vars set in root.hcl ### variable "container_port" { - type = number + type = number + default = 80 } variable "cpu" { From eb1d543b456ee76de6227698ec90dff26f20ae6b Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 12:57:52 +0100 Subject: [PATCH 12/23] chore: slim down vars/inputs --- .github/actions/terragrunt/README.md | 5 +- .github/actions/terragrunt/action.yml | 21 ----- .github/docs/README.md | 10 +- .github/workflows/shared_infra.yml | 93 +------------------ .../shared_infra_apply_from_plan.yml | 16 +++- .github/workflows/shared_infra_plan.yml | 18 +++- justfile.ci | 36 ++----- justfile.tg | 67 +++++++------ 8 files changed, 82 insertions(+), 184 deletions(-) diff --git a/.github/actions/terragrunt/README.md b/.github/actions/terragrunt/README.md index c59b1aef..e3c15b54 100644 --- a/.github/actions/terragrunt/README.md +++ b/.github/actions/terragrunt/README.md @@ -21,9 +21,6 @@ This GitHub Action sets up **Terraform** and **Terragrunt**, authenticates to AW | `override_tg_vars` | Terragrunt variables in JSON, written to `override_tg_vars.tfvars.json` | No | `{}` | | `aws_oidc_role_arn` | IAM role ARN to assume via OIDC | Yes | — | | `manage_plan_artifacts` | When `true`, downloads saved plan artifacts for `apply_plan` and uploads plan artifacts for `plan` via `justfile.tg` | No | `false` | -| `plan_bucket_name` | S3 bucket used for saved plan artifacts (required when `manage_plan_artifacts: true`) | No | `""` | -| `plan_environment` | Environment name used in the plan artifact prefix (required when `manage_plan_artifacts: true`) | No | `""` | -| `plan_run_id` | Run ID used in the plan artifact prefix (required when `manage_plan_artifacts: true`) | No | `""` | | `tg_directory` | Directory containing the Terragrunt config | Yes | — | | `tg_action` | Terragrunt action: `apply`, `plan`, `apply_plan`, `destroy`, or `init` | Yes | `apply` | @@ -41,7 +38,7 @@ This GitHub Action sets up **Terraform** and **Terragrunt**, authenticates to AW - `plan` Runs `terragrunt plan -detailed-exitcode -out=/terragrunt.tfplan`, then renders `terragrunt.plan.txt` and writes `terragrunt.plan.meta.json` via the repo `justfile.tg` recipe `terragrunt-plan-render`. If `manage_plan_artifacts: true`, it also uploads those files to S3 via `justfile.tg`. - `apply_plan` - If `manage_plan_artifacts: true`, downloads the saved plan files into `tg_directory` via `justfile.tg`. Otherwise it expects the saved plan files to already exist in `tg_directory`. In both cases it 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. + If `manage_plan_artifacts: true`, downloads the saved plan files into `tg_directory` via `justfile.tg` using the caller-provided `PLAN_ARTIFACT_S3_PREFIX` environment variable plus the stack-derived suffix from `tg_directory`. Otherwise it expects the saved plan files to already exist in `tg_directory`. In both cases it 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` diff --git a/.github/actions/terragrunt/action.yml b/.github/actions/terragrunt/action.yml index d4862114..2979a617 100644 --- a/.github/actions/terragrunt/action.yml +++ b/.github/actions/terragrunt/action.yml @@ -24,18 +24,6 @@ inputs: description: "When true, downloads saved plan artifacts for apply_plan and uploads plan artifacts for plan" required: false default: "false" - plan_bucket_name: - description: "S3 bucket used for saved plan artifacts (required when manage_plan_artifacts is true)" - required: false - default: "" - plan_environment: - description: "Environment name used for saved plan artifact prefixes (required when manage_plan_artifacts is true)" - required: false - default: "" - plan_run_id: - description: "Run ID used for saved plan artifact prefixes (required when manage_plan_artifacts is true)" - required: false - default: "" tg_directory: description: "Module directory to perform action upon" required: true @@ -82,14 +70,10 @@ runs: if: inputs.tg_action == 'apply_plan' && inputs.manage_plan_artifacts == 'true' uses: ./.github/actions/just env: - BUCKET_NAME: ${{ inputs.plan_bucket_name }} - ENVIRONMENT: ${{ inputs.plan_environment }} - RUN_ID: ${{ inputs.plan_run_id }} TG_DIRECTORY: ${{ inputs.tg_directory }} with: justfile_path: justfile.tg just_action: terragrunt-plan-download - mask_result: "false" - name: Verify plan artifact files exist if: inputs.tg_action == 'apply_plan' @@ -181,20 +165,15 @@ runs: with: justfile_path: justfile.tg just_action: terragrunt-plan-render - mask_result: "false" - name: Upload saved plan artifacts if: inputs.tg_action == 'plan' && inputs.manage_plan_artifacts == 'true' uses: ./.github/actions/just env: - BUCKET_NAME: ${{ inputs.plan_bucket_name }} - ENVIRONMENT: ${{ inputs.plan_environment }} - RUN_ID: ${{ inputs.plan_run_id }} TG_DIRECTORY: ${{ inputs.tg_directory }} with: justfile_path: justfile.tg just_action: terragrunt-plan-upload - mask_result: "false" - name: Capture Terraform Outputs if: inputs.tg_action != 'destroy' && (inputs.tg_action != 'apply_plan' || steps.apply_plan_guard.outputs.should_apply == 'true') diff --git a/.github/docs/README.md b/.github/docs/README.md index 2054b368..607ec396 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -82,13 +82,13 @@ flowchart LR ### Infra And Code Rollout - `shared_infra_plan.yml` - Plan wrapper around `shared_infra.yml`. It takes resolved workflow inputs directly, writes `infra-plan-metadata`, uploads it to the resolved code bucket under `terragrunt_plan///infra-plan-metadata/` via `justfile.ci`, and then calls `shared_infra.yml` with `tg_action: plan`. The bucket resolution follows the same artifact split as ECR and build outputs: `dev` uses the `dev` code bucket, while non-`dev` environments reuse the `ci` code bucket. After the plan completes, it prints the current workflow `github.run_id` into both the logs and the GitHub Actions step summary as `plan_artifact_run_id`, and exposes that value as a reusable-workflow output. + Plan wrapper around `shared_infra.yml`. It takes resolved workflow inputs directly, derives a single plan-artifact S3 prefix via `justfile.tg`, uploads `infra-plan-metadata` under `/infra-plan-metadata/` via `justfile.ci`, and then calls `shared_infra.yml` with `tg_action: plan` plus that same resolved prefix. The bucket resolution follows the same artifact split as ECR and build outputs: `dev` uses the `dev` code bucket, while non-`dev` environments reuse the `ci` code bucket. After the plan completes, it prints the current workflow `github.run_id` into both the logs and the GitHub Actions step summary as `plan_artifact_run_id`, and exposes that value as a reusable-workflow output. - `shared_infra_apply.yml` Direct-input apply wrapper around `shared_infra.yml`. It takes resolved workflow inputs directly and calls `shared_infra.yml` with `tg_action: apply`. - `shared_infra_apply_from_plan.yml` - Apply-from-plan wrapper around `shared_infra.yml`. It takes `plan_artifact_run_id`, resolves the same artifact bucket split used by release artifacts (`dev` stays on `dev`, non-`dev` uses `ci`), downloads `infra-plan-metadata` from that code bucket via `justfile.ci`, reads the frozen graph inputs back out, and then calls `shared_infra.yml` with `tg_action: apply_plan`. + Apply-from-plan wrapper around `shared_infra.yml`. It takes `plan_artifact_run_id`, resolves the same artifact bucket split used by release artifacts (`dev` stays on `dev`, non-`dev` uses `ci`), derives the matching plan-artifact S3 prefix via `justfile.tg`, downloads `infra-plan-metadata` from that location via `justfile.ci`, reads the frozen graph inputs back out, and then calls `shared_infra.yml` with `tg_action: apply_plan` plus that same resolved prefix. - `shared_infra.yml` - Pure ordered infra graph executor. It applies shared stacks first, then runtime stacks, then frontend infrastructure. Shared stacks now include the CloudWatch observability dashboard. It accepts `tg_action` so the same graph can run a normal apply, upload derived per-stack plan artifacts to the resolved code bucket under `terragrunt_plan/`, or apply from previously uploaded plan artifacts. In `apply_plan` mode, each stack job first downloads its own saved plan files via `justfile.tg` through the Terragrunt action, then the Terragrunt action validates and applies those local files. Its visible step labels now follow `tg_action` too, so plan and apply-from-plan runs no longer show everything as `Deploy ...`. The `security -> network` edge is a real bootstrap dependency because `network` reads security outputs like `vpc_endpoint_sg` from remote state; if those outputs do not exist yet, `network` fails with an upstream `Unsupported attribute` error rather than a networking-specific error. + Pure ordered infra graph executor. It applies shared stacks first, then runtime stacks, then frontend infrastructure. Shared stacks now include the CloudWatch observability dashboard. It accepts `tg_action` so the same graph can run a normal apply, upload derived per-stack plan artifacts to the resolved code bucket under `terragrunt_plan/`, or apply from previously uploaded plan artifacts. The wrapper workflows resolve one `plan_artifact_s3_prefix` and set it in the workflow env once; the Terragrunt action reads that env directly and appends the stack-specific suffix from `tg_directory`, so `shared_infra.yml` does not need to rebuild the bucket or env/run-id tuple in every job. In `apply_plan` mode, each stack job first downloads its own saved plan files via `justfile.tg` through the Terragrunt action, then the Terragrunt action validates and applies those local files. Its visible step labels now follow `tg_action` too, so plan and apply-from-plan runs no longer show everything as `Deploy ...`. The `security -> network` edge is a real bootstrap dependency because `network` reads security outputs like `vpc_endpoint_sg` from remote state; if those outputs do not exist yet, `network` fails with an upstream `Unsupported attribute` error rather than a networking-specific error. - The shared infra wrappers must forward the permissions required by the nested reusable call chain. In practice that means `id-token: write` everywhere the Terragrunt action may assume AWS OIDC and `contents: read` for checkout. The shared plan/apply wrappers now rely on AWS access to the shared code bucket rather than GitHub artifact permissions for cross-run recovery. - `shared_deploy.yml` Rolls out Lambda code, optional migrations, optional reconciliation Lambdas, ECS task and service updates, and optional frontend deploys. The reusable workflow renders its Lambda and ECS CodeDeploy AppSpec files from the shared templates under `config/deploy/`, and its mutating `just` steps should target `justfile.deploy` rather than the repo-root `justfile`. @@ -142,11 +142,11 @@ Run these checks on every CI, workflow, or deploy-contract change. - compare expected outputs against actual `jobs..outputs.*` - verify optional inputs are intentionally omitted, not accidentally missing - the repo-local `./.github/actions/terragrunt` action supports `tg_action: plan` for producing the binary plan locally; it renders `terragrunt.plan.txt` and writes `terragrunt.plan.meta.json` via `justfile.tg` (`terragrunt-plan-render`) -- when `manage_plan_artifacts: true`, `./.github/actions/terragrunt` also uploads (on `plan`) and downloads (on `apply_plan`) the per-stack plan artifacts to/from the shared code bucket under `terragrunt_plan/`, so graph executors like `shared_infra.yml` do not need separate `./.github/actions/just` steps for those transfers +- when `manage_plan_artifacts: true`, `./.github/actions/terragrunt` also uploads (on `plan`) and downloads (on `apply_plan`) the per-stack plan artifacts using the caller-provided `PLAN_ARTIFACT_S3_PREFIX` environment variable, so graph executors like `shared_infra.yml` do not need separate `./.github/actions/just` steps for those transfers - plan artifact storage follows the same artifact environment split as ECR and build outputs: `dev` uses the `dev` code bucket, while non-`dev` environments read and write `terragrunt_plan/` in the shared `ci` code bucket - `./.github/actions/terragrunt` skips `apply_plan` with a warning when the saved `terragrunt.plan.meta.json` reports `has_changes: false`; when it does not need to download plan artifacts (`manage_plan_artifacts: false`), it also skips `aws-actions/configure-aws-credentials` in that no-op case - `./.github/actions/terragrunt` derives its plan artifact name from `tg_directory`, so callers do not need to pass artifact naming inputs -- if `apply_plan` is used across separate workflow runs, pass the earlier workflow `run_id` through `plan_artifact_run_id`; the shared wrappers recover both metadata and per-stack plan files from the shared code bucket under `terragrunt_plan///...` +- if `apply_plan` is used across separate workflow runs, pass the earlier workflow `run_id` through `plan_artifact_run_id`; the shared wrappers recover both metadata and per-stack plan files by deriving the matching `plan_artifact_s3_prefix` and reading from the shared code bucket under `terragrunt_plan///...` - if a cross-run apply should not ask the operator to re-enter versions or recompute artifact resolution, store both the input versions and the resolved reusable-workflow outputs in a metadata artifact during plan and recover them in the apply wrapper from the earlier `run_id` - keep `shared_infra.yml` as the pure graph executor and prefer handling metadata creation/recovery in the dedicated plan/apply wrappers - when using `./.github/actions/just`, check whether the caller needs the repo-root `justfile` or an explicit `justfile_path` diff --git a/.github/workflows/shared_infra.yml b/.github/workflows/shared_infra.yml index 0e80f801..96f882f2 100644 --- a/.github/workflows/shared_infra.yml +++ b/.github/workflows/shared_infra.yml @@ -33,8 +33,8 @@ on: required: false type: string default: "apply" - plan_artifact_run_id: - description: "Optional workflow run ID used for apply-from-plan artifact downloads" + plan_artifact_s3_prefix: + description: "Optional resolved S3 prefix used for saved plan artifacts" required: false type: string default: "" @@ -52,16 +52,12 @@ permissions: env: AWS_OIDC_ROLE_ARN: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/${{ vars.PROJECT_NAME }}-${{ inputs.environment }}-github-oidc-role DOMAIN_NAME: ${{ vars.DOMAIN_NAME }} + PLAN_ARTIFACT_S3_PREFIX: ${{ inputs.plan_artifact_s3_prefix }} TG_ACTION_LABEL: ${{ inputs.tg_action == 'apply' && 'Apply' || inputs.tg_action == 'plan' && 'Plan' || inputs.tg_action == 'apply_plan' && 'Apply planned' || inputs.tg_action == 'destroy' && 'Destroy' || inputs.tg_action == 'init' && 'Init' || 'Run' }} jobs: oidc: runs-on: ubuntu-latest - env: - BUCKET_NAME: ${{ inputs.code_bucket }} - ENVIRONMENT: ${{ inputs.environment }} - RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} - TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/oidc steps: - uses: actions/checkout@v6 with: @@ -72,20 +68,12 @@ jobs: with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} manage_plan_artifacts: "true" - plan_bucket_name: ${{ env.BUCKET_NAME }} - plan_environment: ${{ env.ENVIRONMENT }} - plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/oidc tg_action: ${{ inputs.tg_action }} worker_messaging: needs: oidc runs-on: ubuntu-latest - env: - BUCKET_NAME: ${{ inputs.code_bucket }} - ENVIRONMENT: ${{ inputs.environment }} - RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} - TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/worker_messaging steps: - uses: actions/checkout@v6 with: @@ -96,20 +84,12 @@ jobs: with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} manage_plan_artifacts: "true" - plan_bucket_name: ${{ env.BUCKET_NAME }} - plan_environment: ${{ env.ENVIRONMENT }} - plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/worker_messaging tg_action: ${{ inputs.tg_action }} observability: needs: oidc runs-on: ubuntu-latest - env: - BUCKET_NAME: ${{ inputs.code_bucket }} - ENVIRONMENT: ${{ inputs.environment }} - RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} - TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/observability steps: - uses: actions/checkout@v6 with: @@ -120,20 +100,12 @@ jobs: with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} manage_plan_artifacts: "true" - plan_bucket_name: ${{ env.BUCKET_NAME }} - plan_environment: ${{ env.ENVIRONMENT }} - plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/observability tg_action: ${{ inputs.tg_action }} cognito: needs: oidc runs-on: ubuntu-latest - env: - BUCKET_NAME: ${{ inputs.code_bucket }} - ENVIRONMENT: ${{ inputs.environment }} - RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} - TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/cognito steps: - uses: actions/checkout@v6 with: @@ -146,9 +118,6 @@ jobs: with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} manage_plan_artifacts: "true" - plan_bucket_name: ${{ env.BUCKET_NAME }} - plan_environment: ${{ env.ENVIRONMENT }} - plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/cognito tg_action: ${{ inputs.tg_action }} @@ -157,11 +126,6 @@ jobs: - network - cognito runs-on: ubuntu-latest - env: - BUCKET_NAME: ${{ inputs.code_bucket }} - ENVIRONMENT: ${{ inputs.environment }} - RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} - TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/frontend steps: - uses: actions/checkout@v6 with: @@ -218,20 +182,12 @@ jobs: with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} manage_plan_artifacts: "true" - plan_bucket_name: ${{ env.BUCKET_NAME }} - plan_environment: ${{ env.ENVIRONMENT }} - plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/frontend tg_action: ${{ inputs.tg_action }} cluster: needs: oidc runs-on: ubuntu-latest - env: - BUCKET_NAME: ${{ inputs.code_bucket }} - ENVIRONMENT: ${{ inputs.environment }} - RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} - TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/cluster steps: - uses: actions/checkout@v6 with: @@ -242,20 +198,12 @@ jobs: with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} manage_plan_artifacts: "true" - plan_bucket_name: ${{ env.BUCKET_NAME }} - plan_environment: ${{ env.ENVIRONMENT }} - plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/cluster tg_action: ${{ inputs.tg_action }} security: needs: oidc runs-on: ubuntu-latest - env: - BUCKET_NAME: ${{ inputs.code_bucket }} - ENVIRONMENT: ${{ inputs.environment }} - RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} - TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/security outputs: postgres_sg: ${{ steps.get_security_outputs.outputs.postgres_sg }} steps: @@ -269,9 +217,6 @@ jobs: with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} manage_plan_artifacts: "true" - plan_bucket_name: ${{ env.BUCKET_NAME }} - plan_environment: ${{ env.ENVIRONMENT }} - plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/security tg_action: ${{ inputs.tg_action }} @@ -288,11 +233,6 @@ jobs: - oidc - security runs-on: ubuntu-latest - env: - BUCKET_NAME: ${{ inputs.code_bucket }} - ENVIRONMENT: ${{ inputs.environment }} - RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} - TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/database steps: - uses: actions/checkout@v6 with: @@ -305,9 +245,6 @@ jobs: with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} manage_plan_artifacts: "true" - plan_bucket_name: ${{ env.BUCKET_NAME }} - plan_environment: ${{ env.ENVIRONMENT }} - plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/database tg_action: ${{ inputs.tg_action }} @@ -316,11 +253,6 @@ jobs: - security - cognito runs-on: ubuntu-latest - env: - BUCKET_NAME: ${{ inputs.code_bucket }} - ENVIRONMENT: ${{ inputs.environment }} - RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} - TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/network steps: - uses: actions/checkout@v6 with: @@ -331,9 +263,6 @@ jobs: with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} manage_plan_artifacts: "true" - plan_bucket_name: ${{ env.BUCKET_NAME }} - plan_environment: ${{ env.ENVIRONMENT }} - plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/network tg_action: ${{ inputs.tg_action }} @@ -349,11 +278,6 @@ jobs: fail-fast: false # this is to prevent terraform lock issues matrix: value: ${{ fromJson(inputs.lambda_matrix) }} - env: - BUCKET_NAME: ${{ inputs.code_bucket }} - ENVIRONMENT: ${{ inputs.environment }} - RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} - TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} steps: - uses: actions/checkout@v6 with: @@ -364,9 +288,6 @@ jobs: with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} manage_plan_artifacts: "true" - plan_bucket_name: ${{ env.BUCKET_NAME }} - plan_environment: ${{ env.ENVIRONMENT }} - plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} tg_action: ${{ inputs.tg_action }} @@ -383,11 +304,6 @@ jobs: fail-fast: false matrix: value: ${{ fromJson(inputs.service_matrix) }} - env: - BUCKET_NAME: ${{ inputs.code_bucket }} - ENVIRONMENT: ${{ inputs.environment }} - RUN_ID: ${{ inputs.tg_action == 'apply_plan' && (inputs.plan_artifact_run_id != '' && inputs.plan_artifact_run_id || github.run_id) || github.run_id }} - TG_DIRECTORY: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} steps: - uses: actions/checkout@v6 with: @@ -401,8 +317,5 @@ jobs: with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} manage_plan_artifacts: "true" - plan_bucket_name: ${{ env.BUCKET_NAME }} - plan_environment: ${{ env.ENVIRONMENT }} - plan_run_id: ${{ env.RUN_ID }} tg_directory: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} tg_action: ${{ inputs.tg_action }} diff --git a/.github/workflows/shared_infra_apply_from_plan.yml b/.github/workflows/shared_infra_apply_from_plan.yml index b5655300..e3d0dcf5 100644 --- a/.github/workflows/shared_infra_apply_from_plan.yml +++ b/.github/workflows/shared_infra_apply_from_plan.yml @@ -53,16 +53,26 @@ jobs: code_bucket: ${{ steps.read_metadata.outputs.code_bucket }} lambda_matrix: ${{ steps.read_metadata.outputs.lambda_matrix }} bootstrap_image_uri: ${{ steps.read_metadata.outputs.bootstrap_image_uri }} + plan_artifact_s3_prefix: ${{ steps.plan_artifact_s3_prefix.outputs.just_outputs }} service_matrix: ${{ steps.read_metadata.outputs.service_matrix }} steps: - uses: actions/checkout@v6 - - name: Download plan metadata from S3 + - name: Get plan artifact S3 prefix + id: plan_artifact_s3_prefix uses: ./.github/actions/just env: BUCKET_NAME: ${{ needs.artifact_bucket.outputs.code_bucket_name }} ENVIRONMENT: ${{ inputs.environment }} RUN_ID: ${{ inputs.plan_artifact_run_id }} + with: + justfile_path: justfile.tg + just_action: terragrunt-plan-base-s3-prefix + + - name: Download plan metadata from S3 + uses: ./.github/actions/just + env: + PLAN_ARTIFACT_S3_PREFIX: ${{ steps.plan_artifact_s3_prefix.outputs.just_outputs }} with: aws_oidc_role_arn: ${{ env.ARTIFACT_AWS_OIDC_ROLE_ARN }} justfile_path: justfile.ci @@ -72,7 +82,7 @@ jobs: shell: bash run: | if [ ! -f plan-metadata.json ]; then - echo "::error title=Missing plan metadata artifact::Expected 'plan-metadata.json' at s3://${{ needs.artifact_bucket.outputs.code_bucket_name }}/terragrunt_plan/${{ inputs.environment }}/${{ inputs.plan_artifact_run_id }}/infra-plan-metadata/plan-metadata.json, but it was not downloaded." + echo "::error title=Missing plan metadata artifact::Expected 'plan-metadata.json' at ${{ steps.plan_artifact_s3_prefix.outputs.just_outputs }}/infra-plan-metadata/plan-metadata.json, but it was not downloaded." exit 1 fi @@ -98,4 +108,4 @@ jobs: bootstrap_image_uri: ${{ needs.metadata.outputs.bootstrap_image_uri }} service_matrix: ${{ needs.metadata.outputs.service_matrix }} tg_action: apply_plan - plan_artifact_run_id: ${{ inputs.plan_artifact_run_id }} + plan_artifact_s3_prefix: ${{ needs.metadata.outputs.plan_artifact_s3_prefix }} diff --git a/.github/workflows/shared_infra_plan.yml b/.github/workflows/shared_infra_plan.yml index 57ab2c0c..508b6e87 100644 --- a/.github/workflows/shared_infra_plan.yml +++ b/.github/workflows/shared_infra_plan.yml @@ -44,11 +44,24 @@ env: jobs: metadata: runs-on: ubuntu-latest + outputs: + plan_artifact_s3_prefix: ${{ steps.plan_artifact_s3_prefix.outputs.just_outputs }} steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.infra_version }} + - name: Get plan artifact S3 prefix + id: plan_artifact_s3_prefix + uses: ./.github/actions/just + env: + BUCKET_NAME: ${{ inputs.code_bucket }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_ID: ${{ github.run_id }} + with: + justfile_path: justfile.tg + just_action: terragrunt-plan-base-s3-prefix + - name: Write plan metadata from workflow inputs shell: bash run: | @@ -65,9 +78,7 @@ jobs: - name: Upload plan metadata to S3 uses: ./.github/actions/just env: - BUCKET_NAME: ${{ inputs.code_bucket }} - ENVIRONMENT: ${{ inputs.environment }} - RUN_ID: ${{ github.run_id }} + PLAN_ARTIFACT_S3_PREFIX: ${{ steps.plan_artifact_s3_prefix.outputs.just_outputs }} with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} justfile_path: justfile.ci @@ -85,6 +96,7 @@ jobs: bootstrap_image_uri: ${{ inputs.bootstrap_image_uri }} service_matrix: ${{ inputs.service_matrix }} tg_action: plan + plan_artifact_s3_prefix: ${{ needs.metadata.outputs.plan_artifact_s3_prefix }} plan_context: name: Plan Context diff --git a/justfile.ci b/justfile.ci index 9766e491..3a0d5dcb 100644 --- a/justfile.ci +++ b/justfile.ci @@ -113,24 +113,14 @@ infra-plan-metadata-upload: #!/usr/bin/env bash set -euo pipefail - if [[ -z "${BUCKET_NAME:-}" ]]; then - echo "❌ BUCKET_NAME environment variable is not set." - exit 1 - fi - - if [[ -z "${ENVIRONMENT:-}" ]]; then - echo "❌ ENVIRONMENT environment variable is not set." - exit 1 - fi - - if [[ -z "${RUN_ID:-}" ]]; then - echo "❌ RUN_ID environment variable is not set." + if [[ -z "${PLAN_ARTIFACT_S3_PREFIX:-}" ]]; then + echo "❌ PLAN_ARTIFACT_S3_PREFIX environment variable is not set." exit 1 fi - artifact_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-base-prefix)" + artifact_s3_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-base-s3-prefix)" - aws s3 cp "plan-metadata.json" "s3://${BUCKET_NAME}/${artifact_prefix}/infra-plan-metadata/plan-metadata.json" + aws s3 cp "plan-metadata.json" "${artifact_s3_prefix}/infra-plan-metadata/plan-metadata.json" # Download shared infra plan metadata from the shared code bucket. @@ -138,24 +128,14 @@ infra-plan-metadata-download: #!/usr/bin/env bash set -euo pipefail - if [[ -z "${BUCKET_NAME:-}" ]]; then - echo "❌ BUCKET_NAME environment variable is not set." - exit 1 - fi - - if [[ -z "${ENVIRONMENT:-}" ]]; then - echo "❌ ENVIRONMENT environment variable is not set." - exit 1 - fi - - if [[ -z "${RUN_ID:-}" ]]; then - echo "❌ RUN_ID environment variable is not set." + if [[ -z "${PLAN_ARTIFACT_S3_PREFIX:-}" ]]; then + echo "❌ PLAN_ARTIFACT_S3_PREFIX environment variable is not set." exit 1 fi - artifact_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-base-prefix)" + artifact_s3_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-base-s3-prefix)" - aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/infra-plan-metadata/plan-metadata.json" "plan-metadata.json" + aws s3 cp "${artifact_s3_prefix}/infra-plan-metadata/plan-metadata.json" "plan-metadata.json" # Return the Lambda artifact directory name from the repo-root justfile. diff --git a/justfile.tg b/justfile.tg index 2eb2c27c..9491885b 100644 --- a/justfile.tg +++ b/justfile.tg @@ -61,6 +61,11 @@ terragrunt-plan-base-prefix: #!/usr/bin/env bash set -euo pipefail + if [[ -n "${PLAN_ARTIFACT_S3_PREFIX:-}" ]]; then + echo "${PLAN_ARTIFACT_S3_PREFIX#s3://*/}" + exit 0 + fi + if [[ -z "${ENVIRONMENT:-}" ]]; then echo "❌ ENVIRONMENT environment variable is not set." exit 1 @@ -76,6 +81,26 @@ terragrunt-plan-base-prefix: echo "${infra_plan_dir}/${ENVIRONMENT}/${RUN_ID}" +# Derive the shared S3 URI prefix for Terragrunt plan artifacts. +terragrunt-plan-base-s3-prefix: + #!/usr/bin/env bash + set -euo pipefail + + if [[ -n "${PLAN_ARTIFACT_S3_PREFIX:-}" ]]; then + echo "${PLAN_ARTIFACT_S3_PREFIX}" + exit 0 + fi + + if [[ -z "${BUCKET_NAME:-}" ]]; then + echo "❌ BUCKET_NAME environment variable is not set." + exit 1 + fi + + artifact_base_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-base-prefix)" + + echo "s3://${BUCKET_NAME}/${artifact_base_prefix}" + + # Derive the shared S3 prefix for Terragrunt plan artifacts. terragrunt-plan-prefix: #!/usr/bin/env bash @@ -99,18 +124,8 @@ terragrunt-plan-download: #!/usr/bin/env bash set -euo pipefail - if [[ -z "${BUCKET_NAME:-}" ]]; then - echo "❌ BUCKET_NAME environment variable is not set." - exit 1 - fi - - if [[ -z "${ENVIRONMENT:-}" ]]; then - echo "❌ ENVIRONMENT environment variable is not set." - exit 1 - fi - - if [[ -z "${RUN_ID:-}" ]]; then - echo "❌ RUN_ID environment variable is not set." + if [[ -z "${PLAN_ARTIFACT_S3_PREFIX:-}" && -z "${BUCKET_NAME:-}" ]]; then + echo "❌ PLAN_ARTIFACT_S3_PREFIX or BUCKET_NAME environment variable is required." exit 1 fi @@ -119,13 +134,14 @@ terragrunt-plan-download: exit 1 fi + artifact_s3_base_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-base-s3-prefix)" artifact_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-prefix)" mkdir -p "$TG_DIRECTORY" - aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/{{PLAN_FILE}}" "${TG_DIRECTORY}/{{PLAN_FILE}}" - aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/{{PLAN_TEXT_FILE}}" "${TG_DIRECTORY}/{{PLAN_TEXT_FILE}}" - aws s3 cp "s3://${BUCKET_NAME}/${artifact_prefix}/{{PLAN_META_FILE}}" "${TG_DIRECTORY}/{{PLAN_META_FILE}}" + aws s3 cp "${artifact_s3_base_prefix}/${artifact_prefix##*/}/{{PLAN_FILE}}" "${TG_DIRECTORY}/{{PLAN_FILE}}" + aws s3 cp "${artifact_s3_base_prefix}/${artifact_prefix##*/}/{{PLAN_TEXT_FILE}}" "${TG_DIRECTORY}/{{PLAN_TEXT_FILE}}" + aws s3 cp "${artifact_s3_base_prefix}/${artifact_prefix##*/}/{{PLAN_META_FILE}}" "${TG_DIRECTORY}/{{PLAN_META_FILE}}" # Upload saved Terragrunt plan files for a stack to the shared code bucket. @@ -133,18 +149,8 @@ terragrunt-plan-upload: #!/usr/bin/env bash set -euo pipefail - if [[ -z "${BUCKET_NAME:-}" ]]; then - echo "❌ BUCKET_NAME environment variable is not set." - exit 1 - fi - - if [[ -z "${ENVIRONMENT:-}" ]]; then - echo "❌ ENVIRONMENT environment variable is not set." - exit 1 - fi - - if [[ -z "${RUN_ID:-}" ]]; then - echo "❌ RUN_ID environment variable is not set." + if [[ -z "${PLAN_ARTIFACT_S3_PREFIX:-}" && -z "${BUCKET_NAME:-}" ]]; then + echo "❌ PLAN_ARTIFACT_S3_PREFIX or BUCKET_NAME environment variable is required." exit 1 fi @@ -153,8 +159,9 @@ terragrunt-plan-upload: exit 1 fi + artifact_s3_base_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-base-s3-prefix)" artifact_prefix="$(just --justfile "{{PROJECT_DIR}}/justfile.tg" terragrunt-plan-prefix)" - aws s3 cp "${TG_DIRECTORY}/{{PLAN_FILE}}" "s3://${BUCKET_NAME}/${artifact_prefix}/{{PLAN_FILE}}" - aws s3 cp "${TG_DIRECTORY}/{{PLAN_TEXT_FILE}}" "s3://${BUCKET_NAME}/${artifact_prefix}/{{PLAN_TEXT_FILE}}" - aws s3 cp "${TG_DIRECTORY}/{{PLAN_META_FILE}}" "s3://${BUCKET_NAME}/${artifact_prefix}/{{PLAN_META_FILE}}" + aws s3 cp "${TG_DIRECTORY}/{{PLAN_FILE}}" "${artifact_s3_base_prefix}/${artifact_prefix##*/}/{{PLAN_FILE}}" + aws s3 cp "${TG_DIRECTORY}/{{PLAN_TEXT_FILE}}" "${artifact_s3_base_prefix}/${artifact_prefix##*/}/{{PLAN_TEXT_FILE}}" + aws s3 cp "${TG_DIRECTORY}/{{PLAN_META_FILE}}" "${artifact_s3_base_prefix}/${artifact_prefix##*/}/{{PLAN_META_FILE}}" From 57a5dc326142f54c1d02c3ec43cb1a44fd22faed Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 13:06:17 +0100 Subject: [PATCH 13/23] chore: rm manage_plan_artifacts: "true" --- .github/actions/terragrunt/README.md | 6 +++--- .github/actions/terragrunt/action.yml | 21 +++++++-------------- .github/docs/README.md | 4 ++-- .github/workflows/shared_infra.yml | 11 ----------- 4 files changed, 12 insertions(+), 30 deletions(-) diff --git a/.github/actions/terragrunt/README.md b/.github/actions/terragrunt/README.md index e3c15b54..d243e5ba 100644 --- a/.github/actions/terragrunt/README.md +++ b/.github/actions/terragrunt/README.md @@ -9,6 +9,7 @@ This GitHub Action sets up **Terraform** and **Terragrunt**, authenticates to AW - 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 with OIDC for saved plan artifact upload and download - Exports Terragrunt outputs as compact JSON when state exists ## Inputs @@ -20,7 +21,6 @@ This GitHub Action sets up **Terraform** and **Terragrunt**, authenticates to AW | `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 | `{}` | | `aws_oidc_role_arn` | IAM role ARN to assume via OIDC | Yes | — | -| `manage_plan_artifacts` | When `true`, downloads saved plan artifacts for `apply_plan` and uploads plan artifacts for `plan` via `justfile.tg` | No | `false` | | `tg_directory` | Directory containing the Terragrunt config | Yes | — | | `tg_action` | Terragrunt action: `apply`, `plan`, `apply_plan`, `destroy`, or `init` | Yes | `apply` | @@ -36,9 +36,9 @@ This GitHub Action sets up **Terraform** and **Terragrunt**, authenticates to AW - `apply` Runs `terragrunt apply -auto-approve` - `plan` - Runs `terragrunt plan -detailed-exitcode -out=/terragrunt.tfplan`, then renders `terragrunt.plan.txt` and writes `terragrunt.plan.meta.json` via the repo `justfile.tg` recipe `terragrunt-plan-render`. If `manage_plan_artifacts: true`, it also uploads those files to S3 via `justfile.tg`. + Runs `terragrunt plan -detailed-exitcode -out=/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` - If `manage_plan_artifacts: true`, downloads the saved plan files into `tg_directory` via `justfile.tg` using the caller-provided `PLAN_ARTIFACT_S3_PREFIX` environment variable plus the stack-derived suffix from `tg_directory`. Otherwise it expects the saved plan files to already exist in `tg_directory`. In both cases it 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. + 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` diff --git a/.github/actions/terragrunt/action.yml b/.github/actions/terragrunt/action.yml index 2979a617..f87e42bb 100644 --- a/.github/actions/terragrunt/action.yml +++ b/.github/actions/terragrunt/action.yml @@ -20,10 +20,6 @@ inputs: aws_oidc_role_arn: description: "AWS iam role arn" required: true - manage_plan_artifacts: - description: "When true, downloads saved plan artifacts for apply_plan and uploads plan artifacts for plan" - required: false - default: "false" tg_directory: description: "Module directory to perform action upon" required: true @@ -59,19 +55,14 @@ runs: run: | echo "$OVERRIDE_TG_VARS" | jq -c . > ${{ inputs.tg_directory }}/override_tg_vars.tfvars.json - - name: Configure AWS OIDC Authentication (plan artifacts) - if: inputs.tg_action == 'apply_plan' && inputs.manage_plan_artifacts == 'true' - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ inputs.aws_oidc_role_arn }} - aws-region: ${{ inputs.aws_region }} - - name: Download saved plan artifacts - if: inputs.tg_action == 'apply_plan' && inputs.manage_plan_artifacts == 'true' + if: inputs.tg_action == 'apply_plan' uses: ./.github/actions/just env: TG_DIRECTORY: ${{ inputs.tg_directory }} with: + aws_oidc_role_arn: ${{ inputs.aws_oidc_role_arn }} + aws_region: ${{ inputs.aws_region }} justfile_path: justfile.tg just_action: terragrunt-plan-download @@ -105,7 +96,7 @@ runs: fi - name: Configure AWS OIDC Authentication - if: inputs.tg_action != 'apply_plan' || (inputs.manage_plan_artifacts != 'true' && steps.apply_plan_guard.outputs.should_apply == 'true') + if: inputs.tg_action != 'apply_plan' || steps.apply_plan_guard.outputs.should_apply == 'true' uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ inputs.aws_oidc_role_arn }} @@ -167,11 +158,13 @@ runs: just_action: terragrunt-plan-render - name: Upload saved plan artifacts - if: inputs.tg_action == 'plan' && inputs.manage_plan_artifacts == 'true' + if: inputs.tg_action == 'plan' uses: ./.github/actions/just env: TG_DIRECTORY: ${{ inputs.tg_directory }} with: + aws_oidc_role_arn: ${{ inputs.aws_oidc_role_arn }} + aws_region: ${{ inputs.aws_region }} justfile_path: justfile.tg just_action: terragrunt-plan-upload diff --git a/.github/docs/README.md b/.github/docs/README.md index 607ec396..75ae657e 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -142,9 +142,9 @@ Run these checks on every CI, workflow, or deploy-contract change. - compare expected outputs against actual `jobs..outputs.*` - verify optional inputs are intentionally omitted, not accidentally missing - the repo-local `./.github/actions/terragrunt` action supports `tg_action: plan` for producing the binary plan locally; it renders `terragrunt.plan.txt` and writes `terragrunt.plan.meta.json` via `justfile.tg` (`terragrunt-plan-render`) -- when `manage_plan_artifacts: true`, `./.github/actions/terragrunt` also uploads (on `plan`) and downloads (on `apply_plan`) the per-stack plan artifacts using the caller-provided `PLAN_ARTIFACT_S3_PREFIX` environment variable, so graph executors like `shared_infra.yml` do not need separate `./.github/actions/just` steps for those transfers +- `./.github/actions/terragrunt` always uploads per-stack plan artifacts on `plan` and always downloads them on `apply_plan`, using the caller-provided `PLAN_ARTIFACT_S3_PREFIX` environment variable, so graph executors like `shared_infra.yml` do not need separate `./.github/actions/just` steps for those transfers - plan artifact storage follows the same artifact environment split as ECR and build outputs: `dev` uses the `dev` code bucket, while non-`dev` environments read and write `terragrunt_plan/` in the shared `ci` code bucket -- `./.github/actions/terragrunt` skips `apply_plan` with a warning when the saved `terragrunt.plan.meta.json` reports `has_changes: false`; when it does not need to download plan artifacts (`manage_plan_artifacts: false`), it also skips `aws-actions/configure-aws-credentials` in that no-op case +- `./.github/actions/terragrunt` skips `apply_plan` with a warning when the saved `terragrunt.plan.meta.json` reports `has_changes: false` - `./.github/actions/terragrunt` derives its plan artifact name from `tg_directory`, so callers do not need to pass artifact naming inputs - if `apply_plan` is used across separate workflow runs, pass the earlier workflow `run_id` through `plan_artifact_run_id`; the shared wrappers recover both metadata and per-stack plan files by deriving the matching `plan_artifact_s3_prefix` and reading from the shared code bucket under `terragrunt_plan///...` - if a cross-run apply should not ask the operator to re-enter versions or recompute artifact resolution, store both the input versions and the resolved reusable-workflow outputs in a metadata artifact during plan and recover them in the apply wrapper from the earlier `run_id` diff --git a/.github/workflows/shared_infra.yml b/.github/workflows/shared_infra.yml index 96f882f2..e9e57b73 100644 --- a/.github/workflows/shared_infra.yml +++ b/.github/workflows/shared_infra.yml @@ -67,7 +67,6 @@ jobs: uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - manage_plan_artifacts: "true" tg_directory: infra/live/${{ inputs.environment }}/aws/oidc tg_action: ${{ inputs.tg_action }} @@ -83,7 +82,6 @@ jobs: uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - manage_plan_artifacts: "true" tg_directory: infra/live/${{ inputs.environment }}/aws/worker_messaging tg_action: ${{ inputs.tg_action }} @@ -99,7 +97,6 @@ jobs: uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - manage_plan_artifacts: "true" tg_directory: infra/live/${{ inputs.environment }}/aws/observability tg_action: ${{ inputs.tg_action }} @@ -117,7 +114,6 @@ jobs: TF_VAR_domain_name: ${{ env.DOMAIN_NAME }} with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - manage_plan_artifacts: "true" tg_directory: infra/live/${{ inputs.environment }}/aws/cognito tg_action: ${{ inputs.tg_action }} @@ -181,7 +177,6 @@ jobs: TF_VAR_auth_readonly_group_name: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.readonly_group_name }} with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - manage_plan_artifacts: "true" tg_directory: infra/live/${{ inputs.environment }}/aws/frontend tg_action: ${{ inputs.tg_action }} @@ -197,7 +192,6 @@ jobs: uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - manage_plan_artifacts: "true" tg_directory: infra/live/${{ inputs.environment }}/aws/cluster tg_action: ${{ inputs.tg_action }} @@ -216,7 +210,6 @@ jobs: uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - manage_plan_artifacts: "true" tg_directory: infra/live/${{ inputs.environment }}/aws/security tg_action: ${{ inputs.tg_action }} @@ -244,7 +237,6 @@ jobs: TF_VAR_database_security_group_id: ${{ needs.security.outputs.postgres_sg }} with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - manage_plan_artifacts: "true" tg_directory: infra/live/${{ inputs.environment }}/aws/database tg_action: ${{ inputs.tg_action }} @@ -262,7 +254,6 @@ jobs: uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - manage_plan_artifacts: "true" tg_directory: infra/live/${{ inputs.environment }}/aws/network tg_action: ${{ inputs.tg_action }} @@ -287,7 +278,6 @@ jobs: uses: ./.github/actions/terragrunt with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - manage_plan_artifacts: "true" tg_directory: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} tg_action: ${{ inputs.tg_action }} @@ -316,6 +306,5 @@ jobs: TF_VAR_bootstrap_image_uri: ${{ inputs.bootstrap_image_uri }} with: aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - manage_plan_artifacts: "true" tg_directory: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} tg_action: ${{ inputs.tg_action }} From ba729cd17afb0c96f75773b8d045791b7dd41acd Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 13:10:35 +0100 Subject: [PATCH 14/23] chore: just tidy up --- justfile | 8 -------- 1 file changed, 8 deletions(-) diff --git a/justfile b/justfile index 17c11ac3..9763c9ee 100644 --- a/justfile +++ b/justfile @@ -84,20 +84,12 @@ format: # Run a Terragrunt operation for one environment/module pair. tg env module op: #!/usr/bin/env bash - export TF_VAR_lambda_artifact_dir="{{LAMBDA_DIR}}" - export TF_VAR_frontend_artifact_dir="{{FRONTEND_DIR}}" - export TF_VAR_appspec_artifact_dir="{{APPSPEC_DIR}}" - export TF_VAR_infra_plan_dir="{{INFRA_PLAN_DIR}}" cd {{justfile_directory()}}/infra/live/{{env}}/{{module}} ; terragrunt {{op}} # Run a Terragrunt operation across all live stacks. tg-all op: #!/usr/bin/env bash - export TF_VAR_lambda_artifact_dir="{{LAMBDA_DIR}}" - export TF_VAR_frontend_artifact_dir="{{FRONTEND_DIR}}" - export TF_VAR_appspec_artifact_dir="{{APPSPEC_DIR}}" - export TF_VAR_infra_plan_dir="{{INFRA_PLAN_DIR}}" cd {{justfile_directory()}}/infra/live terragrunt run-all {{op}} From 153a2d926e241fe7064738dfc80640428efeedd3 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 13:12:50 +0100 Subject: [PATCH 15/23] chore: single metadata job --- .github/docs/README.md | 2 +- .../shared_infra_apply_from_plan.yml | 24 +++++++------------ 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/.github/docs/README.md b/.github/docs/README.md index 75ae657e..a5115d7a 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -86,7 +86,7 @@ flowchart LR - `shared_infra_apply.yml` Direct-input apply wrapper around `shared_infra.yml`. It takes resolved workflow inputs directly and calls `shared_infra.yml` with `tg_action: apply`. - `shared_infra_apply_from_plan.yml` - Apply-from-plan wrapper around `shared_infra.yml`. It takes `plan_artifact_run_id`, resolves the same artifact bucket split used by release artifacts (`dev` stays on `dev`, non-`dev` uses `ci`), derives the matching plan-artifact S3 prefix via `justfile.tg`, downloads `infra-plan-metadata` from that location via `justfile.ci`, reads the frozen graph inputs back out, and then calls `shared_infra.yml` with `tg_action: apply_plan` plus that same resolved prefix. + Apply-from-plan wrapper around `shared_infra.yml`. It takes `plan_artifact_run_id`, resolves the same artifact bucket split used by release artifacts (`dev` stays on `dev`, non-`dev` uses `ci`) inside its `metadata` job, derives the matching plan-artifact S3 prefix via `justfile.tg`, downloads `infra-plan-metadata` from that location via `justfile.ci`, reads the frozen graph inputs back out, and then calls `shared_infra.yml` with `tg_action: apply_plan` plus that same resolved prefix. - `shared_infra.yml` Pure ordered infra graph executor. It applies shared stacks first, then runtime stacks, then frontend infrastructure. Shared stacks now include the CloudWatch observability dashboard. It accepts `tg_action` so the same graph can run a normal apply, upload derived per-stack plan artifacts to the resolved code bucket under `terragrunt_plan/`, or apply from previously uploaded plan artifacts. The wrapper workflows resolve one `plan_artifact_s3_prefix` and set it in the workflow env once; the Terragrunt action reads that env directly and appends the stack-specific suffix from `tg_directory`, so `shared_infra.yml` does not need to rebuild the bucket or env/run-id tuple in every job. In `apply_plan` mode, each stack job first downloads its own saved plan files via `justfile.tg` through the Terragrunt action, then the Terragrunt action validates and applies those local files. Its visible step labels now follow `tg_action` too, so plan and apply-from-plan runs no longer show everything as `Deploy ...`. The `security -> network` edge is a real bootstrap dependency because `network` reads security outputs like `vpc_endpoint_sg` from remote state; if those outputs do not exist yet, `network` fails with an upstream `Unsupported attribute` error rather than a networking-specific error. - The shared infra wrappers must forward the permissions required by the nested reusable call chain. In practice that means `id-token: write` everywhere the Terragrunt action may assume AWS OIDC and `contents: read` for checkout. The shared plan/apply wrappers now rely on AWS access to the shared code bucket rather than GitHub artifact permissions for cross-run recovery. diff --git a/.github/workflows/shared_infra_apply_from_plan.yml b/.github/workflows/shared_infra_apply_from_plan.yml index e3d0dcf5..6f27d600 100644 --- a/.github/workflows/shared_infra_apply_from_plan.yml +++ b/.github/workflows/shared_infra_apply_from_plan.yml @@ -23,10 +23,15 @@ env: ARTIFACT_AWS_OIDC_ROLE_ARN: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/${{ vars.PROJECT_NAME }}-${{ inputs.environment == 'dev' && 'dev' || 'ci' }}-github-oidc-role jobs: - artifact_bucket: + metadata: runs-on: ubuntu-latest outputs: - code_bucket_name: ${{ steps.get_bucket_name.outputs.bucket }} + infra_version: ${{ steps.read_metadata.outputs.infra_version }} + code_bucket: ${{ steps.read_metadata.outputs.code_bucket }} + lambda_matrix: ${{ steps.read_metadata.outputs.lambda_matrix }} + bootstrap_image_uri: ${{ steps.read_metadata.outputs.bootstrap_image_uri }} + plan_artifact_s3_prefix: ${{ steps.plan_artifact_s3_prefix.outputs.just_outputs }} + service_matrix: ${{ steps.read_metadata.outputs.service_matrix }} steps: - uses: actions/checkout@v6 @@ -45,24 +50,11 @@ jobs: run: | echo "bucket=$(echo "$TG_OUTPUTS" | jq -r '.bucket.value // empty')" >> "$GITHUB_OUTPUT" - metadata: - needs: artifact_bucket - runs-on: ubuntu-latest - outputs: - infra_version: ${{ steps.read_metadata.outputs.infra_version }} - code_bucket: ${{ steps.read_metadata.outputs.code_bucket }} - lambda_matrix: ${{ steps.read_metadata.outputs.lambda_matrix }} - bootstrap_image_uri: ${{ steps.read_metadata.outputs.bootstrap_image_uri }} - plan_artifact_s3_prefix: ${{ steps.plan_artifact_s3_prefix.outputs.just_outputs }} - service_matrix: ${{ steps.read_metadata.outputs.service_matrix }} - steps: - - uses: actions/checkout@v6 - - name: Get plan artifact S3 prefix id: plan_artifact_s3_prefix uses: ./.github/actions/just env: - BUCKET_NAME: ${{ needs.artifact_bucket.outputs.code_bucket_name }} + BUCKET_NAME: ${{ steps.get_bucket_name.outputs.bucket }} ENVIRONMENT: ${{ inputs.environment }} RUN_ID: ${{ inputs.plan_artifact_run_id }} with: From 0963896441daa2f0820403c1f805b482bd86639c Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 13:15:44 +0100 Subject: [PATCH 16/23] docs: readme updates for plan process --- .github/actions/terragrunt/README.md | 9 +++++++++ .github/docs/README.md | 7 +++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/actions/terragrunt/README.md b/.github/actions/terragrunt/README.md index d243e5ba..18a30da5 100644 --- a/.github/actions/terragrunt/README.md +++ b/.github/actions/terragrunt/README.md @@ -44,6 +44,15 @@ This GitHub Action sets up **Terraform** and **Terragrunt**, authenticates to AW - `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: + - `/infra-plan-metadata/plan-metadata.json` +- Each Terragrunt stack or module stores its own plan bundle at: + - `/terragrunt-plan-/terragrunt.tfplan` + - `/terragrunt-plan-/terragrunt.plan.txt` + - `/terragrunt-plan-/terragrunt.plan.meta.json` + ## Usage ### Minimal Apply diff --git a/.github/docs/README.md b/.github/docs/README.md index a5115d7a..cfdc3a88 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -82,13 +82,13 @@ flowchart LR ### Infra And Code Rollout - `shared_infra_plan.yml` - Plan wrapper around `shared_infra.yml`. It takes resolved workflow inputs directly, derives a single plan-artifact S3 prefix via `justfile.tg`, uploads `infra-plan-metadata` under `/infra-plan-metadata/` via `justfile.ci`, and then calls `shared_infra.yml` with `tg_action: plan` plus that same resolved prefix. The bucket resolution follows the same artifact split as ECR and build outputs: `dev` uses the `dev` code bucket, while non-`dev` environments reuse the `ci` code bucket. After the plan completes, it prints the current workflow `github.run_id` into both the logs and the GitHub Actions step summary as `plan_artifact_run_id`, and exposes that value as a reusable-workflow output. + Plan wrapper around `shared_infra.yml`. It takes resolved workflow inputs directly, derives a single plan-artifact S3 prefix via `justfile.tg`, uploads one run-level metadata file under `/infra-plan-metadata/plan-metadata.json` via `justfile.ci`, and then calls `shared_infra.yml` with `tg_action: plan` plus that same resolved prefix. The bucket resolution follows the same artifact split as ECR and build outputs: `dev` uses the `dev` code bucket, while non-`dev` environments reuse the `ci` code bucket. After the plan completes, it prints the current workflow `github.run_id` into both the logs and the GitHub Actions step summary as `plan_artifact_run_id`, and exposes that value as a reusable-workflow output. - `shared_infra_apply.yml` Direct-input apply wrapper around `shared_infra.yml`. It takes resolved workflow inputs directly and calls `shared_infra.yml` with `tg_action: apply`. - `shared_infra_apply_from_plan.yml` Apply-from-plan wrapper around `shared_infra.yml`. It takes `plan_artifact_run_id`, resolves the same artifact bucket split used by release artifacts (`dev` stays on `dev`, non-`dev` uses `ci`) inside its `metadata` job, derives the matching plan-artifact S3 prefix via `justfile.tg`, downloads `infra-plan-metadata` from that location via `justfile.ci`, reads the frozen graph inputs back out, and then calls `shared_infra.yml` with `tg_action: apply_plan` plus that same resolved prefix. - `shared_infra.yml` - Pure ordered infra graph executor. It applies shared stacks first, then runtime stacks, then frontend infrastructure. Shared stacks now include the CloudWatch observability dashboard. It accepts `tg_action` so the same graph can run a normal apply, upload derived per-stack plan artifacts to the resolved code bucket under `terragrunt_plan/`, or apply from previously uploaded plan artifacts. The wrapper workflows resolve one `plan_artifact_s3_prefix` and set it in the workflow env once; the Terragrunt action reads that env directly and appends the stack-specific suffix from `tg_directory`, so `shared_infra.yml` does not need to rebuild the bucket or env/run-id tuple in every job. In `apply_plan` mode, each stack job first downloads its own saved plan files via `justfile.tg` through the Terragrunt action, then the Terragrunt action validates and applies those local files. Its visible step labels now follow `tg_action` too, so plan and apply-from-plan runs no longer show everything as `Deploy ...`. The `security -> network` edge is a real bootstrap dependency because `network` reads security outputs like `vpc_endpoint_sg` from remote state; if those outputs do not exist yet, `network` fails with an upstream `Unsupported attribute` error rather than a networking-specific error. + Pure ordered infra graph executor. It applies shared stacks first, then runtime stacks, then frontend infrastructure. Shared stacks now include the CloudWatch observability dashboard. It accepts `tg_action` so the same graph can run a normal apply, upload derived per-stack plan artifacts to the resolved code bucket under `terragrunt_plan/`, or apply from previously uploaded plan artifacts. The wrapper workflows resolve one `plan_artifact_s3_prefix` and set it in the workflow env once; the Terragrunt action reads that env directly and appends the stack-specific suffix from `tg_directory`, so `shared_infra.yml` does not need to rebuild the bucket or env/run-id tuple in every job. That means each infra run has one shared `plan-metadata.json` file for the whole graph and one separate saved plan bundle per Terragrunt stack or module. In `apply_plan` mode, each stack job first downloads its own saved plan files via `justfile.tg` through the Terragrunt action, then the Terragrunt action validates and applies those local files. Its visible step labels now follow `tg_action` too, so plan and apply-from-plan runs no longer show everything as `Deploy ...`. The `security -> network` edge is a real bootstrap dependency because `network` reads security outputs like `vpc_endpoint_sg` from remote state; if those outputs do not exist yet, `network` fails with an upstream `Unsupported attribute` error rather than a networking-specific error. - The shared infra wrappers must forward the permissions required by the nested reusable call chain. In practice that means `id-token: write` everywhere the Terragrunt action may assume AWS OIDC and `contents: read` for checkout. The shared plan/apply wrappers now rely on AWS access to the shared code bucket rather than GitHub artifact permissions for cross-run recovery. - `shared_deploy.yml` Rolls out Lambda code, optional migrations, optional reconciliation Lambdas, ECS task and service updates, and optional frontend deploys. The reusable workflow renders its Lambda and ECS CodeDeploy AppSpec files from the shared templates under `config/deploy/`, and its mutating `just` steps should target `justfile.deploy` rather than the repo-root `justfile`. @@ -143,6 +143,9 @@ Run these checks on every CI, workflow, or deploy-contract change. - verify optional inputs are intentionally omitted, not accidentally missing - the repo-local `./.github/actions/terragrunt` action supports `tg_action: plan` for producing the binary plan locally; it renders `terragrunt.plan.txt` and writes `terragrunt.plan.meta.json` via `justfile.tg` (`terragrunt-plan-render`) - `./.github/actions/terragrunt` always uploads per-stack plan artifacts on `plan` and always downloads them on `apply_plan`, using the caller-provided `PLAN_ARTIFACT_S3_PREFIX` environment variable, so graph executors like `shared_infra.yml` do not need separate `./.github/actions/just` steps for those transfers +- saved infra-plan storage is intentionally split into two levels: + - one run-level metadata file at `/infra-plan-metadata/plan-metadata.json` + - one per-stack plan bundle under `/terragrunt-plan-/` - plan artifact storage follows the same artifact environment split as ECR and build outputs: `dev` uses the `dev` code bucket, while non-`dev` environments read and write `terragrunt_plan/` in the shared `ci` code bucket - `./.github/actions/terragrunt` skips `apply_plan` with a warning when the saved `terragrunt.plan.meta.json` reports `has_changes: false` - `./.github/actions/terragrunt` derives its plan artifact name from `tg_directory`, so callers do not need to pass artifact naming inputs From 138d2b161d4f5b23d3c88da45dcfe04acfea083e Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 13:20:06 +0100 Subject: [PATCH 17/23] debug: only deploy messaging --- .github/workflows/shared_infra.yml | 446 ++++++++++++++--------------- 1 file changed, 223 insertions(+), 223 deletions(-) diff --git a/.github/workflows/shared_infra.yml b/.github/workflows/shared_infra.yml index e9e57b73..69a03d1d 100644 --- a/.github/workflows/shared_infra.yml +++ b/.github/workflows/shared_infra.yml @@ -85,226 +85,226 @@ jobs: tg_directory: infra/live/${{ inputs.environment }}/aws/worker_messaging tg_action: ${{ inputs.tg_action }} - observability: - needs: oidc - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.infra_version }} - - - name: ${{ env.TG_ACTION_LABEL }} observability infra - uses: ./.github/actions/terragrunt - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - tg_directory: infra/live/${{ inputs.environment }}/aws/observability - tg_action: ${{ inputs.tg_action }} - - cognito: - needs: oidc - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.infra_version }} - - - name: ${{ env.TG_ACTION_LABEL }} cognito infra - uses: ./.github/actions/terragrunt - env: - TF_VAR_domain_name: ${{ env.DOMAIN_NAME }} - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - tg_directory: infra/live/${{ inputs.environment }}/aws/cognito - tg_action: ${{ inputs.tg_action }} - - frontend: - needs: - - network - - cognito - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.infra_version }} - - - name: Get network infra - if: inputs.tg_action != 'apply_plan' - uses: ./.github/actions/terragrunt - id: get-network - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - tg_directory: infra/live/${{ inputs.environment }}/aws/network - tg_action: init - - - name: Get cognito infra - if: inputs.tg_action != 'apply_plan' - uses: ./.github/actions/terragrunt - id: get-cognito - env: - TF_VAR_domain_name: ${{ env.DOMAIN_NAME }} - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - tg_directory: infra/live/${{ inputs.environment }}/aws/cognito - tg_action: init - - - name: Get api invoke url - if: inputs.tg_action != 'apply_plan' - id: get_api_vars - env: - TG_OUTPUTS: ${{ steps.get-network.outputs.tg_outputs }} - run: | - echo "invoke_url=$(echo $TG_OUTPUTS | jq -r '.api_invoke_url.value')" >> $GITHUB_OUTPUT - - - name: Get cognito values - if: inputs.tg_action != 'apply_plan' - id: get_cognito_vars - env: - TG_OUTPUTS: ${{ steps.get-cognito.outputs.tg_outputs }} - run: | - echo "user_pool_id=$(echo "$TG_OUTPUTS" | jq -r '.user_pool_id.value')" >> "$GITHUB_OUTPUT" - echo "user_pool_client_id=$(echo "$TG_OUTPUTS" | jq -r '.user_pool_client_id.value')" >> "$GITHUB_OUTPUT" - echo "hosted_ui_url=$(echo "$TG_OUTPUTS" | jq -r '.hosted_ui_url.value')" >> "$GITHUB_OUTPUT" - echo "readonly_group_name=$(echo "$TG_OUTPUTS" | jq -r '.readonly_group_name.value')" >> "$GITHUB_OUTPUT" - - - name: ${{ env.TG_ACTION_LABEL }} frontend infra - uses: ./.github/actions/terragrunt - env: - TF_VAR_api_invoke_url: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_api_vars.outputs.invoke_url }} - TF_VAR_domain_name: ${{ env.DOMAIN_NAME }} - TF_VAR_auth_user_pool_id: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.user_pool_id }} - TF_VAR_auth_user_pool_client_id: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.user_pool_client_id }} - TF_VAR_auth_hosted_ui_url: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.hosted_ui_url }} - TF_VAR_auth_readonly_group_name: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.readonly_group_name }} - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - tg_directory: infra/live/${{ inputs.environment }}/aws/frontend - tg_action: ${{ inputs.tg_action }} - - cluster: - needs: oidc - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.infra_version }} - - - name: ${{ env.TG_ACTION_LABEL }} cluster infra - uses: ./.github/actions/terragrunt - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - tg_directory: infra/live/${{ inputs.environment }}/aws/cluster - tg_action: ${{ inputs.tg_action }} - - security: - needs: oidc - runs-on: ubuntu-latest - outputs: - postgres_sg: ${{ steps.get_security_outputs.outputs.postgres_sg }} - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.infra_version }} - - - name: ${{ env.TG_ACTION_LABEL }} security infra - id: deploy-security - uses: ./.github/actions/terragrunt - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - tg_directory: infra/live/${{ inputs.environment }}/aws/security - tg_action: ${{ inputs.tg_action }} - - - name: Get security outputs - id: get_security_outputs - if: inputs.tg_action != 'destroy' - env: - TG_OUTPUTS: ${{ steps.deploy-security.outputs.tg_outputs }} - run: | - echo "postgres_sg=$(echo "$TG_OUTPUTS" | jq -r '.postgres_sg.value')" >> "$GITHUB_OUTPUT" - - database: - needs: - - oidc - - security - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.infra_version }} - - - name: ${{ env.TG_ACTION_LABEL }} database infra - uses: ./.github/actions/terragrunt - env: - TF_VAR_database_security_group_id: ${{ needs.security.outputs.postgres_sg }} - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - tg_directory: infra/live/${{ inputs.environment }}/aws/database - tg_action: ${{ inputs.tg_action }} - - network: - needs: - - security - - cognito - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.infra_version }} - - - name: ${{ env.TG_ACTION_LABEL }} network infra - uses: ./.github/actions/terragrunt - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - tg_directory: infra/live/${{ inputs.environment }}/aws/network - tg_action: ${{ inputs.tg_action }} - - lambdas: - needs: - - oidc - - security - - network - - database - - worker_messaging - runs-on: ubuntu-latest - strategy: - fail-fast: false # this is to prevent terraform lock issues - matrix: - value: ${{ fromJson(inputs.lambda_matrix) }} - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.infra_version }} - - - name: ${{ env.TG_ACTION_LABEL }} ${{ matrix.value }} infra - uses: ./.github/actions/terragrunt - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - tg_directory: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} - tg_action: ${{ inputs.tg_action }} - - services: - needs: - - oidc - - security - - cluster - - network - - database - - worker_messaging - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - value: ${{ fromJson(inputs.service_matrix) }} - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.infra_version }} - - - name: ${{ env.TG_ACTION_LABEL }} ${{ matrix.value }} bootstrap service infra - uses: ./.github/actions/terragrunt - env: - TF_VAR_bootstrap: "true" - TF_VAR_bootstrap_image_uri: ${{ inputs.bootstrap_image_uri }} - with: - aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} - tg_directory: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} - tg_action: ${{ inputs.tg_action }} + # observability: + # needs: oidc + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v6 + # with: + # ref: ${{ inputs.infra_version }} + + # - name: ${{ env.TG_ACTION_LABEL }} observability infra + # uses: ./.github/actions/terragrunt + # with: + # aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + # tg_directory: infra/live/${{ inputs.environment }}/aws/observability + # tg_action: ${{ inputs.tg_action }} + + # cognito: + # needs: oidc + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v6 + # with: + # ref: ${{ inputs.infra_version }} + + # - name: ${{ env.TG_ACTION_LABEL }} cognito infra + # uses: ./.github/actions/terragrunt + # env: + # TF_VAR_domain_name: ${{ env.DOMAIN_NAME }} + # with: + # aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + # tg_directory: infra/live/${{ inputs.environment }}/aws/cognito + # tg_action: ${{ inputs.tg_action }} + + # frontend: + # needs: + # - network + # - cognito + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v6 + # with: + # ref: ${{ inputs.infra_version }} + + # - name: Get network infra + # if: inputs.tg_action != 'apply_plan' + # uses: ./.github/actions/terragrunt + # id: get-network + # with: + # aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + # tg_directory: infra/live/${{ inputs.environment }}/aws/network + # tg_action: init + + # - name: Get cognito infra + # if: inputs.tg_action != 'apply_plan' + # uses: ./.github/actions/terragrunt + # id: get-cognito + # env: + # TF_VAR_domain_name: ${{ env.DOMAIN_NAME }} + # with: + # aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + # tg_directory: infra/live/${{ inputs.environment }}/aws/cognito + # tg_action: init + + # - name: Get api invoke url + # if: inputs.tg_action != 'apply_plan' + # id: get_api_vars + # env: + # TG_OUTPUTS: ${{ steps.get-network.outputs.tg_outputs }} + # run: | + # echo "invoke_url=$(echo $TG_OUTPUTS | jq -r '.api_invoke_url.value')" >> $GITHUB_OUTPUT + + # - name: Get cognito values + # if: inputs.tg_action != 'apply_plan' + # id: get_cognito_vars + # env: + # TG_OUTPUTS: ${{ steps.get-cognito.outputs.tg_outputs }} + # run: | + # echo "user_pool_id=$(echo "$TG_OUTPUTS" | jq -r '.user_pool_id.value')" >> "$GITHUB_OUTPUT" + # echo "user_pool_client_id=$(echo "$TG_OUTPUTS" | jq -r '.user_pool_client_id.value')" >> "$GITHUB_OUTPUT" + # echo "hosted_ui_url=$(echo "$TG_OUTPUTS" | jq -r '.hosted_ui_url.value')" >> "$GITHUB_OUTPUT" + # echo "readonly_group_name=$(echo "$TG_OUTPUTS" | jq -r '.readonly_group_name.value')" >> "$GITHUB_OUTPUT" + + # - name: ${{ env.TG_ACTION_LABEL }} frontend infra + # uses: ./.github/actions/terragrunt + # env: + # TF_VAR_api_invoke_url: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_api_vars.outputs.invoke_url }} + # TF_VAR_domain_name: ${{ env.DOMAIN_NAME }} + # TF_VAR_auth_user_pool_id: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.user_pool_id }} + # TF_VAR_auth_user_pool_client_id: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.user_pool_client_id }} + # TF_VAR_auth_hosted_ui_url: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.hosted_ui_url }} + # TF_VAR_auth_readonly_group_name: ${{ inputs.tg_action == 'apply_plan' && '' || steps.get_cognito_vars.outputs.readonly_group_name }} + # with: + # aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + # tg_directory: infra/live/${{ inputs.environment }}/aws/frontend + # tg_action: ${{ inputs.tg_action }} + + # cluster: + # needs: oidc + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v6 + # with: + # ref: ${{ inputs.infra_version }} + + # - name: ${{ env.TG_ACTION_LABEL }} cluster infra + # uses: ./.github/actions/terragrunt + # with: + # aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + # tg_directory: infra/live/${{ inputs.environment }}/aws/cluster + # tg_action: ${{ inputs.tg_action }} + + # security: + # needs: oidc + # runs-on: ubuntu-latest + # outputs: + # postgres_sg: ${{ steps.get_security_outputs.outputs.postgres_sg }} + # steps: + # - uses: actions/checkout@v6 + # with: + # ref: ${{ inputs.infra_version }} + + # - name: ${{ env.TG_ACTION_LABEL }} security infra + # id: deploy-security + # uses: ./.github/actions/terragrunt + # with: + # aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + # tg_directory: infra/live/${{ inputs.environment }}/aws/security + # tg_action: ${{ inputs.tg_action }} + + # - name: Get security outputs + # id: get_security_outputs + # if: inputs.tg_action != 'destroy' + # env: + # TG_OUTPUTS: ${{ steps.deploy-security.outputs.tg_outputs }} + # run: | + # echo "postgres_sg=$(echo "$TG_OUTPUTS" | jq -r '.postgres_sg.value')" >> "$GITHUB_OUTPUT" + + # database: + # needs: + # - oidc + # - security + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v6 + # with: + # ref: ${{ inputs.infra_version }} + + # - name: ${{ env.TG_ACTION_LABEL }} database infra + # uses: ./.github/actions/terragrunt + # env: + # TF_VAR_database_security_group_id: ${{ needs.security.outputs.postgres_sg }} + # with: + # aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + # tg_directory: infra/live/${{ inputs.environment }}/aws/database + # tg_action: ${{ inputs.tg_action }} + + # network: + # needs: + # - security + # - cognito + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v6 + # with: + # ref: ${{ inputs.infra_version }} + + # - name: ${{ env.TG_ACTION_LABEL }} network infra + # uses: ./.github/actions/terragrunt + # with: + # aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + # tg_directory: infra/live/${{ inputs.environment }}/aws/network + # tg_action: ${{ inputs.tg_action }} + + # lambdas: + # needs: + # - oidc + # - security + # - network + # - database + # - worker_messaging + # runs-on: ubuntu-latest + # strategy: + # fail-fast: false # this is to prevent terraform lock issues + # matrix: + # value: ${{ fromJson(inputs.lambda_matrix) }} + # steps: + # - uses: actions/checkout@v6 + # with: + # ref: ${{ inputs.infra_version }} + + # - name: ${{ env.TG_ACTION_LABEL }} ${{ matrix.value }} infra + # uses: ./.github/actions/terragrunt + # with: + # aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + # tg_directory: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} + # tg_action: ${{ inputs.tg_action }} + + # services: + # needs: + # - oidc + # - security + # - cluster + # - network + # - database + # - worker_messaging + # runs-on: ubuntu-latest + # strategy: + # fail-fast: false + # matrix: + # value: ${{ fromJson(inputs.service_matrix) }} + # steps: + # - uses: actions/checkout@v6 + # with: + # ref: ${{ inputs.infra_version }} + + # - name: ${{ env.TG_ACTION_LABEL }} ${{ matrix.value }} bootstrap service infra + # uses: ./.github/actions/terragrunt + # env: + # TF_VAR_bootstrap: "true" + # TF_VAR_bootstrap_image_uri: ${{ inputs.bootstrap_image_uri }} + # with: + # aws_oidc_role_arn: ${{ env.AWS_OIDC_ROLE_ARN }} + # tg_directory: infra/live/${{ inputs.environment }}/aws/${{ matrix.value }} + # tg_action: ${{ inputs.tg_action }} From 15049ff668271c9c036a47199642adbea346d2b2 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 13:22:42 +0100 Subject: [PATCH 18/23] debug: deploy this branch --- .github/workflows/dev_code_deploy.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/dev_code_deploy.yml b/.github/workflows/dev_code_deploy.yml index 63405573..f7a88332 100644 --- a/.github/workflows/dev_code_deploy.yml +++ b/.github/workflows/dev_code_deploy.yml @@ -3,6 +3,9 @@ name: Dev Code Deploy on: workflow_dispatch: + push: + branches: + - s3-plan-artifacts permissions: id-token: write From f2563c843c2f668f761ba6fe890663ec8c731034 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 13:23:22 +0100 Subject: [PATCH 19/23] fix: fmt --- infra/live/global_vars.hcl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/live/global_vars.hcl b/infra/live/global_vars.hcl index e24023d0..56cecd78 100644 --- a/infra/live/global_vars.hcl +++ b/infra/live/global_vars.hcl @@ -32,9 +32,9 @@ locals { } inputs = { - vpc_name = local.vpc_name - aws_region = local.aws_region - allowed_role_actions = local.allowed_role_actions + vpc_name = local.vpc_name + aws_region = local.aws_region + allowed_role_actions = local.allowed_role_actions code_artifact_expiration_days = local.code_artifact_expiration_days infra_plan_artifact_expiration_days = local.infra_plan_artifact_expiration_days } From e6ea6e9676d59c11c073b632a0d3f2f743e82beb Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 13:25:13 +0100 Subject: [PATCH 20/23] debug: right workflow --- .github/workflows/dev_code_deploy.yml | 3 --- .github/workflows/dev_infra_apply.yml | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dev_code_deploy.yml b/.github/workflows/dev_code_deploy.yml index f7a88332..63405573 100644 --- a/.github/workflows/dev_code_deploy.yml +++ b/.github/workflows/dev_code_deploy.yml @@ -3,9 +3,6 @@ name: Dev Code Deploy on: workflow_dispatch: - push: - branches: - - s3-plan-artifacts permissions: id-token: write diff --git a/.github/workflows/dev_infra_apply.yml b/.github/workflows/dev_infra_apply.yml index d77cd1fd..89a9b914 100644 --- a/.github/workflows/dev_infra_apply.yml +++ b/.github/workflows/dev_infra_apply.yml @@ -2,6 +2,9 @@ name: Dev Infra Apply (no plan) on: workflow_dispatch: + push: + branches: + - s3-plan-artifacts permissions: id-token: write From 87886e6bbd1b5a899962f104d8a5a2f5e34b1747 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 13:27:17 +0100 Subject: [PATCH 21/23] chore: perms fix --- .github/workflows/shared_infra_apply.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/shared_infra_apply.yml b/.github/workflows/shared_infra_apply.yml index ffe5277a..98ec6e1a 100644 --- a/.github/workflows/shared_infra_apply.yml +++ b/.github/workflows/shared_infra_apply.yml @@ -32,6 +32,7 @@ on: permissions: id-token: write contents: read + actions: read jobs: infra: From d2e1862ed9848abc30b0d89a487e0a9e73852e0e Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 13:29:57 +0100 Subject: [PATCH 22/23] fix: more perms --- .github/workflows/dev_infra_apply.yml | 1 + .github/workflows/prod_infra_apply.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/dev_infra_apply.yml b/.github/workflows/dev_infra_apply.yml index 89a9b914..c569f56f 100644 --- a/.github/workflows/dev_infra_apply.yml +++ b/.github/workflows/dev_infra_apply.yml @@ -9,6 +9,7 @@ on: permissions: id-token: write contents: write + actions: read jobs: setup: diff --git a/.github/workflows/prod_infra_apply.yml b/.github/workflows/prod_infra_apply.yml index 81f57ea5..f9f81e19 100644 --- a/.github/workflows/prod_infra_apply.yml +++ b/.github/workflows/prod_infra_apply.yml @@ -6,6 +6,7 @@ on: permissions: id-token: write contents: write + actions: read jobs: get_build: From afbd3363a70dc2a7ff22bced6764f6a8926a1a38 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Mon, 27 Apr 2026 14:03:10 +0100 Subject: [PATCH 23/23] chore: actions updates --- .github/actions/just/action.yml | 4 ++-- .github/actions/terragrunt/action.yml | 6 +++--- .github/workflows/dev_infra_apply.yml | 3 --- .github/workflows/pull_request.yml | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/actions/just/action.yml b/.github/actions/just/action.yml index 2a2d9de3..666088e6 100644 --- a/.github/actions/just/action.yml +++ b/.github/actions/just/action.yml @@ -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 }} diff --git a/.github/actions/terragrunt/action.yml b/.github/actions/terragrunt/action.yml index f87e42bb..615b5bf4 100644 --- a/.github/actions/terragrunt/action.yml +++ b/.github/actions/terragrunt/action.yml @@ -37,13 +37,13 @@ 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 }} @@ -97,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 }} diff --git a/.github/workflows/dev_infra_apply.yml b/.github/workflows/dev_infra_apply.yml index c569f56f..1c088d5d 100644 --- a/.github/workflows/dev_infra_apply.yml +++ b/.github/workflows/dev_infra_apply.yml @@ -2,9 +2,6 @@ name: Dev Infra Apply (no plan) on: workflow_dispatch: - push: - branches: - - s3-plan-artifacts permissions: id-token: write diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 7307c821..1c6cf8bd 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -151,7 +151,7 @@ jobs: timeout-minutes: 2 steps: - uses: actions/checkout@v6 - - uses: hashicorp/setup-terraform@v3 + - uses: hashicorp/setup-terraform@v4 - uses: autero1/action-terragrunt@v3.0.2 with: terragrunt-version: 0.45.10