Skip to content

feat(secrets): add secrets diff to compare environments/paths - #333

Open
wankhede04 wants to merge 2 commits into
Infisical:mainfrom
wankhede04:feat/secrets-diff
Open

feat(secrets): add secrets diff to compare environments/paths#333
wankhede04 wants to merge 2 commits into
Infisical:mainfrom
wankhede04:feat/secrets-diff

Conversation

@wankhede04

Copy link
Copy Markdown

Type of change

  • New feature

Summary

Closes #330.

There's currently no built-in way to compare secrets between two environments (or two paths) before promoting a change (e.g. staging → prod). Users end up hand-rolling this with two infisical export calls plus diff/yq — error-prone and doesn't mask sensitive values.

Changes

Adds infisical secrets diff:

infisical secrets diff --env=staging --env2=production [--path=/] [--path2=/] [--projectId=...] [--show-values] [--output json|yaml|dotenv]
  • Fetches secrets for both sides via the existing util.GetAllEnvironmentVariables path (same one run/export already use) — no new API client code.
  • Prints a table of keys that are added, removed, or changed between the two sides; keys with identical values on both sides are omitted.
  • Values are masked by default (consistent with the recent masking behavior on secrets set); --show-values reveals them.
  • Supports --output json|yaml|dotenv for scripting, matching the pattern used by other secrets subcommands.
  • --path2 defaults to --path if omitted, so a same-path, cross-environment diff is the common one-flag case.

Tests run

$ go build -o infisical .
$ go test -vet=off ./packages/cmd/...
ok  	github.com/Infisical/infisical-merge/packages/cmd	1.0s

Added packages/cmd/secrets_diff_test.go covering:

  • Added/removed/changed/unchanged key detection (computeSecretDiff)
  • Deterministic, sorted diff ordering
  • Value masking behavior for each diff status

Manually verified infisical secrets diff --help output and flag wiring.

Note: go vet ./packages/cmd/... fails on pre-existing, unrelated issues in run.go (non-constant zerolog format strings) that exist on main independent of this change; ran tests with -vet=off to isolate this PR's changes.

I confirm I have read the Contributing Guide and Code of Conduct.

Adds `infisical secrets diff --env=<a> --env2=<b>` to compare secrets
between two environments (and optionally two paths via --path/--path2).
Prints a table of added/removed/changed keys; values are masked by
default and revealed with --show-values. Supports --output json/yaml/
dotenv for scripting.

This reuses the existing GetAllEnvironmentVariables fetch path (same
one used by `run`/`export`) and the GenericTable rendering helper, so
no new API surface or storage is introduced.

Fixes Infisical#330
@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces infisical secrets diff, a new subcommand that compares secrets between two environments (or two paths) and reports added, removed, and changed keys — masking values by default. The core diff logic (computeSecretDiff) and JSON/YAML/dotenv output path are correct.

  • The emptyWhenStatus arguments passed to maskDiffValue for the left and right columns are swapped: for "removed" and "added" entries, the column that actually holds a value shows blank while the column that has no value also shows blank, making masked diffs of those two statuses look identical to keys with empty values.
  • The env2 == env1 guard executes before path2 is resolved, so a valid same-environment, different-path comparison is permanently rejected even when --path and --path2 differ.

Confidence Score: 3/5

The new secrets diff command is not safe to merge in its current state — masked output for "added" and "removed" keys is visually broken, and an overly strict guard prevents a valid comparison mode.

The masking display is wrong for two of the three diff statuses: when --show-values is not set, removed and added keys both show empty cells on both sides of the table rather than showing a masked placeholder on the side where the value exists. A user running the command without --show-values will see no masking signal at all for those rows, undermining the feature's primary safety guarantee. A second issue — the same-env guard firing before path resolution — blocks a valid use case permanently.

packages/cmd/secrets_diff.go — the masking call-site (lines 227-230) and the same-env guard (line 70) both need correction

Important Files Changed

Filename Overview
packages/cmd/secrets_diff.go New secrets diff subcommand — two bugs found: masking arguments swapped for "added"/"removed" entries (both sides show empty), and the same-env guard fires before path2 is resolved (blocking valid same-env, different-path diffs)
packages/cmd/secrets_diff_test.go Unit tests cover diff computation and sorting correctly; the masking test (TestMaskDiffValue_LeavesEmptyForMissingSide) validates the isolated helper in the direction that matches the buggy call-site, inadvertently confirming the wrong behavior rather than catching it

Reviews (1): Last reviewed commit: "feat(secrets): add secrets diff to compa..." | Re-trigger Greptile

Comment on lines +227 to +230
if !showValues {
leftValue = maskDiffValue(entry.Status, secretDiffStatusRemoved, leftValue)
rightValue = maskDiffValue(entry.Status, secretDiffStatusAdded, rightValue)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The emptyWhenStatus arguments are swapped between the left and right column calls, causing "removed" and "added" entries to show blank cells on the wrong side when masking is active.

secretDiffStatusRemoved means the key exists only in env1 (left) and is absent in env2 (right). So the left value should be masked ("******") and the right should be empty — but the current call passes secretDiffStatusRemoved as emptyWhenStatus to the left column, which makes it return "" for removed entries. The same inversion occurs for "added" keys in the right column.

Concrete failure: with --show-values omitted, a removed key displays two blank cells (left="" right=""), making it indistinguishable from a key with empty values on both sides. Added keys suffer the symmetric problem.

Suggested change
if !showValues {
leftValue = maskDiffValue(entry.Status, secretDiffStatusRemoved, leftValue)
rightValue = maskDiffValue(entry.Status, secretDiffStatusAdded, rightValue)
}
if !showValues {
leftValue = maskDiffValue(entry.Status, secretDiffStatusAdded, leftValue)
rightValue = maskDiffValue(entry.Status, secretDiffStatusRemoved, rightValue)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 254559f — swapped emptyWhenStatus so removed keys mask the left column and added keys mask the right. Added maskDiffColumns + unit tests covering all three statuses.

Comment thread packages/cmd/secrets_diff.go Outdated
Comment on lines +70 to +72
if env2 == env1 {
util.PrintErrorMessageAndExit("--env and --env2 must be different environments")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The env2 == env1 guard fires before path2 is resolved, so a legitimate same-environment, different-path comparison (e.g., --env=staging --env2=staging --path=/app --path2=/db) is permanently blocked. The guard should also consider whether the paths differ before rejecting the request. Because path2 is declared after this point, the check must be moved to after path resolution (lines 83–85) to use path2 correctly.

Suggested change
if env2 == env1 {
util.PrintErrorMessageAndExit("--env and --env2 must be different environments")
}
if env2 == env1 && path2 == path1 {
util.PrintErrorMessageAndExit("--env and --env2 must be different environments, or --path and --path2 must be different paths")
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 254559f — path2 is resolved first, then we only reject when both env and path are identical. Same-env / different-path comparisons (e.g. --env=staging --env2=staging --path=/app --path2=/db) are now allowed.

Greptile P1s on Infisical#333:
- Swap emptyWhenStatus args so removed keys mask the left column and
  added keys mask the right column (instead of blanking both sides).
- Allow same-environment diffs when --path and --path2 differ; only
  reject when both env and path are identical.
@wankhede04

Copy link
Copy Markdown
Author

Addressed both Greptile P1s in 254559f:

  1. Masking args — removed keys now show ****** on the left / empty on the right; added keys show the reverse.
  2. Same-env guard — same environment is allowed when --path / --path2 differ; only identical env+path is rejected.

Unit tests cover the masking call-site and the validation cases. Ready for another look when convenient.

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.

Add infisical secrets diff to compare secrets across environments/paths

1 participant