Skip to content
Closed
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
that passed before may now correctly fail.
- [**#96**](https://github.com/psake/PowerShellBuild/issues/96)
`Test-PSBuildScriptAnalysis` no longer fails with a path-resolution error
when `SettingsPath` is not supplied. An unsupplied path was forwarded to

Check warning on line 65 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (unsupplied) Suggestions: (unapplied, unsullied, unspoiled, unstapled, unsupported)
PSScriptAnalyzer as `-Settings ''`, which resolved against the current
directory and threw before any analysis ran, so the function's own
documented example could not run as written.
Expand All @@ -86,6 +86,16 @@
gating at all. The comparison is now strictly more permissive than before,
so a build that passed with a coverage threshold set still passes.

- [**#167**](https://github.com/psake/PowerShellBuild/issues/167)
Builds no longer hang when the HEAD commit message is large. `BuildHelpers`
populates `$env:BHCommitMessage` by running `git log` and waiting for git to
exit before reading its output, which deadlocks once git writes more than the
pipe buffer holds — git waits for the pipe to drain, `BuildHelpers` waits for
git, and the build stops with no output and no timeout. Windows has the
smaller buffer, so it hung there first. `Initialize-PSBuild` and
`build.properties.ps1` now read the commit message directly and hand it to
`BuildHelpers` through the environment, so the git call that hangs never runs.

## [0.8.2] 2026-07-08

### Fixed
Expand Down
156 changes: 156 additions & 0 deletions PowerShellBuild/Private/Invoke-SetBuildEnvironment.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
function Invoke-SetBuildEnvironment {
<#
.SYNOPSIS
Calls BuildHelpers\Set-BuildEnvironment without tripping its Invoke-Git deadlock.
.DESCRIPTION
BuildHelpers' Invoke-Git redirects git's output streams and then calls WaitForExit()
before reading them. When git writes more than the pipe buffer holds, git blocks waiting
for the pipe to drain while BuildHelpers blocks waiting for git to exit, and the build
hangs with no output and no timeout. The payload large enough to trigger it is the HEAD
commit message, which Get-BuildVariable reads with 'git log --format=%B -n 1'. Windows
has the smaller pipe buffer, so it hangs there first.

Get-BuildVariable decides where the commit message comes from with a switch over the
environment variable names it recognises. Some branches read a message straight out of a

Check warning on line 14 in PowerShellBuild/Private/Invoke-SetBuildEnvironment.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (recognises) Suggestions: (recognizes, recognize, recognizer, recognizers, recopies)
variable; the rest run git against a commit SHA held in a variable. This function makes
the first kind win:

1. The SHA variables whose branches shell out to git are removed for the duration of the
call, so none of those branches can be selected. The switch runs over an unordered
hashtable of variables, so removing them is the only way to make the choice
deterministic -- setting a competing variable is a coin toss.
2. The commit message is read here instead. PowerShell drains the pipe while the process
writes, so reading it cannot deadlock.
3. That message is published through the Azure Pipelines variable BuildHelpers consumes
verbatim, and removed again afterwards.

Nothing else keys off the borrowed variable: the build system, branch, build root, and
build number are each detected from different variables. The suppressed SHA variables are
also used to report the commit hash, so the hash BuildHelpers derives from HEAD is
replaced afterwards with the value the build system supplied.

See psake/PowerShellBuild#167. The defect is upstream in BuildHelpers, whose last release
(2.0.16) predates this workaround by five years.

This file is dot-sourced by build.properties.ps1 and by this repository's own build.ps1
as well as being loaded with the module, so it must stay self-contained: no localized
strings and no calls to other PowerShellBuild functions.
.PARAMETER Parameter
Parameters to splat onto Set-BuildEnvironment, for example @{ BuildOutput = $path;
Force = $true }.
.PARAMETER Path
Repository to read the commit message from. Defaults to the current location, which is
what Set-BuildEnvironment itself inspects when no path is supplied.
.EXAMPLE
PS> Invoke-SetBuildEnvironment -Parameter @{ Force = $true }

Populate the BH* environment variables for the current location.
#>
[CmdletBinding()]
param(
[hashtable]
$Parameter = @{},

[string]
$Path = $PWD.Path
)

# Every variable whose Get-BuildVariable branch runs 'git log --format=%B -n 1 <sha>'.
$shaVariableUsingGit = @(
'CI_COMMIT_SHA' # GitLab CI 9.0+
'CI_BUILD_REF' # GitLab CI 8.x
'GIT_COMMIT' # Jenkins
'BUILD_SOURCEVERSION' # Azure Pipelines classic release

Check warning on line 63 in PowerShellBuild/Private/Invoke-SetBuildEnvironment.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (SOURCEVERSION)
'BUILD_VCS_NUMBER' # TeamCity
'BAMBOO_REPOSITORY_REVISION_NUMBER' # Bamboo
'GITHUB_SHA' # GitHub Actions
)
$azureCommitMessageVariable = 'Env:BUILD_SOURCEVERSIONMESSAGE'

Check warning on line 68 in PowerShellBuild/Private/Invoke-SetBuildEnvironment.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (SOURCEVERSIONMESSAGE)

$suppressedVariable = @{}
foreach ($name in $shaVariableUsingGit) {
$variablePath = "Env:$name"
if (Test-Path -Path $variablePath) {
$suppressedVariable[$name] = (Get-Item -Path $variablePath).Value
Remove-Item -Path $variablePath
}
}

# A build system that publishes the message itself already avoids the git call. Only read the
# repository when nothing supplied one.
$ownCommitMessage = $null
if (-not (Test-Path -Path $azureCommitMessageVariable)) {
$ownCommitMessage = Get-HeadCommitMessage -Path $Path
}

try {
if ($ownCommitMessage) {
Set-Item -Path $azureCommitMessageVariable -Value $ownCommitMessage
}

BuildHelpers\Set-BuildEnvironment @Parameter

if ($ownCommitMessage) {
# The Azure Pipelines branch joins the message onto a single line. Restore the form
# the git branch produces so the variable looks the way it always has.
$env:BHCommitMessage = $ownCommitMessage
}

# With the SHA variables hidden, BuildHelpers falls back to HEAD for the commit hash.
# That is the same commit every build system checks out, but report what the build system
# actually said. Only one CI system's variables are ever present at once; if that ever
# stops being true, leave the value BuildHelpers derived rather than guess between them.
if ($suppressedVariable.Count -eq 1) {
$env:BHCommitHash = $suppressedVariable.Values | Select-Object -First 1
}
} finally {
if ($ownCommitMessage) {
Remove-Item -Path $azureCommitMessageVariable -ErrorAction SilentlyContinue
}
foreach ($name in $suppressedVariable.Keys) {
Set-Item -Path "Env:$name" -Value $suppressedVariable[$name]
}
}
Comment on lines +86 to +113
}

function Get-HeadCommitMessage {
<#
.SYNOPSIS
Reads the HEAD commit message the way BuildHelpers would, without the deadlock.
.DESCRIPTION
Returns the HEAD commit message normalized exactly as Get-BuildVariable normalizes it:
blank lines dropped, each remaining line trimmed, joined with newlines. Returns nothing
when git is unavailable, the path is not a repository, or the repository has no commits,
which are the same conditions under which BuildHelpers skips its own git call.
.PARAMETER Path
Repository to read from.
.EXAMPLE
PS> Get-HeadCommitMessage -Path $PWD.Path

Return the current commit message.
#>
[CmdletBinding()]
[OutputType([string])]
param(
[string]
$Path = $PWD.Path
)

$gitCommand = Get-Command -Name 'git' -CommandType 'Application' -ErrorAction 'SilentlyContinue'
if (-not $gitCommand) {
return
}

# Matches the check BuildHelpers makes before using git. A worktree's .git is a file rather

Check warning on line 144 in PowerShellBuild/Private/Invoke-SetBuildEnvironment.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (worktree's) Suggestions: (worker's, workfare's, workteams, workweeks, worsted's)
# than a directory, which Test-Path accepts either way.
if (-not (Test-Path -Path ([IO.Path]::Combine($Path, '.git')))) {
return
}

$messageLines = & $gitCommand[0].Path -C $Path log --format=%B -n 1 2>$null
if ($LASTEXITCODE -ne 0 -or -not $messageLines) {

Check warning on line 151 in PowerShellBuild/Private/Invoke-SetBuildEnvironment.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (LASTEXITCODE)
return
}

($messageLines.Where({ $_ }).ForEach({ $_.Trim() })) -join "`n"
}
5 changes: 4 additions & 1 deletion PowerShellBuild/Public/Initialize-PSBuild.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@ function Initialize-PSBuild {

$params = @{
BuildOutput = $BuildEnvironment.Build.ModuleOutDir
Force = $true
}
Set-BuildEnvironment @params -Force
# Wrapped rather than called directly: BuildHelpers hangs on a large HEAD commit message.
# See Invoke-SetBuildEnvironment and psake/PowerShellBuild#167.
Invoke-SetBuildEnvironment -Parameter $params

Write-Host $LocalizedData.BuildSystemDetails -ForegroundColor 'Yellow'
$psVersion = $PSVersionTable.PSVersion.ToString()
Expand Down
6 changes: 5 additions & 1 deletion PowerShellBuild/build.properties.ps1
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# spell-checker:ignore PSGALLERY BHPS MAML
BuildHelpers\Set-BuildEnvironment -Force
# Dot-sourced rather than calling BuildHelpers directly: Set-BuildEnvironment hangs on a large
# HEAD commit message. This file runs before any task, so the hang would happen before a
# consumer's build produced a single line of output. See psake/PowerShellBuild#167.
. ([IO.Path]::Combine($PSScriptRoot, 'Private', 'Invoke-SetBuildEnvironment.ps1'))
Invoke-SetBuildEnvironment -Parameter @{ Force = $true }

$outDir = [IO.Path]::Combine($env:BHProjectPath, 'Output')
$moduleVersion = (Import-PowerShellDataFile -Path $env:BHPSModuleManifest).ModuleVersion
Expand Down Expand Up @@ -130,7 +134,7 @@
# Value passed to New-MarkdownHelp and Update-MarkdownHelp.
AlphabeticParamsOrder = $false

# Exclude the parameters marked with `DontShow` in the parameter attribute from the help content.

Check warning on line 137 in PowerShellBuild/build.properties.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (Dont) Suggestions: (dent, dint, doit, dolt, dona)
# Value passed to New-MarkdownHelp and Update-MarkdownHelp.
ExcludeDontShow = $false

Expand Down Expand Up @@ -173,10 +177,10 @@

# Name of the environment variable that holds the Base64-encoded PFX certificate.
# Used by the EnvVar source and as the presence-detection key for Auto.
CertificateEnvVar = 'SIGNCERTIFICATE'

Check warning on line 180 in PowerShellBuild/build.properties.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (SIGNCERTIFICATE)

# Name of the environment variable that holds the PFX password (EnvVar source).
CertificatePasswordEnvVar = 'CERTIFICATEPASSWORD'

Check warning on line 183 in PowerShellBuild/build.properties.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (CERTIFICATEPASSWORD)

# File system path to a PFX/P12 certificate file (PfxFile source).
PfxFilePath = $null
Expand Down
6 changes: 5 additions & 1 deletion build.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[cmdletbinding(DefaultParameterSetName = 'Task')]

Check warning on line 1 in build.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (cmdletbinding)
param(
# Build task(s) to execute
[parameter(ParameterSetName = 'task', position = 0)]
Expand Down Expand Up @@ -53,7 +53,11 @@
Get-PSakeScriptTasks -buildFile $psakeFile |
Format-Table -Property Name, Description, Alias, DependsOn
} else {
Set-BuildEnvironment -Force
# Dot-sourced from the module source rather than calling BuildHelpers directly:
# Set-BuildEnvironment hangs on a large HEAD commit message, which stalls this build before
# psake starts. See psake/PowerShellBuild#167.
. ([IO.Path]::Combine($PSScriptRoot, 'PowerShellBuild', 'Private', 'Invoke-SetBuildEnvironment.ps1'))
Invoke-SetBuildEnvironment -Parameter @{ Force = $true }
$parameters = @{}
if ($PSGalleryApiKey) {
$parameters['galleryApiKey'] = $PSGalleryApiKey
Expand Down
Loading
Loading