diff --git a/image/actions.sh b/image/actions.sh index 080a3ca9..9fdff02c 100644 --- a/image/actions.sh +++ b/image/actions.sh @@ -505,7 +505,44 @@ function plan() { # shellcheck disable=SC2086 (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME plan -input=false -no-color -detailed-exitcode -lock-timeout=300s $PARALLEL_ARG $PLAN_OUT_ARG $PLAN_ARGS) \ 2>"$STEP_TMP_DIR/terraform_plan.stderr" \ + | python3 -c " +import sys +# tfmask uses bufio.Scanner which has a 64KB line limit. Resources that embed +# large base64 blobs (e.g. google_api_gateway_api_config openapi_documents) +# produce lines that exceed this limit, causing tfmask to crash mid-pipe and +# terraform to exit via SIGPIPE with a non-standard exit code. That prevents +# PIPESTATUS[0] from returning 2 (changes), so the apply step is skipped. +# Split long lines into chunks with a sentinel prefix so tfmask can process +# each chunk; the unchunker below reassembles them preserving the full content. +CHUNK = 60000 +for line in sys.stdin: + s = line.rstrip('\n') + if len(s) <= CHUNK: + sys.stdout.write(line) + else: + parts = [s[i:i+CHUNK] for i in range(0, len(s), CHUNK)] + for i, p in enumerate(parts): + sys.stdout.write(f'##TF_CHUNK:{i}/{len(parts)}/{p}\n') +" \ | $TFMASK \ + | python3 -c " +import sys +# Reassemble lines that were split by the chunker above. +buf = [] +for line in sys.stdin: + s = line.rstrip('\n') + if s.startswith('##TF_CHUNK:'): + rest = s[11:] + slash1 = rest.index('/') + slash2 = rest.index('/', slash1 + 1) + i, total = int(rest[:slash1]), int(rest[slash1+1:slash2]) + buf.append((i, rest[slash2+1:])) + if len(buf) == total: + sys.stdout.write(''.join(p[1] for p in sorted(buf)) + '\n') + buf = [] + else: + sys.stdout.write(line) +" \ | tee /dev/fd/3 "$STEP_TMP_DIR/terraform_plan.stdout" \ | compact_plan \ >"$STEP_TMP_DIR/plan.txt" diff --git a/image/entrypoints/plan.sh b/image/entrypoints/plan.sh index 7f9a053d..9278a335 100755 --- a/image/entrypoints/plan.sh +++ b/image/entrypoints/plan.sh @@ -11,15 +11,22 @@ set-plan-args exec 3>&1 ### Generate a plan -PLAN_OUT="$STEP_TMP_DIR/plan.out" +if [[ "$INPUT_SPECULATIVE" == "true" ]]; then + # Force speculative plan - no -out flag + PLAN_OUT="" +else + # Normal behavior - try to save plan file + PLAN_OUT="$STEP_TMP_DIR/plan.out" +fi PLAN_ARGS="$PLAN_ARGS -lock=false" plan -if [[ $PLAN_EXIT -eq 1 ]]; then +# If plan failed because remote backend doesn't support -out flag, retry without it +# Skip this retry if we're already running speculative (PLAN_OUT is already empty) +if [[ $PLAN_EXIT -eq 1 && -n "$PLAN_OUT" ]]; then if grep -q "Saving a generated plan is currently not supported" "$STEP_TMP_DIR/terraform_plan.stderr"; then # This terraform module is using the remote backend, which is deficient. PLAN_OUT="" - PLAN_ARGS="$PLAN_ARGS -lock=false" plan fi fi diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py index de4be08b..d789e497 100644 --- a/image/src/github_pr_comment/__main__.py +++ b/image/src/github_pr_comment/__main__.py @@ -543,6 +543,9 @@ def main() -> int: status=status ) + if comment.comment_url: + output('comment_url', comment.comment_url) + elif sys.argv[1] == 'status': if comment.comment_url is None: debug("Can't set status of comment that doesn't exist") diff --git a/terraform-plan/README.md b/terraform-plan/README.md index baccb74b..d915f802 100644 --- a/terraform-plan/README.md +++ b/terraform-plan/README.md @@ -170,6 +170,24 @@ The [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/ - Optional - Default: The Terraform default (10). +* `speculative` + + Set to `true` to force a speculative plan that cannot be applied. + + This creates a "Planned and finished" run in Terraform Cloud instead of "Planned and saved". + Speculative plans don't lock state and can run in parallel with other operations. + + This is useful for PR workflows where you want to preview changes without blocking other runs. + + ```yaml + with: + speculative: true + ``` + + - Type: boolean + - Optional + - Default: `false` + ## Outputs * `changes` @@ -185,6 +203,8 @@ The [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/ The plan can be used as the `plan_file` input to the [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/tree/main/terraform-apply) action. + This won't be set if `speculative` is `true` or if the backend type is `remote`/`cloud` in remote execution mode. + Terraform plans often contain sensitive information, so this output should be treated with care. - Type: string diff --git a/terraform-plan/action.yaml b/terraform-plan/action.yaml index 89a18ff4..1f35f8b2 100644 --- a/terraform-plan/action.yaml +++ b/terraform-plan/action.yaml @@ -78,6 +78,16 @@ inputs: description: Limit the number of concurrent operations required: false default: "0" + speculative: + description: | + Set to `true` to force a speculative plan that cannot be applied. + + This creates a "Planned and finished" run in Terraform Cloud instead of "Planned and saved". + Speculative plans don't lock state and can run in parallel with other operations. + + This is useful for PR workflows where you want to preview changes without blocking other runs. + required: false + default: "false" outputs: changes: @@ -89,6 +99,8 @@ outputs: The plan can be used as the `plan_file` input to the [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/tree/main/terraform-apply) action. + This won't be set if `speculative` is `true` or if the backend type is `remote`/`cloud` in remote execution mode. + Terraform plans often contain sensitive information, so this output should be treated with care. json_plan_path: description: | @@ -112,6 +124,10 @@ outputs: description: The number of resources that would be affected by this operation. run_id: description: If the root module uses the `remote` or `cloud` backend in remote execution mode, this output will be set to the remote run id. + comment_url: + description: | + The URL of the GitHub PR comment that was created or updated with the plan. + This will only be set if a comment was created (i.e., when running on a pull request with add_github_comment enabled). runs: using: docker diff --git a/tofu-plan/action.yaml b/tofu-plan/action.yaml index 6b8b67a2..cd11e154 100644 --- a/tofu-plan/action.yaml +++ b/tofu-plan/action.yaml @@ -120,6 +120,10 @@ outputs: description: The number of resources that would be affected by this operation. run_id: description: If the root module uses the `remote` or `cloud` backend in remote execution mode, this output will be set to the remote run id. + comment_url: + description: | + The URL of the GitHub PR comment that was created or updated with the plan. + This will only be set if a comment was created (i.e., when running on a pull request with add_github_comment enabled). runs: env: