Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2e982a4
👷 ci: setup github actions and dependabot
chriskyfung Aug 7, 2026
ada25a8
💚 ci: simplify and split CI workflow jobs
chriskyfung Aug 7, 2026
fb763d8
💚 ci: fix PS engine control and SARIF scanning
chriskyfung Aug 7, 2026
eaf13b2
✅ test(pester): improve CI and environment skipping
chriskyfung Aug 7, 2026
42739c1
💚 ci: optimize CI workflow and error handling
chriskyfung Aug 7, 2026
947eeea
🧪 test(theBrain): add safety check to cleanup logic
chriskyfung Aug 7, 2026
99e2309
💚 fix(ci): allow PSScriptAnalyzer to fail without stopping CI
chriskyfung Aug 7, 2026
d723e14
✅ test(pester): move skip logic to top-level scope
chriskyfung Aug 7, 2026
9071f4c
💚 fix(ci): add PSSQLite module to setup action
chriskyfung Aug 7, 2026
a6c0da7
💚 fix(ci): replace ampersand with 'and' to avoid invalid character
chriskyfung Aug 7, 2026
4675c1f
✅ test(theBrain): improve error handling test case
chriskyfung Aug 7, 2026
190cdb9
🧪 test(theBrain): mock data directory path
chriskyfung Aug 7, 2026
fe2278f
💚 fix(ci): replace PSScriptAnalyzer script with action
chriskyfung Aug 9, 2026
7180bf0
🐛 fix: resolve PS 5.1 encoding issue by replacing non-ASCII checkmark…
chriskyfung Aug 9, 2026
980eb11
✅ test(theBrain): mock metadata path for independence
chriskyfung Aug 9, 2026
1140746
🐛 fix: replace ampersand with "and" in extension export header
chriskyfung Aug 9, 2026
17b6952
🔥 chore(ci): remove PowerShell 7 lint and test job
chriskyfung Aug 9, 2026
6654723
✨ feat(theBrain): add -DataDirectory parameter
chriskyfung Aug 9, 2026
4daf8c4
Revert "🔥 chore(ci): remove PowerShell 7 lint and test job"
chriskyfung Aug 10, 2026
6699ed7
🧪 test(windows): skip tests on PowerShell Core
chriskyfung Aug 10, 2026
295d761
🧪 test: refine test skip logic
chriskyfung Aug 10, 2026
7cae3e9
👷 chore(ci): update dependabot configuration
chriskyfung Aug 10, 2026
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
29 changes: 29 additions & 0 deletions .github/actions/setup-powershell-modules/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Setup PowerShell modules
description: Installs pinned versions of Pester and PSScriptAnalyzer for the CI jobs.

inputs:
shell:
description: Shell to run the installation in ('pwsh' or 'powershell')
required: false
default: pwsh

runs:
using: composite
steps:
- name: Install Pester 5.7.1
shell: ${{ inputs.shell }}
run: |
$ErrorActionPreference = 'Stop'
Install-Module -Name Pester -RequiredVersion 5.7.1 -Scope CurrentUser -Repository PSGallery -Force -SkipPublisherCheck

- name: Install PSScriptAnalyzer 1.25.0
shell: ${{ inputs.shell }}
run: |
$ErrorActionPreference = 'Stop'
Install-Module -Name PSScriptAnalyzer -RequiredVersion 1.25.0 -Scope CurrentUser -Repository PSGallery -Force -SkipPublisherCheck

- name: Install PSSQLite 1.1.0
shell: ${{ inputs.shell }}
run: |
$ErrorActionPreference = 'Stop'
Install-Module -Name PSSQLite -RequiredVersion 1.1.0 -Scope CurrentUser -Repository PSGallery -Force -SkipPublisherCheck
23 changes: 23 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

# Basic `dependabot.yml` file with
# minimum configuration for three package managers

version: 2
updates:
# Enable version updates for GitHub Actions
- package-ecosystem: "github-actions"
# Workflow files stored in the default location of `.github/workflows`
# You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`.
directory: /
schedule:
interval: weekly
groups:
github-actions:
patterns:
- "*"
commit-message:
prefix: "⬆️ chore(ci)"
102 changes: 102 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: CI

on:
push:
branches: [master]
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
lint-and-test-ps51:
name: Lint & Test (PowerShell 5.1, Windows 2022)
runs-on: windows-2022
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false

- name: Install pinned modules (PowerShell 5.1)
uses: ./.github/actions/setup-powershell-modules
with:
shell: powershell

# Build.ps1 spawns isolated test processes and prefers pwsh when available,
# so modules must also be present for pwsh.
- name: Install pinned modules (pwsh for isolated test processes)
uses: ./.github/actions/setup-powershell-modules
with:
shell: pwsh

- name: Run Build (PSScriptAnalyzer + Pester)
shell: powershell
# Force the Windows PowerShell 5.1 engine for isolated Pester test processes,
# because Build.ps1 prefers pwsh when it is available.
run: .\Build.ps1 -PowerShellExecutable powershell

lint-and-test-ps7:
name: Lint & Test (PowerShell 7, ${{ matrix.runner }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
runner: [windows-2022, windows-2025]
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false

- name: Install pinned PowerShell modules
uses: ./.github/actions/setup-powershell-modules
with:
shell: pwsh

- name: Run Build (PSScriptAnalyzer + Pester)
shell: pwsh
# Force the PowerShell Core engine for isolated Pester test processes.
run: .\Build.ps1 -PowerShellExecutable pwsh

psscriptanalyzer:
name: PSScriptAnalyzer SARIF
runs-on: windows-2025
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false

- name: Run PSScriptAnalyzer and save SARIF
uses: microsoft/psscriptanalyzer-action@v1.1
with:
path: .
recurse: true
settings: PSScriptAnalyzerSettings.psd1
output: analysis-results.sarif

- name: Upload SARIF to GitHub code scanning
uses: github/codeql-action/upload-sarif@9e3211c9a3b9311dfe05da2ed48eea3386f042dd
with:
sarif_file: analysis-results.sarif
category: "/language:powershell"

dependency-review:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false

- name: Dependency Review
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294
with:
fail-on-severity: high
allow-licenses: GPL-3.0-or-later, MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, Unlicense
45 changes: 37 additions & 8 deletions Build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,30 @@
PS C:\> .\Build.ps1
Runs all Pester tests and analyzes all PowerShell scripts in the project.

.EXAMPLE
PS C:\> .\Build.ps1 -PowerShellExecutable pwsh
Forces the PowerShell Core engine (pwsh) for isolated Pester test processes.

.EXAMPLE
PS C:\> .\Build.ps1 -PowerShellExecutable powershell
Forces the Windows PowerShell 5.1 engine (powershell.exe) for isolated Pester test processes.

.PARAMETER PowerShellExecutable
The PowerShell engine to use for isolated Pester test processes.
Valid values: 'auto' (default, picks pwsh if available, otherwise powershell), 'pwsh', or 'powershell'.

.NOTES
Version: 1.1.0
Version: 1.2.0
Author: chriskyfung, Gemini
License: GNU GPLv3 license
Creation Date: 2025-08-02
Last Modified: 2025-09-08
Last Modified: 2026-08-07
#>

param()
param(
[ValidateSet('auto', 'pwsh', 'powershell')]
[string]$PowerShellExecutable = 'auto'
)

$ErrorActionPreference = "Stop"

Expand All @@ -42,15 +57,30 @@ try {

# Determine the correct PowerShell executable to use for isolated processes
$executable = ''
if (Get-Command -Name 'pwsh' -ErrorAction SilentlyContinue) {
if ($PowerShellExecutable -eq 'pwsh') {
if (Get-Command -Name 'pwsh' -ErrorAction SilentlyContinue) {
$executable = 'pwsh'
}
else {
throw "Requested 'pwsh' for isolated Pester tests, but it is not available."
}
}
elseif ($PowerShellExecutable -eq 'powershell') {
if (Get-Command -Name 'powershell' -ErrorAction SilentlyContinue) {
$executable = 'powershell'
}
else {
throw "Requested 'powershell' for isolated Pester tests, but it is not available."
}
}
elseif (Get-Command -Name 'pwsh' -ErrorAction SilentlyContinue) {
$executable = 'pwsh'
}
elseif (Get-Command -Name 'powershell' -ErrorAction SilentlyContinue) {
$executable = 'powershell'
}
else {
Write-Error "Could not find 'pwsh' or 'powershell' executable to run isolated Pester tests."
exit 1
throw "Could not find 'pwsh' or 'powershell' executable to run isolated Pester tests."
}
Write-Host "Using '$executable' for isolated test execution."

Expand Down Expand Up @@ -80,8 +110,7 @@ try {
}

if ($overallResult.FailedCount -gt 0) {
Write-Error "$($overallResult.FailedCount) test file(s) contained failures."
exit 1
throw "$($overallResult.FailedCount) test file(s) contained failures."
}
}
else {
Expand Down
6 changes: 4 additions & 2 deletions Tests/Bluestacks/Optimize-BluestacksVEthernet.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
Tests for the Optimize-BluestacksVEthernet.ps1 script.
#>

Describe "Optimize-BluestacksVEthernet" -Tags "CI" {
# Must be top-level: Pester Discovery evaluates -Skip: before BeforeAll runs.
$script:SkipAll = ($PSEdition -eq 'Core') -or [bool]$env:CI

Describe "Optimize-BluestacksVEthernet" -Tags "CI", "DesktopOnly" {
BeforeAll {
# Get the absolute path to the script under test
$script:ScriptPath = Resolve-Path "$PSScriptRoot\..\..\Bluestacks\Optimize-BluestacksVEthernet.ps1"
}

It "Should run without errors" -Skip:(-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
It "Should run without errors" -Skip:($script:SkipAll -or -not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
# Code that requires admin permissions
Write-Host "Running test with administrative privileges..." -ForegroundColor Green
Mock Get-NetAdapter {
Expand Down
8 changes: 5 additions & 3 deletions Tests/OneNote/Find-OneNotePages.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
Tests for Find-OneNotePages.ps1
#>

Describe "Find-OneNotePages.ps1" {
# Must be top-level: Pester Discovery evaluates -Skip: before BeforeAll runs.
$script:SkipAll = [bool]$env:CI

Describe "Find-OneNotePages.ps1" -Tag "Integration" {
BeforeAll {
# Set the path to the script under test.
$script:ScriptPath = Resolve-Path "$PSScriptRoot\..\..\OneNote\Find-OneNotePages.ps1"
}

# This is an integration test that requires a running OneNote instance.
It "should return formatted output when pages are found" -Tag 'Integration' {
It "should return formatted output when pages are found" -Skip:$script:SkipAll {
$output = (& $script:ScriptPath -Query "MyNote" | Out-String).Trim()
$output | Should -Match "Test Notebook"
$output | Should -Match " > Test Section"
Expand All @@ -23,7 +25,7 @@ Describe "Find-OneNotePages.ps1" {
$output | Should -Match "URI : "
}

It "should return a warning when no pages are found" {
It "should return a warning when no pages are found" -Skip:$script:SkipAll {
$output = (& $script:ScriptPath -Query "NonExistentPage" | Out-String).Trim()
$output | Should -BeNullOrEmpty
$output = (& $script:ScriptPath -Query "NonExistentPage" 3>&1 | Out-String).Trim()
Expand Down
6 changes: 4 additions & 2 deletions Tests/OneNote/Out-OneNoteSections.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
Tests for Out-OneNoteSections.ps1
#>

Describe "Out-OneNoteSections.ps1" {
# Must be top-level: Pester Discovery evaluates -Skip: before BeforeAll runs.
$script:SkipAll = [bool]$env:CI

Describe "Out-OneNoteSections.ps1" -Tag "Integration" {
BeforeAll {
# Set the path to the script under test.
$script:ScriptPath = Resolve-Path "$PSScriptRoot\..\..\OneNote\Out-OneNoteSections.ps1"
}

Context "When OneNote has notebooks" {
It "should list all notebooks and their sections" {
It "should list all notebooks and their sections" -Skip:$script:SkipAll {
$output = (& $script:ScriptPath | Out-String).Trim()
$output | Should -Match "Archive"
$output | Should -Match "### Ideas"
Expand Down
2 changes: 1 addition & 1 deletion Tests/VSCode/Export-VSCodeExtensionList.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Describe "Export-VSCodeExtensionList.ps1" {

It "writes a header with generation timestamp and machine name" {
$content = Get-Content -Path $script:ExpectedOutputFile -Raw
$content | Should -Match "VS Code Profile & Extension Export"
$content | Should -Match "VS Code Profile and Extension Export"
$content | Should -Match "Generated:"
$content | Should -Match "Machine:\s+$env:COMPUTERNAME"
}
Expand Down
21 changes: 12 additions & 9 deletions Tests/VSCode/Export-VSCodeProfiles.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ extension export, settings backup, manifest generation, and WhatIf behavior.
#>

#Requires -Version 5.0
#Requires -PSEdition Desktop
#Requires -Module Pester

$script:SkipAll = $PSEdition -eq 'Core'

Describe "Export-VSCodeProfiles.ps1" {
BeforeAll {
# Use a temp directory to avoid polluting the user's Documents folder
Expand Down Expand Up @@ -55,26 +58,26 @@ echo ext2.vscode' -Encoding ASCII
}

Context "Parameter validation" {
It "accepts a custom OutputDirectory" {
It "accepts a custom OutputDirectory" -Skip:$script:SkipAll {
{ & $script:ScriptPath -OutputDirectory (Join-Path $script:TestRoot "out") -VSCodeUserDataPath (Join-Path $script:TestRoot "User") -CodeCommand $script:CodePath -WhatIf } | Should -Not -Throw
}

It "accepts a custom VSCodeUserDataPath" {
It "accepts a custom VSCodeUserDataPath" -Skip:$script:SkipAll {
{ & $script:ScriptPath -OutputDirectory (Join-Path $script:TestRoot "out2") -VSCodeUserDataPath (Join-Path $script:TestRoot "User") -CodeCommand $script:CodePath -WhatIf } | Should -Not -Throw
}

It "throws for invalid OutputDirectory characters" {
It "throws for invalid OutputDirectory characters" -Skip:$script:SkipAll {
{ & $script:ScriptPath -OutputDirectory "C:\bad|path" -WhatIf } |
Should -Throw "*OutputDirectory contains invalid path characters*"
}
}

Context "Prerequisite checks" {
It "throws when VSCodeUserDataPath does not exist" {
It "throws when VSCodeUserDataPath does not exist" -Skip:$script:SkipAll {
{ & $script:ScriptPath -VSCodeUserDataPath "C:\nonexistent\path" -WhatIf } | Should -Throw "VS Code user data directory not found:*"
}

It "throws when storage.json is missing" {
It "throws when storage.json is missing" -Skip:$script:SkipAll {
Remove-Item -Path (Join-Path $script:TestRoot "User\globalStorage\storage.json") -Force
try {
{ & $script:ScriptPath -VSCodeUserDataPath (Join-Path $script:TestRoot "User") -WhatIf } | Should -Throw "VS Code storage.json not found*"
Expand All @@ -84,15 +87,15 @@ echo ext2.vscode' -Encoding ASCII
}
}

It "throws when 'code' CLI is not in PATH" {
It "throws when 'code' CLI is not in PATH" -Skip:$script:SkipAll {
{ & $script:ScriptPath -VSCodeUserDataPath (Join-Path $script:TestRoot "User") `
-CodeCommand "nonexistent_code_binary_xyz" -WhatIf } |
Should -Throw "VS Code CLI*not available*"
}
}

Context "Profile discovery" {
It "discovers profiles from storage.json" {
It "discovers profiles from storage.json" -Skip:$script:SkipAll {
$outputDir = Join-Path $script:TestRoot "out_discover"
$output = (& $script:ScriptPath -OutputDirectory $outputDir -VSCodeUserDataPath (Join-Path $script:TestRoot "User") -CodeCommand $script:CodePath -WhatIf 6>&1) | Out-String

Expand All @@ -102,7 +105,7 @@ echo ext2.vscode' -Encoding ASCII
}

Context "Extension export" {
It "exports extensions and manifest using a custom CodeCommand" {
It "exports extensions and manifest using a custom CodeCommand" -Skip:$script:SkipAll {
$outputDir = Join-Path $script:TestRoot "out_export"

& $script:ScriptPath -OutputDirectory $outputDir -VSCodeUserDataPath (Join-Path $script:TestRoot "User") -CodeCommand $script:CodePath
Expand All @@ -115,7 +118,7 @@ echo ext2.vscode' -Encoding ASCII
}

Context "WhatIf behavior" {
It "does not create output files when -WhatIf is specified" {
It "does not create output files when -WhatIf is specified" -Skip:$script:SkipAll {
$outputDir = Join-Path $script:TestRoot "out_whatif"
{ & $script:ScriptPath -OutputDirectory $outputDir -VSCodeUserDataPath (Join-Path $script:TestRoot "User") -CodeCommand $script:CodePath -WhatIf } | Should -Not -Throw
$outputDir | Should -Not -Exist
Expand Down
Loading
Loading