--- - name: Enable required Windows features for WSL tags: [packages, services, wsl] ansible.windows.win_optional_feature: name: - Microsoft-Windows-Subsystem-Linux - VirtualMachinePlatform state: present include_parent: true - name: Gather Windows host version details tags: [packages] ansible.windows.win_powershell: script: | $os = Get-ComputerInfo $Ansible.Result = @{ product_name = $os.WindowsProductName version = $os.WindowsVersion build_number = [int]$os.OsBuildNumber is_windows_11 = $os.WindowsProductName -like 'Windows 11*' is_windows_10 = $os.WindowsProductName -like 'Windows 10*' } $Ansible.Changed = $false register: windows_host_version_state - name: Enable dark mode for Windows apps tags: [packages] ansible.windows.win_regedit: path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize name: AppsUseLightTheme data: 0 type: dword register: windows_apps_dark_mode_state when: windows_enable_dark_theme | default(false) - name: Enable dark mode for Windows system surfaces tags: [packages] ansible.windows.win_regedit: path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize name: SystemUsesLightTheme data: 0 type: dword register: windows_system_dark_mode_state when: windows_enable_dark_theme | default(false) - name: Hide Windows taskbar search box tags: [packages] ansible.windows.win_regedit: path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Search name: SearchboxTaskbarMode data: 0 type: dword register: windows_taskbar_search_state when: windows_hide_taskbar_search | default(false) - name: Hide Windows 11 taskbar widgets tags: [packages] ansible.windows.win_regedit: path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced name: TaskbarDa data: 0 type: dword register: windows_11_widgets_state when: - windows_hide_taskbar_widgets | default(false) - windows_host_version_state.result.is_windows_11 | default(false) - name: Disable Windows 11 widgets via policy tags: [packages] ansible.windows.win_regedit: path: HKLM:\SOFTWARE\Policies\Microsoft\Dsh name: AllowNewsAndInterests data: 0 type: dword register: windows_11_widgets_policy_state when: - windows_hide_taskbar_widgets | default(false) - windows_host_version_state.result.is_windows_11 | default(false) - name: Hide Windows 10 news and interests taskbar widget tags: [packages] ansible.windows.win_regedit: path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Feeds name: ShellFeedsTaskbarViewMode data: 2 type: dword register: windows_10_widgets_state when: - windows_hide_taskbar_widgets | default(false) - windows_host_version_state.result.is_windows_10 | default(false) - name: Note when Windows shell settings may require sign out tags: [packages] ansible.builtin.debug: msg: >- Windows shell appearance settings changed. Sign out and back in, or restart explorer.exe, if taskbar or theme updates do not appear immediately. Windows 11 widget policy changes can require signing out before the widget button disappears from the taskbar. changed_when: false when: >- (windows_apps_dark_mode_state is changed) or (windows_system_dark_mode_state is changed) or (windows_taskbar_search_state is changed) or (windows_11_widgets_state is changed) or (windows_11_widgets_policy_state is changed) or (windows_10_widgets_state is changed) - name: Ensure winget is available on Windows host tags: [packages] ansible.windows.win_powershell: script: | $winget = Get-Command winget.exe -ErrorAction SilentlyContinue if ($null -eq $winget) { throw 'winget.exe is not available on the Windows host. Install App Installer or rerun the bootstrap script.' } $Ansible.Changed = $false - name: Ensure WSL 2 is the default backend tags: [packages, services, wsl] ansible.windows.win_powershell: script: | $status = & wsl --status 2>$null if ($LASTEXITCODE -eq 0 -and $status -match 'Default Version:\s*2') { $Ansible.Changed = $false return } & wsl --set-default-version 2 if ($LASTEXITCODE -ne 0) { throw 'Failed to set WSL default version to 2.' } $Ansible.Changed = $true - name: Install Windows workstation applications with winget tags: [packages] ansible.windows.win_powershell: script: | $packageId = '{{ item.id }}' $packageName = '{{ item.name | default(item.id) }}' $packageScope = '{{ item.scope | default('' ) }}' $installArgs = @( 'install' '--id', $packageId '--exact' '--silent' '--accept-package-agreements' '--accept-source-agreements' '--disable-interactivity' ) if (-not [string]::IsNullOrWhiteSpace($packageScope)) { $installArgs += @('--scope', $packageScope) } $installed = & winget list --id $packageId --exact --accept-source-agreements --disable-interactivity 2>$null if ($LASTEXITCODE -eq 0 -and $installed -match [regex]::Escape($packageId)) { $Ansible.Changed = $false return } if ($packageScope -eq 'user') { $interactiveUser = (Get-CimInstance Win32_ComputerSystem).UserName if ([string]::IsNullOrWhiteSpace($interactiveUser)) { throw "Failed to install $packageName with winget: no interactive Windows user session is available for a user-scoped install." } $taskName = "AnsibleWinget-$($packageId -replace '[^A-Za-z0-9.-]', '-')" $taskScriptPath = Join-Path $env:TEMP "$taskName.ps1" $stdoutPath = Join-Path $env:TEMP "$taskName.stdout.log" $stderrPath = Join-Path $env:TEMP "$taskName.stderr.log" $quotedArgs = ($installArgs | ForEach-Object { '"' + ($_ -replace '"', '""') + '"' }) -join ', ' $taskScript = @( "`$installArgs = @($quotedArgs)", "`$process = Start-Process -FilePath 'winget.exe' -ArgumentList `$installArgs -Wait -PassThru -NoNewWindow -RedirectStandardOutput '$stdoutPath' -RedirectStandardError '$stderrPath'", "exit `$process.ExitCode" ) -join [Environment]::NewLine Set-Content -Path $taskScriptPath -Value $taskScript -Encoding Ascii try { $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$taskScriptPath`"" $principal = New-ScheduledTaskPrincipal -UserId $interactiveUser -LogonType InteractiveToken -RunLevel Limited $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings -Force | Out-Null Start-ScheduledTask -TaskName $taskName $deadline = (Get-Date).AddMinutes(10) do { Start-Sleep -Seconds 2 $task = Get-ScheduledTask -TaskName $taskName $taskInfo = Get-ScheduledTaskInfo -TaskName $taskName } while ((Get-Date) -lt $deadline -and ($task.State -ne 'Ready' -or $taskInfo.LastRunTime -eq [datetime]::MinValue)) if ($task.State -ne 'Ready' -or $taskInfo.LastRunTime -eq [datetime]::MinValue) { throw "Failed to install $packageName with winget: timed out waiting for the user-scoped scheduled task to finish." } if ($taskInfo.LastTaskResult -ne 0) { $stderr = if (Test-Path $stderrPath) { Get-Content -Path $stderrPath -Raw } else { '' } $stdout = if (Test-Path $stdoutPath) { Get-Content -Path $stdoutPath -Raw } else { '' } throw "Failed to install $packageName with winget. Exit code: $($taskInfo.LastTaskResult). Stdout: $stdout Stderr: $stderr" } } finally { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue Remove-Item -Path $taskScriptPath, $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue } } else { & winget @installArgs if ($LASTEXITCODE -ne 0) { throw "Failed to install $packageName with winget" } } $Ansible.Changed = $true loop: "{{ windows_winget_packages | default([]) }}" loop_control: label: "{{ item.id }}" - name: Install VS Code WSL extensions on Windows host tags: [packages, vscode] ansible.windows.win_powershell: script: | $extensionId = '{{ item }}' $code = Get-Command code.cmd -ErrorAction SilentlyContinue if ($null -eq $code) { throw 'code.cmd is not available. Ensure Visual Studio Code is installed before managing extensions.' } $installedExtensions = & $code.Source --list-extensions if ($installedExtensions -contains $extensionId) { $Ansible.Changed = $false return } & $code.Source --install-extension $extensionId --force if ($LASTEXITCODE -ne 0) { throw "Failed to install VS Code extension $extensionId" } $Ansible.Changed = $true loop: "{{ windows_vscode_extensions | default([]) }}" loop_control: label: "{{ item }}"