Files
infra/ansible/roles/profile_workstation_host_windows/tasks/main.yml
2026-04-02 16:12:40 +02:00

273 lines
11 KiB
YAML

---
- 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
$buildNumber = [int]$os.OsBuildNumber
$Ansible.Result = @{
product_name = $os.WindowsProductName
build_number = $buildNumber
is_windows_11 = $buildNumber -ge 22000
}
$Ansible.Changed = $false
register: windows_host_version_state
- name: Fail when Windows host is not Windows 11
tags: [packages]
ansible.builtin.fail:
msg: >-
workstation_host_windows is supported only on Windows 11. Detected {{ windows_host_version_state.result.product_name }}
build {{ windows_host_version_state.result.build_number }}.
when: not (windows_host_version_state.result.is_windows_11 | default(false))
- 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)
- 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)
- 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)
- 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('' ) }}'
$packageIdRegex = [regex]::Escape($packageId)
$installArgs = @(
'install'
'--id', $packageId
'--exact'
'--silent'
'--accept-package-agreements'
'--accept-source-agreements'
'--disable-interactivity'
)
if (-not [string]::IsNullOrWhiteSpace($packageScope)) {
$installArgs += @('--scope', $packageScope)
}
if ($packageScope -ne 'user') {
$installed = & winget list --id $packageId --exact --accept-source-agreements --disable-interactivity 2>$null
if ($LASTEXITCODE -eq 0 -and $installed -match $packageIdRegex) {
$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"
$resultPath = Join-Path $env:TEMP "$taskName.result.json"
$quotedArgs = ($installArgs | ForEach-Object { '"' + ($_ -replace '"', '""') + '"' }) -join ', '
$taskScript = @(
"`$ErrorActionPreference = 'Stop'",
"function Test-PackageInstalled {",
" `$installed = & winget.exe list --id '$packageId' --exact --accept-source-agreements --disable-interactivity 2>`$null",
" return (`$LASTEXITCODE -eq 0 -and `$installed -match '$packageIdRegex')",
"}",
"`$result = @{ changed = `$false; installed = `$false }",
"if (-not (Test-PackageInstalled)) {",
"`$installArgs = @($quotedArgs)",
"`$process = Start-Process -FilePath 'winget.exe' -ArgumentList `$installArgs -Wait -PassThru -NoNewWindow -RedirectStandardOutput '$stdoutPath' -RedirectStandardError '$stderrPath'",
" if (`$process.ExitCode -ne 0) { exit `$process.ExitCode }",
" `$deadline = (Get-Date).AddMinutes(2)",
" do {",
" Start-Sleep -Seconds 2",
" `$result.installed = Test-PackageInstalled",
" } while ((Get-Date) -lt `$deadline -and -not `$result.installed)",
" if (-not `$result.installed) { throw 'Package was not detected after the user-scoped install completed.' }",
" `$result.changed = `$true",
"}",
"else {",
" `$result.installed = `$true",
"}",
"`$result | ConvertTo-Json -Compress | Set-Content -Path '$resultPath' -Encoding Ascii"
) -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"
}
if (-not (Test-Path $resultPath)) {
throw "Failed to install $packageName with winget: no result file was produced by the user-scoped scheduled task."
}
$result = Get-Content -Path $resultPath -Raw | ConvertFrom-Json
if (-not $result.installed) {
throw "Failed to install $packageName with winget: the package is still not detected after the user-scoped install task finished."
}
$Ansible.Changed = [bool]$result.changed
return
}
finally {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
Remove-Item -Path $taskScriptPath, $stdoutPath, $stderrPath, $resultPath -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 }}"