added restore point bypass for CI/CD purposes

This commit is contained in:
2026-05-27 22:40:28 -05:00
parent 14e61733e9
commit 1392a503f1
8 changed files with 93 additions and 20 deletions
+1 -1
View File
@@ -18,4 +18,4 @@ jobs:
New-Item -ItemType Directory -Force dist | Out-Null
go build -o dist/bootstrap_windows_env.exe ./cmd/bootstrap_windows_env
- name: Run
run: ./dist/bootstrap_windows_env.exe --yes --headless --no-wsl
run: ./dist/bootstrap_windows_env.exe --yes --headless --no-wsl --no-restore-points
+3
View File
@@ -30,8 +30,11 @@ Tagged GitHub releases build and upload a precompiled `bootstrap_windows_env.exe
.\bootstrap_windows_env.exe --only wsl
.\bootstrap_windows_env.exe --no-ai
.\bootstrap_windows_env.exe --no-wsl
.\bootstrap_windows_env.exe --no-restore-points
```
`--no-restore-points` skips the two System Restore checkpoints in the `os` phase. Use this on Windows Server SKUs (including GitHub Actions `windows-latest` runners), where `Checkpoint-Computer` is not supported.
## Phases
`os` runs before any application installation. It invokes Windows Update/Microsoft Update, stops for reboot when required, creates a restore point, runs Chris Titus Tech WinUtil with the managed sane-default config, enables Developer Mode and Windows sudo, applies power/update/UI/store/taskbar defaults, then creates a second restore point before package installation.
+8 -3
View File
@@ -106,8 +106,13 @@ func PrintPlan(out io.Writer, plan Plan, opts Options) {
}
if phase == PhaseOS {
fmt.Fprintln(out, " os: install Windows/Microsoft updates and stop before packages if reboot is required")
fmt.Fprintln(out, " os: create restore point, run WinUtil config, apply developer/power/update/UI/store/taskbar defaults")
fmt.Fprintln(out, " os: create second restore point before host package installation")
if opts.NoRestorePoints {
fmt.Fprintln(out, " os: skip System Restore checkpoints (--no-restore-points)")
fmt.Fprintln(out, " os: run WinUtil config, apply developer/power/update/UI/store/taskbar defaults")
} else {
fmt.Fprintln(out, " os: create restore point, run WinUtil config, apply developer/power/update/UI/store/taskbar defaults")
fmt.Fprintln(out, " os: create second restore point before host package installation")
}
}
if phase == PhaseConfig {
fmt.Fprintln(out, " config: deploy managed WezTerm, PowerShell, oh-my-posh, and Neovim config")
@@ -189,7 +194,7 @@ func (b *Bootstrapper) Run(ctx context.Context, opts Options) (runErr error) {
func (b *Bootstrapper) runOSPhase(ctx context.Context, report *RunReport) OSPhaseResult {
report.Event("phase os started")
result := RunOSPhase(ctx, b.Runner)
result := RunOSPhase(ctx, b.Runner, report.Options.NoRestorePoints)
for _, notice := range result.Notices {
b.recordNotice(report, notice)
}
+2
View File
@@ -26,6 +26,7 @@ type Options struct {
Yes bool
DryRun bool
Headless bool
NoRestorePoints bool
LinuxReleaseRepo string
}
@@ -40,6 +41,7 @@ func ParseOptions(args []string, stderr io.Writer) (Options, error) {
fs.BoolVar(&opts.Yes, "yes", false, "execute the displayed plan without confirmation")
fs.BoolVar(&opts.DryRun, "dry-run", false, "detect state and display work without making changes")
fs.BoolVar(&opts.Headless, "headless", false, "disable interactive login offers and require --yes to execute")
fs.BoolVar(&opts.NoRestorePoints, "no-restore-points", false, "skip System Restore checkpoints (required on Windows Server/CI where Checkpoint-Computer is unsupported)")
fs.StringVar(&opts.LinuxReleaseRepo, "linux-release-repo", "JMR-dev/bootstrap_dev_env", "GitHub repository containing Linux bootstrap releases")
if err := fs.Parse(args); err != nil {
return Options{}, err
+17
View File
@@ -61,3 +61,20 @@ func TestParseOptionsAcceptsHeadlessWithDryRunAndCustomRepo(t *testing.T) {
t.Fatalf("opts = %#v", opts)
}
}
func TestParseOptionsAcceptsNoRestorePoints(t *testing.T) {
opts, err := ParseOptions([]string{"--yes", "--no-restore-points"}, &bytes.Buffer{})
if err != nil {
t.Fatal(err)
}
if !opts.NoRestorePoints {
t.Fatalf("expected NoRestorePoints to be set: %#v", opts)
}
defaults, err := ParseOptions(nil, &bytes.Buffer{})
if err != nil {
t.Fatal(err)
}
if defaults.NoRestorePoints {
t.Fatal("NoRestorePoints should default to false")
}
}
+19 -8
View File
@@ -12,7 +12,7 @@ type OSPhaseResult struct {
Stop bool
}
func RunOSPhase(ctx context.Context, runner Runner) OSPhaseResult {
func RunOSPhase(ctx context.Context, runner Runner, noRestorePoints bool) OSPhaseResult {
var result OSPhaseResult
if issue := runOSCommand(ctx, runner, "install Windows updates", WindowsUpdateCommand()); issue.Err != nil {
result.Issues = append(result.Issues, issue)
@@ -25,15 +25,19 @@ func RunOSPhase(ctx context.Context, runner Runner) OSPhaseResult {
result.Stop = true
return result
}
if issue := runOSCommand(ctx, runner, "create restore point before OS configuration", RestorePointCommand("bootstrap_windows_env: before OS configuration")); issue.Err != nil {
result.Issues = append(result.Issues, issue)
result.Stop = true
return result
if noRestorePoints {
result.Notices = append(result.Notices, "Skipped System Restore checkpoints (--no-restore-points). Checkpoint-Computer is unsupported on Windows Server SKUs.")
} else {
if issue := runOSCommand(ctx, runner, "create restore point before OS configuration", RestorePointCommand("bootstrap_windows_env: before OS configuration")); issue.Err != nil {
result.Issues = append(result.Issues, issue)
result.Stop = true
return result
}
}
if issue := runOSCommand(ctx, runner, "run Chris Titus Tech WinUtil", WinUtilCommand()); issue.Err != nil {
result.Issues = append(result.Issues, issue)
}
for _, step := range []struct {
steps := []struct {
name string
cmd CommandSpec
fatal bool
@@ -45,8 +49,15 @@ func RunOSPhase(ctx context.Context, runner Runner) OSPhaseResult {
{"set Vivaldi default browser associations", VivaldiDefaultBrowserCommand(), false},
{"update Microsoft Store apps", StoreUpdateCommand(), false},
{"apply taskbar pins", TaskbarCommand(), false},
{"create restore point before package installation", RestorePointCommand("bootstrap_windows_env: before package installation"), true},
} {
}
if !noRestorePoints {
steps = append(steps, struct {
name string
cmd CommandSpec
fatal bool
}{"create restore point before package installation", RestorePointCommand("bootstrap_windows_env: before package installation"), true})
}
for _, step := range steps {
if issue := runOSCommand(ctx, runner, step.name, step.cmd); issue.Err != nil {
result.Issues = append(result.Issues, issue)
if step.fatal {
+41 -7
View File
@@ -51,7 +51,7 @@ func TestRunOSPhaseStopsBeforeRestorePointWhenRebootIsRequired(t *testing.T) {
commandKey(update.Name, update.Args...): {},
commandKey(reboot.Name, reboot.Args...): {Stdout: "true\n"},
}, defaultErr: true}
result := RunOSPhase(context.Background(), runner)
result := RunOSPhase(context.Background(), runner, false)
if !result.Stop {
t.Fatal("expected OS phase to stop for pending reboot")
}
@@ -65,7 +65,7 @@ func TestRunOSPhaseStopsBeforeRestorePointWhenRebootIsRequired(t *testing.T) {
func TestRunOSPhaseRunsSecondRestorePointAfterConfiguration(t *testing.T) {
runner := &fakeRunner{}
result := RunOSPhase(context.Background(), runner)
result := RunOSPhase(context.Background(), runner, false)
if result.Stop {
t.Fatalf("OS phase unexpectedly stopped: %#v", result)
}
@@ -84,7 +84,7 @@ func TestRunOSPhaseContinuesAfterNonRestoreFailure(t *testing.T) {
runner := &fakeRunner{responses: map[string]CommandResult{
commandKey(update.Name, update.Args...): {Err: context.Canceled, Stderr: "update failed"},
}}
result := RunOSPhase(context.Background(), runner)
result := RunOSPhase(context.Background(), runner, false)
if result.Stop {
t.Fatalf("non-restore failure should be reported but should not stop the OS phase: %#v", result)
}
@@ -102,7 +102,7 @@ func TestRunOSPhaseStopsWhenSecondRestorePointFails(t *testing.T) {
runner := &fakeRunner{responses: map[string]CommandResult{
commandKey(second.Name, second.Args...): {Err: context.Canceled, Stderr: "restore failed"},
}}
result := RunOSPhase(context.Background(), runner)
result := RunOSPhase(context.Background(), runner, false)
if !result.Stop {
t.Fatal("expected OS phase to stop when final restore point fails")
}
@@ -117,7 +117,7 @@ func TestRunOSPhaseStopsWhenFirstRestorePointFails(t *testing.T) {
runner := &fakeRunner{responses: map[string]CommandResult{
commandKey(first.Name, first.Args...): {Err: errors.New("restore failed"), Stderr: "blocked"},
}}
result := RunOSPhase(context.Background(), runner)
result := RunOSPhase(context.Background(), runner, false)
if !result.Stop {
t.Fatal("expected first restore point failure to stop OS phase")
}
@@ -135,7 +135,7 @@ func TestRunOSPhaseRecordsWinUtilFailureAndContinuesToFinalRestorePoint(t *testi
runner := &fakeRunner{responses: map[string]CommandResult{
commandKey(winutil.Name, winutil.Args...): {Err: errors.New("winutil failed"), Stderr: "script failed"},
}}
result := RunOSPhase(context.Background(), runner)
result := RunOSPhase(context.Background(), runner, false)
if result.Stop {
t.Fatalf("WinUtil failure should not stop OS phase: %#v", result)
}
@@ -160,7 +160,7 @@ func TestRunOSPhaseRecordsRebootProbeFailureAndContinues(t *testing.T) {
runner := &fakeRunner{responses: map[string]CommandResult{
commandKey(reboot.Name, reboot.Args...): {Err: errors.New("probe failed"), Stderr: "bad registry"},
}}
result := RunOSPhase(context.Background(), runner)
result := RunOSPhase(context.Background(), runner, false)
if result.Stop {
t.Fatalf("reboot probe failure should be recorded but should not stop OS phase: %#v", result)
}
@@ -172,6 +172,40 @@ func TestRunOSPhaseRecordsRebootProbeFailureAndContinues(t *testing.T) {
}
}
func TestRunOSPhaseSkipsRestorePointsWhenDisabled(t *testing.T) {
runner := &fakeRunner{}
result := RunOSPhase(context.Background(), runner, true)
if result.Stop {
t.Fatalf("OS phase unexpectedly stopped: %#v", result)
}
if err := result.Issues.Err(); err != nil {
t.Fatal(err)
}
first := RestorePointCommand("bootstrap_windows_env: before OS configuration")
second := RestorePointCommand("bootstrap_windows_env: before package installation")
if runner.called(first.Name, first.Args...) || runner.called(second.Name, second.Args...) {
t.Fatal("restore point commands should not run when --no-restore-points is set")
}
winutil := WinUtilCommand()
if !runner.called(winutil.Name, winutil.Args...) {
t.Fatal("expected WinUtil to still run when restore points are skipped")
}
power := PowerPolicyCommand()
if !runner.called(power.Name, power.Args...) {
t.Fatal("expected later configuration steps to still run when restore points are skipped")
}
var found bool
for _, n := range result.Notices {
if strings.Contains(n, "Skipped System Restore checkpoints") {
found = true
break
}
}
if !found {
t.Fatalf("expected skip notice in result.Notices, got %#v", result.Notices)
}
}
func TestRestorePointCommandDisablesFrequencyThrottle(t *testing.T) {
cmd := RestorePointCommand("bootstrap_windows_env: test")
joined := strings.Join(cmd.Args, " ")
+2 -1
View File
@@ -66,13 +66,14 @@ func WriteRunLog(paths UserPaths, report *RunReport) (string, error) {
fmt.Fprintf(&b, "duration: %s\n", report.Finished.Sub(report.Started).Round(time.Second))
}
fmt.Fprintf(&b, "phases: %s\n", phasesForLog(report.Phases))
fmt.Fprintf(&b, "options: only=%s no-ai=%t no-wsl=%t yes=%t dry-run=%t headless=%t linux-release-repo=%s\n",
fmt.Fprintf(&b, "options: only=%s no-ai=%t no-wsl=%t yes=%t dry-run=%t headless=%t no-restore-points=%t linux-release-repo=%s\n",
report.Options.Only,
report.Options.NoAI,
report.Options.NoWSL,
report.Options.Yes,
report.Options.DryRun,
report.Options.Headless,
report.Options.NoRestorePoints,
report.Options.LinuxReleaseRepo,
)
if report.FinalError != "" {