runPatchWindows handles patching on Windows hosts. For patch_all: installs all approved WUA updates (by GUID from server) + upgrades all WinGet apps. For patch_package: routes by package name - "KB..." prefix -> WUA, otherwise -> WinGet upgrade.
(ctx context.Context, httpClient *client.Client, patchRunID, patchType string, packageNames []string, dryRun bool)
| 2502 | // For patch_all: installs all approved WUA updates (by GUID from server) + upgrades all WinGet apps. |
| 2503 | // For patch_package: routes by package name - "KB..." prefix -> WUA, otherwise -> WinGet upgrade. |
| 2504 | func runPatchWindows(ctx context.Context, httpClient *client.Client, patchRunID, patchType string, packageNames []string, dryRun bool) error { |
| 2505 | patcher := packages.NewWindowsPatcher() |
| 2506 | var fullOutput strings.Builder |
| 2507 | |
| 2508 | if err := httpClient.SendPatchOutput(ctx, patchRunID, "started", "", ""); err != nil { |
| 2509 | logger.WithError(err).Warn("Failed to send patch started to server") |
| 2510 | } |
| 2511 | |
| 2512 | if patchType == "patch_all" { |
| 2513 | // Step 1: WUA - install approved OS/KB updates |
| 2514 | guids, err := httpClient.GetApprovedWindowsUpdateGUIDs(ctx) |
| 2515 | if err != nil { |
| 2516 | logger.WithError(err).Warn("Could not fetch approved Windows Update GUIDs; skipping WUA step") |
| 2517 | } |
| 2518 | if len(guids) > 0 { |
| 2519 | fmt.Fprintf(&fullOutput, "[Windows Update] Installing %d approved update(s)...\n", len(guids)) |
| 2520 | _ = httpClient.SendPatchOutput(ctx, patchRunID, "progress", fullOutput.String(), "") |
| 2521 | for _, guid := range guids { |
| 2522 | out, err := patcher.InstallWindowsUpdate(ctx, guid) |
| 2523 | fmt.Fprintf(&fullOutput, " [%s] %s\n", guid, out) |
| 2524 | success := err == nil && !packages.IsSuperseded(out) |
| 2525 | result := client.WindowsUpdateResult{GUID: guid, Success: success} |
| 2526 | if err != nil { |
| 2527 | result.Error = err.Error() |
| 2528 | } |
| 2529 | _ = httpClient.SendWindowsUpdateResult(ctx, patchRunID, result) |
| 2530 | _ = httpClient.SendPatchOutput(ctx, patchRunID, "progress", fullOutput.String(), "") |
| 2531 | } |
| 2532 | } |
| 2533 | |
| 2534 | // Step 2: WinGet - upgrade all applications |
| 2535 | fullOutput.WriteString("\n[WinGet] Upgrading applications...\n") |
| 2536 | _ = httpClient.SendPatchOutput(ctx, patchRunID, "progress", fullOutput.String(), "") |
| 2537 | wingetOut, wingetErr := patcher.WinGetUpgradeAll(ctx, dryRun) |
| 2538 | fullOutput.WriteString(wingetOut) |
| 2539 | fullOutput.WriteString("\n") |
| 2540 | if wingetErr != nil { |
| 2541 | logger.WithError(wingetErr).Warn("winget upgrade --all had errors (non-fatal)") |
| 2542 | } |
| 2543 | |
| 2544 | // Step 3: report reboot status |
| 2545 | needsReboot := packages.RebootRequired() |
| 2546 | _ = httpClient.SendWindowsRebootStatus(ctx, patchRunID, needsReboot) |
| 2547 | if needsReboot { |
| 2548 | fullOutput.WriteString("\n[Reboot Required] A system restart is needed to complete the update installation.\n") |
| 2549 | } |
| 2550 | } else { |
| 2551 | // patch_package: each name is either a KB/GUID (WUA) or a WinGet package ID |
| 2552 | if len(packageNames) == 0 { |
| 2553 | _ = httpClient.SendPatchOutput(ctx, patchRunID, "failed", "", "package_names required for patch_package") |
| 2554 | return fmt.Errorf("package_names required for patch_package") |
| 2555 | } |
| 2556 | for _, name := range packageNames { |
| 2557 | name = strings.TrimSpace(name) |
| 2558 | if name == "" { |
| 2559 | continue |
| 2560 | } |
| 2561 | // Treat as WUA GUID if it looks like a UUID (36 chars with dashes), or KB prefix |
no test coverage detected