From a3e6d862de6f780f8cddad10728fcfb0df03181c Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:17:22 -0500 Subject: [PATCH 1/6] Install and validate Windows user fonts --- .github/workflows/validate.yml | 16 +++ README.md | 4 +- install.ps1 | 192 +++++++++++++++++++++----- tests/static/validate-architecture.py | 1 + tests/windows/README.md | 10 ++ tests/windows/assert-fonts.ps1 | 69 +++++++++ 6 files changed, 257 insertions(+), 35 deletions(-) create mode 100644 tests/windows/README.md create mode 100644 tests/windows/assert-fonts.ps1 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index da100e0..33209b2 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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 @@ -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%" diff --git a/README.md b/README.md index 8f4ae50..2acff13 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. diff --git a/install.ps1 b/install.ps1 index 50555f7..ad35064 100644 --- a/install.ps1 +++ b/install.ps1 @@ -281,51 +281,179 @@ function Install-OptionalApps { Refresh-Path } +function Invoke-FontDownload { + param( + [Parameter(Mandatory)][string]$Uri, + [Parameter(Mandatory)][string]$Destination + ) + + $curl = Get-Command curl.exe -ErrorAction SilentlyContinue + if ($null -eq $curl) { + throw "curl.exe is required to download fonts." + } + + & $curl.Source --fail --silent --show-error --location $Uri --output $Destination + if ($LASTEXITCODE -ne 0) { + throw "Font download failed from $Uri (exit code $LASTEXITCODE)." + } +} + +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-LatestReleaseAssetUrl { + param( + [Parameter(Mandatory)][string]$Repository, + [Parameter(Mandatory)][string]$AssetPattern + ) + + $releaseJson = [IO.Path]::GetTempFileName() + try { + Invoke-FontDownload -Uri "https://api.github.com/repos/$Repository/releases/latest" -Destination $releaseJson + $document = [Text.Json.JsonDocument]::Parse([IO.File]::ReadAllText($releaseJson)) + try { + foreach ($asset in $document.RootElement.GetProperty("assets").EnumerateArray()) { + $name = $asset.GetProperty("name").GetString() + if ($name -match $AssetPattern) { + return $asset.GetProperty("browser_download_url").GetString() + } + } + } + finally { + $document.Dispose() + } + } + finally { + Remove-Item -LiteralPath $releaseJson -Force -ErrorAction SilentlyContinue + } + + throw "Unable to locate release asset '$AssetPattern' in $Repository." +} + 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"))) { - $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 (@(Get-ChildItem -LiteralPath $fonts -Filter "CascadiaCode*.ttf" -File -ErrorAction SilentlyContinue).Count -eq 0) { + $assetUrl = Get-LatestReleaseAssetUrl -Repository "microsoft/cascadia-code" -AssetPattern '^CascadiaCode-.*\.zip$' + $archive = Join-Path $fonts "CascadiaCode.zip" + $extract = Join-Path $fonts ".extract-CascadiaCode" + + try { + Write-Host "Downloading Cascadia Code..." -ForegroundColor Cyan + Invoke-FontDownload -Uri $assetUrl -Destination $archive + Expand-FontArchive -Archive $archive -Destination $extract + Remove-Item -Recurse -Force (Join-Path $extract "ttf\static") -ErrorAction SilentlyContinue - $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 + $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-FontDownload -Uri "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/$($font.Asset).tar.xz" -Destination $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 @("CascadiaCode*.ttf", "CaskaydiaCove*.ttf", "CaskaydiaMono*.ttf")) { + 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 { @@ -357,10 +485,8 @@ function Configure-Wsl { Refresh-Path Check-RequiredApps -if (-not $NonInteractive) { - Download-Fonts - Install-UserFonts -} +Download-Fonts +Install-UserFonts Configure-Git Install-MustHaveApps @@ -370,7 +496,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 diff --git a/tests/static/validate-architecture.py b/tests/static/validate-architecture.py index ebfa350..0bca84b 100644 --- a/tests/static/validate-architecture.py +++ b/tests/static/validate-architecture.py @@ -47,6 +47,7 @@ def read_text(path: str) -> str: "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(): diff --git a/tests/windows/README.md b/tests/windows/README.md new file mode 100644 index 0000000..5fc1824 --- /dev/null +++ b/tests/windows/README.md @@ -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. diff --git a/tests/windows/assert-fonts.ps1 b/tests/windows/assert-fonts.ps1 new file mode 100644 index 0000000..e370a0c --- /dev/null +++ b/tests/windows/assert-fonts.ps1 @@ -0,0 +1,69 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)][string]$RepoRoot +) + +$ErrorActionPreference = "Stop" + +function Fail([string]$Message) { + throw "ASSERTION FAILED: $Message" +} + +$sourceDir = Join-Path $RepoRoot "fonts" +$userFontsDir = Join-Path $env:LOCALAPPDATA "Microsoft\Windows\Fonts" +$fontRegistrySubKey = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" + +if (-not (Test-Path -LiteralPath $sourceDir -PathType Container)) { + Fail "font source directory is missing: $sourceDir" +} +if (-not (Test-Path -LiteralPath $userFontsDir -PathType Container)) { + Fail "current-user font directory is missing: $userFontsDir" +} + +$sourceFonts = @(Get-ChildItem -Path $sourceDir -Include *.otc,*.otf,*.ttc,*.ttf -Recurse -File | Sort-Object Name -Unique) +if ($sourceFonts.Count -eq 0) { + Fail "no downloaded fonts were found in $sourceDir" +} + +foreach ($requiredPattern in @("CascadiaCode*.ttf", "CaskaydiaCove*.ttf", "CaskaydiaMono*.ttf")) { + if (@($sourceFonts | Where-Object Name -Like $requiredPattern).Count -eq 0) { + Fail "expected font family is missing from the downloaded source: $requiredPattern" + } +} + +$registryKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($fontRegistrySubKey, $false) +if ($null -eq $registryKey) { + Fail "current-user font registry key is missing: HKCU\$fontRegistrySubKey" +} + +try { + foreach ($font in $sourceFonts) { + $destination = Join-Path $userFontsDir $font.Name + if (-not (Test-Path -LiteralPath $destination -PathType Leaf)) { + Fail "font was not installed for the current user: $destination" + } + + $registryName = "$($font.Name) (dotfiles)" + $registeredPath = $registryKey.GetValue( + $registryName, + $null, + [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames + ) + if ($null -eq $registeredPath) { + Fail "font registry entry is missing: $registryName" + } + + if (-not [string]::Equals( + [IO.Path]::GetFullPath([string]$registeredPath), + [IO.Path]::GetFullPath($destination), + [StringComparison]::OrdinalIgnoreCase + )) { + Fail "font registry entry '$registryName' points to '$registeredPath' instead of '$destination'" + } + } +} +finally { + $registryKey.Dispose() +} + +Write-Host "Windows font assertions passed for $($sourceFonts.Count) installed font files." From 8c9ed9bb2b78008313c41b33da1c2ca7fb4f4fa2 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:19:45 -0500 Subject: [PATCH 2/6] Simplify Windows font provisioning --- install.ps1 | 64 +++++++-------------------- tests/static/validate-architecture.py | 1 + tests/windows/assert-fonts.ps1 | 10 ++++- 3 files changed, 26 insertions(+), 49 deletions(-) diff --git a/install.ps1 b/install.ps1 index ad35064..f323d94 100644 --- a/install.ps1 +++ b/install.ps1 @@ -281,23 +281,6 @@ function Install-OptionalApps { Refresh-Path } -function Invoke-FontDownload { - param( - [Parameter(Mandatory)][string]$Uri, - [Parameter(Mandatory)][string]$Destination - ) - - $curl = Get-Command curl.exe -ErrorAction SilentlyContinue - if ($null -eq $curl) { - throw "curl.exe is required to download fonts." - } - - & $curl.Source --fail --silent --show-error --location $Uri --output $Destination - if ($LASTEXITCODE -ne 0) { - throw "Font download failed from $Uri (exit code $LASTEXITCODE)." - } -} - function Expand-FontArchive { param( [Parameter(Mandatory)][string]$Archive, @@ -318,33 +301,15 @@ function Expand-FontArchive { } } -function Get-LatestReleaseAssetUrl { - param( - [Parameter(Mandatory)][string]$Repository, - [Parameter(Mandatory)][string]$AssetPattern - ) - - $releaseJson = [IO.Path]::GetTempFileName() - try { - Invoke-FontDownload -Uri "https://api.github.com/repos/$Repository/releases/latest" -Destination $releaseJson - $document = [Text.Json.JsonDocument]::Parse([IO.File]::ReadAllText($releaseJson)) - try { - foreach ($asset in $document.RootElement.GetProperty("assets").EnumerateArray()) { - $name = $asset.GetProperty("name").GetString() - if ($name -match $AssetPattern) { - return $asset.GetProperty("browser_download_url").GetString() - } - } - } - finally { - $document.Dispose() - } - } - finally { - Remove-Item -LiteralPath $releaseJson -Force -ErrorAction SilentlyContinue +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." } - throw "Unable to locate release asset '$AssetPattern' in $Repository." + return @(Get-Content -LiteralPath $manifest | Where-Object { + $_.Trim() -and -not $_.Trim().StartsWith("#") + } | ForEach-Object { $_.Trim() }) } function Download-Fonts { @@ -352,13 +317,17 @@ function Download-Fonts { New-Item -ItemType Directory -Force -Path $fonts | Out-Null if (@(Get-ChildItem -LiteralPath $fonts -Filter "CascadiaCode*.ttf" -File -ErrorAction SilentlyContinue).Count -eq 0) { - $assetUrl = Get-LatestReleaseAssetUrl -Repository "microsoft/cascadia-code" -AssetPattern '^CascadiaCode-.*\.zip$' + $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." + } + $archive = Join-Path $fonts "CascadiaCode.zip" $extract = Join-Path $fonts ".extract-CascadiaCode" - try { Write-Host "Downloading Cascadia Code..." -ForegroundColor Cyan - Invoke-FontDownload -Uri $assetUrl -Destination $archive + 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 @@ -384,10 +353,9 @@ function Download-Fonts { $archive = Join-Path $fonts "$($font.Asset).tar.xz" $extract = Join-Path $fonts ".extract-$($font.Asset)" - try { Write-Host "Downloading $($font.Name)..." -ForegroundColor Cyan - Invoke-FontDownload -Uri "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/$($font.Asset).tar.xz" -Destination $archive + 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) @@ -402,7 +370,7 @@ function Download-Fonts { } } - foreach ($requiredPattern in @("CascadiaCode*.ttf", "CaskaydiaCove*.ttf", "CaskaydiaMono*.ttf")) { + 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" } diff --git a/tests/static/validate-architecture.py b/tests/static/validate-architecture.py index 0bca84b..19da9f6 100644 --- a/tests/static/validate-architecture.py +++ b/tests/static/validate-architecture.py @@ -41,6 +41,7 @@ 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", diff --git a/tests/windows/assert-fonts.ps1 b/tests/windows/assert-fonts.ps1 index e370a0c..491eb3c 100644 --- a/tests/windows/assert-fonts.ps1 +++ b/tests/windows/assert-fonts.ps1 @@ -12,6 +12,7 @@ function Fail([string]$Message) { $sourceDir = Join-Path $RepoRoot "fonts" $userFontsDir = Join-Path $env:LOCALAPPDATA "Microsoft\Windows\Fonts" $fontRegistrySubKey = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" +$manifest = Join-Path $RepoRoot "scripts\windows\managed-fonts.txt" if (-not (Test-Path -LiteralPath $sourceDir -PathType Container)) { Fail "font source directory is missing: $sourceDir" @@ -19,13 +20,20 @@ if (-not (Test-Path -LiteralPath $sourceDir -PathType Container)) { if (-not (Test-Path -LiteralPath $userFontsDir -PathType Container)) { Fail "current-user font directory is missing: $userFontsDir" } +if (-not (Test-Path -LiteralPath $manifest -PathType Leaf)) { + Fail "managed font manifest is missing: $manifest" +} $sourceFonts = @(Get-ChildItem -Path $sourceDir -Include *.otc,*.otf,*.ttc,*.ttf -Recurse -File | Sort-Object Name -Unique) if ($sourceFonts.Count -eq 0) { Fail "no downloaded fonts were found in $sourceDir" } -foreach ($requiredPattern in @("CascadiaCode*.ttf", "CaskaydiaCove*.ttf", "CaskaydiaMono*.ttf")) { +$requiredPatterns = @(Get-Content -LiteralPath $manifest | Where-Object { + $_.Trim() -and -not $_.Trim().StartsWith("#") +} | ForEach-Object { $_.Trim() }) + +foreach ($requiredPattern in $requiredPatterns) { if (@($sourceFonts | Where-Object Name -Like $requiredPattern).Count -eq 0) { Fail "expected font family is missing from the downloaded source: $requiredPattern" } From 7f31eebc1beb5fad8ba10e3b2318ca5e3ac7567f Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:20:28 -0500 Subject: [PATCH 3/6] Add managed Windows font manifest --- scripts/windows/managed-fonts.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 scripts/windows/managed-fonts.txt diff --git a/scripts/windows/managed-fonts.txt b/scripts/windows/managed-fonts.txt new file mode 100644 index 0000000..24504df --- /dev/null +++ b/scripts/windows/managed-fonts.txt @@ -0,0 +1,4 @@ +# Expected filename globs for the Windows font baseline. +CascadiaCode*.ttf +CaskaydiaCove*.ttf +CaskaydiaMono*.ttf From 05fe57a48bc2e970fc940fe498cb26dcb70282ac Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:23:34 -0500 Subject: [PATCH 4/6] Resolve font assertions from chezmoi source --- tests/windows/assert-fonts.ps1 | 45 +++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/tests/windows/assert-fonts.ps1 b/tests/windows/assert-fonts.ps1 index 491eb3c..a843fe5 100644 --- a/tests/windows/assert-fonts.ps1 +++ b/tests/windows/assert-fonts.ps1 @@ -9,14 +9,36 @@ function Fail([string]$Message) { throw "ASSERTION FAILED: $Message" } -$sourceDir = Join-Path $RepoRoot "fonts" +function Resolve-ChezmoiRepoRoot { + $sourcePath = ((& chezmoi.exe source-path 2>&1) -join "").Trim() + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($sourcePath)) { + Fail "unable to resolve chezmoi source path" + } + + foreach ($candidate in @( + $sourcePath, + [IO.Path]::GetDirectoryName($sourcePath) + )) { + if ([string]::IsNullOrWhiteSpace($candidate)) { + continue + } + if ( + (Test-Path -LiteralPath (Join-Path $candidate "install.ps1") -PathType Leaf) -and + (Test-Path -LiteralPath (Join-Path $candidate "fonts") -PathType Container) + ) { + return [IO.Path]::GetFullPath($candidate) + } + } + + Fail "unable to resolve the chezmoi working tree containing downloaded fonts from '$sourcePath'" +} + +$sourceRepoRoot = Resolve-ChezmoiRepoRoot +$sourceDir = Join-Path $sourceRepoRoot "fonts" $userFontsDir = Join-Path $env:LOCALAPPDATA "Microsoft\Windows\Fonts" $fontRegistrySubKey = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" $manifest = Join-Path $RepoRoot "scripts\windows\managed-fonts.txt" -if (-not (Test-Path -LiteralPath $sourceDir -PathType Container)) { - Fail "font source directory is missing: $sourceDir" -} if (-not (Test-Path -LiteralPath $userFontsDir -PathType Container)) { Fail "current-user font directory is missing: $userFontsDir" } @@ -24,14 +46,19 @@ if (-not (Test-Path -LiteralPath $manifest -PathType Leaf)) { Fail "managed font manifest is missing: $manifest" } -$sourceFonts = @(Get-ChildItem -Path $sourceDir -Include *.otc,*.otf,*.ttc,*.ttf -Recurse -File | Sort-Object Name -Unique) +$sourceFonts = @( + Get-ChildItem -Path $sourceDir -Include *.otc,*.otf,*.ttc,*.ttf -Recurse -File | + Sort-Object Name -Unique +) if ($sourceFonts.Count -eq 0) { Fail "no downloaded fonts were found in $sourceDir" } -$requiredPatterns = @(Get-Content -LiteralPath $manifest | Where-Object { - $_.Trim() -and -not $_.Trim().StartsWith("#") -} | ForEach-Object { $_.Trim() }) +$requiredPatterns = @( + Get-Content -LiteralPath $manifest | + Where-Object { $_.Trim() -and -not $_.Trim().StartsWith("#") } | + ForEach-Object { $_.Trim() } +) foreach ($requiredPattern in $requiredPatterns) { if (@($sourceFonts | Where-Object Name -Like $requiredPattern).Count -eq 0) { @@ -74,4 +101,4 @@ finally { $registryKey.Dispose() } -Write-Host "Windows font assertions passed for $($sourceFonts.Count) installed font files." +Write-Host "Windows font assertions passed for $($sourceFonts.Count) installed font files from $sourceRepoRoot." From cbc6d0723af55444f324ed8d026fe5ed21f4791f Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:23:43 -0500 Subject: [PATCH 5/6] Document Windows font baseline --- docs/windows-fonts.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/windows-fonts.md diff --git a/docs/windows-fonts.md b/docs/windows-fonts.md new file mode 100644 index 0000000..0da4ca3 --- /dev/null +++ b/docs/windows-fonts.md @@ -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`. From f2d623592d2bf8924aca131fadb2adc73125bf63 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:25:29 -0500 Subject: [PATCH 6/6] Restore proven AllSigned-safe font provisioning