Skip to content

Repository files navigation

WinDeploy — Unattended Windows Post-Install Automation

Repo: https://github.com/karolperkowski/win_dell

A fully unattended Windows 10/11 post-install automation framework for Dell hardware. One command triggers it; everything after — Windows Update, system tweaks, app installs, Tailscale setup, and cleanup — runs automatically across reboots until complete. A live WPF monitor window shows progress throughout.


Install

Open PowerShell as Administrator on a fresh Windows install:

irm "https://raw.githubusercontent.com/karolperkowski/win_dell/main/install.ps1" | iex

Re-running on an existing install updates scripts and preserves all state and logs. The monitor window appears immediately after running and stays open for the duration of the deployment.

Uninstall

irm "https://raw.githubusercontent.com/karolperkowski/win_dell/main/uninstall.ps1" | iex

Force-kills all running tasks and processes, removes scheduled tasks, disables auto-logon, removes the repo and state file.

Flag Effect
-KeepLogs Preserve C:\ProgramData\WinDeploy\Logs\
-KeepState Preserve state.json (resume from current stage)
-Silent Skip confirmation prompt

Reinstall

irm "https://raw.githubusercontent.com/karolperkowski/win_dell/main/reinstall.ps1" | iex

Fully unattended: runs uninstall (silent, no prompts), then fresh install. One command, no interaction needed.


What happens after install

irm ... | iex
  └─ install.ps1       downloads repo ZIP, verifies SHA-256, prompts for
       │               auto-logon account + password (WPF), calls bootstrap
       └─ bootstrap.ps1
            ├─ copies repo to C:\ProgramData\WinDeploy\repo\
            ├─ writes state.json
            ├─ configures auto-logon (operator-picked account + password,
            │   or legacy 'Administrator' / blank fallback)
            ├─ registers all scheduled tasks
            ├─ launches Monitor.ps1 ──→ WPF window visible immediately
            └─ triggers WinDeploy-Resume task ──→ Orchestrator runs as SYSTEM

Stage pipeline:
  1. TimeSync                   timezone, w32time service, NTP peers, force
                                resync with poll; reboot-retry fallback (cap 2)
  2. PowerSettings              activates Ultimate Performance plan (falls back
                                to High Performance on Home SKUs), pins display
                                /sleep/hibernate to Never AND lid/sleep/power
                                buttons to Do-nothing on both AC and DC
  3. DisplaySettings            primary display -> max supported resolution,
                                DPI scale -> 100% (via ChangeDisplaySettingsEx
                                + CDS_UPDATEREGISTRY so it survives logon)
  4. Debloat                    removes bloatware per data/bloatware.json
  5. WinTweaks                  WinUtil preset + dark theme + Start menu left
                                aligned + Chrome + display scale 100% + NumLock
                                on + Bing off + verbose login + telemetry off
  6. InstallDellSupportAssist
  7. InstallDellPowerManager
  8. InstallDellCommandUpdate   via WINGET (Dell.CommandUpdate.Universal); DCU
                                drives the BIOS/firmware/driver sweep
  9. ConfigureDellUpdates       SupportAssist auto-consent + weekly SYSTEM-
                                context dcu-cli sweep (BIOS / firmware / drivers
                                / Dell software) so updates keep flowing after
                                the deploy is done. Skips on non-Dell hardware.
 10. InstallRustDesk            via WINGET_MANIFEST (pinned vendor URL + SHA256)
 11. InstallTailscale           `tailscale up --qr` runs in a visible console
                                in the operator's interactive session; a
                                pulsing red/amber screen-edge border flashes
                                green once registration succeeds. Falls back
                                to the legacy stdout-capture + PNG path when
                                no interactive user is signed in.
 12. RemoteAccess               RDP + WinRM (TrustedHosts=100.*) + OpenSSH
 13. WindowsUpdate              installs all updates, reboots as needed
 14. Cleanup                    removes tasks, disables auto-logon, final reboot

After each reboot: WinDeploy-Resume task fires → Orchestrator resumes
After each logon:  WinDeploy-Monitor task fires → progress window reappears

Run a single stage (dev / troubleshooting)

When you want to exercise one stage in isolation — re-running InstallTailscale after a failed QR scan, validating a tweak in WinTweaks after editing the preset, smoke-testing a refactor — use tools/Run-Stage.ps1 from an elevated PowerShell:

cd C:\ProgramData\WinDeploy\repo
powershell.exe -ExecutionPolicy Bypass -File .\tools\Run-Stage.ps1 -Stage <StageName>

Or from a local clone during development:

cd C:\path\to\win_dell
powershell.exe -ExecutionPolicy Bypass -File .\tools\Run-Stage.ps1 -Stage InstallTailscale

The helper loads config/settings.json the same way the orchestrator does, imports the shared modules, dot-sources the stage script with -StageName and -Config bound, and prints the stage's @{ Status; Message } return verbatim. Exit code mirrors success / failure so it's CI-friendly.

Accepted -Stage names

Exactly one of (matches the production pipeline order):

Stage Script Notes
TimeSync core/TimeSync.ps1 Reboot-retry capped at 2; in REBOOT_ALLOWED_STAGES.
PowerSettings core/PowerSettings.ps1 Ultimate Performance + AC/DC lockdown.
DisplaySettings core/DisplaySettings.ps1 Max resolution + 100% scale via ChangeDisplaySettingsEx.
Debloat core/Debloat.ps1 Reads data/bloatware.json.
WinTweaks core/WinTweaks.ps1 WinUtil preset Pass 1 + direct registry Pass 2.
InstallDellSupportAssist core/AppInstall.ps1 Skips cleanly if SupportAssist already present.
InstallDellPowerManager core/AppInstall.ps1 Detection includes Dell Optimizer.
InstallDellCommandUpdate core/AppInstall.ps1 WINGET Dell.CommandUpdate.Universal.
ConfigureDellUpdates core/ConfigureDellUpdates.ps1 Registers the weekly DCU sweep task.
InstallRustDesk core/AppInstall.ps1 WINGET_MANIFEST (pinned vendor URL + SHA256).
InstallTailscale core/Tailscale.ps1 QR popup in interactive user session; uses Tailscale.AuthKey if set in settings.json.
RemoteAccess core/RemoteAccess.ps1 RDP + WinRM + OpenSSH.
WindowsUpdate core/WindowsUpdate.ps1 Long-running; multiple reboots. Prefer letting the orchestrator drive this.
Cleanup core/Cleanup.ps1 Unregisters tasks + disables auto-logon — running this manually mid-deploy will break resume.

Unknown names throw with the full list. Run from the repo root or anywhere — the helper resolves paths relative to itself.

What this bypasses

Run-Stage.ps1 is for testing one stage; it deliberately does NOT touch the orchestrator's bookkeeping:

  • It does not update state.json (CurrentStage, CompletedStages, RebootCount).
  • It does not trigger reboots even if the stage returns RebootRequired — you get the hashtable back and decide what to do.
  • It does not halt-on-error or run the consecutive-failure abort logic.
  • It does not fire the webhook / tray notifications, write the completion report, or run forensics collection.
  • It does not re-register scheduled tasks via Resilience.psm1 (the orchestrator entry point does that).

For a real deploy, use install.ps1. For "the deploy died, fix it and resume," prefer tools/Troubleshoot.ps1 -Action Repair -Stage <name> (which knows the state-file invariants and re-arms the resume task). Run-Stage.ps1 is the lower-level primitive both of those build on.


Repo structure

win_dell/
├── install.ps1              irm|iex entry point
├── uninstall.ps1            removes all WinDeploy components
├── reinstall.ps1            unattended uninstall + fresh install in one step
├── bootstrap.ps1            called by install; sets up tasks and state
├── lint.ps1                 local lint runner (PSScriptAnalyzer + custom checks)
├── manifest.json            auto-generated by CI — do not edit
├── manifest.sig             GPG detached signature of manifest.json
├── RULES.md                 contributor "do / don't" — non-negotiables
├── CLAUDE.md                AI-assisted development context and conventions
├── README.md                this file
│
├── core/
│   ├── Config.psm1          shared constants via Get-WDConfig — single source of truth
│   ├── State.psm1           state.json read/write, reboot counter, stage tracking, StageExtras
│   ├── Logging.psm1         structured logging (console + file, per-stage logs)
│   ├── Resilience.psm1      self-healing: re-registers tasks, watchdog, auto-snapshot on stall
│   ├── Winget.psm1          winget helpers: --source winget, exit-code translator, pre-flight
│   ├── Orchestrator.ps1     master controller — runs stages, auto-snapshots on failure
│   ├── Monitor.ps1          WPF progress window + Tailscale QR + auto-snapshot pointer
│   ├── Notify.ps1           tray notification on completion (self-removing)
│   ├── Notify-Webhook.ps1   webhook notifications (Slack/Teams/etc.) for deploy events
│   ├── TimeSync.ps1                  stage  1 (timezone + w32time + NTP, reboot-retry capped)
│   ├── PowerSettings.ps1             stage  2 (Ultimate Performance + AC/DC sleep/button lockdown)
│   ├── DisplaySettings.ps1           stage  3 (primary display -> max resolution + 100% scale)
│   ├── Debloat.ps1                   stage  4
│   ├── WinTweaks.ps1                 stage  5 (incl. Start menu left alignment)
│   ├── AppInstall.ps1                stages 6, 7, 8, 10 (Dell SupportAssist, Power Manager,
│   │                                                    Dell Command|Update, RustDesk)
│   ├── ConfigureDellUpdates.ps1      stage  9 (SupportAssist auto-consent + weekly DCU task)
│   ├── Tailscale.ps1                 stage 11
│   ├── RemoteAccess.ps1              stage 12 (RDP + WinRM + OpenSSH)
│   ├── WindowsUpdate.ps1             stage 13 (drains via WUA COM + Dell Command|Update sweep)
│   ├── DellCommandUpdate.ps1         helper engine for the DCU sweep (used by WindowsUpdate
│   │                                 + the weekly task + tools/Run-DellCommandUpdate.ps1)
│   └── Cleanup.ps1                   stage 14
│
├── config/
│   ├── settings.json        all deployment configuration — edit this
│   └── winutil-preset.json  WinUtil tweak IDs
│
├── data/
│   ├── bloatware.json       safe/optional app removal lists
│   └── profiles.json        deployment profile definitions (App selection bundles)
│
├── docs/
│   └── GPG-SETUP.md         how to generate and configure the GPG signing key
│
├── tools/
│   ├── Troubleshoot.ps1              Status / Diagnose / Repair entry point (manual + auto-fired)
│   ├── Collect-Forensics.ps1         Per-machine archive collector (D:\ -> C:\ fallback) + manifest.json
│   ├── Update-Index.ps1              regenerates INDEX.md (called by pre-commit hook)
│   ├── Get-WingetManifestFields.ps1  computes WINGET_MANIFEST hash for a vendor URL
│   ├── Invoke-OnInteractiveUser.ps1  pivots a .ps1 to the active console user via transient task
│   ├── Run-DellCommandUpdate.ps1        on-demand DCU sweep (scan + apply + reset to vendor defaults)
│   ├── Run-Stage.ps1                    runs a single stage outside the orchestrator (dev / one-off)
│   ├── Show-AttentionBorder.ps1         reusable pulsing screen-edge border (operator-attention primitive)
│   ├── Show-RebootCountdown.ps1         pre-reboot WPF countdown + Skip popup (uses the attention border)
│   ├── Show-TailscaleQrInConsole.ps1    visible-console QR helper for InstallTailscale
│   ├── Test-AttentionBorder.ps1         standalone preview harness for the attention border + success flash
│   ├── Test-DcuFlow.ps1                 read-only DCU scan diagnostic ("what would DCU do right now")
│   ├── Test-WinUtilDirectApply.ps1      dry-run validator for config/winutil-preset.json
│   └── Test-WinUtilApplyOne.ps1         single-preset apply/verify/revert smoke test
│
├── tests/
│   └── State.Tests.ps1               round-trip tests for core/State.psm1 (run in CI lint job)
│
└── .github/
    ├── dependabot.yml                weekly bumps for github-actions versions
    └── workflows/
    └── ci.yml               lint → validate task XML → sign manifest (in order)

Scheduled tasks

Deploy-time operational tasks (managed by Resilience.psm1::Assert-ScheduledTasks):

Task Runs as Window Removed by
WinDeploy-Resume SYSTEM Hidden (launcher → task_resume.log) Cleanup.ps1
WinDeploy-Monitor Users Visible (direct) Cleanup.ps1
WinDeploy-Notify Users Hidden (launcher → task_notify.log) Notify.ps1 (self)
WinDeploy-AutoLogonSafety SYSTEM Hidden Self-deletes after 6h
WinDeploy-Watchdog SYSTEM Hidden Cleanup.ps1 (was "never" until 2026-05-26)

Resume and Notify run via generated launcher scripts that redirect all output to log files. Monitor runs directly — child processes cannot show WPF windows on the interactive desktop.

Self-healing: Missing operational tasks are re-registered automatically on every bootstrap and orchestrator run. Re-running irm ... | iex fully restores everything.

Post-deploy product tasks (registered by a stage, NOT by Resilience.psm1 — they survive uninstall on purpose):

Task Registered by Schedule Purpose
WinDeploy DCU Weekly Sweep ConfigureDellUpdates stage Weekly (default: Sunday 03:00) Dot-sources core/DellCommandUpdate.ps1 and calls Invoke-DellCommandUpdate so Dell BIOS / firmware / driver / Dell-software updates keep flowing under SYSTEM long after the deploy is finished. StartWhenAvailable covers laptops that were off when the trigger fired.

Log files

All under C:\ProgramData\WinDeploy\Logs\:

File Contents
early.log Pre-logger startup from all scripts
bootstrap.log Bootstrap output
session.log All stages, all runs
task_resume.log Every Orchestrator run
task_monitor.log Every Monitor run + startup entry
task_notify.log Every Notify run
monitor_crash.log WPF crash detail
monitor_crash.txt Plain-text crash readable in Notepad
completion_report.txt Final deployment summary
tailscale_auth_url.txt Fallback auth URL if QR generation fails (in deploy root)
tailscale_qr_terminal.log Mirror of what the QR helper window printed (QR + auth URL + tailscale daemon messages)

Configuration

Edit config/settings.json:

Key Default Effect
Stages.TimeSync.Timezone "Eastern Standard Time" Passed to tzutil /s. Any name from tzutil /l is valid.
Stages.TimeSync.NtpServers ["time.google.com","time.cloudflare.com","time.windows.com","pool.ntp.org"] First-match-wins peer list. Each is configured with 0x9 (SpecialInterval | Client).
Stages.TimeSync.ClearRealTimeIsUniversal true Clears HKLM\...\TimeZoneInformation\RealTimeIsUniversal so Windows reads the BIOS clock as local time (Dell ships BIOS=local). Set false only on dual-boot machines that share UTC with Linux.
Stages.TimeSync.MaxRebootRetries 2 If verification fails (Source still Local CMOS Clock after polling), TimeSync returns RebootRequired up to this many times before returning Failed.
Stages.TimeSync.NetworkWaitSeconds 120 How long to wait for Resolve-DnsName to succeed against an NTP peer before attempting the first resync.
Stages.TimeSync.VerifyTimeoutSeconds 120 Total budget for the resync+poll loop (split across ResyncAttempts).
Stages.TimeSync.ResyncAttempts 5 Number of w32tm /resync /rediscover calls; each is followed by a /query /status poll until Source flips off the local clock.
Stages.PowerSettings.PowerPlan "Ultimate" "Ultimate" = duplicate the hidden Ultimate Performance template via powercfg -duplicatescheme and activate it (falls back to High Performance if duplication is blocked on Home SKUs). "High" = activate the always-present High Performance plan directly.
Stages.PowerSettings.DisableButtons true Pin lid close / sleep button / power button to "Do nothing" on AC and DC.
Stages.PowerSettings.DisableSleepAndScreenOff true Pin display-off / sleep / hibernate timeouts to Never on AC and DC.
Stages.PowerSettings.DisableHibernateFile true Run powercfg /hibernate off to reclaim hiberfil.sys disk space.
Stages.DisplaySettings.Enabled true Toggle the entire stage. Set false to skip on headless / VM / kiosk hardware where changing display state is risky.
Stages.DisplaySettings.SetResolutionToMax true Pin the primary display to its maximum supported resolution via ChangeDisplaySettingsEx + CDS_UPDATEREGISTRY so the next interactive logon inherits it.
Stages.DisplaySettings.Scale 100 DPI scale percent for the active user + default-user hive (LogPixels). 100 = 96 DPI native.
Stages.WinTweaks.RunWinUtil true Run WinUtil Pass 1 (preset apply) before the direct registry tweaks in Pass 2. Set false to run only Pass 2.
Stages.WinTweaks.StartMenuAlign "Left" Windows 11 taskbar/Start alignment. "Left" or "Center". Win10 ignores it. Takes effect on next Explorer restart (Cleanup reboot covers it).
Stages.ConfigureDellUpdates.ConfigureSupportAssist true Apply best-effort SupportAssist registry tweaks (AutoUpdate, scheduled scan frequency, telemetry/analytics consent).
Stages.ConfigureDellUpdates.RegisterWeeklyDcuTask true Register WinDeploy DCU Weekly Sweep -- a SYSTEM-context scheduled task that calls Invoke-DellCommandUpdate once a week.
Stages.ConfigureDellUpdates.WeeklyDayOfWeek "Sunday" Day the weekly DCU task runs. Any value New-ScheduledTaskTrigger -DaysOfWeek accepts.
Stages.ConfigureDellUpdates.WeeklyTime "03:00" Time of day for the weekly DCU task. 24-hour HH:mm.
Stages.ConfigureDellUpdates.DcuResetBeforeConfigure true Every DCU sweep starts with /configure -restoreDefaults so any prior partial / hand-edited DCU config cannot leak in. Set false for environments that customise DCU via GPO or another tool.
Stages.ConfigureDellUpdates.DcuScheduleAction "DownloadInstallAndNotify" DCU schedule action. Vendor-conservative default; flip to "DownloadInstallAndReboot" for fully unattended apply+reboot. Other values: "DownloadAndNotify", "NotifyAvailableUpdates".
Stages.WindowsUpdate.RunDellCommandUpdate true After the native WUA-COM drain reports zero pending, run Invoke-DellCommandUpdate so BIOS / firmware / drivers / Dell software updates not in Microsoft Update get applied.
Stages.WindowsUpdate.InstallDellCommandUpdateIfMissing true If dcu-cli.exe is absent when the WindowsUpdate stage tries to use it, attempt a winget install of Dell.CommandUpdate.Universal first. Belt-and-suspenders with the new InstallDellCommandUpdate stage.
Stages.Cleanup.RunForensics true At the tail of the Cleanup stage, run tools/Collect-Forensics.ps1 to bundle state + logs + hardware facts into <ForensicsRoot>\<hostname>\runs\…. Wrapped in its own try/catch -- collection failure can never promote a Cleanup failure.
Forensics.Root (auto) Override the per-machine forensics root. Resolution order: this key > D:\WinDeploy-Forensics (when D:\ is writable) > C:\WinDeploy-Forensics.
Forensics.AutoZip false Zip each run subfolder when collection finishes. Convenient for emailing; doubles archive size.
Forensics.RedactKeys ["AuthKey","Password","Secret","Token","ApiKey","PrivateKey","ConnectionString"] Keys whose values are redacted from collected JSON before they land in the forensics archive. Add new secret-bearing key names here when you introduce them.
Notifications.Webhook "" Slack / Teams / Discord / generic JSON webhook URL. Empty = disabled.
Notifications.OnStart / .OnStageFailure / .OnComplete true Toggle each webhook event independently.
Tailscale.AuthKey "" Pre-auth key from login.tailscale.com/admin/settings/keys. When non-empty, registers via --authkey and skips the QR/browser flow. Recommended for unattended deploys.
Tailscale.QrTimeoutMinutes 15 How long the QR registration wait loop runs before the stage gives up. The QR + URL are visible from the moment the helper window appears, so 15 min is plenty for a real human at the keyboard.
Tailscale.MaxRebootRetries 2 When QrTimeoutMinutes expires AND no login attempt was detected (BackendState stayed at NeedsLogin the whole window), the stage returns Complete with a deferred-retry marker up to this many times instead of halting the deploy. The orchestrator continues to RemoteAccess / WindowsUpdate / Cleanup; InstallTailscale is in DRAIN_STAGES so it re-runs on every subsequent reboot, showing the QR again on the post-reboot pass. After the cap is exhausted on a still-unregistered machine, the stage returns Failed. The "operator clicked but didn't finish" case (BackendState moved out of NeedsLogin) bypasses this entirely -- straight to Failed.
Tailscale.QrTerminalWindow true Spawn a visible PowerShell console in the interactive user's session and let tailscale up --qr render the QR natively. Falls back to the legacy stdout-capture + PNG path when false or when no interactive user is signed in.
Tailscale.QrTerminalFormat "small" Forwarded to tailscale up --qr-format. Options: auto, ascii, small (Unicode half-blocks, ~21 rows), large (~37 rows). ascii is the most portable for terminals without half-block glyphs.
Tailscale.QrTerminalAutoCloseOnRegister true When true, the helper window closes the moment tailscale up exits with success. When false, it waits for the operator to press Enter so the final state stays readable.
Tailscale.AttentionBorder true While the QR is waiting, pulse a red/amber border (~14 px) along the edges of every connected display. Always-on-top, click-through, leaves the center of the screen unobstructed. Flashes green for ~3 s once registration succeeds, then exits.
Orchestrator.RebootCountdown.Enabled true Show a WPF countdown popup (with the attention border alongside) in the interactive user's session before every reboot. Set false to fall back to the legacy silent Start-Sleep + Restart-Computer. Suppressed automatically when no interactive user is signed in.
Orchestrator.RebootCountdown.Seconds 60 Countdown duration before the reboot fires.
Orchestrator.RebootCountdown.AllowSkip true When true, the popup shows a Skip Reboot button -- the operator can defer this reboot; the stage is already marked Complete so the deploy continues to the next stage without rebooting. When false, the popup is informational only (the Skip button is hidden).
Debloat.RemoveOptional false Remove only the safe bloatware list. Set true to also remove the optional list.
Cleanup.FinalReboot true Reboot after the Cleanup stage completes. Set false to leave the machine running.
Stages.Cleanup.ShowCompletionSummary true At the tail of Cleanup, spawn tools/Show-CompletionSummary.ps1 in the interactive user's session -- a WPF popup with the per-stage table, WindowsUpdate counts (from C1), reboot count, Tailscale state, and the forensics archive path. Suppressed when no interactive user is signed in; a plaintext <LogDir>\completion-summary.txt is written either way.
Stages.Cleanup.SummaryAutoCloseSeconds 60 How long the completion summary popup stays up before auto-closing. Set 0 to leave the popup open until the operator clicks OK.
Monitor.TileDashboard true After bootstrap.ps1 starts the resume task, spawn tools/Show-DeployDashboard.ps1 -- a Windows Terminal window with three live panes: state snapshot (current stage / completed count / WindowsUpdate progress / Tailscale state, refreshed every 2 s), task_resume.log tail (color-tagged by log level), and current-stage log tail (auto-switches as CurrentStage advances). Falls back to three separate powershell.exe windows when wt.exe is absent. Suppressed when no interactive user is signed in; the deploy is never blocked by a dashboard failure.

Auto-logon picker

Before handing off to bootstrap.ps1, install.ps1 spawns a WPF popup (tools/Pick-AutoLogonAccount.ps1) that:

  • Enumerates enabled local accounts via Get-LocalUser (falls back to Win32_UserAccount when the LocalAccounts module is unavailable). Built-in Guest, DefaultAccount, and WDAGUtilityAccount are filtered out.
  • Auto-selects the currently signed-in interactive user when it's in the list; otherwise the first enabled account.
  • Offers a "use a different name" override checkbox for cases like a freshly-provisioned domain account.
  • Captures the password via WPF PasswordBox.SecurePassword -- it lives only as a SecureString in memory.
  • Writes the chosen { Username; PasswordSecure } to a DPAPI-encrypted clixml file under $env:TEMP. The file is decryptable only by the same user on the same machine.
  • install.ps1 forwards the clixml path into bootstrap.ps1 via -AutoLogonPasswordClixml. Set-AutoLogon reads the clixml, decodes the SecureString via SecureStringToBSTR just long enough to write to HKLM\..\Winlogon\DefaultPassword, zeroes the BSTR in finally, and deletes the clixml.

The plaintext-in-registry write is unchanged -- it's the documented Windows auto-logon mechanism. The existing mitigations stay in place: AutoLogonCount=99, the WinDeploy-AutoLogonSafety task flips AutoAdminLogon=0 after 6 hours, and Cleanup.ps1 flips it earlier on a normal deploy exit.

Skipping the picker (unattended / CI / no operator at the keyboard):

irm "https://raw.githubusercontent.com/karolperkowski/win_dell/main/install.ps1" |
    iex -NoAutoLogonPicker

install.ps1 -NoAutoLogonPicker falls back to the legacy Administrator / blank-password defaults. The picker is also auto-skipped when no enabled local accounts can be enumerated -- the deploy proceeds with the legacy defaults plus a logged warning.

[Skip auto-logon] from inside the picker has the same effect: legacy defaults + warning. Use this when the operator explicitly does not want auto-logon for this deploy.


WinTweaks stage — what gets applied

Tweak Detail
Dark theme HKLM + HKCU + Default user hive
Start menu alignment TaskbarAl=0 (Left) per-user + default hive. Configurable via Stages.WinTweaks.StartMenuAlign.
Display scale Set to 100% (96 DPI) for current and default user
NumLock on Applied to current and default user hive
Bing search removed Start menu Bing search disabled
Verbose login Shows user name on login screen
Telemetry AllowTelemetry = 0 (both HKLM paths)
Activity history Disabled
Location tracking Denied
File extensions Always visible in Explorer
Hidden files Shown
Xbox Game DVR Disabled
Fast startup Disabled
Widgets (Win 11) Disabled
Clock seconds Shown in taskbar
Chrome Installed via winget
WinUtil preset Applied from config/winutil-preset.json

CI

ci.yml runs three jobs in order:

lint  ──┐
        ├──→  sign manifest   (push to main only, both must pass)
validate┘

Manifest signing uses GPG. See docs/GPG-SETUP.md. GPG is currently configured and producing real signatures.


Common failure points

Symptom Cause Fix
Monitor window off-screen Previous position from disconnected display Fixed: CenterScreen + bounds clamp on load
stream was not readable Concurrent Add-Content with SYSTEM writer Fixed: [System.IO.File]::AppendAllText in all log writers
$Window cannot be retrieved XAML parse failed before assignment Check monitor_crash.txt for the XAML error
Tasks not registering Resilience ran before repo was copied Fixed: Resilience runs after Copy-RepoToDeployRoot
parameter set cannot be resolved -LogonType with -GroupId Remove -LogonType from group-based principals
Monitor showing error panel State property case mismatch Fixed: all state reads use PascalCase. Lint rule enforces this.
Auto-logon not cleared Cleanup stage skipped AutoLogonSafety task clears it unconditionally after 6h
WindowsUpdate stage returns Failed with MaxCyclesHitWithPending Cap reached and updates still pending after MAX_UPDATE_CYCLES (default 8) Investigate the listed pending titles in the WindowsUpdate log; previously masked as silent Complete. See StageExtras['WindowsUpdate_MaxCyclesHitWithPending'] for the count.
PSGallery unreachable No internet during WindowsUpdate stage Fixed: pre-flight connectivity check with clear error message
QR code missing in Monitor All QR generation methods failed Fixed: auth URL saved to tailscale_auth_url.txt as fallback
Wide-character log files PS 5.1 *>> redirect writes UTF-16LE Fixed: launchers use Out-File -Encoding UTF8
Orchestrator crash on first run Bootstrap ran inline as user, not SYSTEM Fixed: bootstrap triggers WinDeploy-Resume task
All winget installs fail with -1978335138 msstore source's TLS cert pinning fails under SYSTEM Fixed: every winget call passes --source winget; WINGET_MANIFEST bypasses sources entirely
Stage returned invalid result (type: Object[]) Native & winget 2>&1 polluted the script return pipeline Fixed: Invoke-WingetCli in Winget.psm1 captures output into a variable
Tailscale stuck "Starting tailscale up - waiting for auth URL..." Daemon already authed (silent succeed) or out-of-band sign-in Fixed: dual-condition wait (URL OR Test-TailscaleRegistered OR timeout); pre-spawn check skips QR when already registered
Test machine running months-old code bootstrap.ps1 re-uses extracted repo; only install.ps1 pulls from GitHub Fixed: install.ps1 writes VERSION stamp; bootstrap WARNS when VERSION is >7 days old; orchestrator logs VERSION on every startup
Stale LastError for an already-retried stage Set-StageComplete didn't clear it Fixed: Set-StageComplete clears LastError when it matched the now-successful stage
Sticky CI "INDEX.md out of date" failures INDEX.md describes its own line count, but writing changes it Fixed: pre-commit regenerates in a loop until git diff is empty (converges in 1-2 iterations)
Machine wall clock wrong by the timezone offset on first boot Old WinTweaks set RealTimeIsUniversal=1, making Windows interpret the BIOS clock (local time on Dells) as UTC Fixed: registry value removed in WinTweaks; TimeSync clears any leftover value on every run
TimeSync returns Failed with Source='Local CMOS Clock' w32tm /resync returns when the request is dispatched, not after the sync completes; verification ran too early Fixed: TimeSync now polls /query /status for up to VerifyTimeoutSeconds, retries ResyncAttempts times, and falls back to RebootRequired (capped at MaxRebootRetries) for a fresh network stack
TimeSync succeeds but the clock never moves on a machine with a dead CMOS battery Default MaxPosPhaseCorrection / MaxNegPhaseCorrection is 15h, so a multi-year skew is silently refused Fixed: TimeSync sets both to 0xFFFFFFFF (no limit) and restarts w32time before the first resync
Scheduled task launches powershell.exe but no console window appears on the user's desktop Task Scheduler gives the process an interactive token but does NOT allocate a console -- the script runs and file I/O works, nothing renders Fixed: Start-QrTerminalSession registers the task as cmd.exe /c start "<title>" /WAIT powershell.exe ... so start allocates a fresh console in the operator's session, and /WAIT keeps the task in the Running state until the operator closes the window
Tailscale QR renders as mojibake (Γûê etc.) instead of blocks tailscale.exe writes the QR as UTF-8; PowerShell decodes native exe output using the console's OEM code page (CP437 on US-English / CP850 on Western-European Windows) Fixed: Show-TailscaleQrInConsole.ps1 runs chcp 65001 + sets [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 before invoking tailscale
Stage's stdout-capture path prints "NativeCommandError" / call-site banner above the QR PS 5.1 2>&1 on a native exe wraps every stderr line as an ErrorRecord and prints the script path + source line above the actual output Fixed: dropped 2>&1 -- stderr passes through to the console naturally, only stdout is teed to the log
WPF helper script throws "Cannot convert ... to System.Double" on New-Object PS variables are case-insensitive: a typed function param like [double]$W silently shadows a local $w = New-Object Window, so the assignment fails the type constraint and $w ends up as a double Fixed: rename single-letter typed params ($W -> $WidthPx, $H -> $HeightPx) whenever the function uses object locals

Forensics collection

Every deploy automatically bundles its own forensic record at the tail of the Cleanup stage by invoking tools/Collect-Forensics.ps1. The same script can be run manually any time:

powershell -ExecutionPolicy Bypass -File C:\ProgramData\WinDeploy\repo\tools\Collect-Forensics.ps1 -Reason 'post-incident-check'

Output layout (per machine, on D:\ when writable, else C:):

D:\WinDeploy-Forensics\
  <hostname>\
    manifest.json                       <-- grows with every run
    runs\
      20260519-134500-cleanup\          <-- one folder per run
        state.json
        tailscale.json                  (redacted: AuthKey etc.)
        tailscale-live.json
        VERSION
        settings.json                   (redacted copy)
        hardware.json                   (manufacturer/model/serial/BIOS/OS)
        tools-versions.txt              (winget/tailscale/dcu-cli)
        powercfg-active.txt + powercfg-query.txt
        scheduled-tasks.txt             (WinDeploy-* + DCU Weekly Sweep)
        system-errors.txt               (50 most recent ERROR/CRITICAL events)
        auto-snapshot-*.txt             (from Troubleshoot.ps1 -Action Status)
        run-summary.json                (machine-readable single-run summary)
        logs\                           (full copy of $WD.LogDir)

Root-directory resolution: D:\ if it exists and a sentinel file can be created (catches read-only CD-ROMs / locked USB drives); otherwise falls back to C:\WinDeploy-Forensics\. Override either via -ForensicsRoot on the CLI or Forensics.Root in config/settings.json.

Self-copy: on every run the tool also copies itself to <root>\bin\Collect-Forensics.ps1 so the tool stays colocated with its output (survives a C:\ reimage). The repo copy at C:\ProgramData\WinDeploy\repo\tools\ remains the canonical entry point -- the <root>\bin\ copy is a derivative artifact, refreshed on every invocation.

Redaction: any JSON property whose name matches the Forensics.RedactKeys list (default: AuthKey, Password, Secret, Token, ApiKey, PrivateKey, ConnectionString) has its string value replaced with "<redacted len=N>" before the file is copied to the archive. Non-string values are left untouched.

manifest.json is a single per-machine index file listing every run with its timestamp, reason, trigger, outcome (success / partial / failure / in-progress), version SHA, elapsed minutes, failed stages, and the path to its full run folder. Concurrent writers (manual run + Cleanup tail) are serialized via a manifest.lock sentinel file.

Disable the Cleanup-stage auto-run by setting Stages.Cleanup.RunForensics = false in config/settings.json. The manual CLI invocation always works regardless.


Troubleshooting

The unified CLI dispatcher under tools\windeploy.ps1 is the single entry point for all operator-facing scripts. Run with no arguments (or list) to see every subcommand and what it wraps:

# Read-only snapshot of state, logs, tailscale daemon, deployed VERSION.
# Writes to C:\ProgramData\WinDeploy\Logs\auto-snapshot-<timestamp>.txt
.\tools\windeploy.ps1 status

# Stage-specific read-only deep-dive.
.\tools\windeploy.ps1 diagnose InstallTailscale

# Stage-specific destructive recovery (kills hung processes, hot-patches from main, restarts task).
.\tools\windeploy.ps1 repair InstallTailscale

# Other subcommands: forensics, dashboard, dell-update, check-ci, index, run-stage, ...
.\tools\windeploy.ps1 list

The dispatcher is a thin wrapper -- each subcommand forwards verbatim to the canonical script under tools/. The underlying scripts (Troubleshoot.ps1, Collect-Forensics.ps1, Check-CiStatus.ps1, etc.) are still callable directly if you prefer.

Status runs automatically on:

  • any FATAL throw in the orchestrator,
  • a stage failure that halts the deploy,
  • the consecutive-failure abort path,
  • the watchdog detecting a stage idle for 20+ min (excluding WindowsUpdate / Cleanup),
  • the watchdog about to kill an orchestrator that has run for >4 hours.

The Monitor window surfaces a "AUTO-SNAPSHOT" pointer when a recent (<24h) snapshot exists, so the operator sees forensic dumps without log spelunking.


Deployed-version stamp

install.ps1 writes C:\ProgramData\WinDeploy\repo\VERSION containing the commit SHA, branch, extract timestamp, and ZIP URL fetched from the GitHub API at extract time. The orchestrator logs every VERSION line at startup, and bootstrap.ps1 warns loudly if the deployed VERSION is more than 7 days old. Re-run the install.ps1 one-liner to refresh code on an existing deployment — bootstrap.ps1 alone does not pull from GitHub.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages