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/.github/workflows/base-image.yaml b/.github/workflows/base-image.yaml index 8b437a82..61b92e9d 100644 --- a/.github/workflows/base-image.yaml +++ b/.github/workflows/base-image.yaml @@ -4,11 +4,13 @@ on: push: branches: - main + - trixie paths: - image/Dockerfile-base - .github/workflows/base-image.yaml schedule: - cron: 0 1 1 * * + workflow_dispatch: permissions: contents: read @@ -25,7 +27,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,18 +38,18 @@ 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 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 +60,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 \ @@ -68,7 +70,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 1c35165e..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 @@ -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 @@ -115,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 @@ -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" @@ -172,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 @@ -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 @@ -214,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 @@ -302,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 @@ -406,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 @@ -512,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 @@ -616,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 @@ -676,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 @@ -724,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 @@ -773,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 @@ -827,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 @@ -882,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 @@ -936,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 @@ -969,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 @@ -1051,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 @@ -1087,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 @@ -1117,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 @@ -1147,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 @@ -1173,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 @@ -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 @@ -1220,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 @@ -1248,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 @@ -1280,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 @@ -1314,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 @@ -1372,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 @@ -1396,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 @@ -1420,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 @@ -1512,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 bf920a1d..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 @@ -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..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 @@ -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 @@ -64,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 @@ -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 @@ -134,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 @@ -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 @@ -192,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 @@ -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..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 @@ -25,18 +25,24 @@ 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 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -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..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 @@ -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: @@ -317,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 @@ -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-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 193762e1..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 @@ -25,19 +25,25 @@ 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 continue-on-error: true steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -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-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 589c80e7..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 @@ -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-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 35796418..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 @@ -27,24 +27,36 @@ 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 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -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" @@ -87,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 @@ -101,18 +125,24 @@ 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 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -129,18 +159,24 @@ 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 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -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,12 +204,17 @@ 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 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -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,12 +244,17 @@ 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 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -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..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 @@ -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" @@ -127,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 @@ -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..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 @@ -25,18 +25,24 @@ 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 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -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,12 +69,17 @@ 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 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -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,12 +118,17 @@ 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 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -125,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 @@ -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/.github/workflows/test-version.yaml b/.github/workflows/test-version.yaml index 37cc15da..b2d24d82 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 @@ -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 @@ -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..e4fd94a4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,14 +12,14 @@ 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 + python-version: "3.13" - name: Install dependencies run: | @@ -43,14 +43,14 @@ 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 + python-version: "3.13" - name: Install dependencies run: | @@ -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..700092fa 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,7 +1,7 @@ rules: - unpinned-uses: - config: - policies: - dflook/terraform-apply: ref-pin - dflook/terraform-plan: ref-pin - actions/*: ref-pin + 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 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 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/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/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-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/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..129303f8 100644 --- a/image/Dockerfile +++ b/image/Dockerfile @@ -1,16 +1,22 @@ # hadolint ignore=DL3007 -FROM danielflook/terraform-github-actions-base:latest +FROM danielflook/terraform-github-actions-base:trixie 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_* /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 +fi \ + && 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/Dockerfile-base b/image/Dockerfile-base index bea7769c..abd98ded 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 @@ -29,8 +29,8 @@ RUN <"$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" 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 } 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 diff --git a/image/src/github_actions/api.py b/image/src/github_actions/api.py index e2893635..9156337e 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,18 +40,39 @@ 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: 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']: @@ -65,19 +87,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) - response = self.api_request('GET', url, *args, **kwargs) + def paged_get(self, path_or_url: str, **kwargs) -> Iterable[dict[str, Any]]: + url = self._url(path_or_url) + + while True: + response = self._api_request('GET', url, **kwargs) response.raise_for_status() if hasattr(response, 'from_cache'): @@ -90,5 +116,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/commands.py b/image/src/github_actions/commands.py index a307741f..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,12 +6,9 @@ from typing import Any 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) + 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(): 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/src/github_actions/find_pr.py b/image/src/github_actions/find_pr.py index 375639a0..3b6e147a 100644 --- a/image/src/github_actions/find_pr.py +++ b/image/src/github_actions/find_pr.py @@ -59,20 +59,30 @@ 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') - return cast(PrUrl, 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})') + + 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..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) @@ -35,7 +36,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') @@ -157,15 +163,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} @@ -173,7 +189,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 @@ -213,9 +232,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 +246,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: @@ -331,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']) @@ -370,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) @@ -473,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_fingerprint, backup_fingerprints = get_backend_fingerprints(action_inputs, module) - backend_type, backend_config = complete_config(action_inputs, module) - backend_fingerprint = fingerprint(backend_type, backend_config, os.environ) - - 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', '')) @@ -603,6 +669,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") 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 b58df988..aee70818 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -2,21 +2,22 @@ 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']) 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: @@ -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, @@ -375,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() @@ -394,17 +446,14 @@ 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={ - 'query': ''' - mutation { - minimizeComment(input: {subjectId: "''' + comment.node_id + '''", classifier: ''' + classifier + '''}) { - clientMutationId - } + response = github.graphql(json={ + 'query': 'mutation($input: MinimizeCommentInput!) { minimizeComment(input: $input) { clientMutationId } }', + 'variables': { + 'input': { + 'subjectId': comment.node_id, + 'classifier': classifier } - ''' + } } ) debug(f'graphql response: {response.content}') 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/image/src/opentofu/download.py b/image/src/opentofu/download.py index 0fc0bec0..41d6c3c7 100644 --- a/image/src/opentofu/download.py +++ b/image/src/opentofu/download.py @@ -65,7 +65,7 @@ def get_checksums(version: Version, checksum_dir: Path) -> 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 ) @@ -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: @@ -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/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/image/src/terraform/download.py b/image/src/terraform/download.py index 7e97785b..5ca32bd4 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 ) @@ -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: @@ -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 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') 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) 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/image/tools/compact_plan.py b/image/tools/compact_plan.py index 7edc8eac..46f5581c 100755 --- a/image/tools/compact_plan.py +++ b/image/tools/compact_plan.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 import sys diff --git a/image/tools/convert_output.py b/image/tools/convert_output.py index 5430d29d..bda6edb5 100755 --- a/image/tools/convert_output.py +++ b/image/tools/convert_output.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 import json import sys @@ -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 ebccd19e..99b8080a 100755 --- a/image/tools/convert_validate_report.py +++ b/image/tools/convert_validate_report.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 import json import os.path @@ -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 7d7aac6e..074dab8e 100755 --- a/image/tools/convert_version.py +++ b/image/tools/convert_version.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 import json import os @@ -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): diff --git a/image/tools/format_tf_credentials.py b/image/tools/format_tf_credentials.py index c8c3a31d..e00f44f0 100755 --- a/image/tools/format_tf_credentials.py +++ b/image/tools/format_tf_credentials.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 import os import re @@ -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/image/tools/github_comment_react.py b/image/tools/github_comment_react.py index 016d1f21..fee64e2b 100755 --- a/image/tools/github_comment_react.py +++ b/image/tools/github_comment_react.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 import datetime import json 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/image/tools/workspace_exists.py b/image/tools/workspace_exists.py index 35d8a4d0..5de023c0 100755 --- a/image/tools/workspace_exists.py +++ b/image/tools/workspace_exists.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 import sys diff --git a/image/workflow_commands.sh b/image/workflow_commands.sh index 4ac44e9b..56827bba 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 } ## @@ -117,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)))" } diff --git a/terraform-apply/README.md b/terraform-apply/README.md index 53f2d948..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: @@ -439,7 +441,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: @@ -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 3363f9f5..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'. @@ -184,7 +185,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: @@ -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 f076439b..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: @@ -205,7 +207,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: @@ -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 504cee3d..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: @@ -209,7 +211,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: @@ -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-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..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: @@ -228,7 +230,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-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 9f868072..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: @@ -186,7 +188,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-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-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..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'. @@ -156,7 +157,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: @@ -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/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/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( { 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/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' 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 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: 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 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 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 = [ 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 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 diff --git a/tofu-apply/README.md b/tofu-apply/README.md index 6b8b4645..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: @@ -456,7 +458,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: @@ -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 60b9c4bb..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'. @@ -184,7 +185,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: @@ -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 5d2cfe79..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: @@ -205,7 +207,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: @@ -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 600f4357..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: @@ -209,7 +211,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: @@ -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-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..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: @@ -245,7 +247,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-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 7f828085..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: @@ -176,7 +177,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-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-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..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'. @@ -156,7 +157,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: @@ -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'. diff --git a/tofu-version/README.md b/tofu-version/README.md index 60714cbf..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. @@ -190,7 +189,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: