Module 3: The Iac Pipeline Fmt Validate Plan

7. Publishing the `plan` as review evidence

Description

A terraform plan that only lives in a job's logs, buried among hundreds of lines of docker exec output, doesn't fulfill lesson 1's promise: it's not an artifact someone can comfortably review before approving a merge. This lesson closes that gap with two distinct techniques, with complete honesty about which one really runs in this guide and which one is only shown: publishing the plan in the job summary ($GITHUB_STEP_SUMMARY) —executed, confirmed under act— and the real market pattern, commenting the plan directly on the Pull Request via actions/github-script —the same mechanism cited from HashiCorp's official tutorial in lesson 2, shown in complete YAML and honestly labeled as not-executable without a real GitHub.com Pull Request.

Connection to the module

Lesson 6 left plan-output.txt inside the job's container, with Andes Cargo's complete plan. This lesson takes that file and turns it into something a human can read without diving through Docker logs. Lesson 8 —this module's project— brings together everything built in lessons 4 through 7 into a single, complete ci.yml, run end to end.


Analogy: the executive summary, not the meeting's full transcript

A two-hour meeting's complete transcript technically contains all the information — but nobody reads it entirely to make a quick decision. A good executive summary extracts exactly what someone needs to decide, in a format that reads in one minute. act pull_request's complete logs (or a real GitHub Actions job's) are the transcript: everything's there, but finding Plan: N to add among hundreds of lines of docker exec and provider downloads is exactly the kind of friction that makes people stop reading. $GITHUB_STEP_SUMMARY and the PR comment are the two executive-summary formats this lesson builds: the same content, presented where someone's actually going to read it before approving.


Part 1 (EXECUTED) — Publishing to $GITHUB_STEP_SUMMARY

$GITHUB_STEP_SUMMARY is an environment variable GitHub Actions —and act— inject into every step: it points at a temporary file on disk where anything you write becomes the job's complete run's visual summary, in Markdown format, visible in a real GitHub Actions run's Summary tab. Add this step, after Terraform plan:

      - name: Publish the plan to the job summary
        run: |
          {
            echo "## Terraform plan — andes-cargo-infra"
            echo '```'
            cat plan-output.txt
            echo '```'
          } >> "$GITHUB_STEP_SUMMARY"

The pattern is simple: group several echos (and a cat of the file lesson 6 generated) inside braces { ... }, and redirect that whole block, at once, into $GITHUB_STEP_SUMMARY with >> (append to the end, don't overwrite — important if more than one step in your job writes to the summary). Markdown's triple backticks (```) around the plan's content make it show up, in GitHub's real interface, formatted as a code block, not as running text.

act pull_request -e .github/act-events/pr-event.json -j terraform-checks

What to expect (literal output, executed to write this lesson — notice the ⚙ Summary - prefix, confirmation that act does support $GITHUB_STEP_SUMMARY):

[ci/terraform-checks] ⭐ Run Main Publish the plan to the job summary
[ci/terraform-checks]   🐳  docker exec cmd=[bash -e /var/run/act/workflow/9] user= workdir=
[ci/terraform-checks]   ✅  Success - Main Publish the plan to the job summary [70.591959ms]
[ci/terraform-checks]   ⚙  Summary - ## Terraform plan — andes-cargo-infra

data.archive_file.lambda_zip: Reading... data.archive_file.lambda_zip: Read complete after 0s [id=772c6895d44fc6c470f613203a7df0faeee06e87] data.aws_iam_policy_document.require_https: Reading... data.aws_iam_policy_document.app_server_permissions: Reading... data.aws_iam_policy_document.lambda_permissions: Reading... data.aws_iam_policy_document.lambda_trust: Reading... data.aws_iam_policy_document.ec2_trust: Reading... data.aws_iam_policy_document.lambda_permissions: Read complete after 0s [id=4087165242] data.aws_iam_policy_document.ec2_trust: Read complete after 0s [id=2851119427] data.aws_iam_policy_document.lambda_trust: Read complete after 0s [id=2690255455] data.aws_iam_policy_document.require_https: Read complete after 0s [id=4186015114] [ ... the rest of the complete plan-output.txt, including "Plan: 12 to add" ... ]


This is **not** a simulation of what it would look like — it's `act` showing you, with the `⚙ Summary -` prefix, exactly the content it wrote to the summary file, confirmed line by line against `plan-output.txt`. It's direct confirmation —researched and verified, per this guide's design— that `$GITHUB_STEP_SUMMARY` works under `act`, even though `act` doesn't have a web interface where you can "view the Summary tab": the write-to-file mechanism is the same, and `act` shows it to you in the terminal instead of on a rendered page.

On a real GitHub repository, this same content would show up, with complete Markdown formatting, on the workflow run's **Summary** tab — the first place someone reviewing a Pull Request would look, without having to open any step's complete logs.

---

## Part 2 (REPRESENTATIVE) — Commenting the `plan` directly on the Pull Request

The pattern HashiCorp cites in its official tutorial —which this module's lesson 2 already named— goes one step beyond the job summary: it publishes the `plan` as a comment **visible directly in the Pull Request's conversation**, using `actions/github-script`, an Action that gives you full access to GitHub's API (through Octokit, the official library) inside a JavaScript step.

```yaml
      - name: Comment the plan on the pull request
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const plan = fs.readFileSync('plan-output.txt', 'utf8');
            const maxLength = 65000;
            const truncated = plan.length > maxLength
              ? plan.slice(0, maxLength) + '\n... (plan truncated)'
              : plan;

            const body = `## Terraform plan — andes-cargo-infra

            <details><summary>Show plan output</summary>

            \`\`\`
            ${truncated}
            \`\`\`

            </details>

            *Pushed by @${context.actor}, workflow run #${context.runNumber}*`;

            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: body,
            });

Why this step is labeled representative, with the exact technical reason: github.rest.issues.createComment is a real call to GitHub's REST API —it needs a real issue_number (the number of a Pull Request that really exists in a GitHub.com repository) and a token with permission to write comments. act has no real Pull Request to publish anything to: the pr-event.json you wrote in Module 2 simulates the event's payload (context.issue.number would resolve to 42, the number you wrote by hand), but there's no GitHub server on the other side waiting for that call. Running this step under act would fail trying to authenticate and call an API that, in this simulation, isn't there — not because the YAML is written wrong, but because, by design, there's no real Pull Request to comment against.

Notice three pieces of the script worth understanding, even without running it:

  • context.repo.owner / context.repo.repo / context.issue.number — the context actions/github-script automatically exposes, derived from the event that triggered the workflow; in a real Pull Request, these three values identify exactly where to publish the comment.
  • <details><summary>...</summary> inside the Markdown — an Andes Cargo plan with 12 resources is already long; in a real project, with dozens or hundreds of resources, a comment showing everything uncollapsed would make the Pull Request nearly unreadable. The <details> block collapses the plan by default, visible only if someone clicks — the same pattern you already use in every lesson's Exercises in this guide.
  • The maxLength limit — GitHub's API has a real size limit for a comment's body; a large project's plan could exceed it, so explicitly truncating (with a clear notice) is safer than letting the call fail with no explanation.

Pointer, not built out: the real market pattern for avoiding duplicate comments on every new run —looking up and updating an existing bot comment, instead of creating a new one each time, with github.rest.issues.listComments followed by updateComment or createComment as appropriate— is exactly what HashiCorp's tutorial cited in lesson 2 shows. This guide doesn't build it out completely here because, with no real Pull Request to test it against, there's no honest way to confirm it works — it's named, with the exact reference, for anyone who needs it on a real repository.


Comparing the two forms of evidence

$GITHUB_STEP_SUMMARY (executed here)PR comment (shown, not executed)
Where it livesThe workflow run's Summary tabThe Pull Request's conversation, alongside the other comments
Who sees it firstSomeone who opens the CI run specificallySomeone reviewing the Pull Request in general, with no need to open Actions
Updates on every pushEvery run generates its own new summaryThe real pattern updates the same comment, without piling up one per push
Runs under actYes, confirmed in this lessonNo — needs a real GitHub.com Pull Request
Requires additional permissionsNo — it's part of the job's standard environmentYes — the token needs write permission on Issues/Pull Requests

Neither replaces the other in a real pipeline: many teams use both, because they cover two different review moments.


Common mistakes

Using > instead of >> when writing to $GITHUB_STEP_SUMMARY (syntax-based, silent but real). What happens: someone writes echo "something" > "$GITHUB_STEP_SUMMARY" in more than one step of the same job, and each one overwrites what the previous step had written, instead of appending. Why it happens: > and >> look almost identical, and both "work" in the sense that they produce no error. How to spot it: if your final summary only shows the content of the last step that wrote, and earlier ones vanished. How to fix it: always use >> for $GITHUB_STEP_SUMMARY, unless you have an explicit reason to start the summary fresh at that exact point in the job.

Trying to run the actions/github-script step with act and being surprised by the failure (expectation-based, this lesson's central point). What happens: someone copies "Part 2"'s YAML as is, adds it to ci.yml, and runs act pull_request -e pr-event.json, expecting to see a simulated comment somewhere. How to spot it: an authentication or network error trying to call GitHub's API, with no real target Pull Request. How to fix it: this step is designed, in this guide, to be read and understood, not to run under act — the exact technical reason is in the section above. If you want to see it really work, you need a real GitHub repository with a real Pull Request open (a topic Module 8, lesson 5, picks back up with final honesty about what can only be experienced with a real GitHub account).

Forgetting plan-output.txt only exists if lesson 6's step ran earlier in the same job (order-based). What happens: someone adds the summary-publishing step in a job different from the one that ran terraform plan, and cat plan-output.txt fails with "No such file or directory." How to fix it: remember every act job runs in its own container, without sharing a file system with other jobs in the same workflow (unless you use actions/upload-artifact/download-artifact, the mechanism Module 5 uses to pass the plan from ci.yml to apply.yml) — within the same job, on the other hand, every step does share the same file system, which is why lesson 6's tee and this lesson's cat work with no extra step.


Exercises

Exercise 1 — Explain why the PR comment doesn't run under act, without saying "it needs internet." A colleague tells you: "it probably fails because act doesn't have internet." Correct them with this lesson's exact technical reason.

See solution

It's not a general connectivity problem —in fact, act does have internet access, as you confirmed installing terraform, awslocal, and tflocal with real downloads in previous lessons. The problem is that github.rest.issues.createComment needs a real Pull Request, that exists in a real GitHub.com repository, identified by a genuine issue_number — and that Pull Request simply doesn't exist: pr-event.json simulates the event's payload, it doesn't create a real object on GitHub's side. The call would fail because the destination doesn't exist, not because of missing internet connectivity.

Exercise 2 — Decide which evidence format you'd use for each scenario. For each situation, indicate whether you'd use $GITHUB_STEP_SUMMARY, the PR comment, or both: (a) you want anyone opening the Pull Request to see the plan without having to go to the Actions tab; (b) you want a simple copy of the plan, tied to that specific run, without worrying about duplicates on every push; (c) your team reviews Pull Requests almost exclusively from the conversation view, almost never opening the Actions tab.

See solution

(a) The PR comment — it's the only one of the two that shows up directly in the Pull Request's conversation, with nobody needing to navigate to another tab. (b) $GITHUB_STEP_SUMMARY — every run generates its own summary, with no additional logic for finding and updating an existing comment; it's the simplest option if you don't mind having one summary per run. (c) The PR comment (possibly both, but the comment is indispensable here) — if the team almost never opens Actions, a summary living exclusively there would never be seen.

Exercise 3 — Predict the result for a very long plan. If a future Andes Cargo project's plan exceeded the 65,000 characters Part 2's script uses as a limit, what would you see in the Pull Request's comment, based on this lesson's code?

See solution

You'd see the plan truncated exactly at character 65,000, followed by the line ... (plan truncated) — the script explicitly cuts the text with .slice(0, maxLength) and adds that notice, instead of letting the createComment call fail with no explanation for exceeding GitHub's real API limit for a comment's body. You wouldn't see an error — you'd see an incomplete comment that's honest about being incomplete, published successfully.


Summary and next step

In this lesson you closed out half of the pipeline's CI with two ways of turning a plan into review evidence: $GITHUB_STEP_SUMMARY, confirmed working under act with literal output (the ⚙ Summary - prefix you saw, line by line, matches plan-output.txt), and the direct Pull Request comment via actions/github-script —the pattern cited from HashiCorp's official tutorial, shown in complete YAML and explained step by step, honestly labeled as not-executable without a real Pull Request.

Before moving on you should be able to: write the { echo ...; cat file; } >> "$GITHUB_STEP_SUMMARY" pattern from memory; explain, with the exact technical reason, why the PR comment doesn't run under act; and decide, for a given scenario, which of the two evidence forms (or both) makes more sense.

Lesson 8 —this module's project— brings together everything built in lessons 4 through 7 into a single, complete ci.yml, run end to end with act pull_request -e pr-event.json, against a real change to Andes Cargo's HCL.

Resources

  1. GitHub Docs — Adding a job summary — official documentation for $GITHUB_STEP_SUMMARY, used and confirmed under act in this lesson.
  2. actions/github-script — the official repository for the Action used in Part 2.
  3. HashiCorp Developer — Automate Terraform with GitHub Actions — the original source for the pattern of commenting the plan on the Pull Request, cited in lesson 2.
  4. GitHub REST API — Create an issue comment — the exact call github.rest.issues.createComment wraps, for anyone who wants to build it out fully against a real repository.