--- - 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: 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. 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_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 }}"