Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,20 @@ jobs:
shell: pwsh
run: ./tests/windows/assert-migration.ps1

- name: Assert current-user fonts
shell: pwsh
run: ./tests/windows/assert-fonts.ps1 -RepoRoot $env:GITHUB_WORKSPACE

- shell: pwsh
run: ./tests/windows/assert-state.ps1 -RepoRoot $env:GITHUB_WORKSPACE -ExpectedCommit $env:GITHUB_SHA
- shell: cmd
run: call "%GITHUB_WORKSPACE%\tests\windows\create-update-fixture.cmd"
- shell: cmd
run: call "%GITHUB_WORKSPACE%\update.cmd"
- name: Reassert current-user fonts
shell: pwsh
run: ./tests/windows/assert-fonts.ps1 -RepoRoot $env:GITHUB_WORKSPACE

- shell: pwsh
run: ./tests/windows/assert-state.ps1 -RepoRoot $env:GITHUB_WORKSPACE -UpdateMarker $env:UPDATE_MARKER

Expand Down Expand Up @@ -155,11 +163,19 @@ jobs:
shell: cmd
run: call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-migration.ps1"

- name: Assert current-user fonts under AllSigned
shell: cmd
run: call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-fonts.ps1" -RepoRoot "%GITHUB_WORKSPACE%"

- shell: cmd
run: call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-allsigned.ps1" -RepoRoot "%GITHUB_WORKSPACE%" -ThumbprintFile "%RUNNER_TEMP%\dotfiles-cert-thumbprint.txt"
- shell: cmd
run: call "%GITHUB_WORKSPACE%\tests\windows\create-update-fixture.cmd"
- shell: cmd
run: call "%GITHUB_WORKSPACE%\update.cmd"
- name: Reassert current-user fonts under AllSigned
shell: cmd
run: call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-fonts.ps1" -RepoRoot "%GITHUB_WORKSPACE%"

- shell: cmd
run: call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-allsigned.ps1" -RepoRoot "%GITHUB_WORKSPACE%" -ThumbprintFile "%RUNNER_TEMP%\dotfiles-cert-thumbprint.txt" -UpdateMarker "%UPDATE_MARKER%"
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Windows PowerShell 5.1 is only used to download and launch the CMD bootstrap. Th

The Windows bootstrap requires WinGet. Before applying chezmoi, it removes only broken links or reparse points that block a currently managed destination path. Parent components under the user profile are checked as well, but valid links are never traversed and unrelated broken links are left untouched. If App Installer exists but WinGet is not registered for the current user, it attempts current-user App Installer registration. If corporate policy disables WinGet, it stops with an error. Bootstrap installs only Git, PowerShell 7, and chezmoi when they are missing; the platform installer owns the remaining application catalog.

Baseline CLI packages are declared in `scripts/windows/managed-apps.csv` and are installed with WinGet in user scope. An interactive run also installs workstation applications, fonts, Windows Terminal customization, and offers optional applications. `DOTFILES_NONINTERACTIVE=1` installs the complete baseline but skips those interactive/workstation customizations.
Baseline CLI packages are declared in `scripts/windows/managed-apps.csv` and are installed with WinGet in user scope. Cascadia Code plus the Cascadia Code and Cascadia Mono Nerd Font variants are installed for the current user as part of the Windows baseline, including non-interactive runs. An interactive run additionally installs workstation applications, configures Windows Terminal, and offers optional applications. `DOTFILES_NONINTERACTIVE=1` installs the complete CLI/font baseline while skipping those interactive/workstation customizations.

The bootstrap does not require administrator rights or Developer Mode. It invokes PowerShell 7 with `-NoProfile`; an effective `AllSigned` policy is supported automatically. Each machine reuses or creates its own current-user Code Signing certificate and trusts its public certificate locally. No private key, PFX, or KeePass dependency is used.

Expand Down Expand Up @@ -53,7 +53,7 @@ For dotfiles plus installer-managed package/application/module updates, use `upd

The `Validate dotfiles` workflow exercises Debian, Arch Linux, Windows with its normal execution policy, and Windows with a simulated current-user `AllSigned` policy. GitHub Actions only orchestrates the scenarios; reusable fixture and assertion logic lives under `tests/`.

Integration jobs bootstrap from a temporary local bare Git remote containing the exact commit under test. They run the real non-interactive baseline, validate installed CLI tools, exercise create-only Codex files and update wrappers, check idempotent chezmoi apply, and verify clean source/checkout state. Static validation includes ShellCheck, PowerShell parsing, manifest checks, chezmoi template evaluation, bridge-payload synchronization, and pinned `actionlint` validation of the workflow.
Integration jobs bootstrap from a temporary local bare Git remote containing the exact commit under test. They run the real non-interactive baseline, validate installed CLI tools and current-user fonts, exercise create-only Codex files and update wrappers, check idempotent chezmoi apply, and verify clean source/checkout state. Windows font assertions verify both the files under `%LOCALAPPDATA%\Microsoft\Windows\Fonts` and their matching `HKCU\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts` entries. Static validation includes ShellCheck, PowerShell parsing, manifest checks, chezmoi template evaluation, bridge-payload synchronization, and pinned `actionlint` validation of the workflow.

The Windows AllSigned job validates current-user certificate creation and reuse, Authenticode signing, the CMD execution bridge, PowerShell profile startup, and managed module loading. When PowerShell 7 requires signed scripts, the bridge uses inbox Windows PowerShell only for Authenticode signing and executes the resulting signed script with PowerShell 7 under the effective policy. The hosted runner is an administrator with UAC disabled, so CI trusts the test certificate through `LocalMachine\Root` plus `CurrentUser\TrustedPublisher`; runtime helpers also accept `CurrentUser\Root` for the real non-admin path. Corporate GPO/MDM/AppLocker/WDAC policy, enterprise App Installer policy, and a true non-admin corporate Windows 11 token still require validation on a managed machine.

Expand Down
13 changes: 13 additions & 0 deletions docs/windows-fonts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Windows fonts

The Windows baseline installs these font families for the current user:

- Microsoft Cascadia Code
- Caskaydia Cove Nerd Font (Nerd Fonts patched Cascadia Code)
- Caskaydia Mono Nerd Font (Nerd Fonts patched Cascadia Mono)

Expected filename globs are declared in `scripts/windows/managed-fonts.txt`.

The installer downloads fonts into the chezmoi working tree's ignored `fonts/` directory, copies the resulting font files to `%LOCALAPPDATA%\Microsoft\Windows\Fonts`, and maintains matching entries under `HKCU\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts`.

The Windows integration jobs validate the installed files and registry entries after both bootstrap and update, under the normal execution policy and `AllSigned`.
156 changes: 125 additions & 31 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -281,51 +281,147 @@ function Install-OptionalApps {
Refresh-Path
}

function Expand-FontArchive {
param(
[Parameter(Mandatory)][string]$Archive,
[Parameter(Mandatory)][string]$Destination
)

$tar = Get-Command tar.exe -ErrorAction SilentlyContinue
if ($null -eq $tar) {
throw "tar.exe is required to extract font archives."
}

Remove-Item -LiteralPath $Destination -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $Destination -Force | Out-Null

& $tar.Source -xf $Archive -C $Destination
if ($LASTEXITCODE -ne 0) {
throw "Font archive extraction failed for $Archive (exit code $LASTEXITCODE)."
}
}

function Get-ManagedFontPatterns {
$manifest = Join-Path $RepoRoot "scripts\windows\managed-fonts.txt"
if (-not (Test-Path -LiteralPath $manifest -PathType Leaf)) {
throw "The managed font manifest is missing from $RepoRoot."
}

return @(Get-Content -LiteralPath $manifest | Where-Object {
$_.Trim() -and -not $_.Trim().StartsWith("#")
} | ForEach-Object { $_.Trim() })
}

function Download-Fonts {
$fonts = Join-Path $RepoRoot "fonts"
New-Item -ItemType Directory -Force -Path $fonts | Out-Null

if (-not (Test-Path (Join-Path $fonts "CascadiaCode.ttf"))) {
if (@(Get-ChildItem -LiteralPath $fonts -Filter "CascadiaCode*.ttf" -File -ErrorAction SilentlyContinue).Count -eq 0) {
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/microsoft/cascadia-code/releases/latest" -Headers @{ "User-Agent" = "PowerShell" }
$asset = @($release.assets | Where-Object name -Match '^CascadiaCode-.*\.zip$' | Select-Object -First 1)
if ($asset.Count -ne 1) { throw "Unable to locate the Cascadia Code ZIP asset in the latest GitHub release." }
if ($asset.Count -ne 1) {
throw "Unable to locate the Cascadia Code ZIP asset in the latest GitHub release."
}

$zip = Join-Path $fonts "CascadiaCode.zip"
$extract = Join-Path $fonts "CascadiaCode"
Invoke-WebRequest -Uri $asset[0].browser_download_url -OutFile $zip
Expand-Archive $zip -DestinationPath $extract -Force
Remove-Item -Recurse -Force (Join-Path $extract "ttf\static") -ErrorAction SilentlyContinue
Get-ChildItem -Path $extract -Filter *.ttf -Recurse -File | Move-Item -Destination $fonts -Force
Remove-Item -Recurse -Force $zip, $extract
$archive = Join-Path $fonts "CascadiaCode.zip"
$extract = Join-Path $fonts ".extract-CascadiaCode"
try {
Write-Host "Downloading Cascadia Code..." -ForegroundColor Cyan
Invoke-WebRequest -Uri $asset[0].browser_download_url -OutFile $archive
Expand-FontArchive -Archive $archive -Destination $extract
Remove-Item -Recurse -Force (Join-Path $extract "ttf\static") -ErrorAction SilentlyContinue

$fontFiles = @(Get-ChildItem -LiteralPath $extract -Filter *.ttf -Recurse -File)
if ($fontFiles.Count -eq 0) {
throw "Cascadia Code archive did not contain any TTF files."
}
$fontFiles | Move-Item -Destination $fonts -Force
}
finally {
Remove-Item -LiteralPath $archive -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $extract -Recurse -Force -ErrorAction SilentlyContinue
}
}

$nerdRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest" -Headers @{ "User-Agent" = "PowerShell" }
foreach ($font in @(
@{ folder = (Join-Path $fonts "CaskaydiaCoveNerdFont"); filename = "CascadiaCode" },
@{ folder = (Join-Path $fonts "CaskaydiaMonoNerdFont"); filename = "CascadiaMono" }
@{ Name = "Caskaydia Cove Nerd Font"; Asset = "CascadiaCode"; Pattern = "CaskaydiaCove*.ttf" },
@{ Name = "Caskaydia Mono Nerd Font"; Asset = "CascadiaMono"; Pattern = "CaskaydiaMono*.ttf" }
)) {
if (Test-Path "$($font.folder)-Regular.ttf") { continue }
$zip = "$($font.folder).zip"
Invoke-WebRequest -Uri "https://github.com/ryanoasis/nerd-fonts/releases/download/$($nerdRelease.tag_name)/$($font.filename).zip" -OutFile $zip
Expand-Archive $zip -DestinationPath $font.folder -Force
Get-ChildItem -Path $font.folder -Filter *.ttf -Recurse -File | Move-Item -Destination $fonts -Force
Remove-Item -Recurse -Force $zip, $font.folder
if (@(Get-ChildItem -LiteralPath $fonts -Filter $font.Pattern -File -ErrorAction SilentlyContinue).Count -gt 0) {
continue
}

$archive = Join-Path $fonts "$($font.Asset).tar.xz"
$extract = Join-Path $fonts ".extract-$($font.Asset)"
try {
Write-Host "Downloading $($font.Name)..." -ForegroundColor Cyan
Invoke-WebRequest -Uri "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/$($font.Asset).tar.xz" -OutFile $archive
Expand-FontArchive -Archive $archive -Destination $extract

$fontFiles = @(Get-ChildItem -LiteralPath $extract -Filter *.ttf -Recurse -File)
if ($fontFiles.Count -eq 0) {
throw "$($font.Name) archive did not contain any TTF files."
}
$fontFiles | Move-Item -Destination $fonts -Force
}
finally {
Remove-Item -LiteralPath $archive -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $extract -Recurse -Force -ErrorAction SilentlyContinue
}
}

foreach ($requiredPattern in Get-ManagedFontPatterns) {
if (@(Get-ChildItem -LiteralPath $fonts -Filter $requiredPattern -File -ErrorAction SilentlyContinue).Count -eq 0) {
throw "Expected font files are missing after download: $requiredPattern"
}
}
}

function Install-UserFonts {
$sourceDir = Join-Path $RepoRoot "fonts"
$userFontsDir = Join-Path $env:LOCALAPPDATA "Microsoft\Windows\Fonts"
$fontRegistryKey = "HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
if (-not (Test-Path -LiteralPath $sourceDir -PathType Container)) { throw "Font source directory not found: $sourceDir" }
$fontRegistrySubKey = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"

if (-not (Test-Path -LiteralPath $sourceDir -PathType Container)) {
throw "Font source directory not found: $sourceDir"
}

New-Item -ItemType Directory -Path $userFontsDir -Force | Out-Null
$sourceFonts = @(Get-ChildItem -Path $sourceDir -Include *.otc,*.otf,*.ttc,*.ttf -Recurse -File)
foreach ($font in $sourceFonts | Sort-Object Name -Unique) {
$destination = Join-Path $userFontsDir $font.Name
if (Test-Path -LiteralPath $destination) { continue }
Copy-Item -LiteralPath $font.FullName -Destination $destination
New-ItemProperty -Path $fontRegistryKey -Name "$($font.Name) (dotfiles)" -Value $destination -PropertyType String -Force | Out-Null

$sourceFonts = @(Get-ChildItem -Path $sourceDir -Include *.otc,*.otf,*.ttc,*.ttf -Recurse -File | Sort-Object Name -Unique)
if ($sourceFonts.Count -eq 0) {
throw "No font files were downloaded to $sourceDir."
}

$registryKey = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($fontRegistrySubKey)
if ($null -eq $registryKey) {
throw "Unable to open the current-user font registry key."
}

try {
foreach ($font in $sourceFonts) {
$destination = Join-Path $userFontsDir $font.Name
$registryName = "$($font.Name) (dotfiles)"

if (-not (Test-Path -LiteralPath $destination -PathType Leaf)) {
Copy-Item -LiteralPath $font.FullName -Destination $destination
}

$registeredPath = $registryKey.GetValue(
$registryName,
$null,
[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames
)
if ([string]$registeredPath -ne $destination) {
$registryKey.SetValue($registryName, $destination, [Microsoft.Win32.RegistryValueKind]::String)
}
}
}
finally {
$registryKey.Dispose()
}

Write-Host "Installed/registered $($sourceFonts.Count) current-user font files." -ForegroundColor Green
}

function Configure-Git {
Expand Down Expand Up @@ -357,10 +453,8 @@ function Configure-Wsl {
Refresh-Path
Check-RequiredApps

if (-not $NonInteractive) {
Download-Fonts
Install-UserFonts
}
Download-Fonts
Install-UserFonts

Configure-Git
Install-MustHaveApps
Expand All @@ -370,7 +464,7 @@ if (-not $NonInteractive) {
Install-OptionalApps
}
else {
Write-Host "Skipping fonts, terminal configuration, and optional applications in non-interactive mode." -ForegroundColor Yellow
Write-Host "Skipping terminal configuration and optional applications in non-interactive mode." -ForegroundColor Yellow
}

Configure-Wsl
4 changes: 4 additions & 0 deletions scripts/windows/managed-fonts.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Expected filename globs for the Windows font baseline.
CascadiaCode*.ttf
CaskaydiaCove*.ttf
CaskaydiaMono*.ttf
2 changes: 2 additions & 0 deletions tests/static/validate-architecture.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ def read_text(path: str) -> str:
"scripts/windows/managed-apps.csv",
"scripts/windows/cleanup-broken-managed-links.ps1",
"scripts/windows/managed-modules.txt",
"scripts/windows/managed-fonts.txt",
"scripts/windows/invoke-ps-script.cmd",
"scripts/windows/invoke-ps-script-bridge.ps1",
"scripts/windows/signing.ps1",
"scripts/windows/deploy-pwsh.ps1",
"home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl",
"tests/windows/assert-migration.ps1",
"tests/windows/assert-fonts.ps1",
)
for relative in required_paths:
if not (ROOT / relative).exists():
Expand Down
10 changes: 10 additions & 0 deletions tests/windows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Windows integration tests

The Windows jobs exercise the real non-interactive bootstrap and update paths under both the normal execution policy and `AllSigned`.

- `assert-migration.ps1` verifies recovery from broken managed links without modifying unrelated broken links.
- `assert-fonts.ps1` verifies that every downloaded baseline font exists in the current-user Fonts directory and has a matching HKCU font registration.
- `assert-state.ps1` validates the normal-policy runtime state.
- `assert-allsigned.ps1` validates signatures, trusted certificate state, modules, and profile startup under `AllSigned`.

Scripts executed after `AllSigned` becomes effective must run through the repository CMD signing bridge.
Loading