Skip to content

feat(registry): implement automatic registry rollback on execution failure - #613

Open
HetCreep wants to merge 3 commits into
Raphire:masterfrom
HetCreep:feature/registry-rollback
Open

feat(registry): implement automatic registry rollback on execution failure#613
HetCreep wants to merge 3 commits into
Raphire:masterfrom
HetCreep:feature/registry-rollback

Conversation

@HetCreep

@HetCreep HetCreep commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Resolves #612

Changes

  • Intercepts script execution failures during parameter execution.
  • If a failure is caught, automatically restores the pre-execution registry state using the JSON backup file via Restore-RegistryBackupState.

Summary by CodeRabbit

  • Bug Fixes
    • Improved recovery when applying debloat changes fails by capturing the pre-execution registry backup path from New-RegistrySettingsBackup, wrapping the “execute actionable parameters” and “execute undo operations” loops in a try/catch, and triggering an automatic rollback via Restore-RegistryBackupState when parameter execution fails or when $script:RegistryImportFailures increases during either phase; rollback errors are logged and the original failure is re-thrown.

@Raphire

Raphire commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Heya,

I do like this idea, I'll take a look at this soon when I have time. Thanks!

@HetCreep
HetCreep force-pushed the feature/registry-rollback branch 2 times, most recently from 8b5df67 to 328a6e3 Compare June 15, 2026 07:25
@Raphire

Raphire commented Jun 19, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

ExecuteAllChanges in Scripts/Features/ExecuteChanges.ps1 now retains the filesystem path returned by New-RegistrySettingsBackup in a $backupFile variable. The feature execution and undo loops are wrapped in a try/catch that monitors $script:RegistryImportFailures for increases during execution; on failure, if $backupFile is set, the catch block calls Load-RegistryBackupFromFile and Restore-RegistryBackupState to restore the registry, then re-throws the error.

Changes

Automatic Registry Rollback on Execution Failure

Layer / File(s) Summary
Backup capture, execution monitoring, and rollback
Scripts/Features/ExecuteChanges.ps1
$backupFile is initialized to $null and assigned the return value of New-RegistrySettingsBackup instead of discarding it. The feature-execution and undo-operation loops are placed inside a try block that captures the initial $script:RegistryImportFailures counter and throws if it increases during any parameter execution or undo operation. A new catch block logs the failure, conditionally invokes Load-RegistryBackupFromFile and Restore-RegistryBackupState using the captured $backupFile, logs rollback success or failure, and re-throws the original error.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: implementing automatic registry rollback on execution failure, matching the PR's core objective.
Linked Issues check ✅ Passed The implementation fully addresses issue #612 by wrapping execution logic in try-catch, detecting failures, and performing automatic registry rollback using the backup file.
Out of Scope Changes check ✅ Passed All changes are focused on implementing automatic registry rollback functionality as specified in issue #612; no unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Scripts/Features/ExecuteChanges.ps1`:
- Around line 206-252: The ImportRegistryFile function call within the undo
operations section records failures in $script:RegistryImportFailures without
throwing an exception, which prevents the catch block from executing and rolling
back earlier registry writes. To fix this, either modify ImportRegistryFile to
throw an exception when it increments the failure counter, or add a check inside
the try block after the ImportRegistryFile call to throw an exception if
$script:RegistryImportFailures has been incremented. This ensures that registry
import failures trigger the catch block for proper rollback handling.
- Line 255: The Test-Path check for the backup file on line 255 needs to use the
-LiteralPath parameter instead of treating the path as a pattern. Change the
condition that checks "if ($backupFile -and (Test-Path $backupFile))" to use
"-LiteralPath $backupFile" in the Test-Path call, which will prevent wildcard
characters like brackets from being interpreted as patterns and ensure the
rollback attempt is not skipped for valid paths containing these characters.
This should match the approach already used in the Load-RegistryBackupFromFile
function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 45c65df7-0016-44ac-a8f1-82e3cb495fe3

📥 Commits

Reviewing files that changed from the base of the PR and between 2b97021 and 328a6e3.

📒 Files selected for processing (1)
  • Scripts/Features/ExecuteChanges.ps1

Comment thread Scripts/Features/ExecuteChanges.ps1 Outdated
Comment thread Scripts/Features/ExecuteChanges.ps1 Outdated
@HetCreep
HetCreep force-pushed the feature/registry-rollback branch 2 times, most recently from 71ef6c2 to d4a57cb Compare June 21, 2026 17:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
Scripts/Features/ExecuteChanges.ps1 (1)

287-290: 💤 Low value

Dead code: this warning block is now unreachable.

With the new failure detection at lines 242-244 and 266-268 throwing when $script:RegistryImportFailures increases, this block can never execute with a non-zero failure count:

  • If failures occur → exception thrown → function exits before reaching here
  • If no failures → $script:RegistryImportFailures remains 0 → condition is false

Consider removing this block since failure messaging is now handled by the exception path.

♻️ Suggested removal
     }
     throw
 }
-
-if ($script:RegistryImportFailures -gt 0) {
-    Write-Host ""
-    Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow
-}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Scripts/Features/ExecuteChanges.ps1` around lines 287 - 290, The warning
block that checks if $script:RegistryImportFailures is greater than 0 is now
unreachable dead code. With the new failure detection logic that throws
exceptions when $script:RegistryImportFailures increases (added in the preceding
validation blocks), the function will exit via exception before reaching this
warning block, so the condition can never be true. Remove the entire if block
(the condition check and the Write-Host statements within it) since failure
messaging is now handled by the exception throwing paths in the earlier
validation logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@Scripts/Features/ExecuteChanges.ps1`:
- Around line 287-290: The warning block that checks if
$script:RegistryImportFailures is greater than 0 is now unreachable dead code.
With the new failure detection logic that throws exceptions when
$script:RegistryImportFailures increases (added in the preceding validation
blocks), the function will exit via exception before reaching this warning
block, so the condition can never be true. Remove the entire if block (the
condition check and the Write-Host statements within it) since failure messaging
is now handled by the exception throwing paths in the earlier validation logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cf61878b-3d83-4033-aead-398f21c3b0be

📥 Commits

Reviewing files that changed from the base of the PR and between 71ef6c2 and d4a57cb.

📒 Files selected for processing (1)
  • Scripts/Features/ExecuteChanges.ps1

@HetCreep
HetCreep force-pushed the feature/registry-rollback branch 2 times, most recently from 8352468 to 7af95d8 Compare June 21, 2026 19:45
@HetCreep

Copy link
Copy Markdown
Contributor Author

Heads-up on the merge conflict here: #641 refactored ExecuteChanges.ps1 into InvokeChanges.ps1 and split the execute/undo loops out into Invoke-ApplyFeatures / Invoke-UndoFeatures (called from Invoke-AllChanges). This PR's rollback wraps those loops in a single try/catch inside the old monolithic function, so it needs re-applying onto the new phased structure rather than a mechanical rebase.

Since it's the registry-rollback safety path and I can't run-test the restore on my end, I'd rather not force-push an untested rewrite of it. Would you prefer I redo it against the new structure (you'd want to verify the rollback/restore path before merge), or would you rather fold it into your own refactor? Either works for me -- just didn't want to decide that for you.

@Raphire

Raphire commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Heads-up on the merge conflict here: #641 refactored ExecuteChanges.ps1 into InvokeChanges.ps1 and split the execute/undo loops out into Invoke-ApplyFeatures / Invoke-UndoFeatures (called from Invoke-AllChanges). This PR's rollback wraps those loops in a single try/catch inside the old monolithic function, so it needs re-applying onto the new phased structure rather than a mechanical rebase.

Since it's the registry-rollback safety path and I can't run-test the restore on my end, I'd rather not force-push an untested rewrite of it. Would you prefer I redo it against the new structure (you'd want to verify the rollback/restore path before merge), or would you rather fold it into your own refactor? Either works for me -- just didn't want to decide that for you.

Heya,

That's a fair assessment. If you'd be open to redoing it that would be great. I do apologize for the issues caused by the recent refactoring going on with the script, it was something that was sorely needed to keep things clear and well structured. As this feature is quite impactful I wasn't satisfied yet to merge it before fully testing.

@HetCreep HetCreep closed this Jun 26, 2026
@HetCreep
HetCreep force-pushed the feature/registry-rollback branch from 7af95d8 to 26b3133 Compare June 26, 2026 06:51
@HetCreep HetCreep reopened this Jun 26, 2026
@HetCreep

Copy link
Copy Markdown
Contributor Author

Redone against the new structure, as discussed. The rollback now lives in Invoke-AllChanges: it captures the path returned by New-RegistrySettingsBackup, wraps the apply (Invoke-ApplyFeatures) and undo (Invoke-UndoFeatures) phases in a try/catch, throws when RegistryImportFailures increases, and on failure restores the pre-execution state via Load-RegistryBackupFromFile + Restore-RegistryBackupState before rethrowing. WhatIf is unaffected -- no backup is created in WhatIf mode, so no rollback runs.

It's parse-clean, but I haven't run-tested the restore path on my end (no debloat run here), so please verify the rollback/restore behaviour before merge, as you mentioned. (Apologies for the brief close/reopen above -- a force-push hiccup on my side while resetting the branch onto the new structure.)

@STiFLeR7

Copy link
Copy Markdown
Contributor

Reviewed this against the registry restore/import code paths it touches (I was just working on #686 / #704 in the restore path, which is directly relevant here). Two grounded findings, one blocking-ish and one informational:

1. The failure-detection trigger has a silent blind spot for partial access-denial.

The rollback only fires when $script:RegistryImportFailures -gt $initialFailures (lines added around Invoke-ApplyFeatures/Invoke-UndoFeatures). That counter is only incremented in ImportRegistryFile's outer catch (Scripts/Features/ImportRegistryFile.ps1:109-113), which only fires if the import actually throws. But Invoke-RegistryOperationsFromRegFile (Scripts/Helpers/ApplyRegistryRegFile.ps1:196-228) only throws when all operations in a .reg file are access-denied ($accessDeniedCount -eq $totalOperations) — on a partial denial it just Write-Warnings and returns normally, no exception. So on a GPO-hardened/MDM-managed machine where some keys are writable and others aren't, some registry changes can genuinely fail to apply without $script:RegistryImportFailures ever incrementing — meaning this PR's whole point (auto-rollback on failure) silently won't trigger for that failure mode. Might be worth having Invoke-RegistryOperationsFromRegFile return (or the caller inspect) the access-denied count directly rather than relying only on the throw/no-throw signal.

2. The rollback mechanism itself reuses a currently-unsafe restore path.

Restore-RegistryBackupState -Backup $backupData (called in the new catch block) delegates to Restore-RegistryKeySnapshot, which deletes the live registry subtree before rewriting it, with no rollback of its own if the rewrite throws partway (see #686). I just opened #704, which closes the "corrupted/hand-edited backup" trigger for that bug (Data/Kind validation before any restore begins) — but the backup this PR generates is a fresh, in-memory snapshot of pre-execution state, not user-edited, so #704 is unlikely to matter for this call site specifically. The remaining trigger from #686 (a runtime SetValue failure mid-restore, e.g. permission drift between backup and rollback) is still a real, if narrow, risk for this PR's rollback path — worth being aware that a mid-rollback failure here has the same "already deleted, not yet rewritten" exposure as a mid-restore failure in the manual Restore Backup flow.

Neither of these is necessarily a reason to block — #613's overall direction (backup before apply, roll back on detected failure) is a real safety improvement over the status quo of no rollback at all — but I wanted to flag both gaps explicitly since they're easy to miss without having just traced through the restore path.

@HetCreep
HetCreep force-pushed the feature/registry-rollback branch from 8e56d0d to b3cc180 Compare July 16, 2026 05:06
@HetCreep
HetCreep force-pushed the feature/registry-rollback branch from b3cc180 to 0a934cf Compare July 25, 2026 16:27
@HetCreep

Copy link
Copy Markdown
Contributor Author

Rebased onto current master: re-applied the rollback onto the renamed Invoke-Changes.ps1 (post #708), switched the restore call to the renamed Import-RegistryBackup, and preserved the #700 cancellation check between the apply and undo phases (a cancel returns without triggering rollback, since cancellation is not a failure). Parse-clean; I could not run the new Pester suite locally (in-box Pester 3.4, the suite needs v5 and I avoid installing tools on this machine) -- the tests.yml workflow should exercise it on this PR.

On STiFLeR7's two findings above -- both are accurate, and worth stating how they relate to this PR's scope: (1) the partial-access-denial blind spot lives in Invoke-RegistryOperationsFromRegFile's throw-only-on-total-denial contract, so the right fix is surfacing the denied count from that helper (which would then flow through this PR's failure detection for free) rather than widening this PR's trigger heuristically; (2) the rollback path inheriting #686's delete-before-rewrite exposure is real -- with #704 merged, the corrupted-backup trigger is now validated out, and since the backup here is freshly tool-generated in the same run, the residual risk is limited to a runtime SetValue failure mid-rollback (narrow, but real until #686's case 2 gets a mitigation design). Both feel like follow-ups at the restore-layer level rather than blockers for adding rollback-on-failure at all, but that's your call.

@Raphire

Raphire commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Thanks @STiFLeR7 and @HetCreep! I think the second issue should be fixed now as of #710, the script should now handle those restores much more conservatively and with some extra validation, it no longer deletes keys that could potentially not get restored if something fails along the way.

I will fully review this when I have more time this week.

@Raphire

Raphire commented Aug 3, 2026

Copy link
Copy Markdown
Owner

This generally looks fine from a technical perspective, although I am worried a rollback to the backup might fail for the same reason the original apply action failed. Looking at it from that perspective this might not really add much.

I also don't think we should automatically rollback any changes without prompting the user, to let them choose.

What are your thoughts on that?

@HetCreep

HetCreep commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Heya,

Both points land -- pivoted this to match.

On the rollback-reliability point: confirmed by reading through the restore path (Restore-RegistryKeySnapshotAtPath in Restore-RegistryApplyState.ps1, post #710) -- for whichever key failed to apply, the revert would call the identical SetValue/CreateSubKey against the identical ACL, so it hits the identical wall. An unattended rollback isn't a reliable safety net for the failure mode it exists to catch.

(Aside, not blocking this PR: while tracing that path I noticed Restore-RegistryBackupState's restore loop has no per-key isolation -- one exception on any root snapshot aborts the remaining ones too, so a manual Restore Backup that hits a bad key partway through would silently stop reverting the rest. Separate from this PR since it's the existing GUI restore path, happy to open an issue for it if useful.)

On the "don't auto-revert without asking" point: agreed, and it also sidesteps the reliability problem above entirely -- no rollback is attempted, so there's nothing to fail. On a registry import failure the script now reports the backup file location and points to the existing Restore Backup option instead of touching the registry again on its own.

Pushed (17abdf5). Updated the two rollback specs to match -- parse-clean, but same as last round I couldn't run the v5 Pester suite locally; tests.yml should cover it.

Reimplemented against the refactored InvokeChanges structure (Raphire#641): execute/undo loops are now Invoke-ApplyFeatures / Invoke-UndoFeatures called from Invoke-AllChanges. Capture the backup path from New-RegistrySettingsBackup, wrap the apply and undo phases in a try/catch, throw when RegistryImportFailures increases, and on failure restore the pre-execution state via Load-RegistryBackupFromFile + Restore-RegistryBackupState before rethrowing. WhatIf unaffected (no backup created, so no rollback). Implements Raphire#612
The 'reports registry import failures after all requested work completes' spec pinned the pre-rollback behavior (failures let the run complete and print a summary). With automatic rollback, a phase whose imports fail now throws, rolls back to the pre-execution backup, and rethrows -- so that spec is replaced with three covering the new contract: the throw on apply-phase failure (undo phase not entered), the rollback path (Import-RegistryBackup + Restore-RegistryBackupState invoked with the captured backup file), and the no-backup case (SkipRegistryBackup set -> no rollback attempted).
An unattended rollback can hit the same access-denied failure that broke
the apply in the first place, so it isn't a reliable safety net for the
failure mode it exists to handle. Reverting registry state without
asking is also not a call the script should make on the user's behalf.
On failure, point to the pre-execution backup and the existing Restore
Backup option instead of attempting the revert automatically.
@HetCreep
HetCreep force-pushed the feature/registry-rollback branch from 17abdf5 to c96bd8e Compare August 9, 2026 06: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.

Enhancement: Implement automatic registry rollback on execution failure

3 participants