Skip to content
Open
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
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
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 10
labels:
- dependencies
- github-actions
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
shell: pwsh
# Exit code 1 means violations were found; we still want the SARIF uploaded.
continue-on-error: true
run: |
$ErrorActionPreference = 'Stop'
Invoke-ScriptAnalyzer -Path . -Recurse -Settings PSScriptAnalyzerSettings.psd1 -Save 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
13 changes: 9 additions & 4 deletions Tests/theBrain/Format-TheBrainNotesYouTubeThumbnail.Tests.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Test for Format-TheBrainNotesYouTubeThumbnail.ps1
# Requires -Modules Pester

# NOTE: Must be top-level (not inside BeforeAll) so Pester Discovery phase
# can evaluate -Skip: expressions before BeforeAll runs.
$script:SkipAll = $PSEdition -eq 'Core'


BeforeAll {
# Path to the script being tested
$script:ScriptPath = Resolve-Path "$PSScriptRoot\..\..\theBrain\Format-TheBrainNotesYouTubeThumbnail.ps1"
Expand Down Expand Up @@ -33,7 +38,7 @@ AfterAll {
Remove-Item -Path $script:TestDrive.FullName -Recurse -Force
}

Describe 'Format-TheBrainNotesYouTubeThumbnail.ps1' {
Describe 'Format-TheBrainNotesYouTubeThumbnail.ps1' -Tag "DesktopOnly" {

BeforeEach {
# Reset all mocks before each test to ensure isolation
Expand All @@ -49,7 +54,7 @@ Describe 'Format-TheBrainNotesYouTubeThumbnail.ps1' {
Mock Convert-Path { return $Path } -Verifiable
}

It 'should find, back up, and replace a YouTube thumbnail link' {
It 'should find, back up, and replace a YouTube thumbnail link' -Skip:$script:SkipAll {
# Arrange
# This object simulates the output of Select-String with a found match
$MatchObject = @(
Expand Down Expand Up @@ -101,7 +106,7 @@ Describe 'Format-TheBrainNotesYouTubeThumbnail.ps1' {
}
}

It 'should do nothing if no matching links are found' {
It 'should do nothing if no matching links are found' -Skip:$script:SkipAll {
# Arrange
# Mock Select-String to return no matches
Mock Get-ChildItem -Verifiable
Expand All @@ -122,7 +127,7 @@ Describe 'Format-TheBrainNotesYouTubeThumbnail.ps1' {
}
}

It 'should handle errors during file operations' {
It 'should handle errors during file operations' -Skip:$script:SkipAll {
# Arrange
# Simulate a match being found, same as the happy path test
$MatchObject = @(
Expand Down
Loading
Loading