Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
34bfebf
test: print returned objects in integration tests
mikemadeja Sep 19, 2026
ce3e0a5
test: seed real DNS query traffic before stats integration tests
mikemadeja Sep 19, 2026
b4b3688
fix: Get-PiHoleStatsTopDomain silently returned nothing by default
mikemadeja Sep 19, 2026
4bc12c8
Merge pull request #93 from mikemadeja/test/visible-integration-output
mikemadeja Sep 20, 2026
ef1ff87
feat: accept local DateTime for From/Until on database stats functions
mikemadeja Sep 20, 2026
caf13fd
feat: make From/Until optional on database stats functions, defaultin…
mikemadeja Sep 20, 2026
d7f9398
fix: add explicit RawOutput = $false default for consistency
mikemadeja Sep 20, 2026
7b4ddb9
feat: make Set-PiHoleDnsBlocking's TimeInSeconds a required parameter
mikemadeja Sep 20, 2026
e6d98c6
feat: restructure QueryType functions as Type/Count row arrays
mikemadeja Sep 20, 2026
b7d4bd7
docs: use a consistent, simpler placeholder password in examples
mikemadeja Sep 20, 2026
82a6bba
test: add integration tests for the remaining Get-* functions
mikemadeja Sep 20, 2026
ff504db
fix: replace TODO docstring on Get-PiHolePadd and fix Evicted typo
mikemadeja Sep 20, 2026
dc59fa9
fix: sync PowerShell Gallery release notes with GitHub's
mikemadeja Sep 20, 2026
2fbe771
feat: make Get-PiHoleConfig return the full config tree, PascalCased
mikemadeja Sep 20, 2026
00cb3dc
fix: session cleanup failures no longer fail the calling function
mikemadeja Sep 20, 2026
3143adc
Merge pull request #94 from mikemadeja/feat/datetime-params
mikemadeja Sep 20, 2026
2aed3de
fix: README sync workflow can't push to a now-protected develop
mikemadeja Sep 20, 2026
1a872fc
Merge pull request #96 from mikemadeja/fix/readme-sync-workflow
mikemadeja Sep 20, 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
31 changes: 23 additions & 8 deletions .github/workflows/CreateRelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ jobs:
git tag ${{ steps.bump.outputs.new_tag }}
git push origin ${{ steps.bump.outputs.new_tag }}

- name: Generate release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api "repos/${{ github.repository }}/releases/generate-notes" \
-f tag_name="${{ steps.bump.outputs.new_tag }}" \
-f previous_tag_name="${{ steps.get_tag.outputs.latest_tag }}" \
--jq .body > release-notes.md
cat release-notes.md

- name: Copy README into module folder
shell: pwsh
run: |
Expand Down Expand Up @@ -82,7 +92,7 @@ jobs:
with:
tag_name: ${{ steps.bump.outputs.new_tag }}
name: "Release ${{ steps.bump.outputs.new_tag }}"
generate_release_notes: true
body_path: release-notes.md
files: output/release.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -94,14 +104,19 @@ jobs:
$modulePath = "PiHoleShell" # Change to your actual module folder
$manifest = Get-ChildItem -Path $modulePath -Filter PiHoleShell.psd1 -Recurse | Select-Object -First 1

(Get-Content $($manifest.fullname)) -replace '0.0.0', ($ENV:TAG -replace "v", "") | Out-File $manifest.fullname
Write-Host "Replacing 0.0.0 with $ENV:NEW_TAG"

if (-not $manifest) {
throw "No module manifest (*.psd1) found in $modulePath"
}


# Surgical replace rather than Update-ModuleManifest, which rewrites and reformats
# the entire file (drops comments, changes array literal styles, etc.). The source
# manifest always has this exact static placeholder, since the version/notes bump
# here is never committed back to the repo.
$releaseNotes = (Get-Content -Path release-notes.md -Raw).Trim()
$releaseNotesEscaped = $releaseNotes -replace "'", "''"
$manifestContent = Get-Content -Path $manifest.FullName -Raw
$manifestContent = $manifestContent.Replace("'Initial release targeting PowerShell 7+'", "'$releaseNotesEscaped'")
Set-Content -Path $manifest.FullName -Value $manifestContent -NoNewline

Write-Host "Publishing module: $($manifest.FullName)"
Publish-Module -Path $manifest.DirectoryName -NuGetApiKey $apiKey -Verbose
env:
NEW_TAG: ${{ steps.bump.outputs.new_tag }}
Publish-Module -Path $manifest.DirectoryName -NuGetApiKey $apiKey -Verbose
22 changes: 7 additions & 15 deletions .github/workflows/SyncReadmeCommandReference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
branches: [ "main" ]

permissions:
contents: write
contents: read

jobs:
sync-readme:
Expand All @@ -20,18 +20,10 @@ jobs:
ref: ${{ github.head_ref }}
fetch-depth: 0

- name: Regenerate README command reference
# develop requires PRs for all changes, so this can't auto-commit/push a fix directly to
# develop (that used to work before branch protection was added, and now fails every time
# there's real drift to fix). Instead this just fails the check with instructions, the same
# way Invoke-ScriptAnalyzer already gates PSScriptAnalyzer.yml.
- name: Check README command reference is up to date
shell: pwsh
run: ./tools/Update-ReadmeCommandReference.ps1

- name: Commit changes if needed
run: |
if [ -n "$(git status --porcelain README.md)" ]; then
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add README.md
git commit -m "docs: sync README command reference [skip ci]"
git push origin HEAD:${{ github.head_ref }}
else
echo "README.md command reference already up to date."
fi
run: ./tools/Update-ReadmeCommandReference.ps1 -Check
57 changes: 56 additions & 1 deletion PiHoleShell/Private/Misc.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,57 @@ function Convert-LocalTimeToPiHoleUnixTime {
Write-Output $ObjectFinal
}

function ConvertTo-PiHolePascalCase {
#INTERNAL FUNCTION
param (
[string]$Name
)

if ([string]::IsNullOrEmpty($Name)) {
return $Name
}

$Segments = $Name -split '_' | Where-Object { $_.Length -gt 0 }
$PascalSegments = foreach ($Segment in $Segments) {
$Segment.Substring(0, 1).ToUpper() + $Segment.Substring(1)
}
return ($PascalSegments -join '')
}

function ConvertTo-PiHolePascalCaseObject {
#INTERNAL FUNCTION
#
# Recursively rebuilds an API response as nested PSCustomObjects/arrays with PascalCase
# property names (e.g. EXTERNAL_BLOCKED_IP / app_pwhash -> ExternalBlockedIp / AppPwhash),
# so deep/wide response trees don't need every field hardcoded by hand to be PowerShell
# object friendly - and so newly added API fields show up automatically instead of being
# silently dropped.
param (
[Parameter(ValueFromPipeline = $true)]
$InputObject
)
process {
if ($null -eq $InputObject) {
return $null
}

if ($InputObject -is [System.Management.Automation.PSCustomObject]) {
$Result = [ordered]@{}
foreach ($Prop in $InputObject.PSObject.Properties) {
$Key = ConvertTo-PiHolePascalCase -Name $Prop.Name
$Result[$Key] = ConvertTo-PiHolePascalCaseObject -InputObject $Prop.Value
}
return [PSCustomObject]$Result
}

if (($InputObject -is [System.Collections.IEnumerable]) -and ($InputObject -isnot [string])) {
return @($InputObject | ForEach-Object { ConvertTo-PiHolePascalCaseObject -InputObject $_ })
}

return $InputObject
}
}

function Remove-PiHoleCurrentAuthSession {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification = "It removes sessions from PiHole only")]
[CmdletBinding()]
Expand All @@ -68,6 +119,10 @@ function Remove-PiHoleCurrentAuthSession {
}

catch {
Write-Error -Message $_.Exception.Message
# Best-effort logout, called from every public function's finally block - a transient
# failure here (e.g. the server briefly unreachable right after a restart) must never
# fail the caller. Write-Error would do exactly that under $ErrorActionPreference =
# 'Stop', which Azure Pipelines' pwsh task sets by default.
Write-Warning -Message "Failed to close Pi-hole session: $($_.Exception.Message)"
}
}
2 changes: 1 addition & 1 deletion PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Set to $true to skip SSL certificate validation
This will dump the response instead of the formatted object

.EXAMPLE
Invoke-PiHoleFlushNetwork -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl"
Invoke-PiHoleFlushNetwork -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
#>
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/action/flush/network')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Flushes PiHole logs')]
Expand Down
2 changes: 1 addition & 1 deletion PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Set to $true to skip SSL certificate validation
This will dump the response instead of the formatted object

.EXAMPLE
Invoke-PiHoleRestartDns -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl"
Invoke-PiHoleRestartDns -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
#>
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/action/restartdns')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Restarts PiHole DNS')]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Ignore SSL when interacting with the PiHole API
This will dump the response instead of the formatted object

.EXAMPLE
Get-PiHoleCurrentAuthSession -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl"
Get-PiHoleCurrentAuthSession -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
#>
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/auth/sessions')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The API Password you generated from your PiHole server
Ignore SSL when interacting with the PiHole API

.EXAMPLE
Get-PiHoleCurrentAuthSession -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl"
Get-PiHoleCurrentAuthSession -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not change state')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
Expand Down
63 changes: 29 additions & 34 deletions PiHoleShell/Public/Config/Get-PiHoleConfig.ps1
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
function Get-PiHoleConfig {
<#
.SYNOPSIS
https://ftl.pi-hole.net/master/docs/#get-/config
Get current configuration of Pi-hole

.DESCRIPTION
Request Pi-hole's full configuration tree (dns, dhcp, ntp, resolver, database, webserver,
files, misc, and debug settings). The formatted output mirrors the API response as nested
objects with PascalCase property names, so the entire configuration is available for
inspection rather than a hand-picked subset.

.PARAMETER PiHoleServer
The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"

.PARAMETER Password
The API Password you generated from your PiHole server

.PARAMETER IgnoreSsl
Set to $true to skip SSL certificate validation

.PARAMETER RawOutput
This will dump the response instead of the formatted object

.EXAMPLE
Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"

.EXAMPLE
(Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password").Dns.Upstreams
#>
#Work In Progress
[CmdletBinding()]
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/config')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
param (
[Parameter(Mandatory = $true)]
Expand All @@ -15,6 +37,7 @@ https://ftl.pi-hole.net/master/docs/#get-/config
[bool]$IgnoreSsl = $false,
[bool]$RawOutput = $false
)

try {
$Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
$Params = @{
Expand All @@ -31,46 +54,18 @@ https://ftl.pi-hole.net/master/docs/#get-/config
Write-Output $Response
}
else {
$ObjectFinal = @()
$Dns = [PSCustomObject]@{
Upstreams = $Response.config.dns.upstreams
}

$Dhcp = [PSCustomObject]@{
Active = $Response.config.dhcp.active
Start = $Response.config.dhcp.start
End = $Response.config.dhcp.end
Hosts = $Response.config.dhcp.hosts
IgnoreUnknownClients = $Response.config.dhcp.ignoreUnknownClients
Ipv6 = $Response.config.dhcp.ipv6
LeaseTime = $Response.config.dhcp.leaseTime
Logging = $Response.config.dhcp.logging
MultiDNS = $Response.config.dhcp.multiDNS
Netmask = $Response.config.dhcp.netmask
RapidCommit = $Response.config.dhcp.rapidCommit
Router = $Response.config.dhcp.router
}

$Object = [PSCustomObject]@{
Dns = $Dns
Dhcp = $Dhcp
}

if ($Object) {
$ObjectFinal += $Object
}
Write-Output $ObjectFinal
$Object = ConvertTo-PiHolePascalCaseObject -InputObject $Response.config
Write-Output $Object
}
}

catch {
Write-Error -Message $_.Exception.Message
break
}

finally {
if ($Sid) {
Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Ignore SSL when interacting with the PiHole API
This will dump the response instead of the formatted object

.EXAMPLE
Get-PiHoleDnsBlockingStatus -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl"
Get-PiHoleDnsBlockingStatus -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
#>
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
param (
Expand Down
7 changes: 4 additions & 3 deletions PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ The API Password you generated from your PiHole server
True or False, if you set it to False when Blocking was set to true, it will disable blocking

.PARAMETER TimeInSeconds
How long should the opposite setting last, if you do not set a time, it will be set forever until you change it
How long the opposite setting should last, in seconds

.PARAMETER RawOutput
This will dump the response instead of the formatted object

.EXAMPLE
Set-PiHoleDnsBlocking -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -Blocking $false -TimeInSeconds 60
Set-PiHoleDnsBlocking -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Blocking $false -TimeInSeconds 60
#>
[CmdletBinding()]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not change state')]
Expand All @@ -31,7 +31,8 @@ Set-PiHoleDnsBlocking -PiHoleServer "http://pihole.domain.com:8080" -Password "f
[string]$Password,
[ValidateSet("True", "False")]
$Blocking,
[int]$TimeInSeconds = $null,
[Parameter(Mandatory = $true)]
[int]$TimeInSeconds,
[bool]$IgnoreSsl = $false,
[bool]$RawOutput = $false
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Set to $true to skip SSL certificate validation
This will dump the response instead of the formatted object

.EXAMPLE
Search-PiHoleListDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -Domain "doubleclick.net"
Search-PiHoleListDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Domain "doubleclick.net"
#>
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/search/-domain-')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
Expand Down
Loading
Loading