added restore point bypass for CI/CD purposes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, " ")
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
Reference in New Issue
Block a user