Skip to content

[test] do not merge: terraform plan as structured outputs, log and annotations - #29

Open
posquit0 wants to merge 18 commits into
mainfrom
test/plan-json-demo
Open

posquit0 wants to merge 18 commits into
mainfrom
test/plan-json-demo

Conversation

@posquit0

Copy link
Copy Markdown
Member

Demo for the terraform.plan rewrite in #5. Two fixtures fail or change on purpose:

  • fixtures/plan-demo has a committed terraform.tfstate standing in for a previous apply, and managed.txt is deliberately absent. The plan therefore reports a create, an update, a replace, a destroy, drift on local_file.managed, and a warning from a check block that fails on purpose.
  • fixtures/plan-demo-error references an undeclared variable, so the plan fails.

What to look at:

  1. The job log of the first plan step. It shows the readable plan exactly as terraform plan prints it, rendered by terraform show from the saved plan file. The JSON message stream never reaches the log.
  2. The job summary, which lists every output, the structured JSON, and the rendered summary.
  3. The Files changed tab, where the warning and the error are annotated on their lines.

Not meant to be merged.

- Turn an arbitrary string into an artifact-safe slug
- Append a short digest so two different values never collide
- Run `terraform plan -out` with `-detailed-exitcode` and expose whether
  the workspace has changes
- Build the summary from `terraform show -json`, never from the log text,
  and render it as a Markdown table of resource actions
- Truncate the resource list at `max_resources` so one workspace cannot
  fill a pull request comment
- Fall back to a plan without `-out` when the backend refuses to save one,
  which is what a workspace with remote execution does, and say so
- Apply a saved plan file, so the applied change is the reviewed change
- Report a stale plan separately from a real failure, since it means the
  state moved after the plan and needs a fresh plan rather than a retry
- Expose added, changed, and destroyed counts and a Markdown summary
- Resolve the merged pull request behind a commit and the completed run of
  a given workflow against that pull request's head
- Lets a job triggered by the merge download artifacts the pull request
  run uploaded, such as a Terraform plan file
- Add a `details` input rendered as a collapsed block per row, for content
  too long for a table cell such as a Terraform plan
- Add a `pr_number` input so the report can be posted from events without a
  pull request context, such as the push that follows a merge
- Reuse the integration resolver to find the changed workspaces
- Plan each workspace in parallel and upload its plan file as an artifact
- Publish one sticky comment holding a per-workspace collapsed plan, so a
  re-run updates the same comment instead of adding another
- Resolve the merged pull request and download the plan files its run saved
- Apply one workspace at a time, each under its own concurrency group
- Publish the result as its own comment on the merged pull request, kept
  separate from the plan comment
….apply

- Guard the job summary steps with `always()`. A bare `if:` is implicitly
  combined with `success()`, so a step conditioned only on another step's
  failure never runs once that step has failed
- Add a summary step that runs whatever happened, carrying the tail of the
  command that failed, so the caller never reports a bare failure with no
  reason
- Export `KEY=value` lines into the job environment
- Mask every value, and skip a line whose value is empty, which is what an
  unset secret looks like
- Reject a malformed line, an invalid variable name, and any name the runner
  owns, so the input cannot change how later steps run
…d apply workflows

- Add a `provider_env` secret carrying `KEY=value` lines
- Export it before planning or applying, so providers that read credentials
  from the environment, such as Okta, work without the reusable workflow
  needing an input per provider
…pply

- Replace the `tee` pipeline of the init step with `shell.run`, as
  `terraform.validate` does, and point the failure summary and the plan
  summary fallback at its `log_file` output
@github-actions

Copy link
Copy Markdown

GitHub Actions Integration

Repository actionlint check-jsonschema (workflows) check-jsonschema (actions)
tedilabs/github-actions

✅ All 1 passed.

… diagnostics

- Run the plan as `terraform plan -json -detailed-exitcode -out=<file>` with the
  message stream going to a file, then render the readable plan into the log
  with `terraform show -no-color <file>`. One plan run yields both the log a
  plain `terraform plan` would have printed and the typed message stream
- Add `drift_count`, `warning_count`, and `error_count`, and the JSON outputs
  `changes`, `drift`, `output_changes`, and `diagnostics`, each truncated to
  `max_resources`. They carry addresses, actions, and reasons, never attribute
  values, so they stay far below the 1 MB limit on step outputs
- Pass the bulky renderings as paths instead: `plan_json_file` and `stream_file`
- Add `annotations_enabled` (default `true`). Diagnostics only exist in the
  message stream, not in the plan file, so warnings on a successful plan were
  previously dropped; they are now annotated and listed in the summary
- Report drift in the summary as a collapsed list
- Stop reporting `plan_file` and `plan_file_saved` when the plan failed and no
  file was written, which made the caller's artifact upload fail instead of skip
@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown

Terraform Plan

❌ 1 of 2 failed · see the run summary for details.

fixtures/plan-demo  🟢 +1 🟡 ~1 🔴 -1 🟠 ±1  ·  🌀 1 drifted  ·  ⚠️ 1 warning

Resource changes

🟢 + local_file.managed
+ resource "local_file" "managed" {
    + content              = "original"
    + content_base64sha256 = (known after apply)
    + content_base64sha512 = (known after apply)
    + content_md5          = (known after apply)
    + content_sha1         = (known after apply)
    + content_sha256       = (known after apply)
    + content_sha512       = (known after apply)
    + directory_permission = "0777"
    + file_permission      = "0777"
    + filename             = "./managed.txt"
    + id                   = (known after apply)
  }
🟡 ~ terraform_data.change_me
~ resource "terraform_data" "change_me" {
      id     = "540dfd82-907b-51b8-9eea-bd37a834d390"
    ~ input  = "small" -> "large"
    ~ output = "small" -> (known after apply)
  }
🔴 - terraform_data.gone — because terraform_data.gone is not in configuration
- resource "terraform_data" "gone" {
    - id     = "70e89507-6664-49c6-2702-3a28e33c4a07" -> null
    - input  = "bye" -> null
    - output = "bye" -> null
  }
🟠 ± terraform_data.replace_me — replace because cannot update
-/+ resource "terraform_data" "replace_me" {
      ~ id               = "4383baf0-4fca-b2eb-fc13-f630eec01651" -> (known after apply)
      ~ output           = "x" -> (known after apply)
      ~ triggers_replace = [
          ~ "small" -> "large",
        ]
        # (1 unchanged attribute hidden)
    }

Changed outside of Terraform

🌀 ~ local_file.managed
- # the resource no longer exists

Warnings


fixtures/plan-demo-error  ❌ 1 error

Errors

- Move the summary rendering into `scripts/render-summary.py`, following the
  `web.s3.delivery` precedent, since slicing the plan text, diffing drift and
  laying out Markdown outgrew an inline `jq` program
- Show each changed resource as a collapsed block titled with an emoji for the
  action (➕ create, 📝 update, 🗑️ destroy, ♻️ replace) and the reason, holding
  the attribute diff sliced out of `terraform show` so it reads exactly as
  Terraform prints it and keeps its redaction of sensitive values
- Give drift the same treatment, diffed from `resource_drift` in the plan JSON
  because Terraform renders no drift section of its own
- Link every diagnostic to its line on the commit being planned, using the
  pull request head rather than the merge commit
- Add `diff_enabled`, `diff_max_lines` and `summary_max_bytes`. The last one
  keeps one workspace from spending the whole 65,536 character budget of the
  shared pull request comment, closing any open fence or block when it cuts
- Report each target under its own heading in `github.matrix-report`, and fall
  back to the table alone when the comment would exceed the GitHub limit
…eport

- Replace the pictographic emoji with Terraform's own notation, coloured so a
  block can be placed at a glance: 🟩 `+`, 🟨 `~`, 🟥 `-`, 🟧 `±`, 🟪 for drift
- Add a `headline` output to `terraform.plan`, a single line such as
  `🟩 +1 · 🟨 ~1 · 🟪 1 drifted · ⚠️ 1 warning`, and take the counts out of the
  body so they are read next to the workspace name instead of above the diffs
- Give `github.matrix-report` a `headline` input and render each target as a
  collapsed section headed by `<h3>`, with ✅ or ❌ and the headline on the same
  line, a rule between targets, and the section left open when it failed
- Add `table_enabled`, so a report whose targets each carry their own section
  can drop the table that would only repeat the headings
…eated counts

- Remove the fixed `terraform plan failed` banner. It never varied, and the
  target's heading already carries ❌ and the error count. A failure with no
  diagnostics at all now says so instead
- Head every part of the body: `Resource changes`, `Changed outside of
  Terraform`, `Errors`, `Warnings`. The counts stay on the target heading, so
  a section only needs a name
- Settle the levels: the report title drops to `##`, each target keeps its
  `<h3>`, and the sections inside one sit at `####`
- Divide the target headline into groups, so the resource counts, the drift
  and the diagnostics read as three things rather than one run-on list
…ic row

- Take the report title to `#`, each target to `<h2>` and the sections inside
  a target to `###`, so the three levels are told apart by size
- Set the verdict above the targets as a quote
- Separate the headline groups with a middle dot carrying space on both sides
- Read a diagnostic location first, `main.tf:5 — Reference to undeclared
  input variable`, with the whole row as the link to that line
…piral

- 🟢 `+`, 🟡 `~`, 🔴 `-`, 🟠 `±` for the planned actions
- 🌀 for drift, deliberately outside that family: it is not a change Terraform
  planned, so it reads as a different kind of thing and the headline groups
  separate on colour as well as on the divider
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant