AtlassianPS.Standards.psm1
|
function Get-BuildEnvironmentMetadata { [CmdletBinding()] [OutputType([pscustomobject])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] param( [Parameter(Mandatory)] [String]$ProjectPath ) $buildSystem = 'Unknown' $branchName = '' $commitHash = '' $buildNumber = '0' $commitMessage = '' if ($env:GITHUB_ACTIONS) { $buildSystem = 'GitHub Actions' $branchName = if ($env:GITHUB_HEAD_REF) { $env:GITHUB_HEAD_REF } else { $env:GITHUB_REF_NAME } $commitHash = $env:GITHUB_SHA $buildNumber = $env:GITHUB_RUN_NUMBER $commitMessage = $env:GITHUB_EVENT_HEAD_COMMIT_MESSAGE } else { $branchName = git -C $ProjectPath rev-parse --abbrev-ref HEAD 2>$null $commitHash = git -C $ProjectPath rev-parse HEAD 2>$null $commitMessage = (git -C $ProjectPath log -1 --pretty=%B 2>$null) -join "`n" } return [PSCustomObject]@{ BuildSystem = $buildSystem BranchName = $branchName CommitHash = $commitHash BuildNumber = $buildNumber CommitMessage = $commitMessage } } function Get-HostPlatformInfo { [CmdletBinding()] [OutputType([pscustomobject])] param() $isWindows = (-not (Get-Variable -Name IsWindows -ErrorAction Ignore)) -or $IsWindows $isLinux = (Get-Variable -Name IsLinux -ErrorAction Ignore) -and $IsLinux $isMacOS = (Get-Variable -Name IsMacOS -ErrorAction Ignore) -and $IsMacOS $isCoreCLR = $PSVersionTable.ContainsKey('PSEdition') -and $PSVersionTable.PSEdition -eq 'Core' $os = 'Unknown' $osVersion = $PSVersionTable.OS switch ($true) { { $isWindows } { $os = 'Windows' if (-not $isCoreCLR) { $osVersion = $PSVersionTable.BuildVersion.ToString() } break } { $isLinux } { $os = 'Linux' break } { $isMacOS } { $os = 'OSX' break } } return [PSCustomObject]@{ OS = $os OSVersion = $osVersion } } function Get-ProjectRelativePath { [CmdletBinding()] [OutputType([String])] param( [Parameter(Mandatory)] [String]$BasePath, [Parameter(Mandatory)] [String]$TargetPath ) $getRelativePathMethod = [System.IO.Path].GetMethod('GetRelativePath', [Type[]]@([String], [String])) if ($getRelativePathMethod) { return [System.IO.Path]::GetRelativePath($BasePath, $TargetPath) } if ($TargetPath.StartsWith($BasePath, [System.StringComparison]::OrdinalIgnoreCase)) { return $TargetPath.Substring($BasePath.Length).TrimStart('\', '/') } return $TargetPath } function Get-UsablePesterVersion { [CmdletBinding()] [OutputType([Version])] param( [Parameter(Mandatory)] [Version]$MinimumVersion, [Parameter()] [Version]$MaximumVersion ) $availablePesterModules = @( Get-Module -Name 'Pester' -ListAvailable | Sort-Object -Property Version -Descending ) if ($availablePesterModules.Count -eq 0) { throw "Pester version $MinimumVersion or newer is required, but no Pester module is installed." } $selectedModule = $availablePesterModules | Where-Object { $_.Version -ge $MinimumVersion -and ( (-not $MaximumVersion) -or $_.Version -le $MaximumVersion ) } | Select-Object -First 1 if (-not $selectedModule) { if ($MaximumVersion) { throw "Pester version between $MinimumVersion and $MaximumVersion is required, but no installed version satisfies that range." } throw "Pester version $MinimumVersion or newer is required, but the highest available version is $($availablePesterModules[0].Version)." } return $selectedModule.Version } function Import-PesterVersion { [CmdletBinding()] [OutputType([Version])] param( [Parameter()] [Version]$MinimumVersion = [Version]'5.7.0', [Parameter()] [Version]$MaximumVersion ) $pesterVersionToUse = Get-UsablePesterVersion -MinimumVersion $MinimumVersion -MaximumVersion $MaximumVersion $loadedPester = Get-Module -Name 'Pester' | Sort-Object -Property Version -Descending | Select-Object -First 1 if ((-not $loadedPester) -or ($loadedPester.Version -ne $pesterVersionToUse)) { if ($loadedPester) { Get-Module -Name 'Pester' | Remove-Module -Force -ErrorAction SilentlyContinue } Import-Module -Name 'Pester' -RequiredVersion $pesterVersionToUse -ErrorAction Stop } return $pesterVersionToUse } function Invoke-ScriptAnalyzerLint { [CmdletBinding()] [OutputType([PSCustomObject[]])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String[]]$AnalyzerPaths, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$AnalyzerSettingsPath, [Parameter()] [ValidateSet('Error', 'Warning', 'Information', 'ParseError')] [String[]]$Severity = @('Error', 'Warning'), [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$ProjectPath, [Parameter()] [Boolean]$IsGitHubActions ) $analyzerParams = @{ Settings = $AnalyzerSettingsPath Severity = $Severity Recurse = $true } $results = @( foreach ($path in $AnalyzerPaths) { Invoke-ScriptAnalyzer -Path $path @analyzerParams } ) foreach ($result in $results) { $color = if ($result.Severity -eq 'Error') { 'Red' } else { 'Yellow' } $location = if ($result.ScriptName) { $result.ScriptName } else { '<unknown>' } Write-LintMessage -Color $color -Message "[$($result.Severity)] ${location}:$($result.Line) - $($result.RuleName): $($result.Message)" if ($IsGitHubActions -and $result.ScriptPath) { $level = if ($result.Severity -eq 'Error') { 'error' } else { 'warning' } $relativePath = Get-ProjectRelativePath -BasePath $ProjectPath -TargetPath $result.ScriptPath $message = ($result.Message -replace '%', '%25' -replace "`r", '%0D' -replace "`n", '%0A') Write-WorkflowCommand -Command "::${level} file=$relativePath,line=$($result.Line),col=$($result.Column),title=$($result.RuleName)::$message" } } return $results } function Invoke-StyleLintTests { [CmdletBinding()] [OutputType([PSCustomObject])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$StyleTestPath, [Parameter()] [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] [String]$PesterVerbosity = 'Normal', [Parameter()] [Version]$MinimumPesterVersion = [Version]'5.7.0' ) $pesterVersion = Import-PesterVersion -MinimumVersion $MinimumPesterVersion if (-not $pesterVersion) { $pesterVersion = [Version]'5.7.0' } if ($pesterVersion.Major -ge 5) { $pesterConfigHash = @{ Run = @{ PassThru = $true Path = $StyleTestPath } Output = @{ Verbosity = $PesterVerbosity } } $pesterConfig = New-PesterConfiguration -Hashtable $pesterConfigHash return (Invoke-Pester -Configuration $pesterConfig) } return (Invoke-Pester -Script $StyleTestPath -PassThru) } function Write-LintMessage { [CmdletBinding()] param( [Parameter(Mandatory)] [String]$Color, [Parameter(Mandatory)] [String]$Message ) if (Get-Command -Name Write-Build -ErrorAction SilentlyContinue) { Write-Build $Color $Message return } Write-Output $Message } function Write-WorkflowCommand { <# .SYNOPSIS Emit a GitHub Actions workflow command on stdout. .DESCRIPTION GitHub Actions workflow commands must reach stdout for the runner to parse them as inline annotations. #> [System.Diagnostics.CodeAnalysis.SuppressMessage( 'PSAvoidUsingWriteHost', '', Justification = 'GitHub Actions workflow commands must reach stdout; Write-Output is captured by Invoke-Build pipelines.' )] [CmdletBinding()] param( [Parameter(Mandatory)] [String]$Command ) Write-Host $Command } function Copy-ModuleArtifacts { [CmdletBinding()] [OutputType([pscustomobject])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$ProjectPath, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$ModuleName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$BuildOutputPath, [Parameter()] [String[]]$AdditionalFiles = @(), [Parameter()] [Switch]$IncludeTests ) $resolvedProjectPath = (Resolve-Path -LiteralPath $ProjectPath).ProviderPath $sourceModulePath = Join-Path -Path $resolvedProjectPath -ChildPath $ModuleName if (-not (Test-Path -LiteralPath $sourceModulePath -PathType Container)) { throw "Module source path '$sourceModulePath' was not found." } if (-not (Test-Path -LiteralPath $BuildOutputPath -PathType Container)) { $null = New-Item -Path $BuildOutputPath -ItemType Directory -Force } $releaseModulePath = Join-Path -Path $BuildOutputPath -ChildPath $ModuleName if (-not (Test-Path -LiteralPath $releaseModulePath -PathType Container)) { $null = New-Item -Path $releaseModulePath -ItemType Directory -Force } Copy-Item -Path "$sourceModulePath/*" -Destination $releaseModulePath -Recurse -Force foreach ($file in $AdditionalFiles) { $sourceFile = if ([System.IO.Path]::IsPathRooted($file)) { $file } else { Join-Path -Path $resolvedProjectPath -ChildPath $file } if (-not (Test-Path -LiteralPath $sourceFile -PathType Leaf)) { throw "Artifact source file '$sourceFile' was not found." } Copy-Item -Path $sourceFile -Destination $releaseModulePath -Force } if ($IncludeTests) { $testsPath = Join-Path -Path $resolvedProjectPath -ChildPath 'Tests' if (Test-Path -LiteralPath $testsPath -PathType Container) { Copy-Item -Path $testsPath -Destination $BuildOutputPath -Recurse -Force } } return [PSCustomObject]@{ SourceModulePath = $sourceModulePath ReleaseModulePath = $releaseModulePath } } function Get-BuildEnvironmentInfo { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter()] [String]$VersionToPublish ) $platformInfo = Get-HostPlatformInfo $normalizedVersionToPublish = if ($VersionToPublish) { $VersionToPublish.TrimStart('v') } else { $null } $builtManifestPath = if ($env:BHBuildOutput -and $env:BHProjectName) { Join-Path -Path (Join-Path -Path $env:BHBuildOutput -ChildPath $env:BHProjectName) -ChildPath "$($env:BHProjectName).psd1" } else { $null } return [PSCustomObject]@{ BuildSystem = $env:BHBuildSystem ProjectName = $env:BHProjectName ProjectPath = $env:BHProjectPath ModulePath = $env:BHModulePath ModuleManifest = $env:BHPSModuleManifest BuildOutputPath = $env:BHBuildOutput BuiltManifestPath = $builtManifestPath BranchName = $env:BHBranchName CommitHash = $env:BHCommitHash CommitMessage = $env:BHCommitMessage BuildNumber = $env:BHBuildNumber VersionToPublish = $normalizedVersionToPublish OS = $platformInfo.OS OSVersion = $platformInfo.OSVersion } } function Get-ScriptAnalyzerSettingsPath { [CmdletBinding()] [OutputType([string])] param() $moduleBase = $ExecutionContext.SessionState.Module.ModuleBase if (-not $moduleBase) { throw 'Unable to resolve AtlassianPS.Standards module base path.' } $settingsPath = Join-Path -Path $moduleBase -ChildPath 'PSScriptAnalyzerSettings.psd1' if (-not (Test-Path -LiteralPath $settingsPath -PathType Leaf)) { throw "Unable to locate PSScriptAnalyzer settings file at '$settingsPath'. Ensure AtlassianPS.Standards is installed correctly." } return (Resolve-Path -LiteralPath $settingsPath).ProviderPath } function Initialize-BuildEnvironment { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$ProjectName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$ProjectPath, [Parameter()] [ValidateNotNullOrEmpty()] [String]$BuildOutputFolder = 'Release', [Parameter()] [String]$VersionToPublish, [Parameter()] [Switch]$ResetBuildEnvironmentVariables ) $resolvedProjectPath = (Resolve-Path -LiteralPath $ProjectPath).ProviderPath if ($ResetBuildEnvironmentVariables) { Remove-Item -Path env:\BH* -ErrorAction SilentlyContinue } $env:BHProjectName = $ProjectName $env:BHProjectPath = $resolvedProjectPath $env:BHModulePath = Join-Path -Path $env:BHProjectPath -ChildPath $env:BHProjectName $env:BHPSModulePath = $env:BHModulePath $env:BHPSModuleManifest = Join-Path -Path $env:BHModulePath -ChildPath "$($env:BHProjectName).psd1" $env:BHBuildOutput = Join-Path -Path $env:BHProjectPath -ChildPath $BuildOutputFolder $metadata = Get-BuildEnvironmentMetadata -ProjectPath $env:BHProjectPath $env:BHBuildSystem = $metadata.BuildSystem $env:BHBranchName = $metadata.BranchName $env:BHCommitHash = $metadata.CommitHash $env:BHBuildNumber = $metadata.BuildNumber $env:BHCommitMessage = $metadata.CommitMessage return Get-BuildEnvironmentInfo -VersionToPublish $VersionToPublish } function Invoke-Lint { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter()] [ValidateNotNullOrEmpty()] [String]$ProjectPath = $env:BHProjectPath, [Parameter()] [String]$ModulePath = $env:BHModulePath, [Parameter()] [String]$BuildScriptPath, [Parameter()] [String]$StyleTestPath, [Parameter()] [String]$AnalyzerSettingsPath, [Parameter()] [String[]]$AnalyzerPaths, [Parameter()] [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] [String]$PesterVerbosity = 'Normal', [Parameter()] [Version]$MinimumPesterVersion = [Version]'5.7.0', [Parameter()] [ValidateSet('Error', 'Warning', 'Information', 'ParseError')] [String[]]$Severity = @('Error', 'Warning'), [Parameter()] [Switch]$SkipStyleTests, [Parameter()] [Switch]$SkipScriptAnalyzer ) if (-not $ProjectPath) { throw 'ProjectPath is required. Provide -ProjectPath or set $env:BHProjectPath.' } $projectPathResolved = (Resolve-Path -LiteralPath $ProjectPath).ProviderPath $failures = [System.Collections.Generic.List[String]]::new() $styleFailures = 0 $analyzerIssueCount = 0 $isGitHubActions = [bool]$env:GITHUB_ACTIONS if (-not $BuildScriptPath -and $env:BHProjectName) { $BuildScriptPath = Join-Path -Path $projectPathResolved -ChildPath "$($env:BHProjectName).build.ps1" } if (-not $StyleTestPath) { $StyleTestPath = Join-Path -Path $projectPathResolved -ChildPath 'Tests/Style.Tests.ps1' } if (-not $AnalyzerSettingsPath) { $AnalyzerSettingsPath = Get-ScriptAnalyzerSettingsPath } if (-not $AnalyzerPaths) { $AnalyzerPaths = @( $ModulePath (Join-Path -Path $projectPathResolved -ChildPath 'Tests') (Join-Path -Path $projectPathResolved -ChildPath 'Tools') $BuildScriptPath ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } } if (-not (Test-Path -LiteralPath $AnalyzerSettingsPath -PathType Leaf)) { throw "Analyzer settings file was not found at '$AnalyzerSettingsPath'." } if ($AnalyzerPaths.Count -eq 0 -and -not $SkipScriptAnalyzer) { throw 'No analyzer paths were discovered. Provide -AnalyzerPaths or set build environment paths.' } if (-not $SkipStyleTests) { if (Test-Path -LiteralPath $StyleTestPath -PathType Leaf) { Write-LintMessage -Color Gray -Message 'Running style tests...' $testResults = Invoke-StyleLintTests ` -StyleTestPath $StyleTestPath ` -PesterVerbosity $PesterVerbosity ` -MinimumPesterVersion $MinimumPesterVersion $styleFailures = [int]$testResults.FailedCount if ($styleFailures -gt 0) { $failures.Add("$styleFailures style test(s) failed.") } else { Write-LintMessage -Color Green -Message 'Style tests: passed.' } } else { Write-LintMessage -Color Yellow -Message "Style tests skipped because '$StyleTestPath' was not found." } } if (-not $SkipScriptAnalyzer) { Write-LintMessage -Color Gray -Message 'Running PSScriptAnalyzer...' $results = Invoke-ScriptAnalyzerLint ` -AnalyzerPaths $AnalyzerPaths ` -AnalyzerSettingsPath $AnalyzerSettingsPath ` -Severity $Severity ` -ProjectPath $projectPathResolved ` -IsGitHubActions $isGitHubActions $analyzerIssueCount = @($results).Count if ($analyzerIssueCount -gt 0) { $failures.Add("$analyzerIssueCount PSScriptAnalyzer issue(s) found.") } else { Write-LintMessage -Color Green -Message 'PSScriptAnalyzer: no issues found.' } } if ($failures.Count -gt 0) { throw ("Lint failed:`n - " + ($failures -join "`n - ")) } return [PSCustomObject]@{ StyleFailedCount = $styleFailures AnalyzerIssueCount = $analyzerIssueCount AnalyzerPathCount = $AnalyzerPaths.Count } } function Invoke-ModuleTests { [CmdletBinding()] [OutputType([PSCustomObject])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$TestPath, [Parameter()] [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] [String]$PesterVerbosity = 'Normal', [Parameter()] [String[]]$Tag, [Parameter()] [String[]]$ExcludeTag, [Parameter()] [String[]]$DefaultExcludeTag = @('Integration'), [Parameter()] [String[]]$ExcludePath = @(), [Parameter()] [Version]$MinimumPesterVersion = [Version]'5.7.0', [Parameter()] [Version]$MaximumPesterVersion, [Parameter()] [String]$ResultOutputPath ) $resolvedTestPath = (Resolve-Path -LiteralPath $TestPath).ProviderPath $pesterVersion = Import-PesterVersion -MinimumVersion $MinimumPesterVersion -MaximumVersion $MaximumPesterVersion if (-not $pesterVersion) { $pesterVersion = [Version]'5.7.0' } if (-not $ResultOutputPath) { $platformInfo = Get-HostPlatformInfo $resultRootPath = if ($env:BHProjectPath) { $env:BHProjectPath } else { Split-Path -Path $resolvedTestPath -Parent } $ResultOutputPath = Join-Path -Path $resultRootPath -ChildPath "Test-$($platformInfo.OS)-$($PSVersionTable.PSVersion.ToString()).xml" } $pesterConfigHash = @{ Run = @{ PassThru = $true Path = $resolvedTestPath } TestResult = @{ Enabled = $true OutputFormat = 'NUnitXml' OutputPath = $ResultOutputPath } Output = @{ Verbosity = $PesterVerbosity } Filter = @{ ExcludeTag = @($DefaultExcludeTag) } } if ($ExcludePath.Count -gt 0) { $pesterConfigHash.Run.ExcludePath = @($ExcludePath) } if ($Tag) { $pesterConfigHash.Filter.Tag = $Tag $pesterConfigHash.Filter.ExcludeTag = @($pesterConfigHash.Filter.ExcludeTag | Where-Object { $_ -notin $Tag }) if ($Tag -contains 'Integration') { $pesterConfigHash.Run.ExcludePath = @() } } if ($ExcludeTag) { $merged = @($pesterConfigHash.Filter.ExcludeTag) + @($ExcludeTag) | Select-Object -Unique if ($Tag) { $merged = @($merged | Where-Object { $_ -notin $Tag }) } $pesterConfigHash.Filter.ExcludeTag = @($merged) } if ($pesterVersion.Major -ge 5) { $pesterConfig = New-PesterConfiguration -Hashtable $pesterConfigHash $testResults = Invoke-Pester -Configuration $pesterConfig } else { $invokePesterParams = @{ Script = $resolvedTestPath PassThru = $true OutputFile = $ResultOutputPath OutputFormat = 'NUnitXml' } if ($pesterConfigHash.Filter.Tag) { $invokePesterParams.Tag = $pesterConfigHash.Filter.Tag } if ($pesterConfigHash.Filter.ExcludeTag.Count -gt 0) { $invokePesterParams.ExcludeTag = $pesterConfigHash.Filter.ExcludeTag } $testResults = Invoke-Pester @invokePesterParams } $containerFailureCount = 0 if ($testResults.PSObject.Properties.Name -contains 'ContainersFailedCount') { $containerFailureCount = [int]$testResults.ContainersFailedCount } if (($testResults.FailedCount -gt 0) -or ($containerFailureCount -gt 0)) { throw ("Pester reported failures. Failed tests: {0}; failed containers: {1}." -f $testResults.FailedCount, $containerFailureCount) } return $testResults } function Join-ModuleSource { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$ReleaseModulePath, [Parameter()] [String[]]$SourceFolders = @('Public', 'Private'), [Parameter()] [String[]]$RegionsToKeep = @('Dependencies', 'Configuration'), [Parameter()] [Boolean]$RemoveSourceFolders = $true ) $resolvedReleaseModulePath = (Resolve-Path -LiteralPath $ReleaseModulePath).ProviderPath $releaseRootPath = [System.IO.Path]::GetFullPath($resolvedReleaseModulePath) if (-not $releaseRootPath.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { $releaseRootPath += [System.IO.Path]::DirectorySeparatorChar } $validatedSourceFolders = @( foreach ($folder in $SourceFolders) { if ([string]::IsNullOrWhiteSpace($folder)) { throw 'SourceFolders cannot contain empty values.' } if ([System.IO.Path]::IsPathRooted($folder)) { throw "Source folder '$folder' must be relative to the release module path." } $sourceFolderPath = Join-Path -Path $resolvedReleaseModulePath -ChildPath $folder $resolvedSourceFolderPath = [System.IO.Path]::GetFullPath($sourceFolderPath) if (-not $resolvedSourceFolderPath.StartsWith($releaseRootPath, [System.StringComparison]::OrdinalIgnoreCase)) { throw "Source folder '$folder' resolves outside release module path '$resolvedReleaseModulePath'." } [PSCustomObject]@{ Name = $folder Path = $resolvedSourceFolderPath } } ) $moduleName = Split-Path -Path $resolvedReleaseModulePath -Leaf $targetFile = Join-Path -Path $resolvedReleaseModulePath -ChildPath "$moduleName.psm1" if (-not (Test-Path -LiteralPath $targetFile -PathType Leaf)) { throw "Module source file '$targetFile' was not found." } $content = Get-Content -Encoding UTF8 -LiteralPath $targetFile $capture = $false $regions = [System.Collections.Generic.HashSet[String]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($region in $RegionsToKeep) { $null = $regions.Add($region) } $compiled = [System.Text.StringBuilder]::new() foreach ($line in $content) { if ($line -match '^#region\s+(.+)$') { $capture = $regions.Contains($Matches[1].Trim()) } if ($capture) { $null = $compiled.Append($line) $null = $compiled.Append("`r`n") } if ($capture -and $line -match '^#endregion\b') { $capture = $false } } $sourceFiles = foreach ($sourceFolder in $validatedSourceFolders) { if (Test-Path -LiteralPath $sourceFolder.Path -PathType Container) { Get-ChildItem -LiteralPath $sourceFolder.Path -Filter '*.ps1' -File -ErrorAction SilentlyContinue } } $sourceFiles = @( $sourceFiles | Sort-Object -Property FullName ) foreach ($file in $sourceFiles) { $fileContent = Get-Content -LiteralPath $file.FullName -Raw $null = $compiled.Append($fileContent) if ($fileContent -and $fileContent[-1] -ne "`n") { $null = $compiled.Append("`r`n") } } $utf8Bom = [System.Text.UTF8Encoding]::new($true) [System.IO.File]::WriteAllText($targetFile, $compiled.ToString(), $utf8Bom) if ($RemoveSourceFolders) { foreach ($sourceFolder in $validatedSourceFolders) { if (Test-Path -LiteralPath $sourceFolder.Path -PathType Container) { Remove-Item -LiteralPath $sourceFolder.Path -Recurse -Force } } } return $targetFile } function New-ModulePackage { [CmdletBinding(SupportsShouldProcess)] [OutputType([string])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$BuildOutputPath, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$ModuleName, [Parameter()] [String]$DestinationPath ) $sourcePath = Join-Path -Path $BuildOutputPath -ChildPath $ModuleName if (-not (Test-Path -LiteralPath $sourcePath -PathType Container)) { throw "Missing files to package at '$sourcePath'." } if (-not $DestinationPath) { $DestinationPath = Join-Path -Path $BuildOutputPath -ChildPath "$ModuleName.zip" } if ($PSCmdlet.ShouldProcess($DestinationPath, "Create package from '$sourcePath'")) { Remove-Item -Path $DestinationPath -ErrorAction SilentlyContinue $null = Compress-Archive -Path $sourcePath -DestinationPath $DestinationPath } return $DestinationPath } function Publish-ModuleRelease { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$BuildOutputPath, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$ModuleName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$ApiKey ) $releasePath = Join-Path -Path $BuildOutputPath -ChildPath $ModuleName if (-not (Test-Path -LiteralPath $releasePath -PathType Container)) { throw "Expected release path '$releasePath' does not exist. Run the Build task before publishing." } Publish-Module -Path $releasePath -NuGetApiKey $ApiKey -ErrorAction Stop } function Set-ModuleManifestVersion { [CmdletBinding(SupportsShouldProcess)] [OutputType([string])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$BuiltManifestPath, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$ModuleName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$VersionToPublish ) if (-not (Get-Command -Name 'Metadata\Update-Metadata' -ErrorAction SilentlyContinue)) { throw "Metadata\Update-Metadata is not available. Ensure the required metadata tooling is installed." } if (-not (Test-Path -LiteralPath $BuiltManifestPath -PathType Leaf)) { throw "Built module manifest '$BuiltManifestPath' was not found." } function ConvertTo-VersionDescriptor { param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$VersionText ) $trimmedVersion = $VersionText.Trim() if ($trimmedVersion -match '^v') { $trimmedVersion = $trimmedVersion.Substring(1) } if ($trimmedVersion -notmatch '^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<prerelease>[0-9A-Za-z][0-9A-Za-z\.-]*))?$') { throw "Invalid semantic version '$VersionText'. Expected format: <major>.<minor>.<patch>[-prerelease]." } return [PSCustomObject]@{ Major = [int]$matches.major Minor = [int]$matches.minor Patch = [int]$matches.patch PreReleaseLabel = $matches.prerelease CoreVersion = [Version]::new([int]$matches.major, [int]$matches.minor, [int]$matches.patch) } } function Compare-VersionDescriptor { param( [Parameter(Mandatory)] [PSCustomObject]$Left, [Parameter(Mandatory)] [PSCustomObject]$Right ) $coreComparison = $Left.CoreVersion.CompareTo($Right.CoreVersion) if ($coreComparison -ne 0) { return $coreComparison } if ([string]::IsNullOrEmpty($Left.PreReleaseLabel) -and [string]::IsNullOrEmpty($Right.PreReleaseLabel)) { return 0 } if ([string]::IsNullOrEmpty($Left.PreReleaseLabel)) { return 1 } if ([string]::IsNullOrEmpty($Right.PreReleaseLabel)) { return -1 } return [string]::CompareOrdinal($Left.PreReleaseLabel, $Right.PreReleaseLabel) } $normalizedVersion = ConvertTo-VersionDescriptor -VersionText $VersionToPublish $published = Find-Module -Name $ModuleName -ErrorAction SilentlyContinue if ($published) { $latestPublished = ConvertTo-VersionDescriptor -VersionText $published.Version.ToString() if ((Compare-VersionDescriptor -Left $normalizedVersion -Right $latestPublished) -le 0) { throw "Version must be greater than latest published version: $($published.Version)" } } $versionString = "{0}.{1}.{2}" -f $normalizedVersion.Major, $normalizedVersion.Minor, $normalizedVersion.Patch if ($PSCmdlet.ShouldProcess($BuiltManifestPath, "Set module version to $versionString")) { Metadata\Update-Metadata -Path $BuiltManifestPath -PropertyName 'ModuleVersion' -Value $versionString if ($normalizedVersion.PreReleaseLabel) { Metadata\Update-Metadata -Path $BuiltManifestPath -PropertyName 'Prerelease' -Value $normalizedVersion.PreReleaseLabel } else { Metadata\Update-Metadata -Path $BuiltManifestPath -PropertyName 'Prerelease' -Value '' } } return $versionString } function Sync-ScriptAnalyzerSettings { [CmdletBinding(SupportsShouldProcess)] [OutputType([String])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$DestinationPath ) $sourceSettingsPath = Get-ScriptAnalyzerSettingsPath if (-not (Test-Path -LiteralPath $sourceSettingsPath -PathType Leaf)) { throw "Shared PSScriptAnalyzer settings file was not found at '$sourceSettingsPath'." } $destinationDirectory = Split-Path -Path $DestinationPath -Parent if ($destinationDirectory -and -not (Test-Path -LiteralPath $destinationDirectory -PathType Container)) { $null = New-Item -Path $destinationDirectory -ItemType Directory -Force } if ($PSCmdlet.ShouldProcess($DestinationPath, "Copy analyzer settings from '$sourceSettingsPath'")) { Copy-Item -LiteralPath $sourceSettingsPath -Destination $DestinationPath -Force -ErrorAction Stop } return (Resolve-Path -LiteralPath $DestinationPath).ProviderPath } function Update-ModuleManifestExports { [CmdletBinding(SupportsShouldProcess)] [OutputType([pscustomobject])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$SourceModulePath, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$BuiltManifestPath, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String]$ModuleName ) if (-not (Get-Command -Name 'Metadata\Update-Metadata' -ErrorAction SilentlyContinue)) { throw "Metadata\Update-Metadata is not available. Ensure the required metadata tooling is installed." } if (-not (Test-Path -LiteralPath $BuiltManifestPath -PathType Leaf)) { throw "Built module manifest '$BuiltManifestPath' was not found." } $sourceManifestPath = Join-Path -Path $SourceModulePath -ChildPath "$ModuleName.psd1" if (-not (Test-Path -LiteralPath $sourceManifestPath -PathType Leaf)) { throw "Source module manifest '$sourceManifestPath' was not found." } $moduleFunctions = @( Get-ChildItem -Path (Join-Path -Path $SourceModulePath -ChildPath 'Public/*.ps1') -ErrorAction SilentlyContinue ).BaseName $sourceModuleInfo = Test-ModuleManifest -Path $sourceManifestPath -ErrorAction Stop $moduleAlias = @($sourceModuleInfo.ExportedAliases.Keys) if ($PSCmdlet.ShouldProcess($BuiltManifestPath, 'Update exported functions and aliases')) { Metadata\Update-Metadata -Path $BuiltManifestPath -PropertyName 'FunctionsToExport' -Value @($moduleFunctions) Metadata\Update-Metadata -Path $BuiltManifestPath -PropertyName 'AliasesToExport' -Value '' if ($moduleAlias.Count -gt 0) { Metadata\Update-Metadata -Path $BuiltManifestPath -PropertyName 'AliasesToExport' -Value @($moduleAlias) } } return [PSCustomObject]@{ FunctionsToExport = @($moduleFunctions) AliasesToExport = @($moduleAlias) } } function Write-BuildInfo { [CmdletBinding()] param( [Parameter()] [String]$VersionToPublish, [Parameter()] [PSObject]$BuildInfo ) if (-not $BuildInfo) { $BuildInfo = Get-BuildEnvironmentInfo -VersionToPublish $VersionToPublish } $writer = if (Get-Command -Name Write-Build -ErrorAction SilentlyContinue) { { param([String]$Message) Write-Build Gray $Message } } else { { param([String]$Message) Write-Output $Message } } & $writer '' & $writer ('BHBuildSystem: {0}' -f $BuildInfo.BuildSystem) & $writer '-------------------------------------------------------' & $writer ('BHProjectName: {0}' -f $BuildInfo.ProjectName) & $writer ('BHProjectPath: {0}' -f $BuildInfo.ProjectPath) & $writer ('BHModulePath: {0}' -f $BuildInfo.ModulePath) & $writer ('BHPSModuleManifest: {0}' -f $BuildInfo.ModuleManifest) & $writer ('BHBuildOutput: {0}' -f $BuildInfo.BuildOutputPath) & $writer ('builtManifestPath: {0}' -f $BuildInfo.BuiltManifestPath) & $writer '-------------------------------------------------------' & $writer ('BHBranchName: {0}' -f $BuildInfo.BranchName) & $writer ('BHCommitHash: {0}' -f $BuildInfo.CommitHash) & $writer ('BHCommitMessage: {0}' -f $BuildInfo.CommitMessage) & $writer ('BHBuildNumber: {0}' -f $BuildInfo.BuildNumber) & $writer ('VersionToPublish: {0}' -f $BuildInfo.VersionToPublish) & $writer '-------------------------------------------------------' & $writer ('PowerShell version: {0}' -f $PSVersionTable.PSVersion.ToString()) & $writer ('OS: {0}' -f $BuildInfo.OS) & $writer ('OS Version: {0}' -f $BuildInfo.OSVersion) & $writer '' } |