From fa4948f63562c9673e41dc075d2964ea6db7fdc7 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Thu, 30 Apr 2026 16:59:53 +0100 Subject: [PATCH 01/10] chore: dev cleanup --- .github/docs/README.md | 4 +- .github/workflows/destroy.yml | 28 ++++++++++++ justfile.deploy | 83 +++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/.github/docs/README.md b/.github/docs/README.md index 59171313..9f2d8e8b 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -128,7 +128,7 @@ flowchart LR ### Cleanup And Discovery - `destroy.yml` - 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. + 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. After the main graph completes, `dev` runs a final tagged-resource sweep through `justfile.deploy` that currently deletes leaked Cognito user pools and deregisters leaked ECS task-definition revisions, then fails if tagged leftovers still remain. - `shared_directories_get.yml` Derives the directory-based matrices used by wrapper workflows and PR action-test discovery. @@ -206,6 +206,8 @@ Run these checks on every CI, workflow, or deploy-contract change. - confirm destroy ordering still removes downstream consumers before shared stacks - check required Terraform variables on destroy as well as apply - prefer depending on real downstream consumers rather than serializing unrelated shared stacks +- when a module creates manual backup artifacts outside Terraform ownership, decide explicitly whether destroy should delete or retain them by environment +- if destroy relies on a final tagged-resource sweep, keep the cleanup logic in `justfile.deploy` and fail the workflow on unsupported tagged leftovers so new leak classes are visible ## Wrapper Workflow Summary diff --git a/.github/workflows/destroy.yml b/.github/workflows/destroy.yml index cf9c7075..3fb71ca7 100644 --- a/.github/workflows/destroy.yml +++ b/.github/workflows/destroy.yml @@ -316,3 +316,31 @@ jobs: with: tg_directory: infra/live/${{ inputs.environment }}/aws/cluster tg_action: destroy + + dev-cleanup: + name: Dev Cleanup + if: inputs.environment != 'prod' + needs: + - observability + - cognito + - security + - build-bucket + - ecr + - cluster + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: ${{ env.AWS_OIDC_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - name: Sweep tagged dev resources + uses: ./.github/actions/just + env: + ENVIRONMENT: ${{ inputs.environment }} + PROJECT_NAME: ${{ vars.PROJECT_NAME }} + with: + justfile_path: justfile.deploy + just_action: cleanup-dev-tagged-resources diff --git a/justfile.deploy b/justfile.deploy index 1f989600..2c9bfca7 100644 --- a/justfile.deploy +++ b/justfile.deploy @@ -667,3 +667,86 @@ ecs-rolling-deploy: --services "$SERVICE_NAME" echo "✅ ECS rolling deployment completed for $SERVICE_NAME" + + +# Sweep remaining tagged dev resources that Terraform destroy may leave behind. +cleanup-dev-tagged-resources: + #!/usr/bin/env bash + set -euo pipefail + + if [[ -z "${PROJECT_NAME:-}" ]]; then + echo "❌ PROJECT_NAME environment variable is not set." + exit 1 + fi + + if [[ -z "${ENVIRONMENT:-}" ]]; then + echo "❌ ENVIRONMENT environment variable is not set." + exit 1 + fi + + if [[ "$ENVIRONMENT" == "prod" ]]; then + echo "Refusing to run dev tagged-resource cleanup against prod." + exit 1 + fi + + mapfile -t resource_arns < <( + aws resourcegroupstaggingapi get-resources \ + --tag-filters "Key=Project,Values=${PROJECT_NAME}" "Key=Environment,Values=${ENVIRONMENT}" \ + --query 'ResourceTagMappingList[].ResourceARN' \ + --output text \ + | tr '\t' '\n' \ + | sed '/^$/d' \ + | sort -u + ) + + if [[ ${#resource_arns[@]} -eq 0 ]]; then + echo "No tagged ${ENVIRONMENT} resources remain." + exit 0 + fi + + unsupported=() + + for arn in "${resource_arns[@]}"; do + case "$arn" in + arn:aws:cognito-idp:*:userpool/*) + user_pool_id="${arn##*/}" + echo "Deleting Cognito user pool $user_pool_id" + aws cognito-idp delete-user-pool --user-pool-id "$user_pool_id" + ;; + arn:aws:ecs:*:task-definition/*) + task_definition_arn="${arn#arn:aws:ecs:}" + task_definition_arn="arn:aws:ecs:${task_definition_arn}" + echo "Deregistering ECS task definition $task_definition_arn" + aws ecs deregister-task-definition --task-definition "$task_definition_arn" >/dev/null + ;; + *) + unsupported+=("$arn") + ;; + esac + done + + if [[ ${#unsupported[@]} -gt 0 ]]; then + echo "Unsupported tagged resources remain after cleanup attempt:" + printf '%s\n' "${unsupported[@]}" + exit 1 + fi + + sleep 10 + + mapfile -t remaining_arns < <( + aws resourcegroupstaggingapi get-resources \ + --tag-filters "Key=Project,Values=${PROJECT_NAME}" "Key=Environment,Values=${ENVIRONMENT}" \ + --query 'ResourceTagMappingList[].ResourceARN' \ + --output text \ + | tr '\t' '\n' \ + | sed '/^$/d' \ + | sort -u + ) + + if [[ ${#remaining_arns[@]} -gt 0 ]]; then + echo "Tagged resources still remain after cleanup:" + printf '%s\n' "${remaining_arns[@]}" + exit 1 + fi + + echo "✅ Tagged ${ENVIRONMENT} resources were fully cleaned up." From 8795a41a24bbedd2124e55b2e56d278770b3663c Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Thu, 30 Apr 2026 17:05:00 +0100 Subject: [PATCH 02/10] chore: switch to rm prod resources --- .github/docs/README.md | 2 +- .github/workflows/destroy.yml | 24 ++++++++++++++++++------ justfile.deploy | 8 ++++---- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.github/docs/README.md b/.github/docs/README.md index 9f2d8e8b..4b05e205 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -128,7 +128,7 @@ flowchart LR ### Cleanup And Discovery - `destroy.yml` - 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. After the main graph completes, `dev` runs a final tagged-resource sweep through `justfile.deploy` that currently deletes leaked Cognito user pools and deregisters leaked ECS task-definition revisions, then fails if tagged leftovers still remain. + 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. The workflow-dispatch input `allow_prod_cleanup` now gates every cleanup or destroy job that is normally skipped for `prod`, including the `Code Bucket`, `ECR`, and final tagged-resource sweep jobs. After the main graph completes, `dev` always runs a final tagged-resource sweep through `justfile.deploy` that currently deletes leaked Cognito user pools and deregisters leaked ECS task-definition revisions, then fails if tagged leftovers still remain. `prod` runs that same sweep only when `allow_prod_cleanup` is enabled, and the workflow prints a conspicuous warning first. - `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 3fb71ca7..c1cdcc2c 100644 --- a/.github/workflows/destroy.yml +++ b/.github/workflows/destroy.yml @@ -10,6 +10,11 @@ on: options: - dev - prod + allow_prod_cleanup: + description: "Also run prod-only cleanup and destroy jobs that are normally skipped for prod" + required: true + type: boolean + default: false concurrency: # only run destroy when no other deploy/destroy is running for the same environment group: deploy-${{ inputs.environment }} @@ -258,7 +263,7 @@ jobs: build-bucket: name: Code Bucket - if: inputs.environment != 'prod' + if: inputs.environment != 'prod' || inputs.allow_prod_cleanup needs: - lambdas runs-on: ubuntu-latest @@ -278,7 +283,7 @@ jobs: ecr: name: ECR - if: inputs.environment != 'prod' + if: inputs.environment != 'prod' || inputs.allow_prod_cleanup needs: - network runs-on: ubuntu-latest @@ -318,8 +323,8 @@ jobs: tg_action: destroy dev-cleanup: - name: Dev Cleanup - if: inputs.environment != 'prod' + name: Tagged Cleanup + if: inputs.environment != 'prod' || inputs.allow_prod_cleanup needs: - observability - cognito @@ -336,11 +341,18 @@ jobs: role-to-assume: ${{ env.AWS_OIDC_ROLE_ARN }} aws-region: ${{ env.AWS_REGION }} - - name: Sweep tagged dev resources + - name: Warn before prod tagged cleanup + if: inputs.environment == 'prod' + run: | + printf '\033[1;31m%s\033[0m\n' 'WARNING: running retained tagged-resource cleanup for prod.' + printf '%s\n' 'This may delete leaked Cognito user pools and deregister tagged ECS task-definition revisions.' + + - name: Sweep tagged resources uses: ./.github/actions/just env: ENVIRONMENT: ${{ inputs.environment }} PROJECT_NAME: ${{ vars.PROJECT_NAME }} + ALLOW_PROD_CLEANUP: ${{ inputs.allow_prod_cleanup }} with: justfile_path: justfile.deploy - just_action: cleanup-dev-tagged-resources + just_action: cleanup-tagged-resources diff --git a/justfile.deploy b/justfile.deploy index 2c9bfca7..c699d77c 100644 --- a/justfile.deploy +++ b/justfile.deploy @@ -669,8 +669,8 @@ ecs-rolling-deploy: echo "✅ ECS rolling deployment completed for $SERVICE_NAME" -# Sweep remaining tagged dev resources that Terraform destroy may leave behind. -cleanup-dev-tagged-resources: +# Sweep remaining tagged resources that Terraform destroy may leave behind. +cleanup-tagged-resources: #!/usr/bin/env bash set -euo pipefail @@ -684,8 +684,8 @@ cleanup-dev-tagged-resources: exit 1 fi - if [[ "$ENVIRONMENT" == "prod" ]]; then - echo "Refusing to run dev tagged-resource cleanup against prod." + if [[ "$ENVIRONMENT" == "prod" && "${ALLOW_PROD_CLEANUP:-false}" != "true" ]]; then + echo "Refusing to run tagged-resource cleanup against prod without ALLOW_PROD_CLEANUP=true." exit 1 fi From d843237865d77f349b35d594205bd2defcac58e4 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Fri, 1 May 2026 09:45:45 +0100 Subject: [PATCH 03/10] chore: add cleanup job --- .github/docs/README.md | 5 +- .github/workflows/destroy.yml | 23 ++++++- justfile | 2 + justfile.deploy | 82 ------------------------ justfile.destroy | 113 ++++++++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 87 deletions(-) create mode 100644 justfile.destroy diff --git a/.github/docs/README.md b/.github/docs/README.md index 4b05e205..66427c97 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -128,7 +128,7 @@ flowchart LR ### Cleanup And Discovery - `destroy.yml` - 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. The workflow-dispatch input `allow_prod_cleanup` now gates every cleanup or destroy job that is normally skipped for `prod`, including the `Code Bucket`, `ECR`, and final tagged-resource sweep jobs. After the main graph completes, `dev` always runs a final tagged-resource sweep through `justfile.deploy` that currently deletes leaked Cognito user pools and deregisters leaked ECS task-definition revisions, then fails if tagged leftovers still remain. `prod` runs that same sweep only when `allow_prod_cleanup` is enabled, and the workflow prints a conspicuous warning first. + 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. The workflow-dispatch input `allow_prod_cleanup` now gates every cleanup or destroy job that is normally skipped for `prod`, including the `Code Bucket`, `ECR`, and final tagged-resource cleanup jobs. After the main graph completes, the workflow first counts tagged leftovers through `justfile.destroy`, prints a warning only when any remain, and then runs the cleanup recipe. That cleanup currently deletes leaked Cognito user pools and deregisters leaked ECS task-definition revisions, then fails if tagged leftovers still remain. `prod` runs that same path only when `allow_prod_cleanup` is enabled, and the workflow prints a conspicuous warning first. - `shared_directories_get.yml` Derives the directory-based matrices used by wrapper workflows and PR action-test discovery. @@ -160,6 +160,7 @@ Run these checks on every CI, workflow, or deploy-contract change. - `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 + - `justfile.destroy` for explicit teardown and post-destroy cleanup steps ### Release Tagging Checks @@ -207,7 +208,7 @@ Run these checks on every CI, workflow, or deploy-contract change. - check required Terraform variables on destroy as well as apply - prefer depending on real downstream consumers rather than serializing unrelated shared stacks - when a module creates manual backup artifacts outside Terraform ownership, decide explicitly whether destroy should delete or retain them by environment -- if destroy relies on a final tagged-resource sweep, keep the cleanup logic in `justfile.deploy` and fail the workflow on unsupported tagged leftovers so new leak classes are visible +- if destroy relies on a final tagged-resource sweep, keep both the scan/count step and the cleanup step in `justfile.destroy`, and fail the workflow on unsupported tagged leftovers so new leak classes are visible ## Wrapper Workflow Summary diff --git a/.github/workflows/destroy.yml b/.github/workflows/destroy.yml index c1cdcc2c..037db626 100644 --- a/.github/workflows/destroy.yml +++ b/.github/workflows/destroy.yml @@ -322,7 +322,7 @@ jobs: tg_directory: infra/live/${{ inputs.environment }}/aws/cluster tg_action: destroy - dev-cleanup: + cleanup: name: Tagged Cleanup if: inputs.environment != 'prod' || inputs.allow_prod_cleanup needs: @@ -347,12 +347,29 @@ jobs: printf '\033[1;31m%s\033[0m\n' 'WARNING: running retained tagged-resource cleanup for prod.' printf '%s\n' 'This may delete leaked Cognito user pools and deregister tagged ECS task-definition revisions.' - - name: Sweep tagged resources + - name: Count tagged resources + id: count-tagged uses: ./.github/actions/just env: ENVIRONMENT: ${{ inputs.environment }} PROJECT_NAME: ${{ vars.PROJECT_NAME }} ALLOW_PROD_CLEANUP: ${{ inputs.allow_prod_cleanup }} with: - justfile_path: justfile.deploy + justfile_path: justfile.destroy + just_action: count-tagged-resources + + - name: Warn when tagged resources remain + if: steps.count-tagged.outputs.just_outputs != '0' + run: | + printf '\033[1;33m%s\033[0m\n' "WARNING: found ${{ steps.count-tagged.outputs.just_outputs }} tagged resources after destroy." + + - name: Cleanup tagged resources + if: steps.count-tagged.outputs.just_outputs != '0' + uses: ./.github/actions/just + env: + ENVIRONMENT: ${{ inputs.environment }} + PROJECT_NAME: ${{ vars.PROJECT_NAME }} + ALLOW_PROD_CLEANUP: ${{ inputs.allow_prod_cleanup }} + with: + justfile_path: justfile.destroy just_action: cleanup-tagged-resources diff --git a/justfile b/justfile index 9763c9ee..a92a5176 100644 --- a/justfile +++ b/justfile @@ -7,6 +7,8 @@ _default: @just --justfile justfile.tg --list @printf '\nDeploy recipes (`just --justfile justfile.deploy --list`):\n' @just --justfile justfile.deploy --list + @printf '\nDestroy recipes (`just --justfile justfile.destroy --list`):\n' + @just --justfile justfile.destroy --list PROJECT_DIR := justfile_directory() diff --git a/justfile.deploy b/justfile.deploy index c699d77c..75e1f4c6 100644 --- a/justfile.deploy +++ b/justfile.deploy @@ -668,85 +668,3 @@ ecs-rolling-deploy: echo "✅ ECS rolling deployment completed for $SERVICE_NAME" - -# Sweep remaining tagged resources that Terraform destroy may leave behind. -cleanup-tagged-resources: - #!/usr/bin/env bash - set -euo pipefail - - if [[ -z "${PROJECT_NAME:-}" ]]; then - echo "❌ PROJECT_NAME environment variable is not set." - exit 1 - fi - - if [[ -z "${ENVIRONMENT:-}" ]]; then - echo "❌ ENVIRONMENT environment variable is not set." - exit 1 - fi - - if [[ "$ENVIRONMENT" == "prod" && "${ALLOW_PROD_CLEANUP:-false}" != "true" ]]; then - echo "Refusing to run tagged-resource cleanup against prod without ALLOW_PROD_CLEANUP=true." - exit 1 - fi - - mapfile -t resource_arns < <( - aws resourcegroupstaggingapi get-resources \ - --tag-filters "Key=Project,Values=${PROJECT_NAME}" "Key=Environment,Values=${ENVIRONMENT}" \ - --query 'ResourceTagMappingList[].ResourceARN' \ - --output text \ - | tr '\t' '\n' \ - | sed '/^$/d' \ - | sort -u - ) - - if [[ ${#resource_arns[@]} -eq 0 ]]; then - echo "No tagged ${ENVIRONMENT} resources remain." - exit 0 - fi - - unsupported=() - - for arn in "${resource_arns[@]}"; do - case "$arn" in - arn:aws:cognito-idp:*:userpool/*) - user_pool_id="${arn##*/}" - echo "Deleting Cognito user pool $user_pool_id" - aws cognito-idp delete-user-pool --user-pool-id "$user_pool_id" - ;; - arn:aws:ecs:*:task-definition/*) - task_definition_arn="${arn#arn:aws:ecs:}" - task_definition_arn="arn:aws:ecs:${task_definition_arn}" - echo "Deregistering ECS task definition $task_definition_arn" - aws ecs deregister-task-definition --task-definition "$task_definition_arn" >/dev/null - ;; - *) - unsupported+=("$arn") - ;; - esac - done - - if [[ ${#unsupported[@]} -gt 0 ]]; then - echo "Unsupported tagged resources remain after cleanup attempt:" - printf '%s\n' "${unsupported[@]}" - exit 1 - fi - - sleep 10 - - mapfile -t remaining_arns < <( - aws resourcegroupstaggingapi get-resources \ - --tag-filters "Key=Project,Values=${PROJECT_NAME}" "Key=Environment,Values=${ENVIRONMENT}" \ - --query 'ResourceTagMappingList[].ResourceARN' \ - --output text \ - | tr '\t' '\n' \ - | sed '/^$/d' \ - | sort -u - ) - - if [[ ${#remaining_arns[@]} -gt 0 ]]; then - echo "Tagged resources still remain after cleanup:" - printf '%s\n' "${remaining_arns[@]}" - exit 1 - fi - - echo "✅ Tagged ${ENVIRONMENT} resources were fully cleaned up." diff --git a/justfile.destroy b/justfile.destroy new file mode 100644 index 00000000..f4d2d187 --- /dev/null +++ b/justfile.destroy @@ -0,0 +1,113 @@ +# Destroy and cleanup helpers only. +# This file is for explicit teardown and post-destroy sweep steps. + +PROJECT_DIR := `just --justfile justfile --evaluate PROJECT_DIR` + + +# Count tagged resources that Terraform destroy may leave behind. +count-tagged-resources: + #!/usr/bin/env bash + set -euo pipefail + + if [[ -z "${PROJECT_NAME:-}" ]]; then + echo "❌ PROJECT_NAME environment variable is not set." + exit 1 + fi + + if [[ -z "${ENVIRONMENT:-}" ]]; then + echo "❌ ENVIRONMENT environment variable is not set." + exit 1 + fi + + if [[ "$ENVIRONMENT" == "prod" && "${ALLOW_PROD_CLEANUP:-false}" != "true" ]]; then + echo "Refusing to run tagged-resource cleanup against prod without ALLOW_PROD_CLEANUP=true." + exit 1 + fi + + mapfile -t resource_arns < <( + aws resourcegroupstaggingapi get-resources \ + --tag-filters "Key=Project,Values=${PROJECT_NAME}" "Key=Environment,Values=${ENVIRONMENT}" \ + --query 'ResourceTagMappingList[].ResourceARN' \ + --output text \ + | tr '\t' '\n' \ + | sed '/^$/d' \ + | sort -u + ) + + echo "${#resource_arns[@]}" + +# Clean up tagged resources that Terraform destroy may leave behind. +cleanup-tagged-resources: + #!/usr/bin/env bash + set -euo pipefail + + if [[ -z "${PROJECT_NAME:-}" ]]; then + echo "❌ PROJECT_NAME environment variable is not set." + exit 1 + fi + + if [[ -z "${ENVIRONMENT:-}" ]]; then + echo "❌ ENVIRONMENT environment variable is not set." + exit 1 + fi + + if [[ "$ENVIRONMENT" == "prod" && "${ALLOW_PROD_CLEANUP:-false}" != "true" ]]; then + echo "Refusing to run tagged-resource cleanup against prod without ALLOW_PROD_CLEANUP=true." + exit 1 + fi + + mapfile -t resource_arns < <( + aws resourcegroupstaggingapi get-resources \ + --tag-filters "Key=Project,Values=${PROJECT_NAME}" "Key=Environment,Values=${ENVIRONMENT}" \ + --query 'ResourceTagMappingList[].ResourceARN' \ + --output text \ + | tr '\t' '\n' \ + | sed '/^$/d' \ + | sort -u + ) + + if [[ ${#resource_arns[@]} -eq 0 ]]; then + echo "No tagged ${ENVIRONMENT} resources remain." + exit 0 + fi + + unsupported=() + + for arn in "${resource_arns[@]}"; do + case "$arn" in + arn:aws:cognito-idp:*:userpool/*) + user_pool_id="${arn##*/}" + echo "Deleting Cognito user pool ${user_pool_id}" + aws cognito-idp delete-user-pool --user-pool-id "${user_pool_id}" + ;; + arn:aws:ecs:*:task-definition/*) + echo "Deregistering ECS task definition ${arn}" + aws ecs deregister-task-definition --task-definition "${arn}" >/dev/null + ;; + *) + unsupported+=("$arn") + ;; + esac + done + + if [[ ${#unsupported[@]} -gt 0 ]]; then + echo "Unsupported tagged resources remain after cleanup attempt:" + printf '%s\n' "${unsupported[@]}" + exit 1 + fi + + sleep 10 + + remaining_count="$( + PROJECT_NAME="${PROJECT_NAME}" \ + ENVIRONMENT="${ENVIRONMENT}" \ + ALLOW_PROD_CLEANUP="${ALLOW_PROD_CLEANUP:-false}" \ + just --justfile "{{PROJECT_DIR}}/justfile.destroy" count-tagged-resources + )" + + if [[ "${remaining_count}" != "0" ]]; then + echo "Tagged resources still remain after cleanup: ${remaining_count}" + exit 1 + fi + + echo "✅ Tagged ${ENVIRONMENT} resources were fully cleaned up." From 5d6ef1f8cc25040a5c66f55a8d4745db08ffc8de Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Fri, 1 May 2026 10:13:58 +0100 Subject: [PATCH 04/10] chore: oidc fix for get tags --- .github/docs/README.md | 1 + .github/workflows/destroy.yml | 2 +- infra/live/global_vars.hcl | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/docs/README.md b/.github/docs/README.md index 66427c97..7ca50cc9 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -209,6 +209,7 @@ Run these checks on every CI, workflow, or deploy-contract change. - prefer depending on real downstream consumers rather than serializing unrelated shared stacks - when a module creates manual backup artifacts outside Terraform ownership, decide explicitly whether destroy should delete or retain them by environment - if destroy relies on a final tagged-resource sweep, keep both the scan/count step and the cleanup step in `justfile.destroy`, and fail the workflow on unsupported tagged leftovers so new leak classes are visible +- if destroy relies on a final tagged-resource sweep, make sure the deploy OIDC role also allows `tag:GetResources`; the cleanup path uses the Resource Groups Tagging API before running service-specific deletions ## Wrapper Workflow Summary diff --git a/.github/workflows/destroy.yml b/.github/workflows/destroy.yml index 037db626..4ec02e03 100644 --- a/.github/workflows/destroy.yml +++ b/.github/workflows/destroy.yml @@ -323,7 +323,7 @@ jobs: tg_action: destroy cleanup: - name: Tagged Cleanup + name: Cleanup if: inputs.environment != 'prod' || inputs.allow_prod_cleanup needs: - observability diff --git a/infra/live/global_vars.hcl b/infra/live/global_vars.hcl index 56cecd78..fac34513 100644 --- a/infra/live/global_vars.hcl +++ b/infra/live/global_vars.hcl @@ -26,6 +26,7 @@ locals { "acm:*", "route53:*", "cognito-idp:*", + "tag:GetResources", ] code_artifact_expiration_days = 0 infra_plan_artifact_expiration_days = 30 From ae7d9f483649c8aa1573cbff1fc80be6a34f9867 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Fri, 1 May 2026 10:18:21 +0100 Subject: [PATCH 05/10] chore: use gruntwork-io/terragrunt-action@v3 --- .github/actions/terragrunt/README.md | 1 + .github/actions/terragrunt/action.yml | 5 +++-- .github/docs/README.md | 3 ++- .github/workflows/pull_request.yml | 5 +++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/actions/terragrunt/README.md b/.github/actions/terragrunt/README.md index 9bb93d02..5267bffb 100644 --- a/.github/actions/terragrunt/README.md +++ b/.github/actions/terragrunt/README.md @@ -5,6 +5,7 @@ This GitHub Action sets up **Terraform** and **Terragrunt** and runs a specified ## Features - Installs pinned versions of Terraform and Terragrunt +- Installs Terragrunt through `gruntwork-io/terragrunt-action@v3` - Uses AWS credentials already configured earlier in the same job when needed - Optionally passes Terragrunt variables via JSON tfvars - Supports `plan` mode for producing local saved plan files diff --git a/.github/actions/terragrunt/action.yml b/.github/actions/terragrunt/action.yml index a4929c2f..92716ec0 100644 --- a/.github/actions/terragrunt/action.yml +++ b/.github/actions/terragrunt/action.yml @@ -40,9 +40,10 @@ runs: terraform_wrapper: false - name: Install Terragrunt - uses: autero1/action-terragrunt@v3.0.2 + uses: gruntwork-io/terragrunt-action@v3 with: - terragrunt-version: ${{ inputs.tg_version }} + tg_version: ${{ inputs.tg_version }} + tf_path: terraform - name: Normalize and write override_tg_vars if: inputs.tg_action == 'apply' || inputs.tg_action == 'plan' || inputs.tg_action == 'destroy' diff --git a/.github/docs/README.md b/.github/docs/README.md index 7ca50cc9..f60f497f 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -51,7 +51,7 @@ If you are unsure, the live `aws/oidc` stack in the target environment is the so - `release.yml` Creates release tags, prepares shared CI artifacts, builds release outputs, and publishes the GitHub release. Version bumps come from a repo-local action that scans commit subjects since the latest semver tag and matches configurable major/minor/patch prefixes. - `pull_request.yml` - Provides fast validation for workflow syntax, Terraform formatting/linting, changed runtime builds, and a direct execution check of the repo-local `get-next-version` Docker action. The version preview job classifies the PR title, so it reflects the version that would be implied if that PR title lands on `main`. Its `check` job runs the repo-local `get-changes` Docker action directly, using the PR base SHA for a PR-style `base...HEAD` diff. When `.github/actions/**` changed, the workflow reuses `shared_directories_get.yml` to discover action directories with `Dockerfile`s and runs a Docker unit-test matrix for them after the GitHub formatting job. The Lambda naming check only runs when Lambda sources changed, and the ECS task/service pair check runs when container sources or Terragrunt live-stack directories changed; each is an explicit prerequisite for the corresponding build job. + Provides fast validation for workflow syntax, Terraform formatting/linting, changed runtime builds, and a direct execution check of the repo-local `get-next-version` Docker action. The version preview job classifies the PR title, so it reflects the version that would be implied if that PR title lands on `main`. Its `check` job runs the repo-local `get-changes` Docker action directly, using the PR base SHA for a PR-style `base...HEAD` diff. When `.github/actions/**` changed, the workflow reuses `shared_directories_get.yml` to discover action directories with `Dockerfile`s and runs a Docker unit-test matrix for them after the GitHub formatting job. The Lambda naming check only runs when Lambda sources changed, and the ECS task/service pair check runs when container sources or Terragrunt live-stack directories changed; each is an explicit prerequisite for the corresponding build job. Terragrunt installation in that workflow now uses `gruntwork-io/terragrunt-action@v3`. The local version action can also be tested outside GitHub Actions, either by running the Python entrypoint directly or through its dedicated Docker image. @@ -144,6 +144,7 @@ Run these checks on every CI, workflow, or deploy-contract change. - 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 - both repo-local composite actions, `./.github/actions/just` and `./.github/actions/terragrunt`, now assume AWS credentials are already configured in the current job when they need AWS access. The repo pattern is to run `aws-actions/configure-aws-credentials` at the top of each AWS-using job and then call the local actions without extra auth inputs +- `./.github/actions/terragrunt` installs the requested Terragrunt version through `gruntwork-io/terragrunt-action@v3`, passing `tf_path: terraform` so the repo keeps using the separately pinned Terraform binary from `hashicorp/setup-terraform` - 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-/` diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1c6cf8bd..bec000fd 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -152,9 +152,10 @@ jobs: steps: - uses: actions/checkout@v6 - uses: hashicorp/setup-terraform@v4 - - uses: autero1/action-terragrunt@v3.0.2 + - uses: gruntwork-io/terragrunt-action@v3 with: - terragrunt-version: 0.45.10 + tg_version: 0.45.10 + tf_path: terraform - name: Terraform fmt check run: terraform fmt -check -recursive From 03dcff20c98124d43310fae1e6369647f44a3228 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Fri, 1 May 2026 10:23:51 +0100 Subject: [PATCH 06/10] chore: handle stale resources on cleanup --- .github/docs/README.md | 2 +- justfile.destroy | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/docs/README.md b/.github/docs/README.md index f60f497f..4de08f01 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -128,7 +128,7 @@ flowchart LR ### Cleanup And Discovery - `destroy.yml` - 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. The workflow-dispatch input `allow_prod_cleanup` now gates every cleanup or destroy job that is normally skipped for `prod`, including the `Code Bucket`, `ECR`, and final tagged-resource cleanup jobs. After the main graph completes, the workflow first counts tagged leftovers through `justfile.destroy`, prints a warning only when any remain, and then runs the cleanup recipe. That cleanup currently deletes leaked Cognito user pools and deregisters leaked ECS task-definition revisions, then fails if tagged leftovers still remain. `prod` runs that same path only when `allow_prod_cleanup` is enabled, and the workflow prints a conspicuous warning first. + 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. The workflow-dispatch input `allow_prod_cleanup` now gates every cleanup or destroy job that is normally skipped for `prod`, including the `Code Bucket`, `ECR`, and final tagged-resource cleanup jobs. After the main graph completes, the workflow first counts tagged leftovers through `justfile.destroy`, prints a warning only when any remain, and then runs the cleanup recipe. That cleanup currently deletes leaked Cognito user pools and deregisters leaked ECS task-definition revisions, then fails if tagged leftovers still remain. Already-removed Cognito pools or ECS task-definition revisions are treated as successful no-ops so stale tagging API results do not fail cleanup. `prod` runs that same path only when `allow_prod_cleanup` is enabled, and the workflow prints a conspicuous warning first. - `shared_directories_get.yml` Derives the directory-based matrices used by wrapper workflows and PR action-test discovery. diff --git a/justfile.destroy b/justfile.destroy index f4d2d187..c2d759c9 100644 --- a/justfile.destroy +++ b/justfile.destroy @@ -78,11 +78,25 @@ cleanup-tagged-resources: arn:aws:cognito-idp:*:userpool/*) user_pool_id="${arn##*/}" echo "Deleting Cognito user pool ${user_pool_id}" - aws cognito-idp delete-user-pool --user-pool-id "${user_pool_id}" + if ! delete_output="$(aws cognito-idp delete-user-pool --user-pool-id "${user_pool_id}" 2>&1)"; then + if grep -q "ResourceNotFoundException" <<<"${delete_output}"; then + echo "Cognito user pool ${user_pool_id} is already gone." + else + echo "${delete_output}" >&2 + exit 1 + fi + fi ;; arn:aws:ecs:*:task-definition/*) echo "Deregistering ECS task definition ${arn}" - aws ecs deregister-task-definition --task-definition "${arn}" >/dev/null + if ! deregister_output="$(aws ecs deregister-task-definition --task-definition "${arn}" 2>&1 >/dev/null)"; then + if grep -Eq "ClientException|ResourceNotFoundException" <<<"${deregister_output}"; then + echo "ECS task definition ${arn} is already gone or inactive." + else + echo "${deregister_output}" >&2 + exit 1 + fi + fi ;; *) unsupported+=("$arn") From 66e7fe7470757766b302a7d85a114c61b3d5e6c5 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Fri, 1 May 2026 10:54:14 +0100 Subject: [PATCH 07/10] chore: add tidy for cluster and secrets --- .github/docs/README.md | 2 +- justfile.destroy | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/docs/README.md b/.github/docs/README.md index 4de08f01..5680a165 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -128,7 +128,7 @@ flowchart LR ### Cleanup And Discovery - `destroy.yml` - 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. The workflow-dispatch input `allow_prod_cleanup` now gates every cleanup or destroy job that is normally skipped for `prod`, including the `Code Bucket`, `ECR`, and final tagged-resource cleanup jobs. After the main graph completes, the workflow first counts tagged leftovers through `justfile.destroy`, prints a warning only when any remain, and then runs the cleanup recipe. That cleanup currently deletes leaked Cognito user pools and deregisters leaked ECS task-definition revisions, then fails if tagged leftovers still remain. Already-removed Cognito pools or ECS task-definition revisions are treated as successful no-ops so stale tagging API results do not fail cleanup. `prod` runs that same path only when `allow_prod_cleanup` is enabled, and the workflow prints a conspicuous warning first. + 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. The workflow-dispatch input `allow_prod_cleanup` now gates every cleanup or destroy job that is normally skipped for `prod`, including the `Code Bucket`, `ECR`, and final tagged-resource cleanup jobs. After the main graph completes, the workflow first counts tagged leftovers through `justfile.destroy`, prints a warning only when any remain, and then runs the cleanup recipe. That cleanup currently deletes leaked Cognito user pools, deregisters leaked ECS task-definition revisions, deletes leftover ECS clusters, and force-deletes leftover Secrets Manager secrets, then fails if tagged leftovers still remain. Already-removed Cognito pools, ECS task-definition revisions, ECS clusters, or Secrets Manager secrets are treated as successful no-ops so stale tagging API results do not fail cleanup. `prod` runs that same path only when `allow_prod_cleanup` is enabled, and the workflow prints a conspicuous warning first. - `shared_directories_get.yml` Derives the directory-based matrices used by wrapper workflows and PR action-test discovery. diff --git a/justfile.destroy b/justfile.destroy index c2d759c9..0b4f71ba 100644 --- a/justfile.destroy +++ b/justfile.destroy @@ -98,6 +98,28 @@ cleanup-tagged-resources: fi fi ;; + arn:aws:ecs:*:cluster/*) + echo "Deleting ECS cluster ${arn}" + if ! delete_output="$(aws ecs delete-cluster --cluster "${arn}" 2>&1 >/dev/null)"; then + if grep -Eq "ClusterNotFoundException|ClusterNotFoundFault" <<<"${delete_output}"; then + echo "ECS cluster ${arn} is already gone." + else + echo "${delete_output}" >&2 + exit 1 + fi + fi + ;; + arn:aws:secretsmanager:*:secret:*) + echo "Deleting Secrets Manager secret ${arn}" + if ! delete_output="$(aws secretsmanager delete-secret --secret-id "${arn}" --force-delete-without-recovery 2>&1 >/dev/null)"; then + if grep -Eq "ResourceNotFoundException|marked for deletion|scheduled for deletion|InvalidRequestException" <<<"${delete_output}"; then + echo "Secrets Manager secret ${arn} is already gone or pending deletion." + else + echo "${delete_output}" >&2 + exit 1 + fi + fi + ;; *) unsupported+=("$arn") ;; From aa244ab21c5c7c1d17aca1c1cf7e44966cf8ff77 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Fri, 1 May 2026 11:08:43 +0100 Subject: [PATCH 08/10] chore: upgrade just to setup-crate@v2 --- .github/actions/just/README.md | 1 + .github/actions/just/action.yml | 9 +++++++-- .github/docs/README.md | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/actions/just/README.md b/.github/actions/just/README.md index 9c54fa19..2258f16b 100644 --- a/.github/actions/just/README.md +++ b/.github/actions/just/README.md @@ -7,6 +7,7 @@ This GitHub Action sets up [`just`](https://github.com/casey/just) and runs a sp ## 🚀 Features - Installs a specific version of [`just`](https://github.com/casey/just) +- Installs `just` through `extractions/setup-crate@v2` in the same minimal composite-action shape used by `extractions/setup-just` - Uses AWS credentials already configured earlier in the same job when needed - Executes any `just` command (recipe) - Captures and returns the final line of output as an action output diff --git a/.github/actions/just/action.yml b/.github/actions/just/action.yml index 4a960b30..1147a323 100644 --- a/.github/actions/just/action.yml +++ b/.github/actions/just/action.yml @@ -30,9 +30,14 @@ runs: using: "composite" steps: - name: Install Just - uses: extractions/setup-just@v4 + uses: extractions/setup-crate@v2 with: - just-version: ${{ inputs.just_version }} + repo: casey/just@${{ inputs.just_version }} + github-token: ${{ github.token }} + + - name: Verify Just installation + shell: bash + run: just --version - name: Run just action (try/catch + capture) id: capture diff --git a/.github/docs/README.md b/.github/docs/README.md index 5680a165..6fd7f4c2 100644 --- a/.github/docs/README.md +++ b/.github/docs/README.md @@ -128,7 +128,7 @@ flowchart LR ### Cleanup And Discovery - `destroy.yml` - 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. The workflow-dispatch input `allow_prod_cleanup` now gates every cleanup or destroy job that is normally skipped for `prod`, including the `Code Bucket`, `ECR`, and final tagged-resource cleanup jobs. After the main graph completes, the workflow first counts tagged leftovers through `justfile.destroy`, prints a warning only when any remain, and then runs the cleanup recipe. That cleanup currently deletes leaked Cognito user pools, deregisters leaked ECS task-definition revisions, deletes leftover ECS clusters, and force-deletes leftover Secrets Manager secrets, then fails if tagged leftovers still remain. Already-removed Cognito pools, ECS task-definition revisions, ECS clusters, or Secrets Manager secrets are treated as successful no-ops so stale tagging API results do not fail cleanup. `prod` runs that same path only when `allow_prod_cleanup` is enabled, and the workflow prints a conspicuous warning first. + 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. The workflow-dispatch input `allow_prod_cleanup` now gates every cleanup or destroy job that is normally skipped for `prod`, including the `Code Bucket`, `ECR`, and final tagged-resource cleanup jobs. After the main graph completes, the workflow first counts tagged leftovers through `justfile.destroy`, prints a warning only when any remain, and then runs the cleanup recipe. That cleanup currently deletes leaked Cognito user pools, deregisters and then deletes leaked ECS task-definition revisions, deletes leftover ECS clusters, and force-deletes leftover Secrets Manager secrets, then validates the remaining tagged ARNs against the underlying service APIs rather than treating the tagging index as the source of truth. Already-removed Cognito pools, ECS task-definition revisions, ECS clusters, or Secrets Manager secrets are treated as successful no-ops so stale tagging API results do not fail cleanup. `prod` runs that same path only when `allow_prod_cleanup` is enabled, and the workflow prints a conspicuous warning first. - `shared_directories_get.yml` Derives the directory-based matrices used by wrapper workflows and PR action-test discovery. @@ -144,6 +144,7 @@ Run these checks on every CI, workflow, or deploy-contract change. - 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 - both repo-local composite actions, `./.github/actions/just` and `./.github/actions/terragrunt`, now assume AWS credentials are already configured in the current job when they need AWS access. The repo pattern is to run `aws-actions/configure-aws-credentials` at the top of each AWS-using job and then call the local actions without extra auth inputs +- `./.github/actions/just` installs the requested `just` version through `extractions/setup-crate@v2` in the same minimal composite-action shape as `extractions/setup-just`, rather than depending on `extractions/setup-just` itself - `./.github/actions/terragrunt` installs the requested Terragrunt version through `gruntwork-io/terragrunt-action@v3`, passing `tf_path: terraform` so the repo keeps using the separately pinned Terraform binary from `hashicorp/setup-terraform` - saved infra-plan storage is intentionally split into two levels: - one run-level metadata file at `/infra-plan-metadata/plan-metadata.json` From 4b57cbdfc0246045b65b75637bb50a646cae4865 Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Fri, 1 May 2026 11:09:01 +0100 Subject: [PATCH 09/10] chore: harden delete further --- justfile.destroy | 78 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/justfile.destroy b/justfile.destroy index 0b4f71ba..d737db91 100644 --- a/justfile.destroy +++ b/justfile.destroy @@ -97,6 +97,15 @@ cleanup-tagged-resources: exit 1 fi fi + echo "Deleting ECS task definition ${arn}" + if ! delete_output="$(aws ecs delete-task-definitions --task-definitions "${arn}" 2>&1 >/dev/null)"; then + if grep -Eq "ClientException|ResourceNotFoundException" <<<"${delete_output}"; then + echo "ECS task definition ${arn} is already gone or delete-in-progress." + else + echo "${delete_output}" >&2 + exit 1 + fi + fi ;; arn:aws:ecs:*:cluster/*) echo "Deleting ECS cluster ${arn}" @@ -134,15 +143,68 @@ cleanup-tagged-resources: sleep 10 - remaining_count="$( - PROJECT_NAME="${PROJECT_NAME}" \ - ENVIRONMENT="${ENVIRONMENT}" \ - ALLOW_PROD_CLEANUP="${ALLOW_PROD_CLEANUP:-false}" \ - just --justfile "{{PROJECT_DIR}}/justfile.destroy" count-tagged-resources - )" + mapfile -t remaining_arns < <( + aws resourcegroupstaggingapi get-resources \ + --tag-filters "Key=Project,Values=${PROJECT_NAME}" "Key=Environment,Values=${ENVIRONMENT}" \ + --query 'ResourceTagMappingList[].ResourceARN' \ + --output text \ + | tr '\t' '\n' \ + | sed '/^$/d' \ + | sort -u + ) + + blocking=() + + for arn in "${remaining_arns[@]}"; do + case "$arn" in + arn:aws:cognito-idp:*:userpool/*) + user_pool_id="${arn##*/}" + if aws cognito-idp describe-user-pool --user-pool-id "${user_pool_id}" >/dev/null 2>&1; then + blocking+=("$arn") + fi + ;; + arn:aws:ecs:*:task-definition/*) + status="$( + aws ecs describe-task-definition \ + --task-definition "${arn}" \ + --query 'taskDefinition.status' \ + --output text 2>/dev/null || true + )" + if [[ -n "${status}" && "${status}" != "None" && "${status}" != "DELETE_IN_PROGRESS" ]]; then + blocking+=("$arn") + fi + ;; + arn:aws:ecs:*:cluster/*) + status="$( + aws ecs describe-clusters \ + --clusters "${arn}" \ + --query 'clusters[0].status' \ + --output text 2>/dev/null || true + )" + if [[ -n "${status}" && "${status}" != "None" && "${status}" != "INACTIVE" ]]; then + blocking+=("$arn") + fi + ;; + arn:aws:secretsmanager:*:secret:*) + deleted_date="$( + aws secretsmanager describe-secret \ + --secret-id "${arn}" \ + --query 'DeletedDate' \ + --output text 2>/dev/null || true + )" + if [[ -z "${deleted_date}" || "${deleted_date}" == "None" ]]; then + blocking+=("$arn") + fi + ;; + *) + blocking+=("$arn") + ;; + esac + done - if [[ "${remaining_count}" != "0" ]]; then - echo "Tagged resources still remain after cleanup: ${remaining_count}" + if [[ ${#blocking[@]} -gt 0 ]]; then + echo "Tagged resources still remain after cleanup:" + printf '%s\n' "${blocking[@]}" exit 1 fi From 3c3eb458dc821ec6a78fc54567666816fa42517f Mon Sep 17 00:00:00 2001 From: chrispsheehan Date: Fri, 1 May 2026 11:22:50 +0100 Subject: [PATCH 10/10] chore: bump setup-crate to v2.0.1 --- .github/actions/just/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/just/action.yml b/.github/actions/just/action.yml index 1147a323..65bef080 100644 --- a/.github/actions/just/action.yml +++ b/.github/actions/just/action.yml @@ -30,7 +30,7 @@ runs: using: "composite" steps: - name: Install Just - uses: extractions/setup-crate@v2 + uses: extractions/setup-crate@v2.0.1 with: repo: casey/just@${{ inputs.just_version }} github-token: ${{ github.token }}