Skip to content

feat: add functions for oh-my-posh command invocation and prompt cont… #18

feat: add functions for oh-my-posh command invocation and prompt cont…

feat: add functions for oh-my-posh command invocation and prompt cont… #18

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
lint:
runs-on: windows-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install PSScriptAnalyzer
shell: pwsh
run: Install-Module -Name PSScriptAnalyzer -RequiredVersion 1.24.0 -Force -Scope CurrentUser
- name: Run PSScriptAnalyzer
shell: pwsh
run: |
$results = Invoke-ScriptAnalyzer -Path . -Recurse -ExcludeRule @(
'PSAvoidUsingWriteHost' # Profile needs colored console output
'PSAvoidUsingWMICmdlet' # PS5 compatibility path in uptime
'PSUseShouldProcessForStateChangingFunctions' # Overkill for a profile
'PSUseBOMForUnicodeEncodedFile' # Not needed
'PSReviewUnusedParameter' # Scriptblock params required by completer API
'PSUseSingularNouns' # Style preference
) | Where-Object { $_.ScriptName -ne 'ci-functional.ps1' }
# ci-functional.ps1 is excluded: it intentionally calls profile aliases (gc, ls, cat, uptime,
# eventlog) by their short names to exercise the profile's own function definitions.
$results | Format-Table -AutoSize
if ($results | Where-Object Severity -in 'Error','Warning') {
Write-Error "PSScriptAnalyzer found warnings or errors"
exit 1
}
- name: Smoke-test profile (non-interactive)
shell: pwsh
run: |
$env:CI = 'true'
pwsh -NonInteractive -NoProfile -Command ". '${{ github.workspace }}/Microsoft.PowerShell_profile.ps1'"
if ($LASTEXITCODE -ne 0) {
Write-Error "Profile failed to load non-interactive (exit code: $LASTEXITCODE)"
exit 1
}
Write-Host "Profile loaded successfully in non-interactive mode." -ForegroundColor Green
- name: PS5 parse-check (all .ps1 files)
shell: powershell
run: |
Write-Host "PS5 version: $($PSVersionTable.PSVersion)" -ForegroundColor Cyan
$errors = 0
$files = Get-ChildItem -Path '${{ github.workspace }}' -Filter *.ps1 -Recurse
foreach ($file in $files) {
$tokens = $null; $parseErrors = $null
[System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) | Out-Null
if ($parseErrors.Count -gt 0) {
Write-Host "FAIL: $($file.FullName)" -ForegroundColor Red
foreach ($pe in $parseErrors) {
Write-Host " Line $($pe.Extent.StartLineNumber) Col $($pe.Extent.StartColumnNumber): $($pe.Message)" -ForegroundColor Red
Write-Host " Text: $($pe.Extent.Text)" -ForegroundColor Yellow
}
$errors++
} else {
Write-Host "OK: $($file.Name)" -ForegroundColor Green
}
}
if ($errors -gt 0) { exit 1 }
- name: Check for hardcoded paths
shell: pwsh
run: |
# Regex patterns for matching hardcoded user paths (used directly with -match, no extra escaping)
$patterns = @(
'C:\\Users\\'
'C:/Users/'
'/home/'
'\\\\Users\\\\'
)
$files = Get-ChildItem -Recurse -Include *.ps1 | Where-Object { $_.FullName -notlike '*\.git\*' }
$found = @()
foreach ($file in $files) {
$lines = Get-Content $file.FullName -ErrorAction SilentlyContinue
for ($i = 0; $i -lt $lines.Count; $i++) {
foreach ($pattern in $patterns) {
if ($lines[$i] -match $pattern) {
$found += [PSCustomObject]@{
File = $file.Name
Line = $i + 1
Match = $lines[$i].Trim()
}
}
}
}
}
if ($found) {
$found | Format-Table -AutoSize
Write-Error "Hardcoded user paths found"
exit 1
}
Write-Host "No hardcoded paths found." -ForegroundColor Green
- name: Check for non-ASCII characters in scripts
shell: pwsh
run: |
$files = Get-ChildItem -Path '${{ github.workspace }}' -Filter *.ps1 -Recurse
$found = @()
foreach ($file in $files) {
$lines = Get-Content $file.FullName -ErrorAction SilentlyContinue
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match '[^\x00-\x7E]') {
$found += [PSCustomObject]@{
File = $file.Name
Line = $i + 1
Match = $lines[$i].Trim()
}
}
}
}
if ($found) {
$found | Format-Table -AutoSize
Write-Error "Non-ASCII characters found in .ps1 files (em dashes, smart quotes, etc. break PS5)"
exit 1
}
Write-Host "No non-ASCII characters found." -ForegroundColor Green
- name: Check for UTF-8 BOM
shell: pwsh
run: |
$files = Get-ChildItem -Recurse -Include *.ps1,*.json | Where-Object { $_.FullName -notlike '*\.git\*' }
$found = @()
foreach ($file in $files) {
$bytes = [System.IO.File]::ReadAllBytes($file.FullName)
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
$found += $file.Name
}
}
if ($found) {
$found | ForEach-Object { Write-Host "BOM found: $_" -ForegroundColor Red }
Write-Error "UTF-8 BOM detected. Use [System.IO.File]::WriteAllText() with UTF8Encoding(false)"
exit 1
}
Write-Host "No UTF-8 BOM found." -ForegroundColor Green
- name: Check for Set-Content -Encoding UTF8
shell: pwsh
run: |
$files = Get-ChildItem -Recurse -Include *.ps1 | Where-Object { $_.FullName -notlike '*\.git\*' }
$found = @()
foreach ($file in $files) {
$lines = Get-Content $file.FullName -ErrorAction SilentlyContinue
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match 'Set-Content\s.*-Encoding\s+UTF8' -and $lines[$i] -notmatch '^\s*#') {
$found += [PSCustomObject]@{
File = $file.Name
Line = $i + 1
Match = $lines[$i].Trim()
}
}
}
}
if ($found) {
$found | Format-Table -AutoSize
Write-Error "Set-Content -Encoding UTF8 writes BOM on PS5. Use [System.IO.File]::WriteAllText() instead"
exit 1
}
Write-Host "No Set-Content -Encoding UTF8 found." -ForegroundColor Green
- name: Check for secrets
shell: pwsh
run: |
$patterns = @(
'(?i)(api[_-]?key|apikey)\s*[:=]\s*[''"][A-Za-z0-9+/=]{16,}[''"]'
'(?i)(secret|token|password)\s*[:=]\s*[''"][^''"]{8,}[''"]'
'(?i)(aws_access_key_id|aws_secret_access_key)\s*[:=]'
'ghp_[A-Za-z0-9]{36}'
'github_pat_[A-Za-z0-9_]{82}'
'sk-[A-Za-z0-9]{32,}'
'(?i)connectionstring\s*[:=]\s*[''"]Server='
)
$files = Get-ChildItem -Recurse -Include *.ps1,*.md,*.yml,*.json | Where-Object { $_.FullName -notlike '*\.git\*' -and $_.FullName -notlike '*\.github\workflows\*' }
$found = @()
foreach ($file in $files) {
$content = Get-Content $file.FullName -Raw -ErrorAction SilentlyContinue
if (-not $content) { continue }
foreach ($pattern in $patterns) {
if ($content -match $pattern) {
$found += [PSCustomObject]@{
File = $file.Name
Pattern = $pattern.Substring(0, [Math]::Min(40, $pattern.Length)) + '...'
}
}
}
}
if ($found) {
$found | Format-Table -AutoSize
Write-Error "Potential secrets found"
exit 1
}
Write-Host "No secrets found." -ForegroundColor Green
install-flow:
runs-on: windows-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate JSON configs parse correctly
shell: pwsh
run: |
$errors = 0
foreach ($file in @('theme.json', 'terminal-config.json')) {
try {
$null = Get-Content $file -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
Write-Host "OK: $file" -ForegroundColor Green
} catch {
Write-Host "FAIL: $file - $_" -ForegroundColor Red
$errors++
}
}
if ($errors -gt 0) { exit 1 }
- name: Validate config schema (required keys)
shell: pwsh
run: |
$theme = Get-Content 'theme.json' -Raw | ConvertFrom-Json
$terminal = Get-Content 'terminal-config.json' -Raw | ConvertFrom-Json
$errors = @()
# theme.json must have theme.name and theme.url
if (-not $theme.theme.name) { $errors += 'theme.json: missing theme.name' }
if (-not $theme.theme.url) { $errors += 'theme.json: missing theme.url' }
# terminal-config.json must have defaults and fontInstall
if (-not $terminal.defaults) { $errors += 'terminal-config.json: missing defaults' }
if (-not $terminal.fontInstall) { $errors += 'terminal-config.json: missing fontInstall' }
if (-not $terminal.fontInstall.name) { $errors += 'terminal-config.json: missing fontInstall.name' }
if (-not $terminal.fontInstall.displayName) { $errors += 'terminal-config.json: missing fontInstall.displayName' }
if (-not $terminal.fontInstall.version) { $errors += 'terminal-config.json: missing fontInstall.version' }
if ($errors.Count -gt 0) {
$errors | ForEach-Object { Write-Host "FAIL: $_" -ForegroundColor Red }
exit 1
}
Write-Host "Config schema valid." -ForegroundColor Green
- name: Dry-run setup.ps1 (parse + function definitions)
shell: pwsh
run: |
# Source setup.ps1 functions without running the main install flow
# by extracting and testing the helper functions in isolation
$content = Get-Content 'setup.ps1' -Raw
# Verify required functions are defined in setup.ps1
$requiredFunctions = @('Test-InternetConnection', 'Install-NerdFonts', 'Install-OhMyPoshTheme', 'Install-WingetPackage', 'Merge-JsonObject', 'Select-PreferredEditor', 'Invoke-DownloadWithRetry')
$errors = 0
foreach ($fn in $requiredFunctions) {
if ($content -notmatch "function\s+$fn\b") {
Write-Host "FAIL: setup.ps1 missing function $fn" -ForegroundColor Red
$errors++
} else {
Write-Host "OK: $fn defined" -ForegroundColor Green
}
}
if ($errors -gt 0) { exit 1 }
- name: Test Merge-JsonObject logic
shell: pwsh
run: |
# Extract Merge-JsonObject from setup.ps1 and test it (with null guard like production)
function Merge-JsonObject($base, $override) {
if (-not $base -or -not $override) { return }
foreach ($prop in $override.PSObject.Properties) {
$baseVal = $base.PSObject.Properties[$prop.Name]
if ($baseVal -and $baseVal.Value -is [PSCustomObject] -and $prop.Value -is [PSCustomObject]) {
Merge-JsonObject $baseVal.Value $prop.Value
} else {
$base | Add-Member -NotePropertyName $prop.Name -NotePropertyValue $prop.Value -Force
}
}
}
# Test 1: flat merge
$base = [PSCustomObject]@{ a = 1; b = 2 }
$override = [PSCustomObject]@{ b = 99; c = 3 }
Merge-JsonObject $base $override
if ($base.a -ne 1 -or $base.b -ne 99 -or $base.c -ne 3) {
Write-Error "Flat merge failed: $($base | ConvertTo-Json -Compress)"
exit 1
}
Write-Host "OK: flat merge" -ForegroundColor Green
# Test 2: deep merge preserves nested keys
$base = [PSCustomObject]@{ font = [PSCustomObject]@{ face = "Consolas"; size = 11 }; opacity = 75 }
$override = [PSCustomObject]@{ font = [PSCustomObject]@{ size = 14 } }
Merge-JsonObject $base $override
if ($base.font.face -ne "Consolas" -or $base.font.size -ne 14 -or $base.opacity -ne 75) {
Write-Error "Deep merge failed: $($base | ConvertTo-Json -Compress)"
exit 1
}
Write-Host "OK: deep merge" -ForegroundColor Green
# Test 3: override replaces non-object with object
$base = [PSCustomObject]@{ theme = "simple" }
$override = [PSCustomObject]@{ theme = [PSCustomObject]@{ name = "pure" } }
Merge-JsonObject $base $override
if ($base.theme.name -ne "pure") {
Write-Error "Object replacement failed: $($base | ConvertTo-Json -Compress)"
exit 1
}
Write-Host "OK: object replacement" -ForegroundColor Green
# Test 4: null base or override returns without throwing (matches production guard)
$base = [PSCustomObject]@{ a = 1 }
Merge-JsonObject $null $base
Merge-JsonObject $base $null
if ($base.a -ne 1) { Write-Error "Null guard should not modify base"; exit 1 }
Write-Host "OK: null guard" -ForegroundColor Green
- name: Test WT settings merge (mock)
shell: pwsh
run: |
# Simulate the WT settings update flow with mock data
$mockWt = [PSCustomObject]@{
profiles = [PSCustomObject]@{
defaults = [PSCustomObject]@{ font = [PSCustomObject]@{ face = "Consolas"; size = 10 } }
list = @()
}
schemes = @()
actions = @()
}
$theme = Get-Content 'theme.json' -Raw | ConvertFrom-Json
$terminal = Get-Content 'terminal-config.json' -Raw | ConvertFrom-Json
$defaults = $mockWt.profiles.defaults
# Apply terminal defaults
$terminal.defaults.PSObject.Properties | ForEach-Object {
$defaults | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.Value -Force
}
# Apply theme colors
if ($theme.windowsTerminal.colorScheme) {
$defaults | Add-Member -NotePropertyName "colorScheme" -NotePropertyValue $theme.windowsTerminal.colorScheme -Force
}
# Verify results (read expected values from config, not hardcoded)
$errors = 0
if ($defaults.opacity -ne $terminal.defaults.opacity) { Write-Host "FAIL: opacity=$($defaults.opacity), expected $($terminal.defaults.opacity)" -ForegroundColor Red; $errors++ }
if ($defaults.colorScheme -ne $theme.windowsTerminal.colorScheme) { Write-Host "FAIL: colorScheme=$($defaults.colorScheme), expected $($theme.windowsTerminal.colorScheme)" -ForegroundColor Red; $errors++ }
if ($defaults.font.face -ne $terminal.defaults.font.face) { Write-Host "FAIL: font.face=$($defaults.font.face), expected $($terminal.defaults.font.face)" -ForegroundColor Red; $errors++ }
# Verify scheme upsert
$schemeDef = $theme.windowsTerminal.scheme
$mockWt.schemes = @(@($mockWt.schemes | Where-Object { $_ -and $_.name -ne $schemeDef.name }) + ([PSCustomObject]$schemeDef))
if ($mockWt.schemes.Count -ne 1 -or $mockWt.schemes[0].name -ne $schemeDef.name) {
Write-Host "FAIL: scheme upsert" -ForegroundColor Red; $errors++
}
# Verify keybinding upsert
foreach ($kb in $terminal.keybindings) {
$bindingId = "User.profile.$($kb.keys -replace '[^a-zA-Z0-9]', '')"
$mockWt.actions = @($mockWt.actions) + ([PSCustomObject]@{ keys = $kb.keys; command = $kb.command })
}
$firstKb = @($terminal.keybindings)[0]
$found = $mockWt.actions | Where-Object { $_.keys -eq $firstKb.keys }
if (-not $found -or $found.command -ne $firstKb.command) {
Write-Host "FAIL: keybinding upsert (expected keys=$($firstKb.keys) command=$($firstKb.command))" -ForegroundColor Red; $errors++
}
if ($errors -gt 0) { exit 1 }
# Verify JSON roundtrip
$json = $mockWt | ConvertTo-Json -Depth 10
$null = $json | ConvertFrom-Json -ErrorAction Stop
Write-Host "OK: WT settings merge + JSON roundtrip" -ForegroundColor Green
functional:
runs-on: windows-latest
needs: [lint, install-flow]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run functional profile tests
shell: pwsh
run: |
pwsh -NoProfile -File '${{ github.workspace }}/ci-functional.ps1'