Skip to content

Use JSON plan output for deterministic plan hashing#429

Open
toast-gear wants to merge 1 commit into
dflook:mainfrom
toast-gear:fix/json-plan-hash
Open

Use JSON plan output for deterministic plan hashing#429
toast-gear wants to merge 1 commit into
dflook:mainfrom
toast-gear:fix/json-plan-hash

Conversation

@toast-gear
Copy link
Copy Markdown
Contributor

Fixes #196.

What changed

Switch plan comparison hashing from the human-readable terraform plan text output to the machine-readable terraform show -json output.

image/src/github_pr_comment/hash.py

  • Added plan_json_hash(json_plan_path, salt): reads the full JSON plan, removes only the timestamp field (the single volatile field that changes on every run but carries no information about what will change), canonicalises with canonicaljson for key-order determinism, and hashes.
  • plan_hash() (text-based) is retained unchanged as a backward-compatibility fallback.

image/src/github_pr_comment/comment.py

  • Registered the new plan_json_hash PR comment header field alongside the existing plan_hash.

image/src/github_pr_comment/__main__.py

  • _json_plan_path(): constructs the path to plan.json from the GITHUB_WORKSPACE and WORKSPACE_TMP_DIR env vars already exported by actions.sh, returning None if the file doesn't exist.
  • When storing a plan comment: writes plan_json_hash when plan.json is available, and always writes the legacy plan_hash alongside it so older versions of the apply action can still verify.
  • When verifying at apply time: is_approved() prefers plan_json_hash → falls back to plan_hash → falls back to direct text comparison.

Why

Terraform's text output is non-deterministic across runs even when the planned changes are identical. Cosmetic differences (ordering, whitespace, provider-injected warnings) were causing spurious "plan changed" failures at apply time and blocking deployments. The JSON output is stable for the same intended changes.

Backward compatibility

Fully preserved. PR comments created before this change carry plan_hash but no plan_json_hash. The three-level fallback in is_approved() handles them transparently — no re-plan required for in-flight PRs.

When JSON is unavailable

JSON plan output is not produced when using remote/cloud backends that do not support saving plan files (PLAN_OUT=""). In that case _json_plan_path() returns None, plan_json_hash is not stored, and the existing text-based path is used unchanged.

Fixes dflook#196. Plan comparison was hashing the human-readable text output of
terraform plan, which is non-deterministic across runs even when the actual
intended changes are identical. This caused spurious "plan changed" failures
during apply.

Switch to hashing the JSON plan output (terraform show -json) instead, which
is stable for the same set of changes. The only excluded field is `timestamp`,
which is regenerated on every plan run and carries no information about what
will change.

Backward compatibility is preserved: PR comments created before this change
carry a `plan_hash` (text-based) but no `plan_json_hash`. The approval logic
falls back through plan_json_hash → plan_hash → text comparison, so existing
in-flight PRs continue to work without requiring a re-plan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@toast-gear
Copy link
Copy Markdown
Contributor Author

This is entirely AI made FYI, I was just curious at how good it is at this sort of thing so decided to see if it could fix and issue I raised many many years ago when I was using this action

@toast-gear toast-gear marked this pull request as ready for review May 22, 2026 11:17
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.

Current comparison approach is non-deterministic resulting in false positives plan drifts

1 participant