From 088fc149667074006bc6b79dc6b4bf6f64f32d6b Mon Sep 17 00:00:00 2001 From: Jeremy Wood Date: Tue, 10 Feb 2026 14:22:09 -0500 Subject: [PATCH 1/4] Show logs link if plan output too long. --- action.yml | 54 ++++++++++++++++++++++++++++++++++++++++---- terraform_comment.js | 20 +++++++++++++--- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/action.yml b/action.yml index cc15025..631b131 100644 --- a/action.yml +++ b/action.yml @@ -279,11 +279,51 @@ runs: working-directory: ${{ inputs.directory }} shell: bash run: | + PLAN_COMMENT_MAX_BYTES=60000 + plan_stdout_file=$(mktemp) + plan_stderr_file=$(mktemp) + + cleanup_plan_files() { + rm -f "$plan_stdout_file" "$plan_stderr_file" + } + trap cleanup_plan_files EXIT + + set +e ${{ inputs.tf-cli }} plan -input=false \ ${{ github.event_name == 'pull_request' && '-no-color' || '' }} \ ${{ fromJSON(inputs.destroy) && '-destroy' || '' }} \ ${{ inputs.plan-file-name && format('-out={0}', inputs.plan-file-name) || '' }} \ - ${{ inputs.action-args }} + ${{ inputs.action-args }} \ + > >(tee "$plan_stdout_file") \ + 2> >(tee "$plan_stderr_file" >&2) + plan_exit_code=$? + set -e + + plan_stdout_bytes=$(wc -c < "$plan_stdout_file" | tr -d '[:space:]') + plan_stderr_bytes=$(wc -c < "$plan_stderr_file" | tr -d '[:space:]') + plan_total_bytes=$((plan_stdout_bytes + plan_stderr_bytes)) + + if [ "$plan_total_bytes" -le "$PLAN_COMMENT_MAX_BYTES" ]; then + plan_stdout_delimiter="PLAN_STDOUT_$(date +%s)_$RANDOM" + plan_stderr_delimiter="PLAN_STDERR_$(date +%s)_$RANDOM" + { + echo "comment_overflow=false" + echo "comment_stdout<<$plan_stdout_delimiter" + cat "$plan_stdout_file" + echo "$plan_stdout_delimiter" + echo "comment_stderr<<$plan_stderr_delimiter" + cat "$plan_stderr_file" + echo "$plan_stderr_delimiter" + } >> "$GITHUB_OUTPUT" + else + { + echo "comment_overflow=true" + echo "comment_stdout=" + echo "comment_stderr=" + } >> "$GITHUB_OUTPUT" + fi + + exit "$plan_exit_code" - name: Encrypt Plan if: inputs.action == 'plan' && inputs.plan-file-name && inputs.encrypted-plan-password @@ -367,8 +407,10 @@ runs: validateStderr: ${{ steps.validate.outputs.stderr }} testStdout: ${{ steps.test.outputs.stdout }} testStderr: ${{ steps.test.outputs.stderr }} - planStdout: ${{ steps.plan.outputs.stdout }} - planStderr: ${{ steps.plan.outputs.stderr }} + planCommentStdout: ${{ steps.plan.outputs.comment_stdout }} + planCommentStderr: ${{ steps.plan.outputs.comment_stderr }} + planCommentOverflow: ${{ steps.plan.outputs.comment_overflow }} + workflowRunUrl: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} outputStdout: ${{ steps.output.outputs.stdout }} outputStderr: ${{ steps.output.outputs.stderr }} with: @@ -410,8 +452,10 @@ runs: plan: { enabled: ${{ inputs.action == 'plan' }}, outcome: "${{ steps.plan.outcome }}", - stdout: `${process.env.planStdout}`, - stderr: `${process.env.planStderr}` + stdout: `${process.env.planCommentStdout}`, + stderr: `${process.env.planCommentStderr}`, + overflowed: `${process.env.planCommentOverflow}` === 'true', + logsUrl: `${process.env.workflowRunUrl}` }, output: { enabled: ${{ inputs.action == 'output' }}, diff --git a/terraform_comment.js b/terraform_comment.js index 81d492a..daf903c 100644 --- a/terraform_comment.js +++ b/terraform_comment.js @@ -110,7 +110,21 @@ ${outcome !== 'skipped' ? createTestDetails(stdout, stderr) : ''} ` : ''; } -const createPlanDetails = (stdout, stderr) => { +const createPlanDetails = ({stdout, stderr, overflowed, logsUrl}) => { + if (overflowed) { + const workflowLogs = logsUrl ? `[workflow run logs](${logsUrl})` : 'workflow run logs'; + return ` + +
Show Plan + +Terraform plan output was omitted because it exceeds size constraints. + +Instead, you can view the plan output in the ${workflowLogs}. + +
+ `; + } + return `
Show Plan @@ -123,11 +137,11 @@ ${stdout}${stderr ? `\n${stderr}` : ''} `; }; -const createPlanOutput = ({enabled, outcome, stdout, stderr}) => { +const createPlanOutput = ({enabled, outcome, stdout, stderr, overflowed, logsUrl}) => { return enabled ? ` #### Terraform Plan 📖 \`${outcome}\` -${outcome !== 'skipped' ? createPlanDetails(stdout, stderr) : ''} +${outcome !== 'skipped' ? createPlanDetails({stdout, stderr, overflowed, logsUrl}) : ''} ` : ''; }; From 8fe59aed1636cf8b8bae4fa720b7d165dbc308a8 Mon Sep 17 00:00:00 2001 From: Jeremy Wood Date: Tue, 10 Feb 2026 15:08:57 -0500 Subject: [PATCH 2/4] Show excerpt of plan when plan too long. Also moves the plan script into separate script file. --- action.yml | 56 +++++------------------- scripts/terraform_plan.sh | 89 +++++++++++++++++++++++++++++++++++++++ terraform_comment.js | 37 +++++++++++++--- 3 files changed, 132 insertions(+), 50 deletions(-) create mode 100755 scripts/terraform_plan.sh diff --git a/action.yml b/action.yml index 631b131..fa11a86 100644 --- a/action.yml +++ b/action.yml @@ -277,53 +277,17 @@ runs: id: plan if: inputs.action == 'plan' working-directory: ${{ inputs.directory }} + env: + TF_CLI: ${{ inputs.tf-cli }} + TF_PLAN_NO_COLOR: ${{ github.event_name == 'pull_request' && '-no-color' || '' }} + TF_PLAN_DESTROY: ${{ fromJSON(inputs.destroy) && '-destroy' || '' }} + TF_PLAN_OUT: ${{ inputs.plan-file-name && format('-out={0}', inputs.plan-file-name) || '' }} + TF_PLAN_ACTION_ARGS: ${{ inputs.action-args }} + PLAN_COMMENT_MAX_BYTES: '60000' + PLAN_COMMENT_EXCERPT_MAX_BYTES: '12288' shell: bash run: | - PLAN_COMMENT_MAX_BYTES=60000 - plan_stdout_file=$(mktemp) - plan_stderr_file=$(mktemp) - - cleanup_plan_files() { - rm -f "$plan_stdout_file" "$plan_stderr_file" - } - trap cleanup_plan_files EXIT - - set +e - ${{ inputs.tf-cli }} plan -input=false \ - ${{ github.event_name == 'pull_request' && '-no-color' || '' }} \ - ${{ fromJSON(inputs.destroy) && '-destroy' || '' }} \ - ${{ inputs.plan-file-name && format('-out={0}', inputs.plan-file-name) || '' }} \ - ${{ inputs.action-args }} \ - > >(tee "$plan_stdout_file") \ - 2> >(tee "$plan_stderr_file" >&2) - plan_exit_code=$? - set -e - - plan_stdout_bytes=$(wc -c < "$plan_stdout_file" | tr -d '[:space:]') - plan_stderr_bytes=$(wc -c < "$plan_stderr_file" | tr -d '[:space:]') - plan_total_bytes=$((plan_stdout_bytes + plan_stderr_bytes)) - - if [ "$plan_total_bytes" -le "$PLAN_COMMENT_MAX_BYTES" ]; then - plan_stdout_delimiter="PLAN_STDOUT_$(date +%s)_$RANDOM" - plan_stderr_delimiter="PLAN_STDERR_$(date +%s)_$RANDOM" - { - echo "comment_overflow=false" - echo "comment_stdout<<$plan_stdout_delimiter" - cat "$plan_stdout_file" - echo "$plan_stdout_delimiter" - echo "comment_stderr<<$plan_stderr_delimiter" - cat "$plan_stderr_file" - echo "$plan_stderr_delimiter" - } >> "$GITHUB_OUTPUT" - else - { - echo "comment_overflow=true" - echo "comment_stdout=" - echo "comment_stderr=" - } >> "$GITHUB_OUTPUT" - fi - - exit "$plan_exit_code" + bash "${{ github.action_path }}/scripts/terraform_plan.sh" - name: Encrypt Plan if: inputs.action == 'plan' && inputs.plan-file-name && inputs.encrypted-plan-password @@ -410,6 +374,7 @@ runs: planCommentStdout: ${{ steps.plan.outputs.comment_stdout }} planCommentStderr: ${{ steps.plan.outputs.comment_stderr }} planCommentOverflow: ${{ steps.plan.outputs.comment_overflow }} + planCommentExcerptSource: ${{ steps.plan.outputs.comment_excerpt_source }} workflowRunUrl: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} outputStdout: ${{ steps.output.outputs.stdout }} outputStderr: ${{ steps.output.outputs.stderr }} @@ -455,6 +420,7 @@ runs: stdout: `${process.env.planCommentStdout}`, stderr: `${process.env.planCommentStderr}`, overflowed: `${process.env.planCommentOverflow}` === 'true', + excerptSource: `${process.env.planCommentExcerptSource}`, logsUrl: `${process.env.workflowRunUrl}` }, output: { diff --git a/scripts/terraform_plan.sh b/scripts/terraform_plan.sh new file mode 100755 index 0000000..f0dc31a --- /dev/null +++ b/scripts/terraform_plan.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +set -euo pipefail + +plan_stdout_file=$(mktemp) +plan_stderr_file=$(mktemp) +plan_excerpt_file=$(mktemp) +plan_combined_file=$(mktemp) + +cleanup_plan_files() { + rm -f "$plan_stdout_file" "$plan_stderr_file" "$plan_excerpt_file" "$plan_combined_file" +} +trap cleanup_plan_files EXIT + +PLAN_COMMENT_MAX_BYTES="${PLAN_COMMENT_MAX_BYTES:-60000}" +PLAN_COMMENT_EXCERPT_MAX_BYTES="${PLAN_COMMENT_EXCERPT_MAX_BYTES:-12288}" + +write_multiline_output() { + local output_name="$1" + local file_path="$2" + local delimiter="${output_name}_$(date +%s)_$RANDOM" + + { + echo "${output_name}<<${delimiter}" + cat "$file_path" + printf '\n%s\n' "$delimiter" + } >> "$GITHUB_OUTPUT" +} + +plan_command="${TF_CLI} plan -input=false" + +if [ -n "${TF_PLAN_NO_COLOR:-}" ]; then + plan_command="${plan_command} ${TF_PLAN_NO_COLOR}" +fi + +if [ -n "${TF_PLAN_DESTROY:-}" ]; then + plan_command="${plan_command} ${TF_PLAN_DESTROY}" +fi + +if [ -n "${TF_PLAN_OUT:-}" ]; then + plan_command="${plan_command} ${TF_PLAN_OUT}" +fi + +if [ -n "${TF_PLAN_ACTION_ARGS:-}" ]; then + plan_command="${plan_command} ${TF_PLAN_ACTION_ARGS}" +fi + +set +e +eval "$plan_command" \ + > >(tee "$plan_stdout_file") \ + 2> >(tee "$plan_stderr_file" >&2) +plan_exit_code=$? +set -e + +plan_stdout_bytes=$(wc -c < "$plan_stdout_file" | tr -d '[:space:]') +plan_stderr_bytes=$(wc -c < "$plan_stderr_file" | tr -d '[:space:]') +plan_total_bytes=$((plan_stdout_bytes + plan_stderr_bytes)) + +if [ "$plan_total_bytes" -le "$PLAN_COMMENT_MAX_BYTES" ]; then + { + echo "comment_overflow=false" + echo "comment_excerpt_source=full" + } >> "$GITHUB_OUTPUT" + + write_multiline_output "comment_stdout" "$plan_stdout_file" + write_multiline_output "comment_stderr" "$plan_stderr_file" +else + plan_summary_match=$(grep -nE -m 1 '^Plan: [0-9]+ to add, [0-9]+ to change, [0-9]+ to destroy\.$' "$plan_stdout_file" || true) + + if [ -n "$plan_summary_match" ]; then + plan_summary_line="${plan_summary_match%%:*}" + tail -n +"$plan_summary_line" "$plan_stdout_file" > "$plan_combined_file" + head -c "$PLAN_COMMENT_EXCERPT_MAX_BYTES" "$plan_combined_file" > "$plan_excerpt_file" + comment_excerpt_source="plan-summary" + else + cat "$plan_stdout_file" "$plan_stderr_file" > "$plan_combined_file" + tail -c "$PLAN_COMMENT_EXCERPT_MAX_BYTES" "$plan_combined_file" > "$plan_excerpt_file" + comment_excerpt_source="tail" + fi + + { + echo "comment_overflow=true" + echo "comment_excerpt_source=${comment_excerpt_source}" + echo "comment_stderr=" + } >> "$GITHUB_OUTPUT" + write_multiline_output "comment_stdout" "$plan_excerpt_file" +fi + +exit "$plan_exit_code" diff --git a/terraform_comment.js b/terraform_comment.js index daf903c..279ffd8 100644 --- a/terraform_comment.js +++ b/terraform_comment.js @@ -110,16 +110,43 @@ ${outcome !== 'skipped' ? createTestDetails(stdout, stderr) : ''} ` : ''; } -const createPlanDetails = ({stdout, stderr, overflowed, logsUrl}) => { +const createPlanExcerptSourceNote = (excerptSource) => { + if (excerptSource === 'plan-summary') { + return 'This excerpt starts at the Terraform summary line (`Plan: ...`).'; + } + + if (excerptSource === 'tail') { + return 'The Terraform summary line was not found, so this excerpt shows the tail of combined stdout/stderr output.'; + } + + return 'This excerpt shows a truncated portion of the Terraform plan output.'; +}; + +const createPlanDetails = ({stdout, stderr, overflowed, excerptSource, logsUrl}) => { if (overflowed) { const workflowLogs = logsUrl ? `[workflow run logs](${logsUrl})` : 'workflow run logs'; + const excerptSourceNote = createPlanExcerptSourceNote(excerptSource); + const excerptOutput = stdout + ? ` +\`\`\`hcl\n +${stdout} +\`\`\` +` + : ` +_No plan excerpt was available in this comment._ +`; + return `
Show Plan -Terraform plan output was omitted because it exceeds size constraints. +Terraform plan output exceeded size constraints, so this comment includes only a partial excerpt. + +${excerptSourceNote} + +View the full plan output in the ${workflowLogs}. -Instead, you can view the plan output in the ${workflowLogs}. +${excerptOutput}
`; @@ -137,11 +164,11 @@ ${stdout}${stderr ? `\n${stderr}` : ''} `; }; -const createPlanOutput = ({enabled, outcome, stdout, stderr, overflowed, logsUrl}) => { +const createPlanOutput = ({enabled, outcome, stdout, stderr, overflowed, excerptSource, logsUrl}) => { return enabled ? ` #### Terraform Plan 📖 \`${outcome}\` -${outcome !== 'skipped' ? createPlanDetails({stdout, stderr, overflowed, logsUrl}) : ''} +${outcome !== 'skipped' ? createPlanDetails({stdout, stderr, overflowed, excerptSource, logsUrl}) : ''} ` : ''; }; From 0db402066bff39633c462ca1d7cad0dde2f0dcd0 Mon Sep 17 00:00:00 2001 From: Jeremy Wood Date: Tue, 10 Feb 2026 15:17:16 -0500 Subject: [PATCH 3/4] Include job ID in workflow link. --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index fa11a86..498fa70 100644 --- a/action.yml +++ b/action.yml @@ -375,7 +375,7 @@ runs: planCommentStderr: ${{ steps.plan.outputs.comment_stderr }} planCommentOverflow: ${{ steps.plan.outputs.comment_overflow }} planCommentExcerptSource: ${{ steps.plan.outputs.comment_excerpt_source }} - workflowRunUrl: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + workflowRunUrl: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/job/${{ github.job }} outputStdout: ${{ steps.output.outputs.stdout }} outputStderr: ${{ steps.output.outputs.stderr }} with: From 036abf07f2c26d9647678977626b9af13e13be0b Mon Sep 17 00:00:00 2001 From: Jeremy Wood Date: Tue, 10 Feb 2026 15:26:24 -0500 Subject: [PATCH 4/4] Use job.check_run_id instead of github.job. --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 498fa70..3b716e6 100644 --- a/action.yml +++ b/action.yml @@ -375,7 +375,7 @@ runs: planCommentStderr: ${{ steps.plan.outputs.comment_stderr }} planCommentOverflow: ${{ steps.plan.outputs.comment_overflow }} planCommentExcerptSource: ${{ steps.plan.outputs.comment_excerpt_source }} - workflowRunUrl: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/job/${{ github.job }} + workflowRunUrl: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/job/${{ job.check_run_id }} outputStdout: ${{ steps.output.outputs.stdout }} outputStderr: ${{ steps.output.outputs.stderr }} with: