From d118406b978879696e5d17b0653f7fb826478809 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 28 Feb 2026 11:08:42 +0000 Subject: [PATCH 01/32] Build a trixie base image --- .github/workflows/base-image.yaml | 10 +++++----- docs-gen/environment_variables/TERRAFORM_PRE_RUN.py | 2 +- docs/custom_tools.md | 2 +- image/Dockerfile | 2 +- image/Dockerfile-base | 2 +- image/src/github_pr_comment/comment.py | 6 +++--- terraform-apply/README.md | 2 +- terraform-check/README.md | 2 +- terraform-destroy-workspace/README.md | 2 +- terraform-destroy/README.md | 2 +- terraform-new-workspace/README.md | 2 +- terraform-output/README.md | 2 +- terraform-plan/README.md | 2 +- terraform-refresh/README.md | 2 +- terraform-test/README.md | 2 +- terraform-unlock-state/README.md | 2 +- terraform-validate/README.md | 2 +- terraform-version/README.md | 2 +- tofu-apply/README.md | 2 +- tofu-check/README.md | 2 +- tofu-destroy-workspace/README.md | 2 +- tofu-destroy/README.md | 2 +- tofu-new-workspace/README.md | 2 +- tofu-output/README.md | 2 +- tofu-plan/README.md | 2 +- tofu-refresh/README.md | 2 +- tofu-test/README.md | 2 +- tofu-unlock-state/README.md | 2 +- tofu-validate/README.md | 2 +- tofu-version/README.md | 2 +- 30 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/workflows/base-image.yaml b/.github/workflows/base-image.yaml index 8b437a82..cd841208 100644 --- a/.github/workflows/base-image.yaml +++ b/.github/workflows/base-image.yaml @@ -3,7 +3,7 @@ name: Update base image on: push: branches: - - main + - trixie paths: - image/Dockerfile-base - .github/workflows/base-image.yaml @@ -41,13 +41,13 @@ jobs: - name: Base image id: build-and-push run: | - BASE_DIGEST=$(docker buildx imagetools inspect "debian:bookworm-slim" --format '{{json .}}' | jq -r '.manifest.digest') + BASE_DIGEST=$(docker buildx imagetools inspect "debian:trixie-slim" --format '{{json .}}' | jq -r '.manifest.digest') - sed -i "s|FROM debian:bookworm-slim|FROM debian:bookworm-slim@$BASE_DIGEST|" "image/Dockerfile-base" + sed -i "s|FROM debian:trixie-slim|FROM debian:trixie-slim@$BASE_DIGEST|" "image/Dockerfile-base" docker buildx build \ --tag "danielflook/terraform-github-actions-base:$GITHUB_RUN_ID" \ - --tag danielflook/terraform-github-actions-base:latest \ + --tag danielflook/terraform-github-actions-base:trixie \ --platform linux/amd64,linux/arm64 \ --attest "type=provenance,mode=max,builder-id=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ --annotation "index,manifest:org.opencontainers.image.created=$(date '+%Y-%m-%dT%H:%M:%S%z')" \ @@ -58,7 +58,7 @@ jobs: --annotation "index:org.opencontainers.image.ref.name=docker.io/danielflook/terraform-github-actions-base:$GITHUB_RUN_ID" \ --annotation "index,manifest:builder-id=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ --annotation "index,manifest:ref.tag=$GITHUB_RUN_ID" \ - --annotation "index,manifest:org.opencontainers.image.base.name=docker.io/debian:bookworm-slim" \ + --annotation "index,manifest:org.opencontainers.image.base.name=docker.io/debian:trixie-slim" \ --annotation "index,manifest:base.manifest.digest=$BASE_DIGEST" \ --file image/Dockerfile-base \ --push \ diff --git a/docs-gen/environment_variables/TERRAFORM_PRE_RUN.py b/docs-gen/environment_variables/TERRAFORM_PRE_RUN.py index 88624b17..d6003788 100644 --- a/docs-gen/environment_variables/TERRAFORM_PRE_RUN.py +++ b/docs-gen/environment_variables/TERRAFORM_PRE_RUN.py @@ -7,7 +7,7 @@ The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. -The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. +The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/docs/custom_tools.md b/docs/custom_tools.md index b81cffe4..24d239a4 100644 --- a/docs/custom_tools.md +++ b/docs/custom_tools.md @@ -6,7 +6,7 @@ These are the options available to you. ## Using the released actions The published actions run Terraform/OpenTofu in a container using a pre-prepared image. -The image is based on `debian:bookworm-slim` and is designed to be as small as possible. +The image is based on `debian:trixie-slim` and is designed to be as small as possible. It is possible to use the `TERRAFORM_PRE_RUN` environment variable to customise the environment before running Terraform. The command is run using `bash -xeo pipefail`. diff --git a/image/Dockerfile b/image/Dockerfile index 1c215b17..ce0f2e55 100644 --- a/image/Dockerfile +++ b/image/Dockerfile @@ -1,5 +1,5 @@ # hadolint ignore=DL3007 -FROM danielflook/terraform-github-actions-base:latest +FROM danielflook/terraform-github-actions-base:trixie ARG TARGETARCH diff --git a/image/Dockerfile-base b/image/Dockerfile-base index bea7769c..3681f4e3 100644 --- a/image/Dockerfile-base +++ b/image/Dockerfile-base @@ -7,7 +7,7 @@ RUN git clone https://github.com/cloudposse/tfmask.git \ && make \ && make go/build -FROM debian:bookworm-slim AS terraform-github-actions-base +FROM debian:trixie-slim AS terraform-github-actions-base # Terraform environment variables ENV CHECKPOINT_DISABLE=true diff --git a/image/src/github_pr_comment/comment.py b/image/src/github_pr_comment/comment.py index b58df988..69135775 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -12,11 +12,11 @@ except (ValueError, KeyError): collapse_threshold = 10 -from pkg_resources import get_distribution, DistributionNotFound +from importlib.metadata import version as get_version, PackageNotFoundError try: - version = get_distribution('terraform-github-actions').version -except DistributionNotFound: + version = get_version('terraform-github-actions') +except PackageNotFoundError: version = '0.0.0' class TerraformComment: diff --git a/terraform-apply/README.md b/terraform-apply/README.md index 53f2d948..efe631bb 100644 --- a/terraform-apply/README.md +++ b/terraform-apply/README.md @@ -439,7 +439,7 @@ These input values must be the same as any [`dflook/terraform-plan`](https://git The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/terraform-check/README.md b/terraform-check/README.md index 3363f9f5..79a9756b 100644 --- a/terraform-check/README.md +++ b/terraform-check/README.md @@ -184,7 +184,7 @@ This is intended to run on a schedule to notify if manual changes to your infras The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/terraform-destroy-workspace/README.md b/terraform-destroy-workspace/README.md index f076439b..51dc8611 100644 --- a/terraform-destroy-workspace/README.md +++ b/terraform-destroy-workspace/README.md @@ -205,7 +205,7 @@ This action uses the `terraform destroy` command to destroy all resources in a T The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/terraform-destroy/README.md b/terraform-destroy/README.md index 504cee3d..6d648aca 100644 --- a/terraform-destroy/README.md +++ b/terraform-destroy/README.md @@ -209,7 +209,7 @@ and [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/ The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/terraform-new-workspace/README.md b/terraform-new-workspace/README.md index 1c1565ee..05ee0ac3 100644 --- a/terraform-new-workspace/README.md +++ b/terraform-new-workspace/README.md @@ -129,7 +129,7 @@ Creates a new Terraform workspace. If the workspace already exists, succeeds wit The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/terraform-output/README.md b/terraform-output/README.md index f2856a3e..f0638cce 100644 --- a/terraform-output/README.md +++ b/terraform-output/README.md @@ -181,7 +181,7 @@ Retrieve the root-level outputs from a Terraform configuration. The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/terraform-plan/README.md b/terraform-plan/README.md index baccb74b..9d34d186 100644 --- a/terraform-plan/README.md +++ b/terraform-plan/README.md @@ -360,7 +360,7 @@ The [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/ The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/terraform-refresh/README.md b/terraform-refresh/README.md index e937c6c5..2fd66ce9 100644 --- a/terraform-refresh/README.md +++ b/terraform-refresh/README.md @@ -228,7 +228,7 @@ This will synchronise the Terraform state with the actual resources, but will no The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/terraform-test/README.md b/terraform-test/README.md index 9f868072..0d2fcc41 100644 --- a/terraform-test/README.md +++ b/terraform-test/README.md @@ -186,7 +186,7 @@ If the tests fail, the job will stop with a failure status. The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/terraform-unlock-state/README.md b/terraform-unlock-state/README.md index edc0de2c..7c77a67e 100644 --- a/terraform-unlock-state/README.md +++ b/terraform-unlock-state/README.md @@ -137,7 +137,7 @@ Force unlocks a Terraform remote state. The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/terraform-validate/README.md b/terraform-validate/README.md index 84231da5..f3731b79 100644 --- a/terraform-validate/README.md +++ b/terraform-validate/README.md @@ -156,7 +156,7 @@ If the Terraform configuration is not valid, the build is failed. The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/terraform-version/README.md b/terraform-version/README.md index 58ad92a2..7fa63d62 100644 --- a/terraform-version/README.md +++ b/terraform-version/README.md @@ -187,7 +187,7 @@ outputs yourself. The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/tofu-apply/README.md b/tofu-apply/README.md index 6b8b4645..1eab9396 100644 --- a/tofu-apply/README.md +++ b/tofu-apply/README.md @@ -456,7 +456,7 @@ These input values must be the same as any [`dflook/tofu-plan`](https://github.c The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/tofu-check/README.md b/tofu-check/README.md index 60b9c4bb..cc637eaf 100644 --- a/tofu-check/README.md +++ b/tofu-check/README.md @@ -184,7 +184,7 @@ This is intended to run on a schedule to notify if manual changes to your infras The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/tofu-destroy-workspace/README.md b/tofu-destroy-workspace/README.md index 5d2cfe79..91889868 100644 --- a/tofu-destroy-workspace/README.md +++ b/tofu-destroy-workspace/README.md @@ -205,7 +205,7 @@ This action uses the `tofu destroy` command to destroy all resources in an OpenT The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/tofu-destroy/README.md b/tofu-destroy/README.md index 600f4357..8ca41f1a 100644 --- a/tofu-destroy/README.md +++ b/tofu-destroy/README.md @@ -209,7 +209,7 @@ and [dflook/tofu-apply](https://github.com/dflook/terraform-github-actions/tree/ The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/tofu-new-workspace/README.md b/tofu-new-workspace/README.md index 056ae32b..bd68f1fc 100644 --- a/tofu-new-workspace/README.md +++ b/tofu-new-workspace/README.md @@ -163,7 +163,7 @@ Creates a new OpenTofu workspace. If the workspace already exists, succeeds with The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/tofu-output/README.md b/tofu-output/README.md index ee7c7a89..7f90ac66 100644 --- a/tofu-output/README.md +++ b/tofu-output/README.md @@ -215,7 +215,7 @@ Retrieve the root-level outputs from an OpenTofu configuration. The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/tofu-plan/README.md b/tofu-plan/README.md index 787ed381..871a2d81 100644 --- a/tofu-plan/README.md +++ b/tofu-plan/README.md @@ -377,7 +377,7 @@ The [dflook/tofu-apply](https://github.com/dflook/terraform-github-actions/tree/ The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/tofu-refresh/README.md b/tofu-refresh/README.md index 8827132b..ec4d08b1 100644 --- a/tofu-refresh/README.md +++ b/tofu-refresh/README.md @@ -245,7 +245,7 @@ This will synchronise the OpenTofu state with the actual resources, but will not The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/tofu-test/README.md b/tofu-test/README.md index 7f828085..f6c237a4 100644 --- a/tofu-test/README.md +++ b/tofu-test/README.md @@ -176,7 +176,7 @@ If the tests fail, the job will stop with a failure status. The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/tofu-unlock-state/README.md b/tofu-unlock-state/README.md index b8d9d7b8..246ecdc1 100644 --- a/tofu-unlock-state/README.md +++ b/tofu-unlock-state/README.md @@ -137,7 +137,7 @@ Force unlocks an OpenTofu remote state. The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/tofu-validate/README.md b/tofu-validate/README.md index 0c4464ee..fe102620 100644 --- a/tofu-validate/README.md +++ b/tofu-validate/README.md @@ -156,7 +156,7 @@ If the OpenTofu configuration is not valid, the build is failed. The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: diff --git a/tofu-version/README.md b/tofu-version/README.md index 60714cbf..d8a369cf 100644 --- a/tofu-version/README.md +++ b/tofu-version/README.md @@ -190,7 +190,7 @@ outputs yourself. The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. - The runtime image is currently based on `debian:bookworm`, with the command run using `bash -xeo pipefail`. + The runtime image is currently based on `debian:trixie`, with the command run using `bash -xeo pipefail`. For example: From 3268ae9ad428cd9c2823781f314ed9eb7441d1e2 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Mon, 2 Mar 2026 10:12:17 +0000 Subject: [PATCH 02/32] Add underscored output variants Output names use an annoying mix of underscores and hypens. This adds an underscored variant for the hypenated variants for consistency. The hypen outputs are not deprecated and will not be removed. --- .github/workflows/test-apply.yaml | 24 ++++++++++ .github/workflows/test-binary-plan.yaml | 6 +++ .github/workflows/test-changes-only.yaml | 24 ++++++++++ .github/workflows/test-check.yaml | 12 +++++ .github/workflows/test-cloud.yaml | 12 +++++ .github/workflows/test-fmt-check.yaml | 12 +++++ .github/workflows/test-refresh.yaml | 6 +++ .github/workflows/test-test.yaml | 54 +++++++++++++++++++++ .github/workflows/test-unlock-state.yaml | 60 ++++++++++++++++++++++++ .github/workflows/test-validate.yaml | 24 ++++++++++ docs-gen/action.py | 6 +-- docs-gen/actions/apply.py | 2 +- docs-gen/actions/check.py | 2 +- docs-gen/actions/destroy.py | 2 +- docs-gen/actions/destroy_workspace.py | 2 +- docs-gen/actions/fmt_check.py | 2 +- docs-gen/actions/validate.py | 2 +- docs-gen/outputs/failure_reason.py | 3 +- docs-gen/outputs/junit_xml.py | 3 +- docs-gen/outputs/lock_info.py | 5 +- image/src/github_actions/commands.py | 15 ++++-- image/workflow_commands.sh | 9 ++++ terraform-apply/README.md | 6 ++- terraform-apply/action.yaml | 29 +++++++++++- terraform-check/README.md | 3 +- terraform-check/action.yaml | 5 ++ terraform-destroy-workspace/README.md | 6 ++- terraform-destroy-workspace/action.yaml | 28 ++++++++++- terraform-destroy/README.md | 6 ++- terraform-destroy/action.yaml | 28 ++++++++++- terraform-fmt-check/README.md | 3 +- terraform-fmt-check/action.yaml | 5 ++ terraform-refresh/README.md | 4 +- terraform-refresh/action.yaml | 28 ++++++++++- terraform-test/README.md | 2 + terraform-test/action.yaml | 16 +++++++ terraform-validate/README.md | 3 +- terraform-validate/action.yaml | 5 ++ tofu-apply/README.md | 6 ++- tofu-apply/action.yaml | 29 +++++++++++- tofu-check/README.md | 3 +- tofu-check/action.yaml | 5 ++ tofu-destroy-workspace/README.md | 6 ++- tofu-destroy-workspace/action.yaml | 28 ++++++++++- tofu-destroy/README.md | 6 ++- tofu-destroy/action.yaml | 28 ++++++++++- tofu-fmt-check/README.md | 3 +- tofu-fmt-check/action.yaml | 5 ++ tofu-refresh/README.md | 4 +- tofu-refresh/action.yaml | 28 ++++++++++- tofu-test/README.md | 1 + tofu-test/action.yaml | 9 ++++ tofu-validate/README.md | 3 +- tofu-validate/action.yaml | 5 ++ 54 files changed, 588 insertions(+), 45 deletions(-) diff --git a/.github/workflows/test-apply.yaml b/.github/workflows/test-apply.yaml index 1c35165e..344ec777 100644 --- a/.github/workflows/test-apply.yaml +++ b/.github/workflows/test-apply.yaml @@ -74,6 +74,7 @@ jobs: env: OUTCOME: ${{ steps.apply.outcome }} FAILURE_REASON: ${{ steps.apply.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.apply.outputs.failure_reason }} JSON_PLAN_PATH: ${{ steps.apply.outputs.json_plan_path }} TEXT_PLAN_PATH: ${{ steps.apply.outputs.text_plan_path }} RUN_ID: ${{ steps.apply.outputs.run_id }} @@ -88,6 +89,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + if [[ -n "$JSON_PLAN_PATH" ]]; then echo "::error:: json_plan_path should not be set" exit 1 @@ -137,6 +143,7 @@ jobs: env: OUTCOME: ${{ steps.apply.outcome }} FAILURE_REASON: ${{ steps.apply.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.apply.outputs.failure_reason }} JSON_PLAN_PATH: ${{ steps.apply.outputs.json_plan_path }} TEXT_PLAN_PATH: ${{ steps.apply.outputs.text_plan_path }} RUN_ID: ${{ steps.apply.outputs.run_id }} @@ -151,6 +158,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + cat "$JSON_PLAN_PATH" if [[ $(jq -r .format_version "$JSON_PLAN_PATH") != "1.2" ]]; then echo "::error:: json_plan_path not set correctly" @@ -187,6 +199,7 @@ jobs: env: OUTCOME: ${{ steps.apply.outcome }} FAILURE_REASON: ${{ steps.apply.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.apply.outputs.failure_reason }} RUN_ID: ${{ steps.apply.outputs.run_id }} run: | if [[ "$OUTCOME" != "failure" ]]; then @@ -199,6 +212,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + if [[ -n "$RUN_ID" ]]; then echo "::error:: run_id should not be set" exit 1 @@ -1197,6 +1215,7 @@ jobs: env: OUTCOME: ${{ steps.apply.outcome }} FAILURE_REASON: ${{ steps.apply.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.apply.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Apply did not fail correctly" @@ -1208,6 +1227,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + partial_backend_fingerprint: runs-on: ubuntu-24.04 name: Get comment using partial fingerprint diff --git a/.github/workflows/test-binary-plan.yaml b/.github/workflows/test-binary-plan.yaml index bf920a1d..9c74f882 100644 --- a/.github/workflows/test-binary-plan.yaml +++ b/.github/workflows/test-binary-plan.yaml @@ -128,6 +128,7 @@ jobs: env: OUTCOME: ${{ steps.apply.outcome }} FAILURE_REASON: ${{ steps.apply.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.apply.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Apply did not fail correctly" @@ -138,3 +139,8 @@ jobs: echo "::error:: failure-reason not set correctly" exit 1 fi + + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi diff --git a/.github/workflows/test-changes-only.yaml b/.github/workflows/test-changes-only.yaml index b5e26a33..4a34994d 100644 --- a/.github/workflows/test-changes-only.yaml +++ b/.github/workflows/test-changes-only.yaml @@ -48,12 +48,18 @@ jobs: - name: Check failure-reason env: FAILURE_REASON: ${{ steps.apply.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.apply.outputs.failure_reason }} run: | if [[ "$FAILURE_REASON" != "" ]]; then echo "::error:: failure-reason not set correctly" exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + change_then_no_changes: runs-on: ubuntu-24.04 name: changes-only should still replace a change comment @@ -118,12 +124,18 @@ jobs: - name: Check failure-reason env: FAILURE_REASON: ${{ steps.apply.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.apply.outputs.failure_reason }} run: | if [[ "$FAILURE_REASON" != "" ]]; then echo "::error:: failure-reason not set correctly" exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + no_changes_then_changes: runs-on: ubuntu-24.04 name: Apply with changes should fail after a changes-only plan with no changes @@ -171,6 +183,7 @@ jobs: env: OUTCOME: ${{ steps.apply.outcome }} FAILURE_REASON: ${{ steps.apply.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.apply.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Apply did not fail correctly" @@ -182,6 +195,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + apply_when_plan_has_changed: runs-on: ubuntu-24.04 name: Apply should fail if the approved plan has changed @@ -219,6 +237,7 @@ jobs: env: OUTCOME: ${{ steps.apply.outcome }} FAILURE_REASON: ${{ steps.apply.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.apply.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Apply did not fail correctly" @@ -229,3 +248,8 @@ jobs: echo "::error:: failure-reason not set correctly" exit 1 fi + + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi diff --git a/.github/workflows/test-check.yaml b/.github/workflows/test-check.yaml index cb3631d2..e08dbf86 100644 --- a/.github/workflows/test-check.yaml +++ b/.github/workflows/test-check.yaml @@ -25,12 +25,18 @@ jobs: - name: Check failure-reason env: FAILURE_REASON: ${{ steps.check.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.check.outputs.failure_reason }} run: | if [[ "$FAILURE_REASON" != "" ]]; then echo "::error:: failure-reason not set correctly" exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + plan_change_comment: runs-on: ubuntu-24.04 name: Changes @@ -51,6 +57,7 @@ jobs: env: OUTCOME: ${{ steps.check.outcome }} FAILURE_REASON: ${{ steps.check.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.check.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Check did not fail correctly" @@ -61,3 +68,8 @@ jobs: echo "::error:: failure-reason not set correctly" exit 1 fi + + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi diff --git a/.github/workflows/test-cloud.yaml b/.github/workflows/test-cloud.yaml index 7c44d600..c19f8b9d 100644 --- a/.github/workflows/test-cloud.yaml +++ b/.github/workflows/test-cloud.yaml @@ -168,6 +168,7 @@ jobs: env: CHECK_OUTCOME: ${{ steps.check.outcome }} FAILURE_REASON: ${{ steps.check.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.check.outputs.failure_reason }} run: | if [[ "$CHECK_OUTCOME" != "failure" ]]; then echo "Check didn't fail correctly" @@ -179,6 +180,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + - name: Destroy workspace uses: ./terraform-destroy-workspace with: @@ -410,6 +416,7 @@ jobs: env: CHECK_OUTCOME: ${{ steps.check.outcome }} FAILURE_REASON: ${{ steps.check.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.check.outputs.failure_reason }} run: | if [[ "$CHECK_OUTCOME" != "failure" ]]; then echo "Check didn't fail correctly" @@ -421,6 +428,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + - name: Destroy workspace uses: ./terraform-destroy-workspace with: diff --git a/.github/workflows/test-fmt-check.yaml b/.github/workflows/test-fmt-check.yaml index 193762e1..7512af88 100644 --- a/.github/workflows/test-fmt-check.yaml +++ b/.github/workflows/test-fmt-check.yaml @@ -25,12 +25,18 @@ jobs: - name: Check valid env: FAILURE_REASON: ${{ steps.fmt-check.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.fmt-check.outputs.failure_reason }} run: | if [[ "$FAILURE_REASON" != "" ]]; then echo "::error:: failure-reason not set correctly" exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + non_canonical_fmt: runs-on: ubuntu-24.04 name: Non canonical fmt @@ -52,6 +58,7 @@ jobs: env: OUTCOME: ${{ steps.fmt-check.outcome }} FAILURE_REASON: ${{ steps.fmt-check.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.fmt-check.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "fmt-check did not fail correctly" @@ -62,3 +69,8 @@ jobs: echo "::error:: failure-reason not set correctly" exit 1 fi + + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi diff --git a/.github/workflows/test-refresh.yaml b/.github/workflows/test-refresh.yaml index 589c80e7..5bf83990 100644 --- a/.github/workflows/test-refresh.yaml +++ b/.github/workflows/test-refresh.yaml @@ -119,6 +119,7 @@ jobs: env: OUTCOME: ${{ steps.apply.outcome }} FAILURE_REASON: ${{ steps.apply.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.apply.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Apply did not fail correctly" @@ -130,6 +131,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + - name: Apply without refresh uses: ./terraform-apply with: diff --git a/.github/workflows/test-test.yaml b/.github/workflows/test-test.yaml index 35796418..10ad0e37 100644 --- a/.github/workflows/test-test.yaml +++ b/.github/workflows/test-test.yaml @@ -27,18 +27,30 @@ jobs: - name: Check Passed env: FAILURE_REASON: ${{ steps.test.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.test.outputs.failure_reason }} JUNIT_XML_PATH: ${{ steps.test.outputs.junit-xml-path }} + JUNIT_XML_PATH_UNDERSCORE: ${{ steps.test.outputs.junit_xml_path }} run: | if [[ "$FAILURE_REASON" != "" ]]; then echo "::error:: failure-reason not set correctly" exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + if [[ "$JUNIT_XML_PATH" != "" ]]; then echo "::error:: junit-xml-path should not be set" exit 1 fi + if [[ "$JUNIT_XML_PATH_UNDERSCORE" != "$JUNIT_XML_PATH" ]]; then + echo "::error:: junit_xml_path not set correctly" + exit 1 + fi + junit: runs-on: ubuntu-24.04 name: Junit support @@ -59,18 +71,30 @@ jobs: - name: Check Passed env: FAILURE_REASON: ${{ steps.test.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.test.outputs.failure_reason }} JUNIT_XML_PATH: ${{ steps.test.outputs.junit-xml-path }} + JUNIT_XML_PATH_UNDERSCORE: ${{ steps.test.outputs.junit_xml_path }} run: | if [[ "$FAILURE_REASON" != "" ]]; then echo "::error:: failure-reason not set correctly" exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + if [[ "$JUNIT_XML_PATH" == "" ]]; then echo "::error:: junit-xml-path should be set" exit 1 fi + if [[ "$JUNIT_XML_PATH_UNDERSCORE" != "$JUNIT_XML_PATH" ]]; then + echo "::error:: junit_xml_path not set correctly" + exit 1 + fi + # Check the output looks right if [[ ! -f "$JUNIT_XML_PATH" ]]; then echo "::error:: junit-xml-path does not point to a file" @@ -101,12 +125,18 @@ jobs: - name: Check Passed env: FAILURE_REASON: ${{ steps.test.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.test.outputs.failure_reason }} run: | if [[ "$FAILURE_REASON" != "" ]]; then echo "::error:: failure-reason not set correctly" exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + test_dir: runs-on: ubuntu-24.04 name: Custom test directory @@ -129,12 +159,18 @@ jobs: - name: Check Passed env: FAILURE_REASON: ${{ steps.test.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.test.outputs.failure_reason }} run: | if [[ "$FAILURE_REASON" != "" ]]; then echo "::error:: failure-reason not set correctly" exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + nonexistent_test_dir: runs-on: ubuntu-24.04 name: Missing test directory @@ -156,6 +192,7 @@ jobs: env: OUTCOME: ${{ steps.nonexistent_test_dir.outcome }} FAILURE_REASON: ${{ steps.nonexistent_test_dir.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.nonexistent_test_dir.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Test did not fail correctly" @@ -167,6 +204,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + faulty_filter: runs-on: ubuntu-24.04 name: Filter matches no tests @@ -190,6 +232,7 @@ jobs: env: OUTCOME: ${{ steps.faulty_filter.outcome }} FAILURE_REASON: ${{ steps.faulty_filter.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.faulty_filter.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Test did not fail correctly" @@ -201,6 +244,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + failing: runs-on: ubuntu-24.04 name: A failing test using variables @@ -224,6 +272,7 @@ jobs: env: OUTCOME: ${{ steps.failing.outcome }} FAILURE_REASON: ${{ steps.failing.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.failing.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Test did not fail correctly" @@ -234,3 +283,8 @@ jobs: echo "::error:: failure-reason not set correctly" exit 1 fi + + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi diff --git a/.github/workflows/test-unlock-state.yaml b/.github/workflows/test-unlock-state.yaml index f9761b0b..50efd51e 100644 --- a/.github/workflows/test-unlock-state.yaml +++ b/.github/workflows/test-unlock-state.yaml @@ -42,6 +42,7 @@ jobs: env: OUTCOME: ${{ steps.failed-apply.outcome }} FAILURE_REASON: ${{ steps.failed-apply.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.failed-apply.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Apply did not fail correctly" @@ -53,6 +54,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + # Check state-locked - name: Try using locked state using terraform-apply uses: ./terraform-apply @@ -66,7 +72,9 @@ jobs: env: OUTCOME: ${{ steps.locked-state-apply.outcome }} FAILURE_REASON: ${{ steps.locked-state-apply.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.locked-state-apply.outputs.failure_reason }} LOCK_INFO: ${{ steps.locked-state-apply.outputs.lock-info }} + LOCK_INFO_UNDERSCORE: ${{ steps.locked-state-apply.outputs.lock_info }} LOCK_ID: ${{ fromJson(steps.locked-state-apply.outputs.lock-info).ID }} run: | if [[ "$OUTCOME" != "failure" ]]; then @@ -79,6 +87,16 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + + if [[ "$LOCK_INFO_UNDERSCORE" != "$LOCK_INFO" ]]; then + echo "::error:: lock_info not set correctly" + exit 1 + fi + echo "$LOCK_INFO" echo "Lock id is $LOCK_ID" @@ -93,7 +111,9 @@ jobs: env: OUTCOME: ${{ steps.locked-state-destroy.outcome }} FAILURE_REASON: ${{ steps.locked-state-destroy.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.locked-state-destroy.outputs.failure_reason }} LOCK_INFO: ${{ steps.locked-state-destroy.outputs.lock-info }} + LOCK_INFO_UNDERSCORE: ${{ steps.locked-state-destroy.outputs.lock_info }} LOCK_ID: ${{ fromJson(steps.locked-state-destroy.outputs.lock-info).ID }} run: | if [[ "$OUTCOME" != "failure" ]]; then @@ -106,6 +126,16 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + + if [[ "$LOCK_INFO_UNDERSCORE" != "$LOCK_INFO" ]]; then + echo "::error:: lock_info not set correctly" + exit 1 + fi + echo "$LOCK_INFO" echo "Lock id is $LOCK_ID" @@ -161,6 +191,7 @@ jobs: env: OUTCOME: ${{ steps.failed-apply-workspace.outcome }} FAILURE_REASON: ${{ steps.failed-apply-workspace.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.failed-apply-workspace.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Apply did not fail correctly" @@ -172,6 +203,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + # Check state-locked - name: Try using locked state uses: ./terraform-apply @@ -186,7 +222,9 @@ jobs: env: OUTCOME: ${{ steps.locked-state-workspace.outcome }} FAILURE_REASON: ${{ steps.locked-state-workspace.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.locked-state-workspace.outputs.failure_reason }} LOCK_INFO: ${{ steps.locked-state-workspace.outputs.lock-info }} + LOCK_INFO_UNDERSCORE: ${{ steps.locked-state-workspace.outputs.lock_info }} LOCK_ID: ${{ fromJson(steps.locked-state-workspace.outputs.lock-info).ID }} run: | if [[ "$OUTCOME" != "failure" ]]; then @@ -199,6 +237,16 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + + if [[ "$LOCK_INFO_UNDERSCORE" != "$LOCK_INFO" ]]; then + echo "::error:: lock_info not set correctly" + exit 1 + fi + echo "$LOCK_INFO" echo "Lock id is $LOCK_ID" @@ -214,7 +262,9 @@ jobs: env: OUTCOME: ${{ steps.locked-state-destroy-workspace.outcome }} FAILURE_REASON: ${{ steps.locked-state-destroy-workspace.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.locked-state-destroy-workspace.outputs.failure_reason }} LOCK_INFO: ${{ steps.locked-state-destroy-workspace.outputs.lock-info }} + LOCK_INFO_UNDERSCORE: ${{ steps.locked-state-destroy-workspace.outputs.lock_info }} LOCK_ID: ${{ fromJson(steps.locked-state-destroy-workspace.outputs.lock-info).ID }} run: | if [[ "$OUTCOME" != "failure" ]]; then @@ -227,6 +277,16 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + + if [[ "$LOCK_INFO_UNDERSCORE" != "$LOCK_INFO" ]]; then + echo "::error:: lock_info not set correctly" + exit 1 + fi + echo "$LOCK_INFO" echo "Lock id is $LOCK_ID" diff --git a/.github/workflows/test-validate.yaml b/.github/workflows/test-validate.yaml index f7be0418..4bbe3841 100644 --- a/.github/workflows/test-validate.yaml +++ b/.github/workflows/test-validate.yaml @@ -25,12 +25,18 @@ jobs: - name: Check valid env: FAILURE_REASON: ${{ steps.validate.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.validate.outputs.failure_reason }} run: | if [[ "$FAILURE_REASON" != "" ]]; then echo "::error:: failure-reason not set correctly" exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + invalid: runs-on: ubuntu-24.04 name: Invalid terraform configuration @@ -51,6 +57,7 @@ jobs: env: OUTCOME: ${{ steps.validate.outcome }} FAILURE_REASON: ${{ steps.validate.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.validate.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Validate did not fail correctly" @@ -62,6 +69,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + validate_workspace: runs-on: ubuntu-24.04 name: Use workspace name during validation @@ -94,6 +106,7 @@ jobs: env: OUTCOME: ${{ steps.validate.outcome }} FAILURE_REASON: ${{ steps.validate.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.validate.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Validate did not fail correctly" @@ -105,6 +118,11 @@ jobs: exit 1 fi + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi + validate_remote_workspace: runs-on: ubuntu-24.04 name: Use workspace name during validation @@ -140,6 +158,7 @@ jobs: env: OUTCOME: ${{ steps.validate.outcome }} FAILURE_REASON: ${{ steps.validate.outputs.failure-reason }} + FAILURE_REASON_UNDERSCORE: ${{ steps.validate.outputs.failure_reason }} run: | if [[ "$OUTCOME" != "failure" ]]; then echo "Validate did not fail correctly" @@ -150,3 +169,8 @@ jobs: echo "::error:: failure-reason not set correctly" exit 1 fi + + if [[ "$FAILURE_REASON_UNDERSCORE" != "$FAILURE_REASON" ]]; then + echo "::error:: failure_reason not set correctly" + exit 1 + fi diff --git a/docs-gen/action.py b/docs-gen/action.py index 40c14706..2102bd82 100644 --- a/docs-gen/action.py +++ b/docs-gen/action.py @@ -187,10 +187,10 @@ def assert_ordering(self): "plan_path", "json_plan_path", "text_plan_path", - "junit-xml-path", + "junit_xml_path", "to_add", - "failure-reason", - "lock-info", + "failure_reason", + "lock_info", "run_id", "terraform", "tofu", diff --git a/docs-gen/actions/apply.py b/docs-gen/actions/apply.py index d6739157..ffa08ed5 100644 --- a/docs-gen/actions/apply.py +++ b/docs-gen/actions/apply.py @@ -344,7 +344,7 @@ - name: Retry failed apply uses: dflook/$ToolName-apply@v2 - if: ${{ steps.first_try.outputs.failure-reason == 'apply-failed' }} + if: ${{ steps.first_try.outputs.failure_reason == 'apply-failed' }} with: path: $ToolName auto_approve: true diff --git a/docs-gen/actions/check.py b/docs-gen/actions/check.py index a941f516..e0657392 100644 --- a/docs-gen/actions/check.py +++ b/docs-gen/actions/check.py @@ -99,7 +99,7 @@ path: my-$ToolName-configuration - name: Changes detected - if: ${{ failure() && steps.check.outputs.failure-reason == 'changes-to-apply' }} + if: ${{ failure() && steps.check.outputs.failure_reason == 'changes-to-apply' }} run: echo "There are outstanding changes to apply" ``` ''' diff --git a/docs-gen/actions/destroy.py b/docs-gen/actions/destroy.py index 7d6f2f96..f413ae89 100644 --- a/docs-gen/actions/destroy.py +++ b/docs-gen/actions/destroy.py @@ -109,7 +109,7 @@ - name: Retry failed destroy uses: dflook/$ToolName-destroy@v2 - if: ${{ steps.first_try.outputs.failure-reason == 'destroy-failed' }} + if: ${{ steps.first_try.outputs.failure_reason == 'destroy-failed' }} with: path: my-$ToolName-config workspace: ${{ github.head_ref }} diff --git a/docs-gen/actions/destroy_workspace.py b/docs-gen/actions/destroy_workspace.py index b18748f8..35d0d6e0 100644 --- a/docs-gen/actions/destroy_workspace.py +++ b/docs-gen/actions/destroy_workspace.py @@ -108,7 +108,7 @@ - name: Retry failed destroy uses: dflook/$ToolName-destroy-workspace@v2 - if: ${{ steps.first_try.outputs.failure-reason == 'destroy-failed' }} + if: ${{ steps.first_try.outputs.failure_reason == 'destroy-failed' }} with: path: my-$ToolName-config workspace: ${{ github.head_ref }} diff --git a/docs-gen/actions/fmt_check.py b/docs-gen/actions/fmt_check.py index fbd9a7b7..4f43e074 100644 --- a/docs-gen/actions/fmt_check.py +++ b/docs-gen/actions/fmt_check.py @@ -99,7 +99,7 @@ path: my-$ToolName-config - name: Wrong formatting found - if: ${{ failure() && steps.fmt-check.outputs.failure-reason == 'check-failed' }} + if: ${{ failure() && steps.fmt-check.outputs.failure_reason == 'check-failed' }} run: echo "formatting check failed" ``` ''' diff --git a/docs-gen/actions/validate.py b/docs-gen/actions/validate.py index 5b58324f..e21b8063 100644 --- a/docs-gen/actions/validate.py +++ b/docs-gen/actions/validate.py @@ -100,7 +100,7 @@ path: my-$ToolName-config - name: Validate failed - if: ${{ failure() && steps.validate.outputs.failure-reason == 'validate-failed' }} + if: ${{ failure() && steps.validate.outputs.failure_reason == 'validate-failed' }} run: echo "$ToolName validate failed" ``` ''' diff --git a/docs-gen/outputs/failure_reason.py b/docs-gen/outputs/failure_reason.py index 8a6557fe..53747953 100644 --- a/docs-gen/outputs/failure_reason.py +++ b/docs-gen/outputs/failure_reason.py @@ -1,7 +1,8 @@ from action import Output failure_reason = Output( - name='failure-reason', + name='failure_reason', + aliases=['failure-reason'], type='string', description=''' When the job outcome is `failure` because of a known reason, this will be set to that reason. diff --git a/docs-gen/outputs/junit_xml.py b/docs-gen/outputs/junit_xml.py index 3dff3895..d592f6ec 100644 --- a/docs-gen/outputs/junit_xml.py +++ b/docs-gen/outputs/junit_xml.py @@ -1,7 +1,8 @@ from action import Output, Terraform junit_xml_path = Output( - name='junit-xml-path', + name='junit_xml_path', + aliases=['junit-xml-path'], type='string', description=''' A test report in JUnit XML format. diff --git a/docs-gen/outputs/lock_info.py b/docs-gen/outputs/lock_info.py index 2a1d6223..220d4521 100644 --- a/docs-gen/outputs/lock_info.py +++ b/docs-gen/outputs/lock_info.py @@ -1,10 +1,11 @@ from action import Output lock_info = Output( - name='lock-info', + name='lock_info', + aliases=['lock-info'], type='string', description=''' -When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. +When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: diff --git a/image/src/github_actions/commands.py b/image/src/github_actions/commands.py index a307741f..d7b475de 100644 --- a/image/src/github_actions/commands.py +++ b/image/src/github_actions/commands.py @@ -8,10 +8,7 @@ def generate_delimiter(): return ''.join(random.choice(string.ascii_lowercase) for _ in range(20)) -def output(name: str, value: Any) -> None: - if not isinstance(value, str): - value = str(value) - +def _write_output(name: str, value: str) -> None: if 'GITHUB_OUTPUT' in os.environ and Path(os.environ['GITHUB_OUTPUT']).is_file(): with open(os.environ['GITHUB_OUTPUT'], 'a') as f: if len(value.splitlines()) > 1: @@ -27,6 +24,16 @@ def output(name: str, value: Any) -> None: else: sys.stdout.write(f'::set-output name={name}::{value}\n') +def output(name: str, value: Any) -> None: + if not isinstance(value, str): + value = str(value) + + _write_output(name, value) + + underscore_name = name.replace('-', '_') + if underscore_name != name: + _write_output(underscore_name, value) + def mask(value: str) -> None: for line in value.splitlines(): sys.stdout.write(f'::add-mask::{line}\n') diff --git a/image/workflow_commands.sh b/image/workflow_commands.sh index 4ac44e9b..5fad4acd 100644 --- a/image/workflow_commands.sh +++ b/image/workflow_commands.sh @@ -73,6 +73,15 @@ function set_output() { else echo "::set-output name=${name}::${value}" fi + + local underscore_name="${name//-/_}" + if [[ "$underscore_name" != "$name" ]]; then + if [[ -v GITHUB_OUTPUT && -f "$GITHUB_OUTPUT" ]]; then + echo "${underscore_name}=${value}" >> "$GITHUB_OUTPUT" + else + echo "::set-output name=${underscore_name}::${value}" + fi + fi } ## diff --git a/terraform-apply/README.md b/terraform-apply/README.md index efe631bb..4fe08d08 100644 --- a/terraform-apply/README.md +++ b/terraform-apply/README.md @@ -230,6 +230,7 @@ These input values must be the same as any [`dflook/terraform-plan`](https://git - Type: string +* `failure_reason` * `failure-reason` When the job outcome is `failure`, this output may be set. The value may be one of: @@ -243,9 +244,10 @@ These input values must be the same as any [`dflook/terraform-plan`](https://git - Type: string +* `lock_info` * `lock-info` - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: @@ -671,7 +673,7 @@ jobs: - name: Retry failed apply uses: dflook/terraform-apply@v2 - if: ${{ steps.first_try.outputs.failure-reason == 'apply-failed' }} + if: ${{ steps.first_try.outputs.failure_reason == 'apply-failed' }} with: path: terraform auto_approve: true diff --git a/terraform-apply/action.yaml b/terraform-apply/action.yaml index 5c71fb1f..18cd2c6c 100644 --- a/terraform-apply/action.yaml +++ b/terraform-apply/action.yaml @@ -111,6 +111,16 @@ outputs: This is the path to the generated plan in a human-readable format. The path is relative to the Actions workspace. This won't be set if `auto_approve` is true while using a `remote` backend. + failure_reason: + description: | + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `apply-failed` - The Terraform apply operation failed. + - `plan-changed` - The approved plan is no longer accurate, so the apply will not be attempted. + - `state-locked` - The Terraform state lock could not be obtained because it was already locked. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run steps. failure-reason: description: | When the job outcome is `failure`, this output may be set. The value may be one of: @@ -121,9 +131,26 @@ outputs: If the job fails for any other reason this will not be set. This can be used with the Actions expression syntax to conditionally run steps. + lock_info: + description: | + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. + + It is a json object containing any available state lock information and typically has the form: + + ```json + { + "ID": "838fbfde-c5cd-297f-84a4-d7578b4a4880", + "Path": "terraform-github-actions/test-unlock-state", + "Operation": "OperationTypeApply", + "Who": "root@e9d43b0c6478", + "Version": "1.3.7", + "Created": "2023-01-28 00:16:41.560904373 +0000 UTC", + "Info": "" + } + ``` lock-info: description: | - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: diff --git a/terraform-check/README.md b/terraform-check/README.md index 79a9756b..6c46f187 100644 --- a/terraform-check/README.md +++ b/terraform-check/README.md @@ -93,6 +93,7 @@ This is intended to run on a schedule to notify if manual changes to your infras ## Outputs +* `failure_reason` * `failure-reason` When the job outcome is `failure` because the there are outstanding changes to apply, this will be set to 'changes-to-apply'. @@ -251,6 +252,6 @@ jobs: path: my-terraform-configuration - name: Changes detected - if: ${{ failure() && steps.check.outputs.failure-reason == 'changes-to-apply' }} + if: ${{ failure() && steps.check.outputs.failure_reason == 'changes-to-apply' }} run: echo "There are outstanding changes to apply" ``` diff --git a/terraform-check/action.yaml b/terraform-check/action.yaml index 96f4c2af..802a0890 100644 --- a/terraform-check/action.yaml +++ b/terraform-check/action.yaml @@ -38,6 +38,11 @@ inputs: default: "0" outputs: + failure_reason: + description: | + When the job outcome is `failure` because the there are outstanding changes to apply, this will be set to 'changes-to-apply'. + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run a step when there are changes to apply. failure-reason: description: | When the job outcome is `failure` because the there are outstanding changes to apply, this will be set to 'changes-to-apply'. diff --git a/terraform-destroy-workspace/README.md b/terraform-destroy-workspace/README.md index 51dc8611..cb24af4e 100644 --- a/terraform-destroy-workspace/README.md +++ b/terraform-destroy-workspace/README.md @@ -90,6 +90,7 @@ This action uses the `terraform destroy` command to destroy all resources in a T ## Outputs +* `failure_reason` * `failure-reason` When the job outcome is `failure`, this output may be set. The value may be one of: @@ -102,9 +103,10 @@ This action uses the `terraform destroy` command to destroy all resources in a T - Type: string +* `lock_info` * `lock-info` - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: @@ -275,7 +277,7 @@ jobs: - name: Retry failed destroy uses: dflook/terraform-destroy-workspace@v2 - if: ${{ steps.first_try.outputs.failure-reason == 'destroy-failed' }} + if: ${{ steps.first_try.outputs.failure_reason == 'destroy-failed' }} with: path: my-terraform-config workspace: ${{ github.head_ref }} diff --git a/terraform-destroy-workspace/action.yaml b/terraform-destroy-workspace/action.yaml index c21af2f9..43bb6057 100644 --- a/terraform-destroy-workspace/action.yaml +++ b/terraform-destroy-workspace/action.yaml @@ -37,6 +37,15 @@ inputs: default: "0" outputs: + failure_reason: + description: | + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `destroy-failed` - The Terraform destroy operation failed. + - `state-locked` - The Terraform state lock could not be obtained because it was already locked. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run a steps. failure-reason: description: | When the job outcome is `failure`, this output may be set. The value may be one of: @@ -46,9 +55,26 @@ outputs: If the job fails for any other reason this will not be set. This can be used with the Actions expression syntax to conditionally run a steps. + lock_info: + description: | + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. + + It is a json object containing any available state lock information and typically has the form: + + ```json + { + "ID": "838fbfde-c5cd-297f-84a4-d7578b4a4880", + "Path": "terraform-github-actions/test-unlock-state", + "Operation": "OperationTypeApply", + "Who": "root@e9d43b0c6478", + "Version": "1.3.7", + "Created": "2023-01-28 00:16:41.560904373 +0000 UTC", + "Info": "" + } + ``` lock-info: description: | - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: diff --git a/terraform-destroy/README.md b/terraform-destroy/README.md index 6d648aca..77635416 100644 --- a/terraform-destroy/README.md +++ b/terraform-destroy/README.md @@ -94,6 +94,7 @@ and [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/ ## Outputs +* `failure_reason` * `failure-reason` When the job outcome is `failure`, this output may be set. The value may be one of: @@ -106,9 +107,10 @@ and [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/ - Type: string +* `lock_info` * `lock-info` - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: @@ -279,7 +281,7 @@ jobs: - name: Retry failed destroy uses: dflook/terraform-destroy@v2 - if: ${{ steps.first_try.outputs.failure-reason == 'destroy-failed' }} + if: ${{ steps.first_try.outputs.failure_reason == 'destroy-failed' }} with: path: my-terraform-config workspace: ${{ github.head_ref }} diff --git a/terraform-destroy/action.yaml b/terraform-destroy/action.yaml index 8fdbe803..5cc5e72a 100644 --- a/terraform-destroy/action.yaml +++ b/terraform-destroy/action.yaml @@ -38,6 +38,15 @@ inputs: default: "0" outputs: + failure_reason: + description: | + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `destroy-failed` - The Terraform destroy operation failed. + - `state-locked` - The Terraform state lock could not be obtained because it was already locked. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run a steps. failure-reason: description: | When the job outcome is `failure`, this output may be set. The value may be one of: @@ -47,9 +56,26 @@ outputs: If the job fails for any other reason this will not be set. This can be used with the Actions expression syntax to conditionally run a steps. + lock_info: + description: | + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. + + It is a json object containing any available state lock information and typically has the form: + + ```json + { + "ID": "838fbfde-c5cd-297f-84a4-d7578b4a4880", + "Path": "terraform-github-actions/test-unlock-state", + "Operation": "OperationTypeApply", + "Who": "root@e9d43b0c6478", + "Version": "1.3.7", + "Created": "2023-01-28 00:16:41.560904373 +0000 UTC", + "Info": "" + } + ``` lock-info: description: | - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: diff --git a/terraform-fmt-check/README.md b/terraform-fmt-check/README.md index 1e2addba..e89c8a84 100644 --- a/terraform-fmt-check/README.md +++ b/terraform-fmt-check/README.md @@ -55,6 +55,7 @@ If any files are not correctly formatted a failing GitHub check will be added fo ## Outputs +* `failure_reason` * `failure-reason` When the job outcome is `failure` because the format check failed, this will be set to 'check-failed'. @@ -145,6 +146,6 @@ jobs: path: my-terraform-config - name: Wrong formatting found - if: ${{ failure() && steps.fmt-check.outputs.failure-reason == 'check-failed' }} + if: ${{ failure() && steps.fmt-check.outputs.failure_reason == 'check-failed' }} run: echo "formatting check failed" ``` diff --git a/terraform-fmt-check/action.yaml b/terraform-fmt-check/action.yaml index af21add0..0d5dcd65 100644 --- a/terraform-fmt-check/action.yaml +++ b/terraform-fmt-check/action.yaml @@ -28,6 +28,11 @@ inputs: default: "" outputs: + failure_reason: + description: | + When the job outcome is `failure` because the format check failed, this will be set to 'check-failed'. + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run a step when the format check fails. failure-reason: description: | When the job outcome is `failure` because the format check failed, this will be set to 'check-failed'. diff --git a/terraform-refresh/README.md b/terraform-refresh/README.md index 2fd66ce9..eed94fdf 100644 --- a/terraform-refresh/README.md +++ b/terraform-refresh/README.md @@ -107,6 +107,7 @@ This will synchronise the Terraform state with the actual resources, but will no ## Outputs +* `failure_reason` * `failure-reason` When the job outcome is `failure`, this output may be set. The value may be one of: @@ -119,9 +120,10 @@ This will synchronise the Terraform state with the actual resources, but will no - Type: string +* `lock_info` * `lock-info` - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: diff --git a/terraform-refresh/action.yaml b/terraform-refresh/action.yaml index 938730ca..fc577faf 100644 --- a/terraform-refresh/action.yaml +++ b/terraform-refresh/action.yaml @@ -44,6 +44,15 @@ inputs: default: "0" outputs: + failure_reason: + description: | + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `refresh-failed` - The Terraform apply operation failed. + - `state-locked` - The Terraform state lock could not be obtained because it was already locked. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run steps. failure-reason: description: | When the job outcome is `failure`, this output may be set. The value may be one of: @@ -53,9 +62,26 @@ outputs: If the job fails for any other reason this will not be set. This can be used with the Actions expression syntax to conditionally run steps. + lock_info: + description: | + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. + + It is a json object containing any available state lock information and typically has the form: + + ```json + { + "ID": "838fbfde-c5cd-297f-84a4-d7578b4a4880", + "Path": "terraform-github-actions/test-unlock-state", + "Operation": "OperationTypeApply", + "Who": "root@e9d43b0c6478", + "Version": "1.3.7", + "Created": "2023-01-28 00:16:41.560904373 +0000 UTC", + "Info": "" + } + ``` lock-info: description: | - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: diff --git a/terraform-test/README.md b/terraform-test/README.md index 0d2fcc41..dab49aa6 100644 --- a/terraform-test/README.md +++ b/terraform-test/README.md @@ -81,6 +81,7 @@ If the tests fail, the job will stop with a failure status. ## Outputs +* `junit_xml_path` * `junit-xml-path` A test report in JUnit XML format. @@ -91,6 +92,7 @@ If the tests fail, the job will stop with a failure status. - Type: string +* `failure_reason` * `failure-reason` When the job outcome is `failure`, this output may be set. The value may be one of: diff --git a/terraform-test/action.yaml b/terraform-test/action.yaml index c4881c93..3302b6d8 100644 --- a/terraform-test/action.yaml +++ b/terraform-test/action.yaml @@ -31,6 +31,13 @@ inputs: required: false outputs: + junit_xml_path: + description: | + A test report in JUnit XML format. + + The path is relative to the Actions workspace. + + This will only be available when using Terraform 1.11.0 or later. junit-xml-path: description: | A test report in JUnit XML format. @@ -38,6 +45,15 @@ outputs: The path is relative to the Actions workspace. This will only be available when using Terraform 1.11.0 or later. + failure_reason: + description: | + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `no-tests` - No tests were found to run. + - `tests-failed` - One or more tests failed. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run steps. failure-reason: description: | When the job outcome is `failure`, this output may be set. The value may be one of: diff --git a/terraform-validate/README.md b/terraform-validate/README.md index f3731b79..ae355b39 100644 --- a/terraform-validate/README.md +++ b/terraform-validate/README.md @@ -65,6 +65,7 @@ If the Terraform configuration is not valid, the build is failed. ## Outputs +* `failure_reason` * `failure-reason` When the job outcome is `failure` because the validation failed, this will be set to 'validate-failed'. @@ -215,6 +216,6 @@ jobs: path: my-terraform-config - name: Validate failed - if: ${{ failure() && steps.validate.outputs.failure-reason == 'validate-failed' }} + if: ${{ failure() && steps.validate.outputs.failure_reason == 'validate-failed' }} run: echo "terraform validate failed" ``` diff --git a/terraform-validate/action.yaml b/terraform-validate/action.yaml index 98b35036..d615be17 100644 --- a/terraform-validate/action.yaml +++ b/terraform-validate/action.yaml @@ -32,6 +32,11 @@ inputs: default: "" outputs: + failure_reason: + description: | + When the job outcome is `failure` because the validation failed, this will be set to 'validate-failed'. + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run a step when the validate fails. failure-reason: description: | When the job outcome is `failure` because the validation failed, this will be set to 'validate-failed'. diff --git a/tofu-apply/README.md b/tofu-apply/README.md index 1eab9396..d0c2e5b5 100644 --- a/tofu-apply/README.md +++ b/tofu-apply/README.md @@ -247,6 +247,7 @@ These input values must be the same as any [`dflook/tofu-plan`](https://github.c - Type: string +* `failure_reason` * `failure-reason` When the job outcome is `failure`, this output may be set. The value may be one of: @@ -260,9 +261,10 @@ These input values must be the same as any [`dflook/tofu-plan`](https://github.c - Type: string +* `lock_info` * `lock-info` - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: @@ -688,7 +690,7 @@ jobs: - name: Retry failed apply uses: dflook/tofu-apply@v2 - if: ${{ steps.first_try.outputs.failure-reason == 'apply-failed' }} + if: ${{ steps.first_try.outputs.failure_reason == 'apply-failed' }} with: path: tofu auto_approve: true diff --git a/tofu-apply/action.yaml b/tofu-apply/action.yaml index 77468d06..cf95e51f 100644 --- a/tofu-apply/action.yaml +++ b/tofu-apply/action.yaml @@ -119,6 +119,16 @@ outputs: This is the path to the generated plan in a human-readable format. The path is relative to the Actions workspace. This won't be set if `auto_approve` is true while using a `remote` backend. + failure_reason: + description: | + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `apply-failed` - The Terraform apply operation failed. + - `plan-changed` - The approved plan is no longer accurate, so the apply will not be attempted. + - `state-locked` - The Terraform state lock could not be obtained because it was already locked. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run steps. failure-reason: description: | When the job outcome is `failure`, this output may be set. The value may be one of: @@ -129,9 +139,26 @@ outputs: If the job fails for any other reason this will not be set. This can be used with the Actions expression syntax to conditionally run steps. + lock_info: + description: | + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. + + It is a json object containing any available state lock information and typically has the form: + + ```json + { + "ID": "838fbfde-c5cd-297f-84a4-d7578b4a4880", + "Path": "terraform-github-actions/test-unlock-state", + "Operation": "OperationTypeApply", + "Who": "root@e9d43b0c6478", + "Version": "1.3.7", + "Created": "2023-01-28 00:16:41.560904373 +0000 UTC", + "Info": "" + } + ``` lock-info: description: | - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: diff --git a/tofu-check/README.md b/tofu-check/README.md index cc637eaf..b556eb0e 100644 --- a/tofu-check/README.md +++ b/tofu-check/README.md @@ -93,6 +93,7 @@ This is intended to run on a schedule to notify if manual changes to your infras ## Outputs +* `failure_reason` * `failure-reason` When the job outcome is `failure` because the there are outstanding changes to apply, this will be set to 'changes-to-apply'. @@ -251,6 +252,6 @@ jobs: path: my-tofu-configuration - name: Changes detected - if: ${{ failure() && steps.check.outputs.failure-reason == 'changes-to-apply' }} + if: ${{ failure() && steps.check.outputs.failure_reason == 'changes-to-apply' }} run: echo "There are outstanding changes to apply" ``` diff --git a/tofu-check/action.yaml b/tofu-check/action.yaml index 66d69e74..31e67acf 100644 --- a/tofu-check/action.yaml +++ b/tofu-check/action.yaml @@ -38,6 +38,11 @@ inputs: default: "0" outputs: + failure_reason: + description: | + When the job outcome is `failure` because the there are outstanding changes to apply, this will be set to 'changes-to-apply'. + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run a step when there are changes to apply. failure-reason: description: | When the job outcome is `failure` because the there are outstanding changes to apply, this will be set to 'changes-to-apply'. diff --git a/tofu-destroy-workspace/README.md b/tofu-destroy-workspace/README.md index 91889868..bb9f4cf0 100644 --- a/tofu-destroy-workspace/README.md +++ b/tofu-destroy-workspace/README.md @@ -90,6 +90,7 @@ This action uses the `tofu destroy` command to destroy all resources in an OpenT ## Outputs +* `failure_reason` * `failure-reason` When the job outcome is `failure`, this output may be set. The value may be one of: @@ -102,9 +103,10 @@ This action uses the `tofu destroy` command to destroy all resources in an OpenT - Type: string +* `lock_info` * `lock-info` - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: @@ -275,7 +277,7 @@ jobs: - name: Retry failed destroy uses: dflook/tofu-destroy-workspace@v2 - if: ${{ steps.first_try.outputs.failure-reason == 'destroy-failed' }} + if: ${{ steps.first_try.outputs.failure_reason == 'destroy-failed' }} with: path: my-tofu-config workspace: ${{ github.head_ref }} diff --git a/tofu-destroy-workspace/action.yaml b/tofu-destroy-workspace/action.yaml index 55f0a937..321037e2 100644 --- a/tofu-destroy-workspace/action.yaml +++ b/tofu-destroy-workspace/action.yaml @@ -37,6 +37,15 @@ inputs: default: "0" outputs: + failure_reason: + description: | + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `destroy-failed` - The OpenTofu destroy operation failed. + - `state-locked` - The OpenTofu state lock could not be obtained because it was already locked. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run a steps. failure-reason: description: | When the job outcome is `failure`, this output may be set. The value may be one of: @@ -46,9 +55,26 @@ outputs: If the job fails for any other reason this will not be set. This can be used with the Actions expression syntax to conditionally run a steps. + lock_info: + description: | + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. + + It is a json object containing any available state lock information and typically has the form: + + ```json + { + "ID": "838fbfde-c5cd-297f-84a4-d7578b4a4880", + "Path": "terraform-github-actions/test-unlock-state", + "Operation": "OperationTypeApply", + "Who": "root@e9d43b0c6478", + "Version": "1.3.7", + "Created": "2023-01-28 00:16:41.560904373 +0000 UTC", + "Info": "" + } + ``` lock-info: description: | - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: diff --git a/tofu-destroy/README.md b/tofu-destroy/README.md index 8ca41f1a..c41dff40 100644 --- a/tofu-destroy/README.md +++ b/tofu-destroy/README.md @@ -94,6 +94,7 @@ and [dflook/tofu-apply](https://github.com/dflook/terraform-github-actions/tree/ ## Outputs +* `failure_reason` * `failure-reason` When the job outcome is `failure`, this output may be set. The value may be one of: @@ -106,9 +107,10 @@ and [dflook/tofu-apply](https://github.com/dflook/terraform-github-actions/tree/ - Type: string +* `lock_info` * `lock-info` - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: @@ -279,7 +281,7 @@ jobs: - name: Retry failed destroy uses: dflook/tofu-destroy@v2 - if: ${{ steps.first_try.outputs.failure-reason == 'destroy-failed' }} + if: ${{ steps.first_try.outputs.failure_reason == 'destroy-failed' }} with: path: my-tofu-config workspace: ${{ github.head_ref }} diff --git a/tofu-destroy/action.yaml b/tofu-destroy/action.yaml index b762fa3c..f47c74dd 100644 --- a/tofu-destroy/action.yaml +++ b/tofu-destroy/action.yaml @@ -38,6 +38,15 @@ inputs: default: "0" outputs: + failure_reason: + description: | + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `destroy-failed` - The OpenTofu destroy operation failed. + - `state-locked` - The OpenTofu state lock could not be obtained because it was already locked. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run a steps. failure-reason: description: | When the job outcome is `failure`, this output may be set. The value may be one of: @@ -47,9 +56,26 @@ outputs: If the job fails for any other reason this will not be set. This can be used with the Actions expression syntax to conditionally run a steps. + lock_info: + description: | + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. + + It is a json object containing any available state lock information and typically has the form: + + ```json + { + "ID": "838fbfde-c5cd-297f-84a4-d7578b4a4880", + "Path": "terraform-github-actions/test-unlock-state", + "Operation": "OperationTypeApply", + "Who": "root@e9d43b0c6478", + "Version": "1.3.7", + "Created": "2023-01-28 00:16:41.560904373 +0000 UTC", + "Info": "" + } + ``` lock-info: description: | - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: diff --git a/tofu-fmt-check/README.md b/tofu-fmt-check/README.md index a7760305..fd35757f 100644 --- a/tofu-fmt-check/README.md +++ b/tofu-fmt-check/README.md @@ -88,6 +88,7 @@ If any files are not correctly formatted a failing GitHub check will be added fo ## Outputs +* `failure_reason` * `failure-reason` When the job outcome is `failure` because the format check failed, this will be set to 'check-failed'. @@ -178,6 +179,6 @@ jobs: path: my-tofu-config - name: Wrong formatting found - if: ${{ failure() && steps.fmt-check.outputs.failure-reason == 'check-failed' }} + if: ${{ failure() && steps.fmt-check.outputs.failure_reason == 'check-failed' }} run: echo "formatting check failed" ``` diff --git a/tofu-fmt-check/action.yaml b/tofu-fmt-check/action.yaml index 1fa24661..bbb677f2 100644 --- a/tofu-fmt-check/action.yaml +++ b/tofu-fmt-check/action.yaml @@ -38,6 +38,11 @@ inputs: default: "" outputs: + failure_reason: + description: | + When the job outcome is `failure` because the format check failed, this will be set to 'check-failed'. + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run a step when the format check fails. failure-reason: description: | When the job outcome is `failure` because the format check failed, this will be set to 'check-failed'. diff --git a/tofu-refresh/README.md b/tofu-refresh/README.md index ec4d08b1..525d2aa5 100644 --- a/tofu-refresh/README.md +++ b/tofu-refresh/README.md @@ -124,6 +124,7 @@ This will synchronise the OpenTofu state with the actual resources, but will not ## Outputs +* `failure_reason` * `failure-reason` When the job outcome is `failure`, this output may be set. The value may be one of: @@ -136,9 +137,10 @@ This will synchronise the OpenTofu state with the actual resources, but will not - Type: string +* `lock_info` * `lock-info` - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: diff --git a/tofu-refresh/action.yaml b/tofu-refresh/action.yaml index b748a37c..78712e65 100644 --- a/tofu-refresh/action.yaml +++ b/tofu-refresh/action.yaml @@ -52,6 +52,15 @@ inputs: default: "0" outputs: + failure_reason: + description: | + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `refresh-failed` - The OpenTofu apply operation failed. + - `state-locked` - The Terraform state lock could not be obtained because it was already locked. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run steps. failure-reason: description: | When the job outcome is `failure`, this output may be set. The value may be one of: @@ -61,9 +70,26 @@ outputs: If the job fails for any other reason this will not be set. This can be used with the Actions expression syntax to conditionally run steps. + lock_info: + description: | + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. + + It is a json object containing any available state lock information and typically has the form: + + ```json + { + "ID": "838fbfde-c5cd-297f-84a4-d7578b4a4880", + "Path": "terraform-github-actions/test-unlock-state", + "Operation": "OperationTypeApply", + "Who": "root@e9d43b0c6478", + "Version": "1.3.7", + "Created": "2023-01-28 00:16:41.560904373 +0000 UTC", + "Info": "" + } + ``` lock-info: description: | - When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + When the job outcome is `failure` and the failure_reason is `state-locked`, this output will be set. It is a json object containing any available state lock information and typically has the form: diff --git a/tofu-test/README.md b/tofu-test/README.md index f6c237a4..5be38275 100644 --- a/tofu-test/README.md +++ b/tofu-test/README.md @@ -81,6 +81,7 @@ If the tests fail, the job will stop with a failure status. ## Outputs +* `failure_reason` * `failure-reason` When the job outcome is `failure`, this output may be set. The value may be one of: diff --git a/tofu-test/action.yaml b/tofu-test/action.yaml index a1c0e804..14cfaca6 100644 --- a/tofu-test/action.yaml +++ b/tofu-test/action.yaml @@ -31,6 +31,15 @@ inputs: required: false outputs: + failure_reason: + description: | + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `no-tests` - No tests were found to run. + - `tests-failed` - One or more tests failed. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run steps. failure-reason: description: | When the job outcome is `failure`, this output may be set. The value may be one of: diff --git a/tofu-validate/README.md b/tofu-validate/README.md index fe102620..0fa618a5 100644 --- a/tofu-validate/README.md +++ b/tofu-validate/README.md @@ -65,6 +65,7 @@ If the OpenTofu configuration is not valid, the build is failed. ## Outputs +* `failure_reason` * `failure-reason` When the job outcome is `failure` because the validation failed, this will be set to 'validate-failed'. @@ -215,6 +216,6 @@ jobs: path: my-tofu-config - name: Validate failed - if: ${{ failure() && steps.validate.outputs.failure-reason == 'validate-failed' }} + if: ${{ failure() && steps.validate.outputs.failure_reason == 'validate-failed' }} run: echo "tofu validate failed" ``` diff --git a/tofu-validate/action.yaml b/tofu-validate/action.yaml index 1d479ebb..0cf9a178 100644 --- a/tofu-validate/action.yaml +++ b/tofu-validate/action.yaml @@ -32,6 +32,11 @@ inputs: default: "" outputs: + failure_reason: + description: | + When the job outcome is `failure` because the validation failed, this will be set to 'validate-failed'. + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run a step when the validate fails. failure-reason: description: | When the job outcome is `failure` because the validation failed, this will be set to 'validate-failed'. From 637948b6240ebf4a84e5f19bbd68bfcd1f061436 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 28 Mar 2026 12:52:39 +0000 Subject: [PATCH 03/32] Install python package in venv --- image/Dockerfile | 16 ++++++++++++---- image/Dockerfile-base | 2 +- image/tools/compact_plan.py | 2 +- image/tools/convert_output.py | 2 +- image/tools/convert_validate_report.py | 2 +- image/tools/convert_version.py | 2 +- image/tools/format_tf_credentials.py | 2 +- image/tools/github_comment_react.py | 2 +- image/tools/workspace_exists.py | 2 +- 9 files changed, 20 insertions(+), 12 deletions(-) diff --git a/image/Dockerfile b/image/Dockerfile index ce0f2e55..d70d5d09 100644 --- a/image/Dockerfile +++ b/image/Dockerfile @@ -6,11 +6,17 @@ ARG TARGETARCH ARG FETCH_CHECKSUMS ARG VERSION=99.0.0 +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +RUN --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt \ + pip install --no-cache-dir --require-hashes -r /tmp/requirements.txt + COPY src/ /tmp/src/ COPY setup.py /tmp RUN sed -i "s|version='.*'|version=\'${VERSION}\'|" /tmp/setup.py \ - && pip install --break-system-packages --no-cache-dir /tmp \ - && rm -rf /tmp/src /tmp/setup.py + && pip install --no-cache-dir --no-deps /tmp \ + && rm -rf /tmp/src /tmp/setup.py /tmp/build /tmp/pip-* RUN if [ "$FETCH_CHECKSUMS" = "yes" ]; then \ TERRAFORM_BIN_CHECKSUM_DIR="/var/terraform" get-terraform-checksums; \ @@ -21,10 +27,12 @@ fi RUN if [ "$TARGETARCH" = "amd64" ]; then \ TERRAFORM_BIN_CACHE_DIR="/var/terraform" TERRAFORM_BIN_CHECKSUM_DIR="/var/terraform" terraform-version 0.9.0 \ && TERRAFORM_BIN_CACHE_DIR="/var/terraform" TERRAFORM_BIN_CHECKSUM_DIR="/var/terraform" terraform-version 0.12.0; \ -fi +fi \ + && rm -rf /tmp/terraform_* RUN if [ "$TARGETARCH" = "arm64" ]; then \ TERRAFORM_BIN_CACHE_DIR="/var/terraform" TERRAFORM_BIN_CHECKSUM_DIR="/var/terraform" terraform-version 0.13.5; \ -fi +fi \ + && rm -rf /tmp/terraform_* COPY entrypoints/ /entrypoints/ COPY actions.sh /usr/local/actions.sh diff --git a/image/Dockerfile-base b/image/Dockerfile-base index 3681f4e3..a2795aa1 100644 --- a/image/Dockerfile-base +++ b/image/Dockerfile-base @@ -29,8 +29,8 @@ RUN < Date: Sat, 28 Mar 2026 12:53:32 +0000 Subject: [PATCH 04/32] Explicitly gpg verify the expected signed --- image/Dockerfile-base | 1 + image/src/opentofu/download.py | 2 +- image/src/terraform/download.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/image/Dockerfile-base b/image/Dockerfile-base index a2795aa1..abd98ded 100644 --- a/image/Dockerfile-base +++ b/image/Dockerfile-base @@ -44,6 +44,7 @@ RUN < Path: if signature_path.exists(): try: subprocess.run( - ['gpg', '--verify', signature_path, checksums_path], + ['gpg', '--assert-signer', 'E3E6E43D84CB852EADB0051D0C0AF313E5FD9F80', '--verify', signature_path, checksums_path], check=True, env={'GNUPGHOME': '/root/.gnupg'} | os.environ ) diff --git a/image/src/terraform/download.py b/image/src/terraform/download.py index 7e97785b..4b470dd7 100644 --- a/image/src/terraform/download.py +++ b/image/src/terraform/download.py @@ -98,7 +98,7 @@ def get_checksums(version: Version, checksum_dir: Path) -> Path: try: subprocess.run( - ['gpg', '--verify', signature_path, checksums_path], + ['gpg', '--assert-signer', 'C874011F0AB405110D02105534365D9472D7468F', '--verify', signature_path, checksums_path], check=True, env={'GNUPGHOME': '/root/.gnupg'} | os.environ ) From 34774e82d72cba0ee7e846f77dd6e26aa2eb283b Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 28 Mar 2026 12:54:03 +0000 Subject: [PATCH 05/32] Update actions versions --- .github/workflows/base-image.yaml | 6 +- .github/workflows/pull_request_review.yaml | 2 +- .github/workflows/pull_request_target.yaml | 4 +- .github/workflows/release.yaml | 12 ++-- .github/workflows/repository_dispatch.yaml | 2 +- .github/workflows/retain-images.yaml | 2 +- .github/workflows/test-apply.yaml | 62 +++++++++---------- .github/workflows/test-binary-plan.yaml | 8 +-- .github/workflows/test-changes-only.yaml | 8 +-- .github/workflows/test-check.yaml | 4 +- .github/workflows/test-cloud.yaml | 4 +- .github/workflows/test-early-eval.yaml | 2 +- .github/workflows/test-fmt-check.yaml | 4 +- .github/workflows/test-fmt.yaml | 2 +- .github/workflows/test-http.yaml | 12 ++-- .github/workflows/test-new-workspace.yaml | 2 +- .github/workflows/test-output.yaml | 2 +- .github/workflows/test-plan.yaml | 50 +++++++-------- .github/workflows/test-refresh.yaml | 2 +- .github/workflows/test-registry.yaml | 6 +- .github/workflows/test-remote-state.yaml | 2 +- .github/workflows/test-ssh.yaml | 4 +- .../test-target-replace-exclude.yaml | 6 +- .github/workflows/test-test.yaml | 14 ++--- .github/workflows/test-unlock-state.yaml | 4 +- .github/workflows/test-validate.yaml | 10 +-- .github/workflows/test-version.yaml | 44 ++++++------- .github/workflows/test.yaml | 24 +++---- .github/workflows/trigger-test-events.yaml | 2 +- .github/zizmor.yml | 8 +-- 30 files changed, 154 insertions(+), 160 deletions(-) diff --git a/.github/workflows/base-image.yaml b/.github/workflows/base-image.yaml index cd841208..4cc4e8f0 100644 --- a/.github/workflows/base-image.yaml +++ b/.github/workflows/base-image.yaml @@ -25,7 +25,7 @@ jobs: url: https://hub.docker.com/r/danielflook/terraform-github-actions-base/tags?name=${{ github.run_id }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -36,7 +36,7 @@ jobs: echo "$DOCKER_TOKEN" | docker login --username danielflook --password-stdin - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Base image id: build-and-push @@ -68,7 +68,7 @@ jobs: echo "digest=$(> "$GITHUB_OUTPUT" - name: Generate image attestation - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: index.docker.io/danielflook/terraform-github-actions-base subject-digest: ${{ steps.build-and-push.outputs.digest }} diff --git a/.github/workflows/pull_request_review.yaml b/.github/workflows/pull_request_review.yaml index 62239326..86d72c40 100644 --- a/.github/workflows/pull_request_review.yaml +++ b/.github/workflows/pull_request_review.yaml @@ -17,7 +17,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/pull_request_target.yaml b/.github/workflows/pull_request_target.yaml index 94a06760..66da8752 100644 --- a/.github/workflows/pull_request_target.yaml +++ b/.github/workflows/pull_request_target.yaml @@ -29,13 +29,13 @@ jobs: EOF - name: Plan - uses: dflook/terraform-plan@v1 + uses: dflook/terraform-plan@7878bff63e2099cdc9be9a6f33cbbbf687f8f0fe # v2.2.3 with: label: pull_request_target path: test-module - name: Apply - uses: dflook/terraform-apply@v1 + uses: dflook/terraform-apply@5489b988934a50bf1489d5b7c5253b46520a7dca # v2.2.3 id: output with: label: pull_request_target diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 050ff879..195f43c2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -25,7 +25,7 @@ jobs: base-digest: ${{ steps.image_build.outputs.base-digest }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -43,7 +43,7 @@ jobs: echo "$DOCKER_TOKEN" | docker login --username danielflook --password-stdin - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build action image id: image_build @@ -85,13 +85,13 @@ jobs: echo "base-digest=$BASE_DIGEST" >> "$GITHUB_OUTPUT" - name: Dockerhub ref attestation - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: index.docker.io/danielflook/terraform-github-actions subject-digest: ${{ steps.image_build.outputs.digest }} - name: GHCR ref attestation - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: ghcr.io/dflook/terraform-github-actions subject-digest: ${{ steps.image_build.outputs.digest }} @@ -106,7 +106,7 @@ jobs: url: https://github.com/dflook/terraform-github-actions/releases/tag/${{ github.event.release.tag_name }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false @@ -206,7 +206,7 @@ jobs: echo "$DOCKER_TOKEN" | docker login --username danielflook --password-stdin - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Tag and push base image with release version env: diff --git a/.github/workflows/repository_dispatch.yaml b/.github/workflows/repository_dispatch.yaml index 13ad72c8..dcceb755 100644 --- a/.github/workflows/repository_dispatch.yaml +++ b/.github/workflows/repository_dispatch.yaml @@ -18,7 +18,7 @@ jobs: pull-requests: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/retain-images.yaml b/.github/workflows/retain-images.yaml index 26e5beb1..184cc8b3 100644 --- a/.github/workflows/retain-images.yaml +++ b/.github/workflows/retain-images.yaml @@ -16,7 +16,7 @@ jobs: name: Pull images steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/test-apply.yaml b/.github/workflows/test-apply.yaml index 344ec777..50852eb9 100644 --- a/.github/workflows/test-apply.yaml +++ b/.github/workflows/test-apply.yaml @@ -12,7 +12,7 @@ jobs: name: Auto Approve steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -58,7 +58,7 @@ jobs: name: Auto Approve plan error steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -121,7 +121,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -184,7 +184,7 @@ jobs: name: Apply without token steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -232,7 +232,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -320,7 +320,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -424,7 +424,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -530,7 +530,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -634,7 +634,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -694,7 +694,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -742,7 +742,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -791,7 +791,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.USER_GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -845,7 +845,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.FINE_PAT_GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -900,7 +900,7 @@ jobs: TERRAFORM_ACTIONS_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -954,7 +954,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -987,7 +987,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1069,7 +1069,7 @@ jobs: echo "testing command 2" steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1105,7 +1105,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1135,12 +1135,12 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Plan - uses: dflook/terraform-plan@v1.22.2 + uses: dflook/terraform-plan@1547271e3127df7ad5b667eeed17e8e3a77a76a0 # v1.22.2 with: label: test-apply warnings_text path: tests/workflows/test-apply/warnings @@ -1165,7 +1165,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1191,7 +1191,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1244,12 +1244,12 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Plan - uses: dflook/terraform-plan@v1.31.0 + uses: dflook/terraform-plan@cf2c225e3088d0cbd3bedc68690c216033e3013a # v1.31.0 with: path: tests/workflows/test-apply/partial_backend backend_config_file: tests/workflows/test-apply/partial_backend/backend_config @@ -1272,7 +1272,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1304,7 +1304,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1338,7 +1338,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.USER_GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1396,7 +1396,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1420,7 +1420,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1444,7 +1444,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1536,7 +1536,7 @@ jobs: name: Apply with multiple var files steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-binary-plan.yaml b/.github/workflows/test-binary-plan.yaml index 9c74f882..1db5d5a4 100644 --- a/.github/workflows/test-binary-plan.yaml +++ b/.github/workflows/test-binary-plan.yaml @@ -12,7 +12,7 @@ jobs: name: Missing plan steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -44,7 +44,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -68,7 +68,7 @@ jobs: name: Apply auto approved changes steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -98,7 +98,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-changes-only.yaml b/.github/workflows/test-changes-only.yaml index 4a34994d..222ba8e8 100644 --- a/.github/workflows/test-changes-only.yaml +++ b/.github/workflows/test-changes-only.yaml @@ -17,7 +17,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -70,7 +70,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -146,7 +146,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -210,7 +210,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-check.yaml b/.github/workflows/test-check.yaml index e08dbf86..b9cb4e1b 100644 --- a/.github/workflows/test-check.yaml +++ b/.github/workflows/test-check.yaml @@ -12,7 +12,7 @@ jobs: name: No changes steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -42,7 +42,7 @@ jobs: name: Changes steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-cloud.yaml b/.github/workflows/test-cloud.yaml index c19f8b9d..8dcf5eaf 100644 --- a/.github/workflows/test-cloud.yaml +++ b/.github/workflows/test-cloud.yaml @@ -19,7 +19,7 @@ jobs: tf_version: ['0.13', '1.0'] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -323,7 +323,7 @@ jobs: TERRAFORM_CLOUD_TOKENS: app.terraform.io=${{ secrets.TF_API_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-early-eval.yaml b/.github/workflows/test-early-eval.yaml index cfe10274..6dfbc49c 100644 --- a/.github/workflows/test-early-eval.yaml +++ b/.github/workflows/test-early-eval.yaml @@ -19,7 +19,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-fmt-check.yaml b/.github/workflows/test-fmt-check.yaml index 7512af88..4b1e6157 100644 --- a/.github/workflows/test-fmt-check.yaml +++ b/.github/workflows/test-fmt-check.yaml @@ -12,7 +12,7 @@ jobs: name: Canonical fmt steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -43,7 +43,7 @@ jobs: continue-on-error: true steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-fmt.yaml b/.github/workflows/test-fmt.yaml index 643984c2..9752cac4 100644 --- a/.github/workflows/test-fmt.yaml +++ b/.github/workflows/test-fmt.yaml @@ -12,7 +12,7 @@ jobs: name: Canonical fmt steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-http.yaml b/.github/workflows/test-http.yaml index 2b0e4e04..6e7c04a5 100644 --- a/.github/workflows/test-http.yaml +++ b/.github/workflows/test-http.yaml @@ -20,7 +20,7 @@ jobs: github.com/dflook/terraform-github-actions-dev.git=dflook:${{ secrets.USER_GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -50,7 +50,7 @@ jobs: github.com/dflook=dflook:${{ secrets.USER_GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -80,7 +80,7 @@ jobs: github.com=dflook:${{ secrets.USER_GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -105,7 +105,7 @@ jobs: name: git+http no creds steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -134,7 +134,7 @@ jobs: 5qcb7mjppk.execute-api.eu-west-2.amazonaws.com=dflook:hello steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -171,7 +171,7 @@ jobs: name: http module source with no credentials steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-new-workspace.yaml b/.github/workflows/test-new-workspace.yaml index 0e85e1dd..99a99b93 100644 --- a/.github/workflows/test-new-workspace.yaml +++ b/.github/workflows/test-new-workspace.yaml @@ -19,7 +19,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-output.yaml b/.github/workflows/test-output.yaml index 05fc79f8..ce4d67e5 100644 --- a/.github/workflows/test-output.yaml +++ b/.github/workflows/test-output.yaml @@ -16,7 +16,7 @@ jobs: name: verify outputs steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-plan.yaml b/.github/workflows/test-plan.yaml index e7e5c63b..cbebafdf 100644 --- a/.github/workflows/test-plan.yaml +++ b/.github/workflows/test-plan.yaml @@ -17,7 +17,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -79,7 +79,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -122,7 +122,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -195,7 +195,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -269,7 +269,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -344,7 +344,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -419,7 +419,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -493,7 +493,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -567,7 +567,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -641,7 +641,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -684,7 +684,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -737,7 +737,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -784,7 +784,7 @@ jobs: pull-requests: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -832,7 +832,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -854,7 +854,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -876,7 +876,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -897,7 +897,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -937,7 +937,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -968,7 +968,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -992,7 +992,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1012,7 +1012,7 @@ jobs: pull-requests: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1075,7 +1075,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1101,7 +1101,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1121,7 +1121,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -1161,7 +1161,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-refresh.yaml b/.github/workflows/test-refresh.yaml index 5bf83990..a3e3eb2f 100644 --- a/.github/workflows/test-refresh.yaml +++ b/.github/workflows/test-refresh.yaml @@ -17,7 +17,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-registry.yaml b/.github/workflows/test-registry.yaml index 4f585360..fb76a823 100644 --- a/.github/workflows/test-registry.yaml +++ b/.github/workflows/test-registry.yaml @@ -18,7 +18,7 @@ jobs: pull-requests: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -74,7 +74,7 @@ jobs: app.terraform.io = ${{ secrets.TF_API_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -105,7 +105,7 @@ jobs: name: Nonsense cloud credentials steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-remote-state.yaml b/.github/workflows/test-remote-state.yaml index f368d5ec..d459a40d 100644 --- a/.github/workflows/test-remote-state.yaml +++ b/.github/workflows/test-remote-state.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-ssh.yaml b/.github/workflows/test-ssh.yaml index df0fdb5d..de5ef384 100644 --- a/.github/workflows/test-ssh.yaml +++ b/.github/workflows/test-ssh.yaml @@ -15,7 +15,7 @@ jobs: pull-requests: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -52,7 +52,7 @@ jobs: name: Git module source with no key steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-target-replace-exclude.yaml b/.github/workflows/test-target-replace-exclude.yaml index 048ef2bf..c506c36d 100644 --- a/.github/workflows/test-target-replace-exclude.yaml +++ b/.github/workflows/test-target-replace-exclude.yaml @@ -18,7 +18,7 @@ jobs: pull-requests: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -262,7 +262,7 @@ jobs: pull-requests: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -557,7 +557,7 @@ jobs: pull-requests: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-test.yaml b/.github/workflows/test-test.yaml index 10ad0e37..d5515c9e 100644 --- a/.github/workflows/test-test.yaml +++ b/.github/workflows/test-test.yaml @@ -12,7 +12,7 @@ jobs: name: Default inputs steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -56,7 +56,7 @@ jobs: name: Junit support steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -111,7 +111,7 @@ jobs: name: Default path with a filter steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -142,7 +142,7 @@ jobs: name: Custom test directory steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -176,7 +176,7 @@ jobs: name: Missing test directory steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -214,7 +214,7 @@ jobs: name: Filter matches no tests steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -254,7 +254,7 @@ jobs: name: A failing test using variables steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-unlock-state.yaml b/.github/workflows/test-unlock-state.yaml index 50efd51e..1d1eeac8 100644 --- a/.github/workflows/test-unlock-state.yaml +++ b/.github/workflows/test-unlock-state.yaml @@ -16,7 +16,7 @@ jobs: name: Default workspace steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -157,7 +157,7 @@ jobs: name: Non Default workspace steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-validate.yaml b/.github/workflows/test-validate.yaml index 4bbe3841..e871407b 100644 --- a/.github/workflows/test-validate.yaml +++ b/.github/workflows/test-validate.yaml @@ -12,7 +12,7 @@ jobs: name: valid steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -42,7 +42,7 @@ jobs: name: Invalid terraform configuration steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -79,7 +79,7 @@ jobs: name: Use workspace name during validation steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -128,7 +128,7 @@ jobs: name: Use workspace name during validation steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -143,7 +143,7 @@ jobs: name: Validate with unterminated string steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test-version.yaml b/.github/workflows/test-version.yaml index 37cc15da..d8398e63 100644 --- a/.github/workflows/test-version.yaml +++ b/.github/workflows/test-version.yaml @@ -12,7 +12,7 @@ jobs: name: specific required_version steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -38,7 +38,7 @@ jobs: name: required_version range steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -64,7 +64,7 @@ jobs: name: required_version with a v prefix steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -90,7 +90,7 @@ jobs: name: tfswitch steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -116,7 +116,7 @@ jobs: name: tfenv steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -142,7 +142,7 @@ jobs: name: tofuenv steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -168,7 +168,7 @@ jobs: name: asdf steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -211,7 +211,7 @@ jobs: name: TERRAFORM_VERSION range steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -239,7 +239,7 @@ jobs: name: TFC Workspace steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -283,7 +283,7 @@ jobs: name: TFC Cloud Configuration steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -329,7 +329,7 @@ jobs: name: Partial TFC Cloud Configuration steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -378,7 +378,7 @@ jobs: name: Partial TFC Cloud Configuration with tags steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -427,7 +427,7 @@ jobs: name: Local State file steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -454,7 +454,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -595,7 +595,7 @@ jobs: name: latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -642,7 +642,7 @@ jobs: name: provider versions steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -729,7 +729,7 @@ jobs: name: Version detection on arm steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -802,7 +802,7 @@ jobs: name: OPENTOFU_VERSION with terraform action steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -836,7 +836,7 @@ jobs: name: OPENTOFU_VERSION with tofu action steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -872,7 +872,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -908,7 +908,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -937,7 +937,7 @@ jobs: name: Get version constraint from hard to parse file steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a1eb5816..8405621f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,12 +12,12 @@ jobs: name: pytest amd64 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.9 @@ -43,12 +43,12 @@ jobs: needs: pytest_amd64 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.9 @@ -73,7 +73,7 @@ jobs: name: Check documentation steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -87,7 +87,7 @@ jobs: name: Linting steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -98,7 +98,7 @@ jobs: ./actionlint example_workflows/*.yaml - name: Install the latest version of uv - uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5.3.1 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 - name: Actions Security Check env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -106,13 +106,13 @@ jobs: uvx zizmor --format plain . - name: Lint CHANGELOG - uses: DavidAnson/markdownlint-cli2-action@05f32210e84442804257b2a6f20b273450ec8265 # v19 + uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0 with: config: '.config/changelog.markdownlint.yaml' globs: 'CHANGELOG.md' - name: Lint Other Markdown - uses: DavidAnson/markdownlint-cli2-action@05f32210e84442804257b2a6f20b273450ec8265 # v19 + uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0 with: config: '.config/.markdownlint.yaml' globs: | @@ -120,13 +120,13 @@ jobs: **/README.md - name: Lint Dockerfile - uses: hadolint/hadolint-action@54c9adbab1582c2ef04b2016b760714a4bfde3cf # v3.1.0 + uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 with: dockerfile: ./image/Dockerfile - name: Lint Dockerfile-base - uses: hadolint/hadolint-action@54c9adbab1582c2ef04b2016b760714a4bfde3cf # v3.1.0 + uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 with: dockerfile: ./image/Dockerfile-base @@ -142,6 +142,6 @@ jobs: V8R_CONFIG_FILE=.config/.v8rrc.yaml npx v8r --ignore-errors - - uses: astral-sh/ruff-action@57714a7c8a2e59f32539362ba31877a1957dded1 # v3.5.1 + - uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1 with: args: --config=.config/ruff.toml check diff --git a/.github/workflows/trigger-test-events.yaml b/.github/workflows/trigger-test-events.yaml index f85207fd..eefd3e7a 100644 --- a/.github/workflows/trigger-test-events.yaml +++ b/.github/workflows/trigger-test-events.yaml @@ -14,7 +14,7 @@ jobs: contents: write steps: - name: Repository Dispatch - uses: peter-evans/repository-dispatch@bf47d102fdb849e755b0b0023ea3e81a44b6f570 # v2 + uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1 with: event-type: test client-payload: '{"pull_request": { "url": "${{ github.event.pull_request.url }}" } }' diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 81a51310..550e33a7 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,7 +1 @@ -rules: - unpinned-uses: - config: - policies: - dflook/terraform-apply: ref-pin - dflook/terraform-plan: ref-pin - actions/*: ref-pin +rules: {} From f16fdcb1cd0b1a68f1582359d557927b291069e8 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 28 Mar 2026 12:58:40 +0000 Subject: [PATCH 06/32] Add python requirements.txt --- image/requirements.txt | 313 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 image/requirements.txt diff --git a/image/requirements.txt b/image/requirements.txt new file mode 100644 index 00000000..a4a1448b --- /dev/null +++ b/image/requirements.txt @@ -0,0 +1,313 @@ +# +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: +# +# pip-compile --generate-hashes --output-file=- --strip-extras setup.py +# +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 + # via + # cattrs + # requests-cache +canonicaljson==2.0.0 \ + --hash=sha256:c38a315de3b5a0532f1ec1f9153cd3d716abfc565a558d00a4835428a34fca5b \ + --hash=sha256:e2fdaef1d7fadc5d9cb59bd3d0d41b064ddda697809ac4325dced721d12f113f + # via terraform-github-actions (setup.py) +cattrs==26.1.0 \ + --hash=sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096 \ + --hash=sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40 + # via requests-cache +certifi==2026.2.25 \ + --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ + --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 + # via requests +charset-normalizer==3.4.6 \ + --hash=sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e \ + --hash=sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c \ + --hash=sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5 \ + --hash=sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815 \ + --hash=sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f \ + --hash=sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0 \ + --hash=sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484 \ + --hash=sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407 \ + --hash=sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6 \ + --hash=sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8 \ + --hash=sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264 \ + --hash=sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815 \ + --hash=sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2 \ + --hash=sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4 \ + --hash=sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579 \ + --hash=sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f \ + --hash=sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa \ + --hash=sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95 \ + --hash=sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab \ + --hash=sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297 \ + --hash=sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a \ + --hash=sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e \ + --hash=sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84 \ + --hash=sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8 \ + --hash=sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0 \ + --hash=sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9 \ + --hash=sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f \ + --hash=sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1 \ + --hash=sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843 \ + --hash=sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565 \ + --hash=sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7 \ + --hash=sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c \ + --hash=sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b \ + --hash=sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7 \ + --hash=sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687 \ + --hash=sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9 \ + --hash=sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14 \ + --hash=sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89 \ + --hash=sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f \ + --hash=sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0 \ + --hash=sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 \ + --hash=sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a \ + --hash=sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389 \ + --hash=sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0 \ + --hash=sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30 \ + --hash=sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd \ + --hash=sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e \ + --hash=sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9 \ + --hash=sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc \ + --hash=sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532 \ + --hash=sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d \ + --hash=sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae \ + --hash=sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2 \ + --hash=sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64 \ + --hash=sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f \ + --hash=sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557 \ + --hash=sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e \ + --hash=sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff \ + --hash=sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398 \ + --hash=sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db \ + --hash=sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a \ + --hash=sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43 \ + --hash=sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597 \ + --hash=sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c \ + --hash=sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e \ + --hash=sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2 \ + --hash=sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54 \ + --hash=sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e \ + --hash=sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4 \ + --hash=sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4 \ + --hash=sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7 \ + --hash=sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6 \ + --hash=sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5 \ + --hash=sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194 \ + --hash=sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69 \ + --hash=sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f \ + --hash=sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316 \ + --hash=sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e \ + --hash=sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73 \ + --hash=sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8 \ + --hash=sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923 \ + --hash=sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88 \ + --hash=sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f \ + --hash=sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21 \ + --hash=sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4 \ + --hash=sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6 \ + --hash=sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc \ + --hash=sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2 \ + --hash=sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866 \ + --hash=sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021 \ + --hash=sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2 \ + --hash=sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d \ + --hash=sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8 \ + --hash=sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de \ + --hash=sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237 \ + --hash=sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4 \ + --hash=sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778 \ + --hash=sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb \ + --hash=sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc \ + --hash=sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602 \ + --hash=sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4 \ + --hash=sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f \ + --hash=sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5 \ + --hash=sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611 \ + --hash=sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8 \ + --hash=sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf \ + --hash=sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d \ + --hash=sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b \ + --hash=sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db \ + --hash=sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e \ + --hash=sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077 \ + --hash=sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd \ + --hash=sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef \ + --hash=sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e \ + --hash=sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8 \ + --hash=sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe \ + --hash=sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058 \ + --hash=sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17 \ + --hash=sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833 \ + --hash=sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421 \ + --hash=sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550 \ + --hash=sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff \ + --hash=sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2 \ + --hash=sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc \ + --hash=sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982 \ + --hash=sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d \ + --hash=sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed \ + --hash=sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104 \ + --hash=sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659 + # via requests +idna==3.11 \ + --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ + --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 + # via + # requests + # url-normalize +lark==1.3.1 \ + --hash=sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905 \ + --hash=sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12 + # via python-hcl2 +platformdirs==4.9.4 \ + --hash=sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934 \ + --hash=sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868 + # via requests-cache +python-hcl2==7.3.1 \ + --hash=sha256:6bf6036d3dffac04e05e6aa58ff6e9a3710bba2171325584dfd7b3f8c99c6d4d \ + --hash=sha256:f8f55583703daf7bbcb595a33c68de891064d565974ea39998b81d15a4c4657b + # via terraform-github-actions (setup.py) +regex==2026.2.28 \ + --hash=sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1 \ + --hash=sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a \ + --hash=sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4 \ + --hash=sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d \ + --hash=sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a \ + --hash=sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911 \ + --hash=sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952 \ + --hash=sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b \ + --hash=sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97 \ + --hash=sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25 \ + --hash=sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8 \ + --hash=sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359 \ + --hash=sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff \ + --hash=sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a \ + --hash=sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7 \ + --hash=sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0 \ + --hash=sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a \ + --hash=sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215 \ + --hash=sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43 \ + --hash=sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451 \ + --hash=sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8 \ + --hash=sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c \ + --hash=sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b \ + --hash=sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692 \ + --hash=sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e \ + --hash=sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d \ + --hash=sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae \ + --hash=sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8 \ + --hash=sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11 \ + --hash=sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae \ + --hash=sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5 \ + --hash=sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64 \ + --hash=sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472 \ + --hash=sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18 \ + --hash=sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a \ + --hash=sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd \ + --hash=sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d \ + --hash=sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d \ + --hash=sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9 \ + --hash=sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96 \ + --hash=sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784 \ + --hash=sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b \ + --hash=sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff \ + --hash=sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff \ + --hash=sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc \ + --hash=sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf \ + --hash=sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5 \ + --hash=sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098 \ + --hash=sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2 \ + --hash=sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05 \ + --hash=sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf \ + --hash=sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6 \ + --hash=sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768 \ + --hash=sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15 \ + --hash=sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb \ + --hash=sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881 \ + --hash=sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f \ + --hash=sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8 \ + --hash=sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c \ + --hash=sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d \ + --hash=sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e \ + --hash=sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e \ + --hash=sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341 \ + --hash=sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e \ + --hash=sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2 \ + --hash=sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550 \ + --hash=sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e \ + --hash=sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27 \ + --hash=sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8 \ + --hash=sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59 \ + --hash=sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b \ + --hash=sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3 \ + --hash=sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117 \ + --hash=sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc \ + --hash=sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea \ + --hash=sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b \ + --hash=sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e \ + --hash=sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703 \ + --hash=sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318 \ + --hash=sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2 \ + --hash=sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952 \ + --hash=sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944 \ + --hash=sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7 \ + --hash=sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b \ + --hash=sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033 \ + --hash=sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4 \ + --hash=sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8 \ + --hash=sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f \ + --hash=sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d \ + --hash=sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5 \ + --hash=sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d \ + --hash=sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec \ + --hash=sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b \ + --hash=sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a \ + --hash=sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92 \ + --hash=sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9 \ + --hash=sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc \ + --hash=sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022 \ + --hash=sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6 \ + --hash=sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c \ + --hash=sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27 \ + --hash=sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b \ + --hash=sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc \ + --hash=sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1 \ + --hash=sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07 \ + --hash=sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c \ + --hash=sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a \ + --hash=sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33 \ + --hash=sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95 \ + --hash=sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081 \ + --hash=sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d \ + --hash=sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7 \ + --hash=sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb \ + --hash=sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61 + # via python-hcl2 +requests==2.33.0 \ + --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \ + --hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652 + # via + # requests-cache + # terraform-github-actions (setup.py) +requests-cache==1.3.1 \ + --hash=sha256:43a67448c3b2964c631ac7027b84607f2f63438e28104b68ad2211f32d9f606c \ + --hash=sha256:784e9d07f72db4fe234830a065230c59eb446489528f271ba288c640897e47c4 + # via terraform-github-actions (setup.py) +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via cattrs +url-normalize==2.2.1 \ + --hash=sha256:3deb687587dc91f7b25c9ae5162ffc0f057ae85d22b1e15cf5698311247f567b \ + --hash=sha256:74a540a3b6eba1d95bdc610c24f2c0141639f3ba903501e61a52a8730247ff37 + # via requests-cache +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + # via + # requests + # requests-cache From 4ec1b65c8d72dd0d093cbd3a653dd0c74dc0a375 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 28 Mar 2026 13:04:54 +0000 Subject: [PATCH 07/32] Remove broken symlink, and better handle broken symlinks --- image/Dockerfile | 4 ++-- image/src/terraform_version/__main__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/image/Dockerfile b/image/Dockerfile index d70d5d09..129303f8 100644 --- a/image/Dockerfile +++ b/image/Dockerfile @@ -28,11 +28,11 @@ RUN if [ "$TARGETARCH" = "amd64" ]; then \ TERRAFORM_BIN_CACHE_DIR="/var/terraform" TERRAFORM_BIN_CHECKSUM_DIR="/var/terraform" terraform-version 0.9.0 \ && TERRAFORM_BIN_CACHE_DIR="/var/terraform" TERRAFORM_BIN_CHECKSUM_DIR="/var/terraform" terraform-version 0.12.0; \ fi \ - && rm -rf /tmp/terraform_* + && rm -rf /tmp/terraform_* /usr/local/bin/terraform /usr/local/bin/tofu RUN if [ "$TARGETARCH" = "arm64" ]; then \ TERRAFORM_BIN_CACHE_DIR="/var/terraform" TERRAFORM_BIN_CHECKSUM_DIR="/var/terraform" terraform-version 0.13.5; \ fi \ - && rm -rf /tmp/terraform_* + && rm -rf /tmp/terraform_* /usr/local/bin/terraform /usr/local/bin/tofu COPY entrypoints/ /entrypoints/ COPY actions.sh /usr/local/actions.sh diff --git a/image/src/terraform_version/__main__.py b/image/src/terraform_version/__main__.py index 69a4776e..00b40712 100644 --- a/image/src/terraform_version/__main__.py +++ b/image/src/terraform_version/__main__.py @@ -114,14 +114,14 @@ def switch(version: Version) -> None: target_path = get_executable(version) link_path = '/usr/local/bin/terraform' - if os.path.exists(link_path): + if os.path.lexists(link_path): os.remove(link_path) os.symlink(target_path, link_path) if version.product == 'OpenTofu': link_path = '/usr/local/bin/tofu' - if os.path.exists(link_path): + if os.path.lexists(link_path): os.remove(link_path) os.symlink(target_path, link_path) From 6409dd29d77bd1a767ba8f13375fa31ac0498e62 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Mon, 30 Mar 2026 10:07:15 +0100 Subject: [PATCH 08/32] Disable dumb rule --- .github/zizmor.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 550e33a7..39d1b180 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1 +1,3 @@ -rules: {} +rules: + secrets-outside-env: + disable: true From cd1e1a8092960a5f544a7f7c7124f35430a277a5 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Tue, 7 Apr 2026 12:45:32 +0100 Subject: [PATCH 09/32] Escape quotes or backslashes in cloud tokens when writing --- image/tools/format_tf_credentials.py | 4 ++- tests/test_write_credentials.py | 41 +++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/image/tools/format_tf_credentials.py b/image/tools/format_tf_credentials.py index fa844744..e00f44f0 100755 --- a/image/tools/format_tf_credentials.py +++ b/image/tools/format_tf_credentials.py @@ -13,8 +13,10 @@ def format_credentials(input): match = re.search(r'(?P.+?)\s*=\s*(?P.+)', line.strip()) if match: + BACKSLASH = '\\' + token = match.group('token').replace(BACKSLASH, BACKSLASH + BACKSLASH).replace('"', BACKSLASH + '"') yield f'''credentials "{match.group('host')}" {{ - token = "{match.group('token')}" + token = "{token}" }} ''' else: diff --git a/tests/test_write_credentials.py b/tests/test_write_credentials.py index 4f1ef02d..4b59ac57 100644 --- a/tests/test_write_credentials.py +++ b/tests/test_write_credentials.py @@ -34,11 +34,11 @@ def test_multiple_creds(): def test_unrecognised_lines(): input = """ - - app.terraform.io=xxxxxx.atlasv1.zzzzzzzzzzzzz - + + app.terraform.io=xxxxxx.atlasv1.zzzzzzzzzzzzz + This doesn't look anything like a credential - + """ try: @@ -47,3 +47,36 @@ def test_unrecognised_lines(): pass else: assert False, 'Should have raised an exception' + +def test_token_with_quotes(): + input = 'app.terraform.io=token"with"quotes' + + expected_output = r'''credentials "app.terraform.io" { + token = "token\"with\"quotes" +} +''' + + output = ''.join(format_credentials(input)) + assert output == expected_output + +def test_token_with_backslashes(): + input = r'app.terraform.io=token\with\backslashes' + + expected_output = r'''credentials "app.terraform.io" { + token = "token\\with\\backslashes" +} +''' + + output = ''.join(format_credentials(input)) + assert output == expected_output + +def test_token_with_backslash_and_quote(): + input = r'app.terraform.io=token\"mixed' + + expected_output = r'''credentials "app.terraform.io" { + token = "token\\\"mixed" +} +''' + + output = ''.join(format_credentials(input)) + assert output == expected_output From 265abb3ae9bf8850870c223ce9d346b4ea67638a Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Tue, 7 Apr 2026 12:47:12 +0100 Subject: [PATCH 10/32] Use secrets module rather than random for random strings These are only used to create unique strings, there's no real security risk. But let's use secrets to avoid false positive findings --- image/actions.sh | 2 +- image/src/github_actions/commands.py | 4 ++-- image/workflow_commands.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/image/actions.sh b/image/actions.sh index 080a3ca9..37c22261 100644 --- a/image/actions.sh +++ b/image/actions.sh @@ -451,7 +451,7 @@ function output() { } function random_string() { - python3 -c "import random; import string; print(''.join(random.choice(string.ascii_lowercase) for i in range(8)))" + python3 -c "import secrets; import string; print(''.join(secrets.choice(string.ascii_lowercase) for i in range(8)))" } function write_credentials() { diff --git a/image/src/github_actions/commands.py b/image/src/github_actions/commands.py index d7b475de..6d3d45f2 100644 --- a/image/src/github_actions/commands.py +++ b/image/src/github_actions/commands.py @@ -1,4 +1,4 @@ -import random +import secrets import string import sys import os @@ -6,7 +6,7 @@ from typing import Any def generate_delimiter(): - return ''.join(random.choice(string.ascii_lowercase) for _ in range(20)) + return ''.join(secrets.choice(string.ascii_lowercase) for _ in range(20)) def _write_output(name: str, value: str) -> None: if 'GITHUB_OUTPUT' in os.environ and Path(os.environ['GITHUB_OUTPUT']).is_file(): diff --git a/image/workflow_commands.sh b/image/workflow_commands.sh index 5fad4acd..56827bba 100644 --- a/image/workflow_commands.sh +++ b/image/workflow_commands.sh @@ -126,5 +126,5 @@ function disable_workflow_commands() { } function generate_command_token() { - python3 -c "import random; import string; print(''.join(random.choice(string.ascii_lowercase) for i in range(64)))" + python3 -c "import secrets; import string; print(''.join(secrets.choice(string.ascii_lowercase) for i in range(64)))" } From b8e2ed5f57bf588259501af531f185323f9386cb Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Tue, 7 Apr 2026 12:47:29 +0100 Subject: [PATCH 11/32] Use correct mode --- image/src/opentofu/download.py | 2 +- image/src/terraform/download.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/image/src/opentofu/download.py b/image/src/opentofu/download.py index 83389386..08f45142 100644 --- a/image/src/opentofu/download.py +++ b/image/src/opentofu/download.py @@ -181,6 +181,6 @@ def get_executable(version: Version) -> Path: executable_path = Path(executable_dir, 'tofu') - os.chmod(executable_path, 755) + os.chmod(executable_path, 0o755) return executable_path diff --git a/image/src/terraform/download.py b/image/src/terraform/download.py index 4b470dd7..22b936c6 100644 --- a/image/src/terraform/download.py +++ b/image/src/terraform/download.py @@ -214,6 +214,6 @@ def get_executable(version: Version) -> Path: executable_path = Path(executable_dir, 'terraform') - os.chmod(executable_path, 755) + os.chmod(executable_path, 0o755) return executable_path From 639677ba927032d52131efec26942c23accb1ace Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Tue, 7 Apr 2026 12:48:00 +0100 Subject: [PATCH 12/32] Use variables in graphql query --- image/src/github_pr_comment/comment.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/image/src/github_pr_comment/comment.py b/image/src/github_pr_comment/comment.py index 69135775..2fa342ae 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -398,13 +398,13 @@ def hide_comment( response = github.post( graphql_url, json={ - 'query': ''' - mutation { - minimizeComment(input: {subjectId: "''' + comment.node_id + '''", classifier: ''' + classifier + '''}) { - clientMutationId - } + 'query': 'mutation($input: MinimizeCommentInput!) { minimizeComment(input: $input) { clientMutationId } }', + 'variables': { + 'input': { + 'subjectId': comment.node_id, + 'classifier': classifier } - ''' + } } ) debug(f'graphql response: {response.content}') From 1489efeaa032f7cdfdb316c28a11c950649c739c Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 17 Jul 2026 19:14:12 +0100 Subject: [PATCH 13/32] Validate request URLs belong to the expected GitHub host The GithubApi client now normalizes paths against its configured host and rejects full URLs belonging to a different host, including pagination links. GraphQL requests go through a dedicated graphql() method with a configurable URL. The pull_request url in repository_dispatch payloads is validated against GITHUB_API_URL. --- .github/workflows/test.yaml | 4 +- image/src/github_actions/api.py | 47 ++++++++++++----- image/src/github_actions/find_pr.py | 10 ++-- image/src/github_pr_comment/__main__.py | 13 +++-- image/src/github_pr_comment/comment.py | 5 +- image/src/opentofu/versions.py | 2 +- tests/github_pr_comment/test_hide_comment.py | 54 ++++++++++++++++++++ tests/test_generate_delimiter.py | 17 ++++++ 8 files changed, 124 insertions(+), 28 deletions(-) create mode 100644 tests/github_pr_comment/test_hide_comment.py create mode 100644 tests/test_generate_delimiter.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8405621f..e4fd94a4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,7 +19,7 @@ jobs: - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: 3.9 + python-version: "3.13" - name: Install dependencies run: | @@ -50,7 +50,7 @@ jobs: - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: 3.9 + python-version: "3.13" - name: Install dependencies run: | diff --git a/image/src/github_actions/api.py b/image/src/github_actions/api.py index e2893635..cc942413 100644 --- a/image/src/github_actions/api.py +++ b/image/src/github_actions/api.py @@ -17,9 +17,10 @@ class GithubApi: - def __init__(self, host: str, token: Optional[str], cache_path: Optional[str] = None): - self._host = host + def __init__(self, host: str, token: Optional[str], graphql_url: Optional[str] = None, cache_path: Optional[str] = None): + self._host = host.rstrip('/') self._token = token + self._graphql_url = graphql_url or f'{self._host}/graphql' if cache_path is not None: urls_expire_after = { @@ -39,8 +40,23 @@ def __init__(self, host: str, token: Optional[str], cache_path: Optional[str] = self._session.headers['user-agent'] = 'terraform-github-actions' self._session.headers['accept'] = 'application/vnd.github.v3+json' - def api_request(self, method: str, *args, **kwargs) -> requests.Response: - response = self._session.request(method, *args, **kwargs) + def _url(self, path_or_url: str) -> str: + """Normalize a path or full GitHub API URL to a full URL on this host. + + Accepts: + - A path starting with / (prepended with host) + - A full URL belonging to this GitHub instance (returned as-is) + + Raises RuntimeError for URLs belonging to a different host. + """ + if path_or_url.startswith('/'): + return f'{self._host}{path_or_url}' + if path_or_url.startswith(self._host + '/') or path_or_url == self._host: + return path_or_url + raise RuntimeError(f'URL does not belong to the expected GitHub API ({self._host}): {path_or_url}') + + def _api_request(self, method: str, url: str, **kwargs) -> requests.Response: + response = self._session.request(method, url, **kwargs) debug(f'{response.request.method} {response.request.url} -> {response.status_code}') if 400 <= response.status_code < 500: @@ -65,19 +81,23 @@ def api_request(self, method: str, *args, **kwargs) -> requests.Response: return response - def get(self, path: str, **kwargs: Any) -> Response: - return self.api_request('GET', path, **kwargs) + def get(self, path_or_url: str, **kwargs: Any) -> Response: + return self._api_request('GET', self._url(path_or_url), **kwargs) - def post(self, path: str, **kwargs: Any) -> Response: - return self.api_request('POST', path, **kwargs) + def post(self, path_or_url: str, **kwargs: Any) -> Response: + return self._api_request('POST', self._url(path_or_url), **kwargs) - def patch(self, path: str, **kwargs: Any) -> Response: - return self.api_request('PATCH', path, **kwargs) + def patch(self, path_or_url: str, **kwargs: Any) -> Response: + return self._api_request('PATCH', self._url(path_or_url), **kwargs) - def paged_get(self, url: GitHubUrl, *args, **kwargs) -> Iterable[dict[str, Any]]: - while True: + def graphql(self, **kwargs: Any) -> Response: + return self._api_request('POST', self._graphql_url, **kwargs) + + def paged_get(self, path_or_url: str, **kwargs) -> Iterable[dict[str, Any]]: + url = self._url(path_or_url) - response = self.api_request('GET', url, *args, **kwargs) + while True: + response = self._api_request('GET', url, **kwargs) response.raise_for_status() if hasattr(response, 'from_cache'): @@ -90,5 +110,6 @@ def paged_get(self, url: GitHubUrl, *args, **kwargs) -> Iterable[dict[str, Any]] # Relevant params are already in the link URL del kwargs['params'] url = response.links['next']['url'] + self._url(url) # validate it belongs to this host else: return diff --git a/image/src/github_actions/find_pr.py b/image/src/github_actions/find_pr.py index 375639a0..6194c6f6 100644 --- a/image/src/github_actions/find_pr.py +++ b/image/src/github_actions/find_pr.py @@ -64,15 +64,19 @@ def find_pr(github: GithubApi, actions_env: GithubEnv) -> PrUrl: if 'url' not in event['client_payload']['pull_request']: raise WorkflowException('The pull_request object in the client_payload must have a url') - return cast(PrUrl, event['client_payload']['pull_request']['url']) + pr_url = event['client_payload']['pull_request']['url'] + github_api_url = actions_env.get('GITHUB_API_URL', 'https://api.github.com').rstrip('/') + if not pr_url.startswith(github_api_url + '/'): + raise WorkflowException(f'The pull_request url in the client_payload does not match the expected GitHub API URL ({github_api_url})') + + return cast(PrUrl, pr_url) elif event_type == 'push': repo = actions_env['GITHUB_REPOSITORY'] commit = actions_env['GITHUB_SHA'] def prs() -> Iterable[dict[str, Any]]: - url = cast(PrUrl, f'{actions_env["GITHUB_API_URL"]}/repos/{repo}/pulls') - yield from github.paged_get(url, params={'state': 'all'}) + yield from github.paged_get(f'/repos/{repo}/pulls', params={'state': 'all'}) for pr in prs(): if pr['merge_commit_sha'] == commit: diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py index de4be08b..a083eb4c 100644 --- a/image/src/github_pr_comment/__main__.py +++ b/image/src/github_pr_comment/__main__.py @@ -35,7 +35,12 @@ env = cast(GithubEnv, os.environ) github_token = env['TERRAFORM_ACTIONS_GITHUB_TOKEN'] -github = GithubApi(env.get('GITHUB_API_URL', 'https://api.github.com'), github_token, os.environ.get('JOB_TMP_DIR', '.')) +github = GithubApi( + env.get('GITHUB_API_URL', 'https://api.github.com'), + github_token, + graphql_url=env.get('GITHUB_GRAPHQL_URL', f'{env.get("GITHUB_API_URL", "https://api.github.com")}/graphql'), + cache_path=os.environ.get('JOB_TMP_DIR', '.') +) ToolProductName = os.environ.get('TOOL_PRODUCT_NAME', 'Terraform') @@ -213,9 +218,7 @@ def current_user(actions_env: GithubEnv) -> str: cache_key = f'token-cache/{token_hash}' def graphql() -> Optional[str]: - graphql_url = actions_env.get('GITHUB_GRAPHQL_URL', f'{actions_env["GITHUB_API_URL"]}/graphql') - - response = github.post(graphql_url, json={ + response = github.graphql(json={ 'query': "query { viewer { login } }" }) debug(f'graphql response: {response.content}') @@ -229,7 +232,7 @@ def graphql() -> Optional[str]: debug('Failed to get current user from graphql') def rest() -> Optional[str]: - response = github.get(f'{actions_env["GITHUB_API_URL"]}/user') + response = github.get('/user') debug(f'rest response: {response.content}') if response.ok: diff --git a/image/src/github_pr_comment/comment.py b/image/src/github_pr_comment/comment.py index 2fa342ae..61b78e50 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -394,10 +394,7 @@ def hide_comment( debug('Comment has unknown node_id - not hiding') return - graphql_url = os.environ.get('GITHUB_GRAPHQL_URL', f'{os.environ["GITHUB_API_URL"]}/graphql') - - response = github.post( - graphql_url, json={ + response = github.graphql(json={ 'query': 'mutation($input: MinimizeCommentInput!) { minimizeComment(input: $input) { clientMutationId } }', 'variables': { 'input': { diff --git a/image/src/opentofu/versions.py b/image/src/opentofu/versions.py index 103a9e6f..a97160f3 100644 --- a/image/src/opentofu/versions.py +++ b/image/src/opentofu/versions.py @@ -7,7 +7,7 @@ def get_opentofu_versions() -> Iterable[Version]: """Return the currently available opentofu versions.""" - response = github.paged_get('https://api.github.com/repos/opentofu/opentofu/releases') + response = github.paged_get('/repos/opentofu/opentofu/releases') for release in response: yield Version(release['tag_name'].lstrip('v'), 'OpenTofu') diff --git a/tests/github_pr_comment/test_hide_comment.py b/tests/github_pr_comment/test_hide_comment.py new file mode 100644 index 00000000..873e4e65 --- /dev/null +++ b/tests/github_pr_comment/test_hide_comment.py @@ -0,0 +1,54 @@ +from unittest.mock import MagicMock + +from github_pr_comment.comment import hide_comment, TerraformComment + + +def test_hide_comment_uses_graphql_variables(monkeypatch): + monkeypatch.setenv('GITHUB_API_URL', 'https://api.github.com') + + mock_github = MagicMock() + + comment = TerraformComment( + issue_url='https://api.github.com/repos/test/test/issues/1', + comment_url='https://api.github.com/repos/test/test/issues/comments/123', + node_id='MDEyOk_test_node_id', + headers={}, + description='test', + summary='test', + body='test', + status='test' + ) + + hide_comment(mock_github, comment, 'OUTDATED') + + mock_github.graphql.assert_called_once_with(json={ + 'query': 'mutation($input: MinimizeCommentInput!) { minimizeComment(input: $input) { clientMutationId } }', + 'variables': { + 'input': { + 'subjectId': 'MDEyOk_test_node_id', + 'classifier': 'OUTDATED' + } + } + } + ) + + +def test_hide_comment_skips_when_no_node_id(monkeypatch): + monkeypatch.setenv('GITHUB_API_URL', 'https://api.github.com') + + mock_github = MagicMock() + + comment = TerraformComment( + issue_url='https://api.github.com/repos/test/test/issues/1', + comment_url='https://api.github.com/repos/test/test/issues/comments/123', + node_id=None, + headers={}, + description='test', + summary='test', + body='test', + status='test' + ) + + hide_comment(mock_github, comment, 'OUTDATED') + + mock_github.graphql.assert_not_called() diff --git a/tests/test_generate_delimiter.py b/tests/test_generate_delimiter.py new file mode 100644 index 00000000..30602117 --- /dev/null +++ b/tests/test_generate_delimiter.py @@ -0,0 +1,17 @@ +from github_actions.commands import generate_delimiter + + +def test_delimiter_length(): + delimiter = generate_delimiter() + assert len(delimiter) == 20 + + +def test_delimiter_is_lowercase_alpha(): + delimiter = generate_delimiter() + assert delimiter.isalpha() + assert delimiter.islower() + + +def test_delimiter_is_unique(): + delimiters = {generate_delimiter() for _ in range(100)} + assert len(delimiters) == 100 From 8f39e8ca56ddbb059bdbc602dd4879a31ce8e511 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 17 Jul 2026 19:22:57 +0100 Subject: [PATCH 14/32] Pin python-hcl2 for tests 8.0 breaks on real terraform --- tests/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index 94e0ec72..aabeaec7 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,7 +1,7 @@ requests requests-cache pytest -python-hcl2 +python-hcl2==7.3.1 canonicaljson types-requests mypy From d36b2dd6fd969f832d4869fb517cebd34a7dc230 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 17 Jul 2026 19:46:28 +0100 Subject: [PATCH 15/32] Disable unpinned-images zizmor rule It causes false positives from the action metadata, which gets images pinned during the release process --- .github/zizmor.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 39d1b180..700092fa 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,3 +1,7 @@ rules: secrets-outside-env: disable: true + unpinned-images: + # action.yaml files reference ../image/Dockerfile for local builds; the + # image is pinned to a digest during the release process. + disable: true From 4d31cf0cbbb7ff8234c6db2ed1ade94cbe42dc1f Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 17 Jul 2026 20:03:35 +0100 Subject: [PATCH 16/32] Update for terraform 1.15 --- .github/workflows/test-version.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-version.yaml b/.github/workflows/test-version.yaml index d8398e63..b2d24d82 100644 --- a/.github/workflows/test-version.yaml +++ b/.github/workflows/test-version.yaml @@ -611,7 +611,7 @@ jobs: run: | echo "The terraform version was $DETECTED_TERRAFORM_VERSION" - if [[ "$DETECTED_TERRAFORM_VERSION" != *"1.14"* ]]; then + if [[ "$DETECTED_TERRAFORM_VERSION" != *"1.15"* ]]; then echo "::error:: Latest version was not used" exit 1 fi @@ -632,7 +632,7 @@ jobs: run: | echo "The terraform version was $DETECTED_TERRAFORM_VERSION" - if [[ "$DETECTED_TERRAFORM_VERSION" != *"1.14"* ]]; then + if [[ "$DETECTED_TERRAFORM_VERSION" != *"1.15"* ]]; then echo "::error:: Latest version was not used" exit 1 fi From dd85953a1985b652f1770df854b61ed7d71aace7 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 17 Jul 2026 20:45:25 +0100 Subject: [PATCH 17/32] Fix missing run-id bug If there is ever a cloud backend that doesn't supply a run-id, this now shows the intended message rather than terminate --- image/entrypoints/plan.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image/entrypoints/plan.sh b/image/entrypoints/plan.sh index 7f9a053d..bc02f14e 100755 --- a/image/entrypoints/plan.sh +++ b/image/entrypoints/plan.sh @@ -89,7 +89,7 @@ if [[ -n "$PLAN_OUT" ]]; then else debug_file "$STEP_TMP_DIR/terraform_show.stderr" fi -elif [[ -n "$RUN_ID" ]]; then +elif [[ -n "${RUN_ID:-}" ]]; then if terraform-cloud-state "$RUN_ID" >"$STEP_TMP_DIR/terraform_cloud_state.stdout" 2>"$STEP_TMP_DIR/terraform_cloud_state.stderr"; then debug_log "Fetched JSON plan from TFC" cp "$STEP_TMP_DIR/terraform_cloud_state.stdout" "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.json" From 11a51974c562b4d855aac9b348d787be7c078027 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 17 Jul 2026 20:49:07 +0100 Subject: [PATCH 18/32] Harden version checks Cast to int and disregard prerelease suffix --- image/actions.sh | 2 +- image/entrypoints/test.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/image/actions.sh b/image/actions.sh index 37c22261..944eee38 100644 --- a/image/actions.sh +++ b/image/actions.sh @@ -69,7 +69,7 @@ function test-terraform-version() { local OP="$1" local VER="$2" - python3 -c "exit(0 if ($TERRAFORM_VER_MAJOR, $TERRAFORM_VER_MINOR, $TERRAFORM_VER_PATCH) $OP tuple(int(v) for v in '$VER'.split('.')) else 1)" + python3 -c "exit(0 if (int('$TERRAFORM_VER_MAJOR'), int('$TERRAFORM_VER_MINOR'), int('$TERRAFORM_VER_PATCH'.split('-')[0])) $OP tuple(int(v) for v in '$VER'.split('.')) else 1)" } function job_markdown_ref() { diff --git a/image/entrypoints/test.sh b/image/entrypoints/test.sh index 34851f87..30c148fc 100755 --- a/image/entrypoints/test.sh +++ b/image/entrypoints/test.sh @@ -27,7 +27,7 @@ function set-test-args() { done fi - if [[ "$TOOL_COMMAND_NAME" == "terraform" && $TERRAFORM_VER_MAJOR -ge 1 && $TERRAFORM_VER_MINOR -ge 11 ]]; then + if [[ "$TOOL_COMMAND_NAME" == "terraform" ]] && test-terraform-version ">=" "1.11.0"; then TEST_ARGS="$TEST_ARGS -junit-xml=$STEP_TMP_DIR/test-result.xml" fi } From f9526dd0e8a3fd65267a8002ea64a791e96a53d5 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 17 Jul 2026 20:59:23 +0100 Subject: [PATCH 19/32] Add specific messaging for missing approved binary plan --- image/src/github_pr_comment/__main__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py index a083eb4c..28ee838b 100644 --- a/image/src/github_pr_comment/__main__.py +++ b/image/src/github_pr_comment/__main__.py @@ -606,6 +606,18 @@ def main() -> int: output('failure-reason', 'plan-changed') sys.exit(1) + if 'plan_out_hash' not in comment.headers: + sys.stdout.write("Not applying the plan - the plan on the PR cannot be verified against the plan file\n") + sys.stdout.write("The PR comment was created without a saved plan, or by an old version of the dflook/terraform-plan action\n") + sys.stdout.write("Regenerate the plan on the PR using the dflook/terraform-plan action. Alternatively, set the auto_approve input to 'true' to apply without verification\n") + + comment = update_comment(github, comment, status=f':x: Plan not applied in {job_markdown_ref()} (Unable to verify the plan)') + + output('failure-reason', 'plan-changed') + + step_cache['comment'] = serialize(comment) + return 1 + if not is_approved_binary_plan(sys.argv[2], comment): sys.stdout.write("Not applying the plan - it has changed from the plan on the PR\n") From 2679e0a5c2cc927204dc8295d4eaa8ad5a4333dc Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 17 Jul 2026 21:24:11 +0100 Subject: [PATCH 20/32] Add message for unparsable variables --- image/src/github_pr_comment/__main__.py | 27 ++++++++++++++++++------- image/src/terraform/hcl.py | 19 ++++++++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py index 28ee838b..680021a3 100644 --- a/image/src/github_pr_comment/__main__.py +++ b/image/src/github_pr_comment/__main__.py @@ -162,15 +162,25 @@ def format_description(action_inputs: PlanPrInputs, sensitive_variables: List[st if action_inputs["INPUT_VAR_FILE"]: label += f'\nWith var files: `{action_inputs["INPUT_VAR_FILE"]}`' - if action_inputs["INPUT_VARIABLES"]: - variables = hcl.loads(action_inputs["INPUT_VARIABLES"]) + def format_variables() -> str: + try: + variables = hcl.loads(action_inputs["INPUT_VARIABLES"]) + except ValueError: + # Without parsing we can't tell which values are sensitive, so only show them when none can be. + if sensitive_variables: + return '\nWith variables: `(variables are hidden because they could not be parsed to mask sensitive values)`' + + stripped_vars = action_inputs["INPUT_VARIABLES"].strip() + + else: + # mask sensitive variables + variables = {name: Sensitive() if name in sensitive_variables else value for name, value in + variables.items()} - # mark sensitive variables - variables = {name: Sensitive() if name in sensitive_variables else value for name, value in variables.items()} + stripped_vars = render_argument_list(variables).strip() - stripped_vars = render_argument_list(variables).strip() if '\n' in stripped_vars: - label += f'''
With variables + return f'''
With variables ```hcl {stripped_vars} @@ -178,7 +188,10 @@ def format_description(action_inputs: PlanPrInputs, sensitive_variables: List[st
''' else: - label += f'\nWith variables: `{stripped_vars}`' + return f'\nWith variables: `{stripped_vars}`' + + if action_inputs["INPUT_VARIABLES"]: + label += format_variables() return label diff --git a/image/src/terraform/hcl.py b/image/src/terraform/hcl.py index ef25cf1b..dc83ae81 100644 --- a/image/src/terraform/hcl.py +++ b/image/src/terraform/hcl.py @@ -5,6 +5,7 @@ import hcl2 # type: ignore import sys import subprocess +import tempfile from pathlib import Path from github_actions.debug import debug @@ -47,13 +48,21 @@ def load(path: Path) -> dict: def loads(hcl: str) -> dict: - tmp_path = Path('/tmp/load_test.hcl') - - with open(tmp_path, 'w') as f: + with tempfile.NamedTemporaryFile('w', suffix='.hcl', delete=False) as f: f.write(hcl) + tmp_path = Path(f.name) - if is_loadable(tmp_path): - return hcl2.loads(hcl) + try: + loadable = is_loadable(tmp_path) + finally: + tmp_path.unlink(missing_ok=True) + + if loadable: + try: + return hcl2.loads(hcl) + except Exception as e: + debug('Failed to parse hcl') + debug(str(e)) debug('Unable to load hcl') raise ValueError('Unable to load hcl') From eb842c37e5293d56be99aac8c89127bc02a15d8a Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 18 Jul 2026 10:24:11 +0100 Subject: [PATCH 21/32] Fix backend fingerprinting for backend fields containing '=' --- image/src/github_pr_comment/__main__.py | 74 ++++- image/src/github_pr_comment/backend_config.py | 75 ++++- image/src/github_pr_comment/comment.py | 72 ++++- image/src/terraform_version/remote_state.py | 2 +- .../test_backend_file.tfvars | 1 + tests/github_pr_comment/test_find_comment.py | 292 ++++++++++++++++++ tests/github_pr_comment/test_fingerprint.py | 41 ++- 7 files changed, 528 insertions(+), 29 deletions(-) create mode 100644 tests/github_pr_comment/test_backend_file.tfvars create mode 100644 tests/github_pr_comment/test_find_comment.py diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py index 680021a3..2eed79d9 100644 --- a/image/src/github_pr_comment/__main__.py +++ b/image/src/github_pr_comment/__main__.py @@ -16,15 +16,16 @@ from github_actions.env import GithubEnv from github_actions.find_pr import find_pr, WorkflowException from github_actions.inputs import PlanPrInputs -from github_pr_comment.backend_config import complete_config, partial_config +from github_pr_comment.backend_config import complete_config, legacy_complete_config, legacy_partial_config, COMPLETE_FINGERPRINT_SINCE_VERSION, FIXED_FINGERPRINT_SINCE_VERSION from github_pr_comment.backend_fingerprint import fingerprint from github_pr_comment.cmp import plan_cmp, remove_warnings, remove_unchanged_attributes -from github_pr_comment.comment import find_comment, TerraformComment, update_comment, serialize, deserialize, hide_comment +from github_pr_comment.comment import find_comment, BackupHeaders, TerraformComment, update_comment, serialize, deserialize, hide_comment from github_pr_comment.hash import comment_hash, plan_hash, plan_out_hash from github_pr_comment.plan_formatting import format_diff from plan_renderer.outputs import render_outputs from plan_renderer.variables import render_argument_list, Sensitive -from terraform.module import load_module, get_sensitive_variables +from terraform.module import load_module, get_sensitive_variables, TerraformModule +from terraform.versions import Version from terraform import hcl Plan = NewType('Plan', str) @@ -347,7 +348,57 @@ def new_pr_comment(backend_fingerprint: bytes) -> TerraformComment: status='' ) -def get_comment(action_inputs: PlanPrInputs, backend_fingerprint: bytes, backup_fingerprint: bytes) -> TerraformComment: +def _widest_min(a: Optional[str], b: Optional[str]) -> Optional[str]: + if a is None or b is None: + return None + return a if Version(a) < Version(b) else b + + +def _widest_max(a: Optional[str], b: Optional[str]) -> Optional[str]: + if a is None or b is None: + return None + return a if Version(b) < Version(a) else b + + +def get_backend_fingerprints(action_inputs: PlanPrInputs, module: TerraformModule) -> Tuple[bytes, List[Tuple[bytes, Optional[str], Optional[str]]]]: + """ + The current backend fingerprint, and the fingerprints that may have been used for comments + created by earlier versions, with the range of versions that used each. + + Earlier fingerprints: + legacy_complete_config, before the backend_config input parsing was fixed. + legacy_partial_config, used by versions before that, which also didn't read backend config files. + + Where the fingerprints for different eras are identical their version ranges are combined. + The eras are contiguous, so the combined range is simply the widest bounds. + """ + + backend_type, backend_config = complete_config(action_inputs, module) + backend_fingerprint = fingerprint(backend_type, backend_config, os.environ) + + backup_fingerprints: List[Tuple[bytes, Optional[str], Optional[str]]] = [] + for fallback_config, min_version, max_version in ( + (legacy_complete_config, COMPLETE_FINGERPRINT_SINCE_VERSION, FIXED_FINGERPRINT_SINCE_VERSION), + (legacy_partial_config, None, COMPLETE_FINGERPRINT_SINCE_VERSION), + ): + fallback_backend_type, fallback_backend_config = fallback_config(action_inputs, module) + backup_fingerprint = fingerprint(fallback_backend_type, fallback_backend_config, os.environ) + + if backup_fingerprint == backend_fingerprint: + # Comments with this fingerprint are matched by the primary headers + continue + + for index, (existing, existing_min, existing_max) in enumerate(backup_fingerprints): + if existing == backup_fingerprint: + backup_fingerprints[index] = (existing, _widest_min(existing_min, min_version), _widest_max(existing_max, max_version)) + break + else: + backup_fingerprints.append((backup_fingerprint, min_version, max_version)) + + return backend_fingerprint, backup_fingerprints + + +def get_comment(action_inputs: PlanPrInputs, backend_fingerprint: bytes, backup_fingerprints: List[Tuple[bytes, Optional[str], Optional[str]]]) -> TerraformComment: if 'comment' in step_cache: return deserialize(step_cache['comment']) @@ -386,10 +437,13 @@ def get_comment(action_inputs: PlanPrInputs, backend_fingerprint: bytes, backup_ debug(f'Plan modifier: {plan_modifier}') headers['plan_modifier'] = hashlib.sha256(canonicaljson.encode_canonical_json(plan_modifier)).hexdigest() - backup_headers = headers.copy() + backup_headers = [] + for backup_fingerprint, min_version, max_version in backup_fingerprints: + backup = headers.copy() + backup['backend'] = comment_hash(backup_fingerprint, pr_url) + backup_headers.append(BackupHeaders(backup, min_version, max_version)) headers['backend'] = comment_hash(backend_fingerprint, pr_url) - backup_headers['backend'] = comment_hash(backup_fingerprint, pr_url) return find_comment(github, issue_url, username, headers, backup_headers, legacy_description) @@ -489,13 +543,9 @@ def main() -> int: module = load_module(Path(action_inputs.get('INPUT_PATH', '.'))) - backend_type, backend_config = partial_config(action_inputs, module) - partial_backend_fingerprint = fingerprint(backend_type, backend_config, os.environ) - - backend_type, backend_config = complete_config(action_inputs, module) - backend_fingerprint = fingerprint(backend_type, backend_config, os.environ) + backend_fingerprint, backup_fingerprints = get_backend_fingerprints(action_inputs, module) - comment = get_comment(action_inputs, backend_fingerprint, partial_backend_fingerprint) + comment = get_comment(action_inputs, backend_fingerprint, backup_fingerprints) status = cast(Status, os.environ.get('STATUS', '')) diff --git a/image/src/github_pr_comment/backend_config.py b/image/src/github_pr_comment/backend_config.py index 81fa4fdc..bdc6062a 100644 --- a/image/src/github_pr_comment/backend_config.py +++ b/image/src/github_pr_comment/backend_config.py @@ -16,10 +16,11 @@ def read_module_backend_config(module: TerraformModule) -> Tuple[BackendType, Ba for terraform in module.get('terraform', []): for backend in terraform.get('backend', []): for backend_type, config in backend.items(): - return backend_type, config + # Copy so callers can overlay additional config without affecting the parsed module + return backend_type, dict(config) for cloud in terraform.get('cloud', []): - return 'cloud', cloud + return 'cloud', dict(cloud) return 'local', {} @@ -45,6 +46,35 @@ def read_backend_config_input(init_inputs: InitInputs) -> BackendConfig: config: BackendConfig = {} + for backend_var in init_inputs.get('INPUT_BACKEND_CONFIG', '').replace(',', '\n').splitlines(): + if match := re.match(r'(.*?)\s*=\s*(.*)', backend_var): + config[match.group(1)] = match.group(2) + + return config + +# The first version that includes backend config files in the fingerprint. This version also +# started stamping comments with a version header, so comments without one are from earlier +# versions. Versions 1.32.0-1.32.1 stamped comments with '1.31.1', which is fine for this purpose. +COMPLETE_FINGERPRINT_SINCE_VERSION = '1.31.1' + +# The first version that parses the backend_config input by splitting key=value pairs on the +# first '=' instead of the last. Comments created by this version or later never carry legacy +# fingerprints, so are never matched against them. +# This must be no later than the first release that includes the fix. +FIXED_FINGERPRINT_SINCE_VERSION = '2.2.4' + +def read_legacy_backend_config_input(init_inputs: InitInputs) -> BackendConfig: + """ + Read any backend config from input variables, as parsed by old versions. + + Old versions split key=value pairs on the last '=' instead of the first, + and kept whitespace around the key. + This is only used to match PR comments created using these old versions, + until enough time has passed that we don't need to use old comments. + """ + + config: BackendConfig = {} + for backend_var in init_inputs.get('INPUT_BACKEND_CONFIG', '').replace(',', '\n').splitlines(): if match := re.match(r'(.*)\s*=\s*(.*)', backend_var): config[match.group(1)] = match.group(2) @@ -59,8 +89,8 @@ def partial_config(action_inputs: InitInputs, module: TerraformModule) -> Tuple[ & any values from the backend_config input. This doesn't read from backend config files. Old versions didn't read from backend config files - and so created incorrect fingerprints. This is still used to match PR comments created using these old - versions, until enough time has passed that we don't need to use old comments. + and so created incorrect fingerprints. Comments created by those versions are matched using + legacy_partial_config, which also parses the backend_config input as they did. """ backend_type, config = read_module_backend_config(module) @@ -91,3 +121,40 @@ def complete_config(action_inputs: InitInputs, module: TerraformModule) -> Tuple config[key] = value return backend_type, config + + +def legacy_partial_config(action_inputs: InitInputs, module: TerraformModule) -> Tuple[BackendType, BackendConfig]: + """ + A partial backend config for the terraform module, as created by old versions. + + This is partial_config with the backend_config input parsed as old versions did. + This is still used to match PR comments created by versions before + COMPLETE_FINGERPRINT_SINCE_VERSION, which also didn't stamp comments with a version header. + """ + + backend_type, config = read_module_backend_config(module) + + for key, value in read_legacy_backend_config_input(action_inputs).items(): + config[key] = value + + return backend_type, config + + +def legacy_complete_config(action_inputs: InitInputs, module: TerraformModule) -> Tuple[BackendType, BackendConfig]: + """ + The complete backend config for the terraform module, as created by old versions. + + This is complete_config with the backend_config input parsed as old versions did. + This is still used to match PR comments created by versions from + COMPLETE_FINGERPRINT_SINCE_VERSION until FIXED_FINGERPRINT_SINCE_VERSION. + """ + + backend_type, config = read_module_backend_config(module) + + for key, value in read_backend_config_files(action_inputs).items(): + config[key] = value + + for key, value in read_legacy_backend_config_input(action_inputs).items(): + config[key] = value + + return backend_type, config diff --git a/image/src/github_pr_comment/comment.py b/image/src/github_pr_comment/comment.py index 61b78e50..4431b2ba 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -2,10 +2,11 @@ import os import re from json import JSONDecodeError -from typing import Optional, Any +from typing import Mapping, NamedTuple, Optional, Any from github_actions.api import IssueUrl, GithubApi, CommentUrl, NodeId from github_actions.debug import debug +from terraform.versions import Version try: collapse_threshold = int(os.environ['TF_PLAN_COLLAPSE_LENGTH']) @@ -161,7 +162,9 @@ def _parse_comment_header(comment_header: Optional[str]) -> dict[str, str]: if header := re.match(r'^', comment_header): try: - return json.loads(header['args']) + # Some versions could write literal null header values, which made the comment + # unmatchable. A null value is never meaningful, so scrub them. + return {k: v for k, v in json.loads(header['args']).items() if v is not None} except JSONDecodeError: return {} @@ -231,7 +234,7 @@ def _to_api_payload(comment: TerraformComment) -> str: return body -def matching_headers(comment: TerraformComment, headers: dict[str, str]) -> bool: +def matching_headers(comment: TerraformComment, headers: Mapping[str, Optional[str]]) -> bool: """ Does a comment have all the specified headers @@ -248,11 +251,27 @@ def matching_headers(comment: TerraformComment, headers: dict[str, str]) -> bool return True -def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: dict[str, str], backup_headers: dict[str, str], legacy_description: str) -> TerraformComment: +class BackupHeaders(NamedTuple): + """ + A set of headers a comment may have been created with by an earlier version of the action. + + Only comments created by a version within [min_version, max_version) are matched. + A comment without a version header predates version stamping, and only matches if min_version is None. + """ + headers: dict[str, Optional[str]] + min_version: Optional[str] = None + max_version: Optional[str] = None + + +def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: dict[str, str], backup_headers: list[BackupHeaders], legacy_description: str) -> TerraformComment: """ Find a github comment that matches the given headers - If no comment is found with the specified headers, tries to find a comment that matches the specified description instead. + If no comment is found with the specified headers, tries to find a comment that matches one of the backup + header sets instead. These are the headers that may have been used by the version range of the action + given in each set. + + If still no comment is found, tries to find a comment that matches the specified description instead. This is in case the comment was made with an earlier version, where comments were matched by description only. If no existing comment is found a new TerraformComment object is returned which represents a PR comment yet to be created. @@ -261,13 +280,40 @@ def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: :param issue_url: The issue to find the comment in :param username: The user who made the comment :param headers: The headers that must be present on the comment + :param backup_headers: Backup sets of headers to match, in order of preference :param legacy_description: The description that must be present on the comment, if not headers are found. """ debug(f"Searching for comment with {headers=}") debug(f"Or backup headers {backup_headers=}") - backup_comment = None + def comment_version_within(comment: TerraformComment, min_version: Optional[str], max_version: Optional[str]) -> bool: + """Could the comment have been created by a version within [min_version, max_version)""" + + if 'version' not in comment.headers: + # Comments without a version header predate version stamping + return min_version is None + + if str(comment.headers['version']) in ('0.0.0', '99.0.0'): + # Placeholder versions stamped by images built without a release version + # (the Dockerfile VERSION default, or the fallback when the package version is unknown). + # They tell us nothing about the version that created the comment. + return True + + try: + comment_version = Version(str(comment.headers['version'])) + except ValueError: + return True + + if min_version is not None and comment_version < Version(min_version): + return False + + if max_version is not None and not comment_version < Version(max_version): + return False + + return True + + backup_comments: dict[int, TerraformComment] = {} legacy_comment = None for comment_payload in github.paged_get(issue_url + '/comments', params={'per_page': 100}): @@ -283,9 +329,11 @@ def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: debug(f'Found comment that matches headers {comment.headers=} ') return comment - if matching_headers(comment, backup_headers): - debug(f'Found comment that matches backup headers {comment.headers=} ') - backup_comment = comment + for backup_index, backup in enumerate(backup_headers): + if comment_version_within(comment, backup.min_version, backup.max_version) and matching_headers(comment, backup.headers): + debug(f'Found comment that matches backup headers {backup_index} {comment.headers=} ') + backup_comments[backup_index] = comment + break else: debug(f"Didn't match comment with {comment.headers=}") @@ -298,7 +346,9 @@ def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: else: debug(f"Didn't match comment with {comment.description=}") - if backup_comment is not None: + if backup_comments: + # Use the comment matching the most preferred backup header set + backup_comment = backup_comments[min(backup_comments)] debug('Using comment matching backup headers') # Use the backup comment but update the headers @@ -306,7 +356,7 @@ def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: issue_url=backup_comment.issue_url, comment_url=backup_comment.comment_url, node_id=backup_comment.node_id, - headers=backup_comment.headers | headers, + headers={k: v for k, v in (backup_comment.headers | headers).items() if v is not None}, description=backup_comment.description, summary=backup_comment.summary, body=backup_comment.body, diff --git a/image/src/terraform_version/remote_state.py b/image/src/terraform_version/remote_state.py index 78ec6507..552240f2 100644 --- a/image/src/terraform_version/remote_state.py +++ b/image/src/terraform_version/remote_state.py @@ -32,7 +32,7 @@ def read_backend_config_vars(init_inputs: InitInputs) -> dict[str, str]: debug(str(e)) for backend_var in init_inputs.get('INPUT_BACKEND_CONFIG', '').replace(',', '\n').splitlines(): - if match := re.match(r'(.*)\s*=\s*(.*)', backend_var): + if match := re.match(r'(.*?)\s*=\s*(.*)', backend_var): config[match.group(1)] = match.group(2) return config diff --git a/tests/github_pr_comment/test_backend_file.tfvars b/tests/github_pr_comment/test_backend_file.tfvars new file mode 100644 index 00000000..d129f9b7 --- /dev/null +++ b/tests/github_pr_comment/test_backend_file.tfvars @@ -0,0 +1 @@ +schema_name = "test" diff --git a/tests/github_pr_comment/test_find_comment.py b/tests/github_pr_comment/test_find_comment.py new file mode 100644 index 00000000..53219923 --- /dev/null +++ b/tests/github_pr_comment/test_find_comment.py @@ -0,0 +1,292 @@ +from github_pr_comment.__main__ import get_backend_fingerprints +from github_pr_comment.backend_config import complete_config, legacy_complete_config, legacy_partial_config, COMPLETE_FINGERPRINT_SINCE_VERSION, FIXED_FINGERPRINT_SINCE_VERSION +from github_pr_comment.backend_fingerprint import fingerprint +from github_pr_comment.comment import find_comment, matching_headers, BackupHeaders, TerraformComment, _to_api_payload, _format_comment_header, _parse_comment_header +from github_pr_comment.hash import comment_hash +from terraform.hcl import loads + +ISSUE_URL = 'https://api.github.com/repos/dflook/test/issues/1' +PR_URL = 'https://api.github.com/repos/dflook/test/pulls/1' +COMMENT_URL = 'https://api.github.com/repos/dflook/test/issues/comments/2' + +COMPLETE_VERSION = COMPLETE_FINGERPRINT_SINCE_VERSION +FIXED_VERSION = FIXED_FINGERPRINT_SINCE_VERSION + + +class StubGithub: + def __init__(self, comments): + self._comments = comments + + def paged_get(self, url, **kwargs): + yield from self._comments + + +def comment_payload(comment): + return { + 'body': _to_api_payload(comment), + 'issue_url': comment.issue_url, + 'url': comment.comment_url, + 'node_id': comment.node_id, + 'user': {'login': 'test-user'} + } + + +def get_fingerprints(monkeypatch, tmp_path, backend_config_file=''): + monkeypatch.setenv('TF_DATA_DIR', str(tmp_path)) + + module = loads(''' +terraform { + backend pg {} +} + ''') + + inputs = { + 'INPUT_BACKEND_CONFIG_FILE': backend_config_file, + 'INPUT_BACKEND_CONFIG': 'conn_str=postgres://host/db?sslmode=disable' + } + + fingerprints = {} + for name, config in [ + ('complete', complete_config), + ('legacy_complete', legacy_complete_config), + ('legacy_partial', legacy_partial_config), + ]: + backend_type, backend_config = config(inputs, module) + fingerprints[name] = fingerprint(backend_type, backend_config, {}) + + return fingerprints + + +def make_comment(backend_hash, version=None): + headers = {'workspace': 'default', 'backend': backend_hash, 'plan_hash': 'p1'} + if version is not None: + headers['version'] = version + + return TerraformComment( + issue_url=ISSUE_URL, + comment_url=COMMENT_URL, + node_id='node', + headers=headers, + description='Terraform plan in __.__', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body='+ resource "random_string" "s"', + status=':memo: Plan generated', + ) + + +def search_headers(fingerprints): + """The primary and backup headers, as constructed by github_pr_comment.__main__""" + + def backup(name, min_version, max_version): + return BackupHeaders({'workspace': 'default', 'closed': None, 'label': None, 'backend': comment_hash(fingerprints[name], PR_URL)}, min_version, max_version) + + headers = {'workspace': 'default', 'closed': None, 'label': None, 'backend': comment_hash(fingerprints['complete'], PR_URL)} + backup_headers = [ + backup('legacy_complete', COMPLETE_VERSION, FIXED_VERSION), + backup('legacy_partial', None, COMPLETE_VERSION), + ] + return headers, backup_headers + + +def test_find_comment_created_before_backend_config_parse_fix(monkeypatch, tmp_path): + """A comment created with the old backend_config parsing must still be found, e.g. by an apply after merge.""" + + fingerprints = get_fingerprints(monkeypatch, tmp_path) + + # The premise: the fixed parse produces a different fingerprint for this input + assert fingerprints['complete'] != fingerprints['legacy_complete'] + + old_comment = make_comment(comment_hash(fingerprints['legacy_complete'], PR_URL), version='2.2.3') + headers, backup_headers = search_headers(fingerprints) + + github = StubGithub([comment_payload(old_comment)]) + found = find_comment(github, ISSUE_URL, 'test-user', headers, backup_headers, 'legacy description') + + # The old comment is found, and its headers are updated with the current fingerprint + assert found.comment_url == COMMENT_URL + assert found.headers['backend'] == comment_hash(fingerprints['complete'], PR_URL) + + # The approval headers survive the merge + assert found.headers['plan_hash'] == 'p1' + + # None valued search headers must not leak into the comment headers, or the comment + # becomes unmatchable once they are written back + assert 'closed' not in found.headers + assert 'label' not in found.headers + + # After the headers are written back to the comment, the next run must still match it + rewritten = _parse_comment_header(_format_comment_header(**found.headers)) + rewritten_comment = TerraformComment( + issue_url=ISSUE_URL, comment_url=COMMENT_URL, node_id='node', headers=rewritten, + description='d', summary='s', body='b', status='') + assert matching_headers(rewritten_comment, headers) + + +def test_find_comment_unversioned_comment_matches_backup(monkeypatch, tmp_path): + """A comment without a version header predates version stamping and may match any unbounded backup set.""" + + fingerprints = get_fingerprints(monkeypatch, tmp_path) + + old_comment = make_comment(comment_hash(fingerprints['legacy_partial'], PR_URL)) + headers, backup_headers = search_headers(fingerprints) + + github = StubGithub([comment_payload(old_comment)]) + found = find_comment(github, ISSUE_URL, 'test-user', headers, backup_headers, 'legacy description') + + assert found.comment_url == COMMENT_URL + assert found.headers['backend'] == comment_hash(fingerprints['complete'], PR_URL) + + +def test_find_comment_with_current_fingerprint(monkeypatch, tmp_path): + fingerprints = get_fingerprints(monkeypatch, tmp_path) + + current_comment = make_comment(comment_hash(fingerprints['complete'], PR_URL), version=FIXED_VERSION) + headers, backup_headers = search_headers(fingerprints) + + github = StubGithub([comment_payload(current_comment)]) + found = find_comment(github, ISSUE_URL, 'test-user', headers, backup_headers, 'legacy description') + + assert found.comment_url == COMMENT_URL + assert found.headers['backend'] == comment_hash(fingerprints['complete'], PR_URL) + + +def test_find_comment_newer_comments_not_matched_by_backup_headers(monkeypatch, tmp_path): + """A comment created by a version outside a backup set's range must not match it. + + This prevents a comment whose genuine fingerprint collides with a legacy fingerprint + (e.g. pg with conn_str set by environment variable) being claimed by another module's plan. + """ + + fingerprints = get_fingerprints(monkeypatch, tmp_path) + headers, backup_headers = search_headers(fingerprints) + + # This comment's backend hash equals the legacy backup hash, but it was created by a fixed version + collision_comment = make_comment(comment_hash(fingerprints['legacy_complete'], PR_URL), version=FIXED_VERSION) + + github = StubGithub([comment_payload(collision_comment)]) + found = find_comment(github, ISSUE_URL, 'test-user', headers, backup_headers, 'legacy description') + + assert found.comment_url is None + + +def test_find_comment_unversioned_comment_not_matched_by_min_bounded_backup(monkeypatch, tmp_path): + """A comment without a version header must not match a backup set with a minimum version. + + Complete fingerprints were only ever written by versions that also stamp a version header, + so an unversioned comment can't carry one. + + A backend config file is used so the partial and complete fingerprints differ. + """ + + fingerprints = get_fingerprints(monkeypatch, tmp_path, backend_config_file='tests/github_pr_comment/test_backend_file.tfvars') + headers, backup_headers = search_headers(fingerprints) + + unversioned_comment = make_comment(comment_hash(fingerprints['legacy_complete'], PR_URL)) + + github = StubGithub([comment_payload(unversioned_comment)]) + found = find_comment(github, ISSUE_URL, 'test-user', headers, backup_headers, 'legacy description') + + assert found.comment_url is None + + +def test_find_comment_stale_stamp_era(monkeypatch, tmp_path): + """Versions 1.32.0-1.32.1 stamped comments with '1.31.1' - they must match the legacy complete fingerprint.""" + + fingerprints = get_fingerprints(monkeypatch, tmp_path) + headers, backup_headers = search_headers(fingerprints) + + stale_stamp_comment = make_comment(comment_hash(fingerprints['legacy_complete'], PR_URL), version='1.31.1') + + github = StubGithub([comment_payload(stale_stamp_comment)]) + found = find_comment(github, ISSUE_URL, 'test-user', headers, backup_headers, 'legacy description') + + assert found.comment_url == COMMENT_URL + assert found.headers['backend'] == comment_hash(fingerprints['complete'], PR_URL) + + +def test_backup_fingerprint_windows_merge_on_collision(monkeypatch, tmp_path): + """With no backend config files, the legacy complete and partial fingerprints are identical. + + The merged entry must keep min_version=None, so unversioned comments from before + version stamping are still matched. + """ + + monkeypatch.setenv('TF_DATA_DIR', str(tmp_path)) + + module = loads(''' +terraform { + backend pg {} +} + ''') + + inputs = { + 'INPUT_BACKEND_CONFIG_FILE': '', + 'INPUT_BACKEND_CONFIG': 'conn_str=postgres://host/db?sslmode=disable' + } + + primary, backups = get_backend_fingerprints(inputs, module) + + assert len(backups) == 1 + backup_fingerprint, min_version, max_version = backups[0] + assert min_version is None + assert max_version == FIXED_VERSION + + headers = {'workspace': 'default', 'closed': None, 'label': None, 'backend': comment_hash(primary, PR_URL)} + backup_headers = [ + BackupHeaders({'workspace': 'default', 'closed': None, 'label': None, 'backend': comment_hash(backup_fingerprint, PR_URL)}, min_version, max_version) + ] + + unversioned_comment = make_comment(comment_hash(backup_fingerprint, PR_URL)) + + github = StubGithub([comment_payload(unversioned_comment)]) + found = find_comment(github, ISSUE_URL, 'test-user', headers, backup_headers, 'legacy description') + + assert found.comment_url == COMMENT_URL + + +def test_find_comment_dev_stamped_comment(monkeypatch, tmp_path): + """Comments stamped with a placeholder version (images built without a release version) may be from any era.""" + + fingerprints = get_fingerprints(monkeypatch, tmp_path) + headers, backup_headers = search_headers(fingerprints) + + dev_comment = make_comment(comment_hash(fingerprints['legacy_complete'], PR_URL), version='99.0.0') + + github = StubGithub([comment_payload(dev_comment)]) + found = find_comment(github, ISSUE_URL, 'test-user', headers, backup_headers, 'legacy description') + + assert found.comment_url == COMMENT_URL + + +def test_find_comment_null_header_values_are_scrubbed(monkeypatch, tmp_path): + """Old versions could write literal null header values, which made comments unmatchable.""" + + fingerprints = get_fingerprints(monkeypatch, tmp_path) + headers, backup_headers = search_headers(fingerprints) + + poisoned_comment = TerraformComment( + issue_url=ISSUE_URL, + comment_url=COMMENT_URL, + node_id='node', + headers={'workspace': 'default', 'backend': comment_hash(fingerprints['complete'], PR_URL), 'plan_hash': 'p1', 'version': '2.2.3', 'closed': None, 'label': None}, + description='d', summary='s', body='b', status='') + + github = StubGithub([comment_payload(poisoned_comment)]) + found = find_comment(github, ISSUE_URL, 'test-user', headers, backup_headers, 'legacy description') + + assert found.comment_url == COMMENT_URL + assert 'closed' not in found.headers + assert 'label' not in found.headers + + +def test_find_comment_ignores_other_backends(monkeypatch, tmp_path): + fingerprints = get_fingerprints(monkeypatch, tmp_path) + + other_comment = make_comment(comment_hash(b'a completely different backend', PR_URL)) + headers, backup_headers = search_headers(fingerprints) + + github = StubGithub([comment_payload(other_comment)]) + found = find_comment(github, ISSUE_URL, 'test-user', headers, backup_headers, 'legacy description') + + # No match - a new comment would be created + assert found.comment_url is None diff --git a/tests/github_pr_comment/test_fingerprint.py b/tests/github_pr_comment/test_fingerprint.py index fd5bc2a1..80012220 100644 --- a/tests/github_pr_comment/test_fingerprint.py +++ b/tests/github_pr_comment/test_fingerprint.py @@ -1,4 +1,4 @@ -from github_pr_comment.backend_config import read_backend_config_files, read_module_backend_config, complete_config, partial_config, read_backend_config_input +from github_pr_comment.backend_config import read_backend_config_files, read_module_backend_config, complete_config, legacy_complete_config, partial_config, read_backend_config_input, read_legacy_backend_config_input from terraform.hcl import loads def test_read_backend_config_files(): @@ -54,6 +54,45 @@ def test_read_backend_config_input(): 'INPUT_BACKEND_CONFIG': '' }) == {} + # Values may contain '=' and whitespace is allowed around the first '=' + assert read_backend_config_input( + { + 'INPUT_BACKEND_CONFIG': 'secret_key=abc==,conn_str=postgres://host/db?sslmode=disable,bucket = my-bucket' + }) == { + 'secret_key': 'abc==', + 'conn_str': 'postgres://host/db?sslmode=disable', + 'bucket': 'my-bucket' + } + +def test_read_legacy_backend_config_input(): + # As parsed by old versions, which split on the last '='. Used to match comments created by them. + assert read_legacy_backend_config_input( + { + 'INPUT_BACKEND_CONFIG': 'hello=world,secret_key=abc==,bucket = my-bucket' + }) == { + 'hello': 'world', + 'secret_key=abc=': '', + 'bucket ': 'my-bucket' + } + +def test_module_config_is_not_shared(): + module = loads(''' +terraform { + backend pg {} +} + ''') + + inputs = { + 'INPUT_BACKEND_CONFIG_FILE': '', + 'INPUT_BACKEND_CONFIG': 'conn_str=postgres://host/db?sslmode=disable' + } + + assert complete_config(inputs, module) == ('pg', {'conn_str': 'postgres://host/db?sslmode=disable'}) + assert legacy_complete_config(inputs, module) == ('pg', {'conn_str=postgres://host/db?sslmode': 'disable'}) + + # the parsed module must not accumulate config keys from previous calls + assert complete_config(inputs, module) == ('pg', {'conn_str': 'postgres://host/db?sslmode=disable'}) + def test_complete_config(): assert complete_config( { From 67bbc002408eff14cf34520208a861292f5084ca Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 18 Jul 2026 10:28:48 +0100 Subject: [PATCH 22/32] Add better messaging for malformed repository_dispatch payloads --- image/src/github_actions/find_pr.py | 12 ++++-- tests/test_find_pr.py | 62 +++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 tests/test_find_pr.py diff --git a/image/src/github_actions/find_pr.py b/image/src/github_actions/find_pr.py index 6194c6f6..3b6e147a 100644 --- a/image/src/github_actions/find_pr.py +++ b/image/src/github_actions/find_pr.py @@ -59,12 +59,18 @@ def find_pr(github: GithubApi, actions_env: GithubEnv) -> PrUrl: 'This can happen when the runner is running in a container') elif event_type == 'repository_dispatch': - if 'pull_request' not in event['client_payload'] or not isinstance(event['client_payload']['pull_request'], dict): + if event is None: + raise WorkflowException(f'Event payload is not available at the GITHUB_EVENT_PATH {actions_env["GITHUB_EVENT_PATH"]!r}. ' + + f'This is required when run by {event_type} events. The environment has not been setup properly by the actions runner. ' + + 'This can happen when the runner is running in a container') + + client_payload = event.get('client_payload') + if not isinstance(client_payload, dict) or not isinstance(client_payload.get('pull_request'), dict): raise WorkflowException('The repository_dispatch event must have a pull_request object in the client_payload') - if 'url' not in event['client_payload']['pull_request']: + if not isinstance(client_payload['pull_request'].get('url'), str): raise WorkflowException('The pull_request object in the client_payload must have a url') - pr_url = event['client_payload']['pull_request']['url'] + pr_url = client_payload['pull_request']['url'] github_api_url = actions_env.get('GITHUB_API_URL', 'https://api.github.com').rstrip('/') if not pr_url.startswith(github_api_url + '/'): raise WorkflowException(f'The pull_request url in the client_payload does not match the expected GitHub API URL ({github_api_url})') diff --git a/tests/test_find_pr.py b/tests/test_find_pr.py new file mode 100644 index 00000000..b22ed43a --- /dev/null +++ b/tests/test_find_pr.py @@ -0,0 +1,62 @@ +import json + +import pytest + +from github_actions.find_pr import find_pr, WorkflowException + + +def env(event_path, api_url='https://api.github.com'): + return { + 'GITHUB_EVENT_PATH': str(event_path), + 'GITHUB_EVENT_NAME': 'repository_dispatch', + 'GITHUB_API_URL': api_url, + } + + +def write_event(tmp_path, payload): + event_path = tmp_path / 'event.json' + event_path.write_text(json.dumps(payload)) + return event_path + + +def test_repository_dispatch_missing_event_payload(tmp_path): + # The event payload file may be missing on a broken actions runner + with pytest.raises(WorkflowException): + find_pr(None, env(tmp_path / 'nonexistent.json')) + + +@pytest.mark.parametrize('payload', [ + {}, + {'client_payload': None}, + {'client_payload': {}}, + {'client_payload': {'pull_request': None}}, + {'client_payload': {'pull_request': 'https://api.github.com/repos/dflook/test/pulls/1'}}, +]) +def test_repository_dispatch_missing_pull_request(tmp_path, payload): + # client_payload is optional when creating a repository_dispatch event + with pytest.raises(WorkflowException): + find_pr(None, env(write_event(tmp_path, payload))) + + +@pytest.mark.parametrize('pull_request', [ + {}, + {'url': None}, + {'url': 5}, +]) +def test_repository_dispatch_missing_url(tmp_path, pull_request): + with pytest.raises(WorkflowException): + find_pr(None, env(write_event(tmp_path, {'client_payload': {'pull_request': pull_request}}))) + + +def test_repository_dispatch_url_for_wrong_host(tmp_path): + event_path = write_event(tmp_path, {'client_payload': {'pull_request': {'url': 'https://evil.example.com/repos/dflook/test/pulls/1'}}}) + + with pytest.raises(WorkflowException): + find_pr(None, env(event_path)) + + +def test_repository_dispatch(tmp_path): + url = 'https://api.github.com/repos/dflook/test/pulls/1' + event_path = write_event(tmp_path, {'client_payload': {'pull_request': {'url': url}}}) + + assert find_pr(None, env(event_path)) == url From b9a70dd3e7a1444de2bb466688ad66102e219080 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 18 Jul 2026 10:44:01 +0100 Subject: [PATCH 23/32] Add better messaging for rate limited non-authenticated requests --- image/src/github_actions/api.py | 14 ++++-- tests/test_github_api.py | 75 +++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 tests/test_github_api.py diff --git a/image/src/github_actions/api.py b/image/src/github_actions/api.py index cc942413..9156337e 100644 --- a/image/src/github_actions/api.py +++ b/image/src/github_actions/api.py @@ -63,10 +63,16 @@ def _api_request(self, method: str, url: str, **kwargs) -> requests.Response: try: message = response.json()['message'] - if response.headers['X-RateLimit-Remaining'] == '0' and response.headers['X-RateLimit-Limit'] != '0': - limit_reset = datetime.datetime.fromtimestamp(int(response.headers['X-RateLimit-Reset'])) - sys.stdout.write(message) - sys.stdout.write(f' Try again when the rate limit resets at {limit_reset} UTC.\n') + if response.headers.get('X-RateLimit-Remaining') == '0' and response.headers.get('X-RateLimit-Limit') != '0': + limit_reset = datetime.datetime.fromtimestamp(int(response.headers['X-RateLimit-Reset']), tz=datetime.timezone.utc) + sys.stdout.write(f'{message}\n') + sys.stdout.write(f'While requesting {response.request.url}\n') + sys.stdout.write(f'Try again when the rate limit resets at {limit_reset:%Y-%m-%d %H:%M:%S} UTC.\n') + + if self._token is None: + sys.stdout.write('This request was not authenticated, and the rate limit for unauthenticated requests is low.\n') + sys.stdout.write('Set the GITHUB_DOT_COM_TOKEN environment variable to a GitHub.com token to authenticate these requests. The token does not need any scopes.\n') + sys.exit(1) if message not in ['Resource not accessible by integration', 'Personal access tokens with fine grained access do not support the GraphQL API']: diff --git a/tests/test_github_api.py b/tests/test_github_api.py new file mode 100644 index 00000000..4aabea74 --- /dev/null +++ b/tests/test_github_api.py @@ -0,0 +1,75 @@ +import json + +import pytest +import requests + +from github_actions.api import GithubApi + +RELEASES_URL = 'https://api.github.com/repos/opentofu/opentofu/releases' + +RATE_LIMIT_HEADERS = { + 'X-RateLimit-Remaining': '0', + 'X-RateLimit-Limit': '60', + 'X-RateLimit-Reset': '1789000000', +} + + +def fake_response(status=403, message='API rate limit exceeded for 1.2.3.4.', headers=None, url=RELEASES_URL): + response = requests.Response() + response.status_code = status + response._content = json.dumps({'message': message}).encode() + response.headers.update(headers or {}) + + request = requests.PreparedRequest() + request.method = 'GET' + request.url = url + response.request = request + + return response + + +def github_api(token, response): + github = GithubApi('https://api.github.com', token) + github._session.request = lambda *args, **kwargs: response + return github + + +def test_rate_limit_without_token(capsys): + github = github_api(None, fake_response(headers=RATE_LIMIT_HEADERS)) + + with pytest.raises(SystemExit): + github.get('/repos/opentofu/opentofu/releases') + + out = capsys.readouterr().out + assert RELEASES_URL in out + assert 'Try again when the rate limit resets at' in out + assert 'GITHUB_DOT_COM_TOKEN' in out + + +def test_rate_limit_with_token(capsys): + github = github_api('token', fake_response(headers=RATE_LIMIT_HEADERS)) + + with pytest.raises(SystemExit): + github.get('/repos/opentofu/opentofu/releases') + + out = capsys.readouterr().out + assert RELEASES_URL in out + assert 'GITHUB_DOT_COM_TOKEN' not in out + + +def test_client_error_without_rate_limit_headers(capsys): + # Not all servers send rate limit headers with client errors + github = github_api('token', fake_response(status=404, message='Not Found', headers={})) + + response = github.get('/repos/opentofu/opentofu/releases') + + assert response.status_code == 404 + assert 'Not Found' in capsys.readouterr().out + + +def test_client_error_with_rate_limit_remaining(capsys): + github = github_api('token', fake_response(status=404, message='Not Found', headers={'X-RateLimit-Remaining': '10', 'X-RateLimit-Limit': '60'})) + + response = github.get('/repos/opentofu/opentofu/releases') + + assert response.status_code == 404 From 2ea34b8622ada46f748bcebe1ebfded2dc7fee3a Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 18 Jul 2026 10:58:22 +0100 Subject: [PATCH 24/32] Fix regex issues with plan diff-formatting Stop replacing non-leading '~', fix regex character range from including ',' and '.', use raw strings for regexes --- image/src/github_pr_comment/plan_formatting.py | 9 +++++---- tests/test_diff_formatting.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/image/src/github_pr_comment/plan_formatting.py b/image/src/github_pr_comment/plan_formatting.py index 28a8de6e..bee536fc 100644 --- a/image/src/github_pr_comment/plan_formatting.py +++ b/image/src/github_pr_comment/plan_formatting.py @@ -17,17 +17,18 @@ def format_diff(plan_text: str) -> str: heredoc = True replaced = (re.sub( - r'^(?P\s+)(?P[+-/~]+)(?P.*)', - '\g\g\g', + r'^(?P\s+)(?P[-+/~]+)(?P.*)', + r'\g\g\g', line, count=1 )) - replaced = replaced.replace('~ ', '!~') + if replaced.startswith('~ '): + replaced = '!~' + replaced[2:] replaced = re.sub( r'(?P\s+)\# (?P\(.*hidden)', - '#\g\g', + r'#\g\g', replaced, count=1 ) diff --git a/tests/test_diff_formatting.py b/tests/test_diff_formatting.py index 2da3a170..018c48ea 100644 --- a/tests/test_diff_formatting.py +++ b/tests/test_diff_formatting.py @@ -159,6 +159,24 @@ def test_diff(): assert format_diff(plan) == expected +def test_leading_punctuation_not_moved(): + # ',' and '.' are not diff operations, and must not be moved to the start of the line + plan = ' , not an operation' + assert format_diff(plan) == plan + + plan = ' ... not an operation' + assert format_diff(plan) == plan + + +def test_tilde_inside_value(): + # only a leading '~' operation is rewritten, not '~ ' inside a value + plan = ' + description = "approximately ~ five"' + assert format_diff(plan) == '+ description = "approximately ~ five"' + + plan = ' ~ description = "approximately ~ five"' + assert format_diff(plan) == '!~ description = "approximately ~ five"' + + def test_heredoc(): plan = ''' Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: From 91d63893eaa3f100ffe6e96ef6942a0a699454bc Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 18 Jul 2026 11:03:39 +0100 Subject: [PATCH 25/32] Fix linting of tools --- .config/ruff.toml | 4 ++-- image/tools/convert_output.py | 20 ++++++++++---------- image/tools/convert_validate_report.py | 2 +- image/tools/convert_version.py | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.config/ruff.toml b/.config/ruff.toml index a59f34bc..c28cb1f0 100644 --- a/.config/ruff.toml +++ b/.config/ruff.toml @@ -7,8 +7,8 @@ src = ["docs-gen", "image/src"] include = [ "docs-gen/*.py", "image/src/*.py", - "image/src/setup.py", - "tools/*.py", + "image/setup.py", + "image/tools/*.py", ] [lint] diff --git a/image/tools/convert_output.py b/image/tools/convert_output.py index 3df258ac..bda6edb5 100755 --- a/image/tools/convert_output.py +++ b/image/tools/convert_output.py @@ -17,26 +17,26 @@ class Output: value: str def convert_to_github(outputs: Dict) -> Iterable[Union[Mask, Output]]: - for name, output in outputs.items(): + for name, tf_output in outputs.items(): - if isinstance(output['type'], str): + if isinstance(tf_output['type'], str): # primitive type - if output['sensitive'] is True: - yield Mask(str(output["value"])) + if tf_output['sensitive'] is True: + yield Mask(str(tf_output["value"])) - if output['type'] in ['string', 'number']: - yield Output(name, str(output["value"])) + if tf_output['type'] in ['string', 'number']: + yield Output(name, str(tf_output["value"])) - if output['type'] == 'bool': - yield Output(name, json.dumps(output["value"])) + if tf_output['type'] == 'bool': + yield Output(name, json.dumps(tf_output["value"])) else: # complex type - value = json.dumps(output["value"], separators=(",", ":")) + value = json.dumps(tf_output["value"], separators=(",", ":")) - if output['sensitive'] is True: + if tf_output['sensitive'] is True: yield Mask(value) yield Output(name, str(value)) diff --git a/image/tools/convert_validate_report.py b/image/tools/convert_validate_report.py index 18f5395c..99b8080a 100755 --- a/image/tools/convert_validate_report.py +++ b/image/tools/convert_validate_report.py @@ -50,7 +50,7 @@ def convert_to_github(report: Dict, base_path: str) -> Iterable[str]: report = json.load(sys.stdin) if not isinstance(report, dict): raise Exception('Unable to parse report') - except: + except Exception: exit(1) for line in convert_to_github(report, sys.argv[1]): diff --git a/image/tools/convert_version.py b/image/tools/convert_version.py index 0f355834..074dab8e 100755 --- a/image/tools/convert_version.py +++ b/image/tools/convert_version.py @@ -68,7 +68,7 @@ def convert_version_from_json(tf_output: Dict) -> Iterable[Union[str, Output]]: """ yield f'{TOOL_PRODUCT_NAME} v{tf_output["terraform_version"]}' - yield Output(f'terraform', tf_output["terraform_version"]) + yield Output('terraform', tf_output["terraform_version"]) if TOOL_COMMAND_NAME != 'terraform': yield Output(TOOL_COMMAND_NAME, tf_output["terraform_version"]) @@ -90,7 +90,7 @@ def convert_version_from_json(tf_output: Dict) -> Iterable[Union[str, Output]]: output(line.name, line.value) else: print(line) - except: + except Exception: print(tf_output) for line in convert_version(tf_output): From 48b1f3fb5a9ed67752f2fec103ac26c496c55234 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 18 Jul 2026 11:07:41 +0100 Subject: [PATCH 26/32] Only log interesting fields --- image/tools/http_credential_actions_helper.py | 5 ++++- tests/test_git_credential_actions.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/image/tools/http_credential_actions_helper.py b/image/tools/http_credential_actions_helper.py index 58764db3..b6cf641c 100755 --- a/image/tools/http_credential_actions_helper.py +++ b/image/tools/http_credential_actions_helper.py @@ -15,9 +15,12 @@ class Credential: password: str +LOGGED_ATTRIBUTES = ('protocol', 'host', 'path', 'username') + + def git_credential(operation: str, attributes: Dict[str, str], credentials: List[Credential]): att = attributes.copy() - sys.stderr.write(repr(att) + '\n') + sys.stderr.write(f'git credential {operation}: ' + repr({k: v for k, v in att.items() if k in LOGGED_ATTRIBUTES}) + '\n') if operation != 'get': return att diff --git a/tests/test_git_credential_actions.py b/tests/test_git_credential_actions.py index 385a369f..b23f5346 100644 --- a/tests/test_git_credential_actions.py +++ b/tests/test_git_credential_actions.py @@ -87,6 +87,24 @@ def test_split_path(): assert ['hello'] == split_path('/hello') assert ['dflook', 'terraform-github-actions.git'] == split_path('/dflook/terraform-github-actions.git') +def test_only_matching_attributes_logged(capsys): + credentials = [ + Credential('example.com', [], 'dflook', 'mypassword'), + ] + + # git sends the password back with the store operation after a successful authentication + attributes = dict(protocol='https', host='example.com', username='dflook', password='mypassword') + assert git_credential('store', attributes, credentials) == attributes + + err = capsys.readouterr().err + assert 'mypassword' not in err + assert 'example.com' in err + + attributes = dict(protocol='https', host='example.com') + git_credential('get', attributes, credentials) + assert 'mypassword' not in capsys.readouterr().err + + def test_get(): credentials = [ From 45aab6e3475f808fca1c8157a77a505454565faf Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 18 Jul 2026 11:13:54 +0100 Subject: [PATCH 27/32] Properly track STEP_TMP_DIR for downloads --- image/src/opentofu/download.py | 2 +- image/src/terraform/download.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/image/src/opentofu/download.py b/image/src/opentofu/download.py index 08f45142..41d6c3c7 100644 --- a/image/src/opentofu/download.py +++ b/image/src/opentofu/download.py @@ -173,7 +173,7 @@ def get_executable(version: Version) -> Path: cache_dir, archive_name = get_archive(version, cache_dirs) verify_archive(version, cache_dir, archive_name, checksum_dir) - executable_dir = os.environ.get('STEP_TEMP_DIR', f'/tmp/tofu_{version}') + executable_dir = Path(os.environ.get('STEP_TMP_DIR', '/tmp'), f'tofu_{version}') Path(executable_dir, 'tofu').unlink(missing_ok=True) with ZipFile(Path(cache_dir, archive_name)) as f: diff --git a/image/src/terraform/download.py b/image/src/terraform/download.py index 22b936c6..5ca32bd4 100644 --- a/image/src/terraform/download.py +++ b/image/src/terraform/download.py @@ -206,7 +206,7 @@ def get_executable(version: Version) -> Path: cache_dir, archive_name = get_archive(version, cache_dirs) verify_archive(version, cache_dir, archive_name, checksum_dir) - executable_dir = os.environ.get('STEP_TEMP_DIR', f'/tmp/terraform_{version}') + executable_dir = Path(os.environ.get('STEP_TMP_DIR', '/tmp'), f'terraform_{version}') Path(executable_dir, 'terraform').unlink(missing_ok=True) with ZipFile(Path(cache_dir, archive_name)) as f: From 649258c4e2f99b207e12159b107ec3e3ab07bbbd Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 18 Jul 2026 11:17:29 +0100 Subject: [PATCH 28/32] Properly serialise node-id of replacement comments obj for later use --- image/src/github_pr_comment/comment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/image/src/github_pr_comment/comment.py b/image/src/github_pr_comment/comment.py index 4431b2ba..aee70818 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -425,7 +425,9 @@ def update_comment( response = github.patch(comment.comment_url, json={'body': _to_api_payload(new_comment)}) response.raise_for_status() if comment.node_id is None: - comment.node_id = response.json().get('node_id') + node_id = response.json().get('node_id') + comment.node_id = node_id + new_comment.node_id = node_id else: response = github.post(comment.issue_url + '/comments', json={'body': _to_api_payload(new_comment)}) response.raise_for_status() From b1eb494a668674958804e956bc1cde417565f334 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 18 Jul 2026 11:34:29 +0100 Subject: [PATCH 29/32] Clarify tool version selection from env var --- docs-gen/actions/version.py | 9 ++++----- tofu-version/README.md | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/docs-gen/actions/version.py b/docs-gen/actions/version.py index f1d5c7e0..2e273099 100644 --- a/docs-gen/actions/version.py +++ b/docs-gen/actions/version.py @@ -60,12 +60,11 @@ def description(tool: Tool) -> str: 4. A [tofuenv](https://github.com/tofuutils/tofuenv) `.opentofu-version` file in the module path 5. A [tfenv](https://github.com/tfutils/tfenv) `.terraform-version` file in the module path 6. An [asdf](https://asdf-vm.com/) `.tool-versions` file in the module path or any parent path - 7. An `OPENTOFU_VERSION` environment variable containing a [version constraint](https://opentofu.org/docs/language/expressions/version-constraints/). + 7. A `TERRAFORM_VERSION` environment variable containing a [version constraint](https://opentofu.org/docs/language/expressions/version-constraints/), + or if not set, an `OPENTOFU_VERSION` environment variable containing a version constraint. If the constraint allows multiple versions, the latest matching version is used. - 8. A `TERRAFORM_VERSION` environment variable containing a [version constraint](https://opentofu.org/docs/language/expressions/version-constraints/). - If the constraint allows multiple versions, the latest matching version is used. - 9. The $ProductName version that created the current state file (best effort). - 10. The latest $ProductName version + 8. The $ProductName version that created the current state file (best effort). + 9. The latest $ProductName version The version of $ProductName and all required providers will be output to the workflow log. diff --git a/tofu-version/README.md b/tofu-version/README.md index d8a369cf..92647d7c 100644 --- a/tofu-version/README.md +++ b/tofu-version/README.md @@ -15,12 +15,11 @@ The version to use is discovered from the first of: 4. A [tofuenv](https://github.com/tofuutils/tofuenv) `.opentofu-version` file in the module path 5. A [tfenv](https://github.com/tfutils/tfenv) `.terraform-version` file in the module path 6. An [asdf](https://asdf-vm.com/) `.tool-versions` file in the module path or any parent path -7. An `OPENTOFU_VERSION` environment variable containing a [version constraint](https://opentofu.org/docs/language/expressions/version-constraints/). +7. A `TERRAFORM_VERSION` environment variable containing a [version constraint](https://opentofu.org/docs/language/expressions/version-constraints/), + or if not set, an `OPENTOFU_VERSION` environment variable containing a version constraint. If the constraint allows multiple versions, the latest matching version is used. -8. A `TERRAFORM_VERSION` environment variable containing a [version constraint](https://opentofu.org/docs/language/expressions/version-constraints/). - If the constraint allows multiple versions, the latest matching version is used. -9. The OpenTofu version that created the current state file (best effort). -10. The latest OpenTofu version +8. The OpenTofu version that created the current state file (best effort). +9. The latest OpenTofu version The version of OpenTofu and all required providers will be output to the workflow log. From 41833d708dc9adfeb9bfed3a14266b75cd32afad Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 18 Jul 2026 11:34:42 +0100 Subject: [PATCH 30/32] Add missing test --- .../github_pr_comment/test_update_comment.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/github_pr_comment/test_update_comment.py diff --git a/tests/github_pr_comment/test_update_comment.py b/tests/github_pr_comment/test_update_comment.py new file mode 100644 index 00000000..6191cbbb --- /dev/null +++ b/tests/github_pr_comment/test_update_comment.py @@ -0,0 +1,76 @@ +from github_pr_comment.comment import update_comment, TerraformComment + +ISSUE_URL = 'https://api.github.com/repos/dflook/test/issues/1' +COMMENT_URL = 'https://api.github.com/repos/dflook/test/issues/comments/2' + + +class StubResponse: + def __init__(self, payload): + self._payload = payload + + def raise_for_status(self): + pass + + def json(self): + return self._payload + + +class StubGithub: + def __init__(self, payload): + self._payload = payload + + def patch(self, url, json): + return StubResponse(self._payload) + + def post(self, url, json): + return StubResponse(self._payload) + + +def test_update_learns_node_id(): + comment = TerraformComment( + issue_url=ISSUE_URL, + comment_url=COMMENT_URL, + node_id=None, + headers={'workspace': 'default'}, + description='d', summary='s', body='b', status='st') + + github = StubGithub({'url': COMMENT_URL, 'node_id': 'N1'}) + updated = update_comment(github, comment, status='new status') + + # The learned node_id must be on the returned comment, which is serialized to the step cache + assert updated.node_id == 'N1' + assert updated.status == 'new status' + assert updated.body == 'b' + + # It is also set on the passed comment, which the always-new flow keeps using + assert comment.node_id == 'N1' + + +def test_update_keeps_known_node_id(): + comment = TerraformComment( + issue_url=ISSUE_URL, + comment_url=COMMENT_URL, + node_id='N0', + headers={'workspace': 'default'}, + description='d', summary='s', body='b', status='st') + + github = StubGithub({'url': COMMENT_URL, 'node_id': 'N1'}) + updated = update_comment(github, comment, status='new status') + + assert updated.node_id == 'N0' + assert comment.node_id == 'N0' + + +def test_create_sets_node_id(): + comment = TerraformComment( + issue_url=ISSUE_URL, + comment_url=None, + node_id=None, + headers={'workspace': 'default'}, + description='d', summary='s', body='b', status='st') + + github = StubGithub({'url': COMMENT_URL, 'node_id': 'N1'}) + created = update_comment(github, comment, status='st') + + assert created.comment_url == COMMENT_URL + assert created.node_id == 'N1' From 915f070433a210b9a34dfda6c25d2d8950a9f528 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 18 Jul 2026 11:54:33 +0100 Subject: [PATCH 31/32] Rebuild trixie base --- .github/workflows/base-image.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/base-image.yaml b/.github/workflows/base-image.yaml index 4cc4e8f0..61b92e9d 100644 --- a/.github/workflows/base-image.yaml +++ b/.github/workflows/base-image.yaml @@ -3,12 +3,14 @@ name: Update base image on: push: branches: + - main - trixie paths: - image/Dockerfile-base - .github/workflows/base-image.yaml schedule: - cron: 0 1 1 * * + workflow_dispatch: permissions: contents: read From 4a1934cc44d9b58887c55f096df7f2539e37c4b9 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 18 Jul 2026 12:31:01 +0100 Subject: [PATCH 32/32] Update changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81a68d7d..fc51328d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,25 @@ When using an action you can specify the version as: - `@v2.2` to use the latest patch release for the specific minor version - `@v2` to use the latest patch release for the specific major version +## [Unreleased] + +### Added +- Outputs with a `-` in their name are now also available with `_` instead, e.g. `failure_reason` as well as `failure-reason`. + Output names previously used a mix of `-` and `_`, this allows using a consistent style. + The existing output names are not deprecated and will not be removed. + +### Changed +- The container image has been updated from debian 12 to debian 13. This may affect you if you rely on the runtime environment of Terraform/OpenTofu. +- When an unauthenticated request to GitHub.com is rate limited, the error message now explains how to authenticate requests by setting the `GITHUB_DOT_COM_TOKEN` environment variable. + +### Fixed +- [dflook/terraform-plan](https://github.com/dflook/terraform-github-actions/tree/main/terraform-plan)/[dflook/tofu-plan](https://github.com/dflook/terraform-github-actions/tree/main/tofu-plan) + and [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/tree/main/terraform-apply)/[dflook/tofu-apply](https://github.com/dflook/terraform-github-actions/tree/main/tofu-apply) + could match the wrong PR comment when a `backend_config` value contained an `=` character, such as a pg backend `conn_str`. +- A malformed `repository_dispatch` payload now results in a helpful error message instead of a Python error. +- Some lines of the plan could be formatted incorrectly in PR comments, e.g. values containing a `~` character. +- `TERRAFORM_CLOUD_TOKENS` values containing `"` or `\` characters now work correctly. + ## [2.2.3] - 2026-01-13 ### Fixed