Skip to content

Commit a2304c3

Browse files
committed
fix(windows-bootstrap): harden elevated logging and WSL first-run apply
Consolidate elevated PowerShell execution into a shared transcript-backed helper, and make WSL cloud-init apply resilient in automation by skipping Bitwarden and externals during bootstrap. Also use runtime CI env detection for brew bundle so CI package gates are honored consistently. Made-with: Cursor
1 parent 9396309 commit a2304c3

11 files changed

Lines changed: 252 additions & 75 deletions

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ Deleting a file from the chezmoi source does **not** remove it from the target (
128128
- **README "What you get" table** – The tool table in `README.md` is sorted by category: shell/prompt, terminal emulators, multiplexer, editor, dev tools, version control, security, package/runtime management, then platform-specific utilities. When adding or removing a managed tool, update this table and preserve the sort order. Platform columns (macOS, Linux, Windows, Codespaces) must reflect what `.chezmoiignore` actually deploys.
129129
- **Install script templates**`install.sh.tmpl` and `install.ps1.tmpl` are the source of truth for release installers. The release workflow renders `install.sh` and `install.ps1` from templates with repository/image/tag values before uploading release assets.
130130
- **Template readability** – Keep chezmoi template source files readable: use clear indentation, split complex logic into understandable blocks, and avoid flattening everything to the left margin with aggressive whitespace trimming unless required for output correctness.
131+
- **Bitwarden in automation** – Set `DOTFILES_SKIP_BITWARDEN=1` when running `chezmoi apply` in non-interactive/agent contexts. Templates that use the `bitwarden` function must honor this variable to avoid `bw` prompts/failures during automated runs.
131132

132133
---
133134

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{{ if eq .chezmoi.os "windows" -}}
22
$ErrorActionPreference = "Stop"
3-
4-
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
3+
{{ includeTemplate ".chezmoitemplates/windows-elevation.ps1.tmpl" . }}
54

65
$elevatedScript = {
76
$ErrorActionPreference = "Stop"
@@ -17,13 +16,9 @@ $elevatedScript = {
1716
}
1817
}
1918

20-
if ($isAdmin) {
21-
& $elevatedScript
22-
} else {
23-
$scriptFile = "$env:TEMP\chezmoi-package-updates-elevated.ps1"
24-
$elevatedScript.ToString() | Out-File $scriptFile -Encoding ascii
25-
Write-Host "Elevating for package updates..."
26-
Start-Process powershell -Verb RunAs -ArgumentList "-ExecutionPolicy Bypass -File `"$scriptFile`"" -Wait
27-
Remove-Item $scriptFile -ErrorAction SilentlyContinue
28-
}
19+
Invoke-ElevatedWithLog `
20+
-Name "package-updates" `
21+
-ScriptBlock $elevatedScript `
22+
-StatusMessage "Elevating for package updates..." `
23+
-IgnoreExitCode
2924
{{- end }}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{{- $dotfilesRepoUser := .profile.git.githubUser -}}
2+
{{- $dotfilesRepoName := .profile.git.githubRepo -}}
3+
{{- $dotfilesRepoUrl := printf "https://github.com/%s/%s" $dotfilesRepoUser $dotfilesRepoName -}}
4+
{{ if eq .chezmoi.os "windows" -}}
5+
$ErrorActionPreference = "Stop"
6+
{{ includeTemplate ".chezmoitemplates/windows-elevation.ps1.tmpl" . }}
7+
8+
$elevatedScript = {
9+
$ErrorActionPreference = "Stop"
10+
11+
if (-not (Get-Command wsl -ErrorAction SilentlyContinue)) {
12+
Write-Host "Installing WSL..."
13+
wsl --install --no-distribution
14+
if ($LASTEXITCODE -ne 0) {
15+
Write-Host "WSL install command failed (exit code $LASTEXITCODE). Reboot Windows, then run 'chezmoi apply' again." -ForegroundColor Yellow
16+
return
17+
}
18+
Write-Host "WSL installed. A reboot may be required before WSL is available."
19+
}
20+
21+
if (-not (Get-Command wsl -ErrorAction SilentlyContinue)) {
22+
Write-Host "WSL is not available yet. Reboot Windows, then run 'chezmoi apply' again." -ForegroundColor Yellow
23+
return
24+
}
25+
26+
$distros = wsl --list --quiet 2>$null
27+
if ($distros -contains "Ubuntu") {
28+
Write-Host "WSL Ubuntu already installed; skipping provisioning."
29+
return
30+
}
31+
32+
$wslUser = $env:USERNAME.ToLower()
33+
$cloudInitDir = "$env:USERPROFILE\.cloud-init"
34+
if (-not (Test-Path $cloudInitDir)) { New-Item -ItemType Directory -Path $cloudInitDir | Out-Null }
35+
$cloudConfig = @"
36+
#cloud-config
37+
users:
38+
- name: $wslUser
39+
groups: [adm,dialout,cdrom,floppy,sudo,audio,dip,video,plugdev,netdev]
40+
sudo: ALL=(ALL) NOPASSWD:ALL
41+
shell: /bin/bash
42+
43+
write_files:
44+
- path: /etc/wsl.conf
45+
append: true
46+
content: |
47+
[user]
48+
default=$wslUser
49+
50+
runcmd:
51+
- sudo -u $wslUser git clone {{ $dotfilesRepoUrl }} /home/$wslUser/.local/share/chezmoi
52+
- sudo -u $wslUser bash -lc 'if ! command -v chezmoi >/dev/null 2>&1; then sh -c "`$(curl -fsLS get.chezmoi.io)" -- -b "`$HOME/.local/bin"; fi; PATH="`$HOME/.local/bin:`$PATH"; DOTFILES_SKIP_BITWARDEN=1 "`$HOME/.local/bin/chezmoi" init --apply --source="`$HOME/.local/share/chezmoi" --exclude=externals -R never'
53+
"@
54+
[System.IO.File]::WriteAllText("$cloudInitDir\Ubuntu.user-data", $cloudConfig)
55+
56+
Write-Host "Installing Ubuntu WSL distribution..."
57+
wsl --install Ubuntu --no-launch
58+
if ($LASTEXITCODE -ne 0) {
59+
Write-Host "Ubuntu installation failed (exit code $LASTEXITCODE). Reboot Windows, then run 'chezmoi apply' again." -ForegroundColor Yellow
60+
return
61+
}
62+
63+
Write-Host "Launching Ubuntu for cloud-init provisioning..."
64+
wsl -d Ubuntu -- true
65+
}
66+
67+
Invoke-ElevatedWithLog `
68+
-Name "wsl-ensure" `
69+
-ScriptBlock $elevatedScript `
70+
-StatusMessage "Elevating for WSL provisioning..." `
71+
-FailureMessage "Elevated WSL provisioning failed"
72+
{{- end }}

home/.chezmoiscripts/run_onchange_after_bootstrap.sh.tmpl

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ BREWFILE_RENDERED="$(mktemp)"
5050
trap 'rm -f "$BREWFILE_RENDERED"' EXIT INT TERM
5151
chezmoi execute-template --file "$CHEZMOI_SOURCE_DIR/Brewfile.tmpl" >"$BREWFILE_RENDERED"
5252

53-
{{- if .codespaces }}
54-
CODESPACES=1 brew bundle -q --file="$BREWFILE_RENDERED" || true
55-
{{- else if .ci }}
56-
CI=1 brew bundle -q --file="$BREWFILE_RENDERED"
57-
{{- else if .private }}
58-
PRIVATE=1 brew bundle -q --file="$BREWFILE_RENDERED"
59-
{{- else }}
60-
brew bundle -q --file="$BREWFILE_RENDERED"
61-
{{- end }}
53+
if [ -n "${CODESPACES:-}" ]; then
54+
CODESPACES=1 brew bundle -q --file="$BREWFILE_RENDERED" || true
55+
elif [ -n "${CI:-}" ]; then
56+
CI=1 brew bundle -q --file="$BREWFILE_RENDERED"
57+
elif [ -n "${PRIVATE:-}" ]; then
58+
PRIVATE=1 brew bundle -q --file="$BREWFILE_RENDERED"
59+
else
60+
brew bundle -q --file="$BREWFILE_RENDERED"
61+
fi
6262

6363
# Prewarm antidote plugin cache so the first shell launch doesn't clone everything
6464
zsh -c '

home/.chezmoiscripts/run_onchange_after_bootstrap_windows.ps1.tmpl

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,9 @@
3737
# {{ join " " $wingetPackages | sha256sum }}
3838

3939
$ErrorActionPreference = "Stop"
40+
{{ includeTemplate ".chezmoitemplates/windows-elevation.ps1.tmpl" . }}
4041

4142
# --- Elevated block: Chocolatey packages + WSL provisioning ---
42-
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
43-
4443
$elevatedScript = {
4544
$ErrorActionPreference = "Stop"
4645

@@ -102,7 +101,7 @@ write_files:
102101
103102
runcmd:
104103
- sudo -u $wslUser git clone {{ $dotfilesRepoUrl }} /home/$wslUser/.local/share/chezmoi
105-
- sudo -u $wslUser bash -lc 'if ! command -v chezmoi >/dev/null 2>&1; then sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin"; fi; PATH="$HOME/.local/bin:$PATH"; "$HOME/.local/bin/chezmoi" init --apply --source="$HOME/.local/share/chezmoi"'
104+
- sudo -u $wslUser bash -lc 'if ! command -v chezmoi >/dev/null 2>&1; then sh -c "`$(curl -fsLS get.chezmoi.io)" -- -b "`$HOME/.local/bin"; fi; PATH="`$HOME/.local/bin:`$PATH"; DOTFILES_SKIP_BITWARDEN=1 "`$HOME/.local/bin/chezmoi" init --apply --source="`$HOME/.local/share/chezmoi" --exclude=externals -R never'
106105
"@
107106
[System.IO.File]::WriteAllText("$cloudInitDir\Ubuntu.user-data", $cloudConfig)
108107

@@ -127,6 +126,24 @@ runcmd:
127126
Write-Host "WSL provisioning complete." -ForegroundColor Green
128127
}
129128
} else {
129+
$wslUser = $env:USERNAME.ToLower()
130+
$wslUserExists = (wsl -d Ubuntu -- bash -lc "id -u $wslUser >/dev/null 2>&1 && echo yes || echo no" 2>$null) -match "yes"
131+
if (-not $wslUserExists) {
132+
Write-Host "WSL user '$wslUser' is missing in Ubuntu. Existing distro detected; skipping automatic user recreation." -ForegroundColor Yellow
133+
Write-Host "To reprovision Ubuntu from scratch, run these commands in an elevated PowerShell session:" -ForegroundColor Yellow
134+
Write-Host " wsl --unregister Ubuntu" -ForegroundColor Yellow
135+
Write-Host " chezmoi apply" -ForegroundColor Yellow
136+
}
137+
138+
# Ensure Ubuntu starts as the expected Windows-derived user.
139+
wsl -d Ubuntu -- bash -lc @"
140+
if ! grep -q '^\[user\]' /etc/wsl.conf 2>/dev/null; then
141+
printf '\n[user]\ndefault=$wslUser\n' | sudo tee -a /etc/wsl.conf >/dev/null
142+
elif ! grep -q '^default=$wslUser$' /etc/wsl.conf 2>/dev/null; then
143+
sudo sed -i '/^\[user\]/,/^\[/ s/^default=.*/default=$wslUser/' /etc/wsl.conf
144+
fi
145+
"@
146+
130147
Write-Host "Updating dotfiles in WSL..."
131148
wsl -d Ubuntu -- bash -lc '
132149
if [ ! -d ~/.local/share/chezmoi ]; then
@@ -136,21 +153,17 @@ runcmd:
136153
if ! command -v chezmoi >/dev/null 2>&1; then
137154
sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin"
138155
fi
139-
PATH="$HOME/.local/bin:$PATH" ~/.local/bin/chezmoi init --apply --source="$HOME/.local/share/chezmoi"
156+
PATH="$HOME/.local/bin:$PATH" DOTFILES_SKIP_BITWARDEN=1 ~/.local/bin/chezmoi init --apply --source="$HOME/.local/share/chezmoi" --exclude=externals -R never
140157
'
141158
}
142159
}
143160
}
144161

145-
if ($isAdmin) {
146-
& $elevatedScript
147-
} else {
148-
$scriptFile = "$env:TEMP\chezmoi-bootstrap-elevated.ps1"
149-
$elevatedScript.ToString() | Out-File $scriptFile -Encoding ascii
150-
Write-Host "Elevating for package installation and WSL provisioning..."
151-
Start-Process powershell -Verb RunAs -ArgumentList "-ExecutionPolicy Bypass -File `"$scriptFile`"" -Wait
152-
Remove-Item $scriptFile -ErrorAction SilentlyContinue
153-
}
162+
Invoke-ElevatedWithLog `
163+
-Name "bootstrap" `
164+
-ScriptBlock $elevatedScript `
165+
-StatusMessage "Elevating for package installation and WSL provisioning..." `
166+
-IgnoreExitCode
154167

155168
# Refresh PATH so newly installed tools are available
156169
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "User")
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
function Invoke-ElevatedWithLog {
2+
param(
3+
[Parameter(Mandatory = $true)]
4+
[string]$Name,
5+
[Parameter(Mandatory = $true)]
6+
[scriptblock]$ScriptBlock,
7+
[Parameter(Mandatory = $true)]
8+
[string]$StatusMessage,
9+
[string]$FailureMessage = "",
10+
[switch]$IgnoreExitCode
11+
)
12+
13+
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
14+
if ($isAdmin) {
15+
& $ScriptBlock
16+
return
17+
}
18+
19+
$runId = [Guid]::NewGuid().ToString("N")
20+
$safeName = $Name -replace '[^A-Za-z0-9_-]', '-'
21+
$payloadFile = Join-Path $env:TEMP "chezmoi-$safeName-elevated-payload-$runId.ps1"
22+
$wrapperFile = Join-Path $env:TEMP "chezmoi-$safeName-elevated-wrapper-$runId.ps1"
23+
$transcriptFile = Join-Path $env:TEMP "chezmoi-$safeName-elevated-$runId.log"
24+
25+
try {
26+
$ScriptBlock.ToString() | Out-File $payloadFile -Encoding ascii
27+
@"
28+
`$ErrorActionPreference = "Stop"
29+
`$transcriptStarted = `$false
30+
try {
31+
Start-Transcript -Path "$transcriptFile" -Force | Out-Null
32+
`$transcriptStarted = `$true
33+
} catch {
34+
# Continue even if transcript cannot start.
35+
}
36+
37+
try {
38+
& "$payloadFile"
39+
} finally {
40+
if (`$transcriptStarted) {
41+
try {
42+
Stop-Transcript | Out-Null
43+
} catch {
44+
# Ignore transcript stop errors.
45+
}
46+
}
47+
}
48+
"@ | Out-File $wrapperFile -Encoding ascii
49+
Write-Host $StatusMessage
50+
$proc = Start-Process powershell -Verb RunAs -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$wrapperFile`"" -Wait -PassThru
51+
if (Test-Path $transcriptFile) {
52+
Get-Content $transcriptFile | ForEach-Object { Write-Host $_ }
53+
}
54+
55+
if ((-not $IgnoreExitCode) -and $proc.ExitCode -ne 0) {
56+
if ([string]::IsNullOrWhiteSpace($FailureMessage)) {
57+
throw "Elevated command failed (exit code $($proc.ExitCode))."
58+
}
59+
throw "$FailureMessage (exit code $($proc.ExitCode))."
60+
}
61+
} finally {
62+
Remove-Item $wrapperFile -ErrorAction SilentlyContinue
63+
Remove-Item $payloadFile -ErrorAction SilentlyContinue
64+
Remove-Item $transcriptFile -ErrorAction SilentlyContinue
65+
}
66+
}

home/dot_config/finicky.js.tmpl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
{{- $workChromeProfile := (bitwarden "item" "work-domain").name -}}
1+
{{- $workChromeProfile := "chipwolf.uk" -}}
2+
{{- if ne (env "DOTFILES_SKIP_BITWARDEN") "1" -}}
3+
{{- $workChromeProfile = (bitwarden "item" "work-domain").name -}}
4+
{{- end -}}
25
// ~/.finicky.js
36

47
export default {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[settings]
2-
{{- if not .codespaces }}
2+
{{- $skipBitwarden := eq (env "DOTFILES_SKIP_BITWARDEN") "1" -}}
3+
{{- if and (not .codespaces) (not $skipBitwarden) }}
34
api_key = {{ (bitwarden "item" "wakatime-api-key").login.password | quote }}
45
{{- end }}

install.ps1.tmpl

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ param()
77
$repoUrl = "{{ $repoURL }}"
88

99
$ErrorActionPreference = "Stop"
10+
{{ includeTemplate ".chezmoitemplates/windows-elevation.ps1.tmpl" . }}
1011

1112
# --- Elevated block: install system packages via Chocolatey ---
1213
# Only choco installs need admin. Everything else (bw login, chezmoi init)
@@ -18,7 +19,7 @@ $elevatedScript = {
1819
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
1920
Write-Host "Installing Chocolatey..." -ForegroundColor Cyan
2021
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
21-
$chocoBootstrapPath = Join-Path $env:TEMP "chocolatey-install.ps1"
22+
$chocoBootstrapPath = Join-Path $env:TEMP "chocolatey-install-$([Guid]::NewGuid().ToString("N")).ps1"
2223
Invoke-WebRequest -Uri "https://community.chocolatey.org/install.ps1" -OutFile $chocoBootstrapPath
2324
$chocoResult = Start-Process powershell -ArgumentList @(
2425
"-NoProfile",
@@ -66,19 +67,11 @@ $elevatedScript = {
6667
}
6768
}
6869

69-
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
70-
if ($isAdmin) {
71-
& $elevatedScript
72-
} else {
73-
$scriptFile = Join-Path $env:TEMP "chezmoi-install-elevated.ps1"
74-
$elevatedScript.ToString() | Out-File $scriptFile -Encoding ascii
75-
Write-Host "Elevating for package installation..." -ForegroundColor Cyan
76-
$proc = Start-Process powershell -Verb RunAs -ArgumentList "-ExecutionPolicy Bypass -File `"$scriptFile`"" -Wait -PassThru
77-
Remove-Item $scriptFile -ErrorAction SilentlyContinue
78-
if ($proc.ExitCode -ne 0) {
79-
throw "Elevated package installation failed (exit code $($proc.ExitCode)). Re-run the script to retry."
80-
}
81-
}
70+
Invoke-ElevatedWithLog `
71+
-Name "install" `
72+
-ScriptBlock $elevatedScript `
73+
-StatusMessage "Elevating for package installation..." `
74+
-FailureMessage "Elevated package installation failed. Re-run the script to retry"
8275

8376
# --- Non-elevated: bw login, chezmoi init, git config ---
8477
# Runs in the caller's original window so interactive prompts are visible
@@ -94,17 +87,32 @@ if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
9487
if ($found) { $env:PATH = "$found;$env:PATH" }
9588
}
9689

97-
# Log in to Bitwarden if unauthenticated (chezmoi manages the session)
98-
$bwStatus = (bw status 2>$null | ConvertFrom-Json).status
99-
if ($bwStatus -eq "unauthenticated") {
100-
Write-Host "Bitwarden vault is not logged in. Please log in:" -ForegroundColor Yellow
101-
bw login
102-
if ($LASTEXITCODE -ne 0) { throw "Bitwarden login failed." }
90+
$forceBitwarden = -not [string]::IsNullOrWhiteSpace($env:DOTFILES_FORCE_BITWARDEN)
91+
$skipBitwarden = $env:DOTFILES_SKIP_BITWARDEN -eq "1"
92+
$interactiveSession = $false
93+
try {
94+
$interactiveSession = ($Host.Name -eq "ConsoleHost") -and (-not [Console]::IsInputRedirected) -and (-not [Console]::IsOutputRedirected)
95+
} catch {
96+
$interactiveSession = $false
10397
}
10498

105-
Write-Host "Syncing Bitwarden vault..." -ForegroundColor Cyan
106-
bw sync
107-
if ($LASTEXITCODE -ne 0) { throw "Bitwarden sync failed." }
99+
if ($skipBitwarden) {
100+
Write-Host "Skipping Bitwarden setup: DOTFILES_SKIP_BITWARDEN=1." -ForegroundColor Yellow
101+
} elseif ($forceBitwarden -or $interactiveSession) {
102+
# Log in to Bitwarden if unauthenticated (chezmoi manages the session)
103+
$bwStatus = (bw status 2>$null | ConvertFrom-Json).status
104+
if ($bwStatus -eq "unauthenticated") {
105+
Write-Host "Bitwarden vault is not logged in. Please log in:" -ForegroundColor Yellow
106+
bw login
107+
if ($LASTEXITCODE -ne 0) { throw "Bitwarden login failed." }
108+
}
109+
110+
Write-Host "Syncing Bitwarden vault..." -ForegroundColor Cyan
111+
bw sync
112+
if ($LASTEXITCODE -ne 0) { throw "Bitwarden sync failed." }
113+
} else {
114+
Write-Host "Skipping Bitwarden setup: non-interactive session detected. Set DOTFILES_FORCE_BITWARDEN=1 to force." -ForegroundColor Yellow
115+
}
108116

109117
# Initialise and apply chezmoi dotfiles.
110118
# If the script is running from a local clone, use that as the source.

0 commit comments

Comments
 (0)