Skip to content

fix(rulesets): resolve {{EXTERNALLY_DEFINED}} before diffing (incorporate PR #1023) - #1048

Merged
decyjphr merged 1 commit into
yadhav/fix-recent-issuesfrom
decyjphr-incorporate-pr-1023-rulesets-externally
Aug 9, 2026
Merged

fix(rulesets): resolve {{EXTERNALLY_DEFINED}} before diffing (incorporate PR #1023)#1048
decyjphr merged 1 commit into
yadhav/fix-recent-issuesfrom
decyjphr-incorporate-pr-1023-rulesets-externally

Conversation

@decyjphr

@decyjphr decyjphr commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Why

Rulesets that use the {{EXTERNALLY_DEFINED}} placeholder (e.g. in required_status_checks) were falsely reported as changed on every run. This branch already resolved the placeholder inside update()/add(), but only on the apply (non-nop) path, so the comparison still saw the literal placeholder string. The result: dry runs reported a redundant "Update Ruleset" and apply mode issued a redundant PUT even when nothing had actually changed (fixes the behavior in #1022).

This incorporates the fix from upstream PR #1023 into the comprehensive yadhav/fix-recent-issues line, where it was not yet handled.

Approach

Diffable.sync() now exposes an optional resolveOverrides(existingRecords, filteredEntries) hook, called after find() and before the comparison. Plugins can opt in to resolve config placeholders against the live records so placeholders never register as differences.

The rulesets plugin implements the hook: each config entry is matched to its live record (via the existing comparator) and passed through Overrides.removeOverrides, so {{EXTERNALLY_DEFINED}} resolves to the live values before changed() ever runs. This mirrors how branches.js already resolves overrides inside its compareDeep call. The hook is opt-in per plugin, so no other diffable plugin changes behavior.

Notes for reviewers

  • Intentional behavior change: a ruleset whose only difference from GitHub is the placeholder no longer plans an update in dry runs and no longer issues a PUT in apply mode. Rulesets with real differences behave exactly as before, since resolution happens against the same live record update() would have used.
  • Entries are cloned with structuredClone before resolution because removeOverrides mutates its input, so the loaded config is never modified.
  • This branch's overrides object uses action: 'delete' for required_status_checks, which short-circuits before the type check, so the array vs dict type difference from PR fix(rulesets): resolve {{EXTERNALLY_DEFINED}} before diffing so unchanged rulesets don't plan updates #1023 is a no-op here and was left as-is.

Testing

  • Unit tests: 340/340 passing. Updated the two affected {{EXTERNALLY_DEFINED}} tests to the new behavior and added nop-mode coverage (placeholder-only match plans no update; a genuine diff still plans an update).
  • Smoke test: ran the full end-to-end suite against a live org. Every ruleset apply phase passed (repo/suborg ruleset creation, rulesets additive/disable_plugins, name/slug resolution). The only failures were pre-existing environmental issues unrelated to this change (drift-injection steps needing broader gh CLI scopes, and the enterprise app_installations phase).

…rate PR #1023)

Diffable.sync() now calls an optional resolveOverrides(existingRecords,
filteredEntries) hook after find() and before the comparison. The rulesets
plugin implements it: each config entry is matched to its live record (via
comparator) and passed through Overrides.removeOverrides, so
{{EXTERNALLY_DEFINED}} placeholders resolve to the live values before
changed() ever sees them. This mirrors how branches.js resolves overrides
inside compareDeep.

Behavior change (intentional, fixes #1022): a ruleset whose only difference
from GitHub is the placeholder no longer reports "Update Ruleset" in dry runs
and no longer issues a redundant PUT in apply mode. Rulesets with real
differences behave exactly as before. Entries are cloned (structuredClone)
before resolution since removeOverrides mutates its input.

Tests updated to the new behavior and nop-mode coverage added.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

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.

Pull request overview

This PR fixes false-positive ruleset diffs when config uses the {{EXTERNALLY_DEFINED}} placeholder (notably in required_status_checks) by resolving overrides against live GitHub rulesets before diffing, preventing redundant “Update Ruleset” plans and unnecessary PUTs.

Changes:

  • Add an opt-in resolveOverrides(existingRecords, filteredEntries) hook to Diffable.sync() that runs after find() and before comparison.
  • Implement resolveOverrides in the rulesets plugin to replace {{EXTERNALLY_DEFINED}} with live values prior to changed()/diffing.
  • Update and extend unit tests to cover the new “placeholder-only == no change” behavior, including nop-mode planning.
Show a summary per file
File Description
lib/plugins/diffable.js Adds an opt-in pre-diff hook to let plugins resolve placeholders against live records.
lib/plugins/rulesets.js Implements the hook for rulesets by resolving {{EXTERNALLY_DEFINED}} via Overrides.removeOverrides before diffing.
test/unit/lib/plugins/rulesets.test.js Updates expectations and adds nop-mode coverage for placeholder-only vs genuine diffs.

Review details

Tip

Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread lib/plugins/diffable.js
@decyjphr
decyjphr merged commit 56ac73b into yadhav/fix-recent-issues Aug 9, 2026
3 checks passed
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.

2 participants