ModuleRoot.psm1
# srcFile: /drone/src/src/PSScriptAnalyzer/Invoke-Linter.ps1 function Invoke-Linter() { <# .SYNOPSIS Runs all PSScriptAnalyzer Rules within this repo. .DESCRIPTION This Cmdlet is used in Drone pipeline to run the PSScriptAnalyzer rules.. .INPUTS [None] No pipeline input. .OUTPUTS [None] No pipeline output. .EXAMPLE Invoke-Linter #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseConsistentWhitespace', '', Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1' )] param() process { $Repo = Get-RepoPath # Use repo local defaults. if not present use the DroneHelper included as defaults. if ($Repo.Resources.ScriptAnalyzerSettingsExist) { $currentRules = $Repo.Resources.ScriptAnalyzerSettingsPath } else { $currentRules = $repo.DroneHelper.ScriptAnalyzerDefaultsPath } $AnalyzerParams = @{ Path = $Repo.Src.Path Recurse = $true Settings = $currentRules Verbose = $VerbosePreference ReportSummary = $true } $AnalyzerResults = Invoke-ScriptAnalyzer @AnalyzerParams if ( $AnalyzerResults ) { $AnalyzerResults | Sort-Object -Property @( "ScriptName", "Line" ) | Format-Table @( "Severity", "ScriptName", "Line", "RuleName", "Message" ) -AutoSize | Out-String | Write-Verbose -Verbose $ResultParams = @{ Type = 'PSScriptAnalyzer' Path = $Repo.Build.ScriptAnalyzerLogPath InputObject = $AnalyzerResults } Write-ResultFile @ResultParams Write-FailureStateFile -StepName 'PSScriptAnalyzer' throw 'PS Script Analyzer failed!' } else { $ResultParams = @{ Type = 'Custom' Path = $Repo.Build.ScriptAnalyzerLogPath InputObject = ':heavy_check_mark: No violations found.' } Write-ResultFile @ResultParams } } } # srcFile: /drone/src/src/State/Invoke-BuidState.ps1 function Invoke-BuildState { <# .SYNOPSIS Sets final Drone pipeline build state. .DESCRIPTION Marks the pipeline ass succeeded of fail based on the custom state file. .INPUTS [None] No pipeline input. .OUTPUTS [None] No pipeline output. .EXAMPLE Invoke-BuildState #> [CmdletBinding()] param() process { $Repo = Get-RepoPath if ( Test-Path -Path $Repo.FailureLogPath ) { throw 'One one more pipeline steps failed. Marking the pipeline as failed!' } } } # srcFile: /drone/src/src/State/Write-FailureStateFile.ps1 function Write-FailureStateFile() { <# .SYNOPSIS Writes the current pipeline step into failure log. .DESCRIPTION This Cmdlet is used to mark single steps as failed without stopping the complete pipeline. .PARAMETER StepName The current DroneHelper step name which should be added into to the log. .INPUTS [None] No pipeline input. .OUTPUTS [None] No pipeline output. .EXAMPLE Write-FailureStateFile #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseConsistentWhitespace', '', Justification = 'justification' )] param ( [Parameter(Mandatory = $true)] [string]$StepName ) process { $Repo = Get-RepoPath $WriteParams = @{ FilePath = $Repo.FailureLogPath Encoding = 'utf8' NoClobber = $true Force = $true InputObject = $StepName } if ( Test-Path -Path $Repo.FailureLogPath ) { $WriteParams.Append = $true } Out-File @WriteParams } } # srcFile: /drone/src/src/State/Write-ResultFile.ps1 function Write-ResultFile { <# .SYNOPSIS Writes the current pipeline step into failure log. .DESCRIPTION This Cmdlet is used to mark single steps as failed without stopping the complete pipeline. .PARAMETER StepName The current DroneHelper step name which should be added into to the log. .INPUTS [None] No pipeline input. .OUTPUTS [None] No pipeline output. .EXAMPLE Write-FailureStateFile #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [PSCustomObject]$InputObject, [Parameter(Mandatory = $true)] [String]$Path, [Parameter(Mandatory = $true)] [ValidateSet('Pester', 'PSScriptAnalyzer', 'FileLinter', 'Custom')] [String]$Type ) process { [String[]]$Output = @() if ($BlockDescription -ne "") { $BlockDescription | Out-File -FilePath $Path -Encoding utf8 -Force -NoClobber -Append } switch ($Type) { 'Pester' { $Output = Format-PesterReport -InputObject $InputObject } 'PSScriptAnalyzer' { $Output = Format-ScriptAnalyzerReport -InputObject $InputObject } 'FileLinter' { $Output = Format-FileLinterReport -InputObject $InputObject } 'Custom' { # nothing to do here $Output = $InputObject + [Environment]::NewLine } } $Output | Out-File -FilePath $Path -Encoding utf8 -Force -NoClobber -Append } } # srcFile: /drone/src/src/Helper/Get-RepoPath.ps1 function Get-RepoPath { <# .SYNOPSIS Updates the module manifest file fields to prepare the new build. .DESCRIPTION Replaces the version fields in the manifest file. Uses Drone env vars populated by pushed tags. .Parameter SubPath An optional string array of sub directories relative to the root. .INPUTS [None] No pipeline input. .OUTPUTS [DroneHelper.Repo.Path] Returns a folder structured like object with relevant full paths.s .EXAMPLE Import-Module -Name DroneHelper; Get-RepoPath #> [CmdletBinding()] [OutputType('DroneHelper.Repo.Path')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseConsistentWhitespace', '', Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1' )] param ( [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String[]]$SubPath ) process { $root = Split-Path -Path (Get-GitDirectory) $BaseName = Get-Item -Path ('{0}/src/*.psd1' -f $root) | Select-Object -ExpandProperty 'BaseName' $failureLogPath = Join-Path -Path $root -ChildPath 'failure.log' # *.psd1 related $manifestPath = Join-Path -Path $root -ChildPath 'src/*.psd1' $manifest = Get-Item -Path $manifestPath # *.psm1 related $scriptModulePath = Join-Path -Path $root -ChildPath 'src/*.psm1' $scriptModule = Get-Item -Path $scriptModulePath # Subdir related $srcPath = Join-Path -Path $root -ChildPath 'src' $binPath = Join-Path -Path $root -ChildPath 'bin' $buildPath = Join-Path -Path $root -ChildPath 'build' $resourcePath = Join-Path -Path $root -ChildPath 'resources' # bin + build artifact related $mergedScriptModulePath = Join-Path -Path $binPath -ChildPath $scriptModule.Name $artifactName = '{0}.zip' -f $BaseName $artifactPath = Join-Path -Path $binPath -ChildPath $artifactName $expandPath = Join-Path -Path $binPath -ChildPath $BaseName # iteration through the optional sub paths $formatPath = Join-Path -Path $srcPath -ChildPath 'Formats/' $cachePath = Join-Path -Path $srcPath -ChildPath 'Cache/' $docsPath = Join-Path -Path $root -ChildPath 'docs' $modulePagePath = Join-Path -Path $docsPath -ChildPath 'README.md' $docsMarkdownFilter = Join-Path -Path $docsPath -ChildPath '*.md' $subDir = @{} foreach ($dir in $SubPath) { $subDir.$dir = Join-Path -Path $root -ChildPath $dir } $changelogPath = Join-Path -Path $root -ChildPath 'CHANGELOG.md' $changelogExits = Test-Path -Path $changelogPath $ps1Filter = Join-Path -Path $srcPath -ChildPath '*.ps1' $pesterLogPath = Join-Path -Path $buildPath -ChildPath 'Pester-Results.log' $scriptAnalyzerLogPath = Join-Path -Path $buildPath -ChildPath 'ScriptAnalyzer-Results.log' $fileLinterLogPath = Join-Path -Path $buildPath -ChildPath 'FileLinter-Results.log' $scriptAnalyzerSettingsPath = Join-Path -Path $resourcePath -ChildPath 'PSScriptAnalyzerSettings.psd1' # DroneHelper Module specific $droneModuleBase = $MyInvocation.MyCommand.Module.ModuleBase $PathParams = @{ Path = $droneModuleBase ChildPath = 'Rules/PSScriptAnalyzerSettings.psd1' } $droneAnalyzerDefaultPath = Join-Path @PathParams if ($changelogExits) { $changelog = Get-Item -Path $changelogPath } else { $changelog = $null } $Path = [PSCustomObject]@{ Artifact = $BaseName Root = $root Src = [PSCustomObject]@{ Path = $srcPath Manifest = [PSCustomObject] @{ Path = $manifestPath Item = $manifest } ScriptModule = [PSCustomObject]@{ Path = $scriptModulePath Item = $scriptModule } Formats = [PSCustomObject]@{ Path = $formatPath Exists = Test-Path -Path $formatPath } Cache = [PSCustomObject]@{ Path = $cachePath Exists = Test-Path -Path $cachePath } PS1Filter = $ps1Filter } Bin = [PSCustomObject]@{ Path = $binPath ScriptModuleName = $mergedScriptModulePath ArtifactName = $artifactName ArtifactPath = $artifactPath ExpandPath = $expandPath } Build = [PSCustomObject]@{ Path = $buildPath PesterLogPath = $pesterLogPath ScriptAnalyzerLogPath = $scriptAnalyzerLogPath FileLinterLogPath = $fileLinterLogPath } Changelog = [PSCustomObject]@{ Path = $changelogPath Exists = $changelogExits Item = $changelog } Docs = [PSCustomObject]@{ Path = $docsPath ModulePagePath = $modulePagePath MarkdownFilter = $docsMarkdownFilter } DroneHelper = [PSCustomObject]@{ ModuleBase = $MyInvocation.MyCommand.Module.ModuleBase ScriptAnalyzerDefaultsPath = $droneAnalyzerDefaultPath } Resources = [PSCustomObject]@{ Path = $resourcePath ScriptAnalyzerSettingsPath = $scriptAnalyzerSettingsPath ScriptAnalyzerSettingsExist = Test-Path -Path $scriptAnalyzerSettingsPath } FailureLogPath = $failureLogPath SubDir = $subDir } $Path.PSObject.TypeNames.Insert(0, 'DroneHelper.Repo.Path') Write-Output -InputObject $Path } } # srcFile: /drone/src/src/Helper/Set-EOL.ps1 function Set-EOL { <# .SYNOPSIS Helper function to set the EOL sequence to LF or CRLF. .DESCRIPTION Helper for changing the EOL independent to the current OS defaults. .PARAMETER Style Optional style parameter for `unix` or `win.`. Default is `unix`. .PARAMETER Path Mandatory path for target file. .INPUTS [None] No pipeline input. .OUTPUTS [DroneHelper.Repo.Path] Returns a folder structured like object with relevant full paths.s .EXAMPLE Import-Module -Name DroneHelper; Set-EOL -Path './Readme.md' #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'system state does not change permanent in temp build clients.' )] param ( [Parameter(Mandatory = $false)] [ValidateSet('unix', 'win')] [String]$Style = 'unix', [Parameter(Mandatory = $true)] [System.IO.FileInfo]$Path ) process { if (!(Test-Path $Path.FullName)) { Write-Error -Message ('{0} not found!' -f $Path.FullName) -ErrorAction Stop } switch ($Style) { 'unix' { $eol = "`n" Write-Verbose -Message ('Reading {0}' -f $Path.FullName) $text = [IO.File]::ReadAllText($Path.FullName) -replace "`r`n", $eol Write-Debug -Message $text } 'win' { $eol = "`r`n" $text = [IO.File]::ReadAllText($Path.FullName) -replace "`n", $eol } } Write-Verbose -Message ("Writing back {0}" -f $Path.FullName) [IO.File]::WriteAllText($Path.FullName, $text) } } # srcFile: /drone/src/src/Docs/New-Docs.ps1 function New-Docs { <# .SYNOPSIS Creates a ne set of markdown based help in the docs folder. .DESCRIPTION This Cmdlet should be used once locally, or after adding new functions. The function `Update-Docs` can be used via pipeline to keep the docs up to date. .INPUTS [None] No pipeline input. .OUTPUTS [None] No pipeline output. .EXAMPLE New-Docs #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseSingularNouns', '', Justification = 'New-Doc already in use by other popular modules.' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'system state does not change permanent in temp build clients.' )] param () process { $Repo = Get-RepoPath Import-Module $Repo.Src.Manifest.Item.FullName -Global -Force Import-Module -Name 'platyPS' $MarkdownParams = @{ Module = $Repo.Artifact OutputFolder = $Repo.Docs.Path WithModulePage = $true ModulePagePath = $Repo.Docs.ModulePagePath Force = $true } New-MarkdownHelp @MarkdownParams $Docs = Get-Item -Path $Repo.Docs.MarkdownFilter foreach ($Doc in $Docs) { Write-Verbose -Message ('Converting {0}' -f $Doc.FullName) Set-EOL -Path $Doc } } } # srcFile: /drone/src/src/Docs/Update-Docs.ps1 function Update-Docs { <# .SYNOPSIS Publishes powershell module to internal Nexus repository. .DESCRIPTION This Cmdlet is used to publish the module via Drone pipeline. .INPUTS [None] No pipeline input. .OUTPUTS [None] No pipeline output. .EXAMPLE Update-Docs #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Underlying platyPS can not be mocked.' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseSingularNouns', '', Justification = 'New-Doc already in use by other popular modules.' )] param () process { $Repo = Get-RepoPath Import-Module $Repo.Src.Manifest.Item.FullName -Global -Force Import-Module -Name 'platyPS' $MarkdownParams = @{ Path = $Repo.Docs.Path RefreshModulePage = $true ModulePagePath = $Repo.Docs.ModulePagePath Force = $true } Update-MarkdownHelpModule @MarkdownParams $Docs = Get-Item -Path $Repo.Docs.MarkdownFilter foreach ($Doc in $Docs) { Write-Verbose -Message ('Converting {0}' -f $Doc.FullName) Set-EOL -Path $Doc } } } # srcFile: /drone/src/src/FileLinter/Invoke-FileLinter.ps1 function Invoke-FileLinter { <# .SYNOPSIS Runs the file linter for all src files found in current repository. .DESCRIPTION Invoke-FileLinter runs the basic file tests and generates a report file for furher usage in the drone pipeline. .INPUTS [None] .OUTPUTS [DroneHelper.FileLinter.Report] .EXAMPLE Invoke-FileLinter .NOTES #> [CmdletBinding()] [OutputType('DroneHelper.FileLinter.Report')] param ( [Parameter(Mandatory = $false)] [switch]$PassThru ) begin { } process { $FileSet = @() $Repo = Get-RepoPath $RawFiles = (Get-ChildItem -Path $Repo.src.Path -Recurse -File).FullName Write-Debug -Message ('EXCLUDE Filter. {0}' -f $Env:EXCLUDE) if ($Env:EXCLUDE) { $Files = $RawFiles -notmatch $Env:EXCLUDE Write-Debug -Message ('Raw File List: {0} | Filtered Files: {1}' -f $RawFiles.Count, $Files.Count) } else { $Files = $RawFiles } foreach ($file in $Files) { Write-Verbose -Message ('Running FileLinter tests for: {0}' -f $file) $FileResults = [PSCustomObject]@{ Name = $file FailedCount = 0 Tests = [ordered]@{ Encoding = (Test-FileEncoding -Path $file) BOM = (Test-FileBOM -Path $file) EOL = (Test-FileEOL -Path $file) EOF = (Test-FileEOF -Path $file) TAB = (Test-FileTab -Path $file) TailingWhite = (Test-FileTailingWhitespace -Path $file) } } Write-Verbose -Message ('Populating property FailedCount for current file.') foreach ($item in $FileResults.Tests.Keys.GetEnumerator()) { if (($FileResults.Tests.$item) -ne $true) { $FileResults.FailedCount++ } } $FileSet += [PSCustomObject]$FileResults } $LinterReport = [PSCustomObject]@{ Success = $null FilesCount = ($FileSet | Measure-Object).Count FailedCount = 0 Files = $FileSet } Write-Verbose -Message ('Populating total FailedCount property.') foreach ($i in $LinterReport.Files.FailedCount) { if ($i -ne 0) { $LinterReport.FailedCount = $LinterReport.FailedCount + $i } } if ($LinterReport.FailedCount -eq 0) { $LinterReport.Success = $true } else { $LinterReport.Success = $false } $LinterReport.PSObject.TypeNames.Insert(0, 'DroneHelper.FileLinter.Report') $LinterReport.Files | Out-String | Write-Verbose -Verbose $LinterReport | Format-Table -Property @( 'Success', 'FilesCount', 'FailedCount' ) | Out-String | Write-Verbose -Verbose $ResultParams = @{ Type = 'FileLinter' Path = $Repo.Build.FileLinterLogPath InputObject = $LinterReport } Write-ResultFile @ResultParams if (-not ($LinterReport.Success)) { Write-FailureStateFile -StepName 'FileLinter' throw 'FileLinter failed!' } } end { if ($PassThru.IsPresent) { Write-Output $LinterReport } } } # srcFile: /drone/src/src/FileLinter/Test-FileBOM.ps1 function Test-FileBOM { <# .SYNOPSIS Tests given file if native utf8 w/o BOM is used. Returns false if BOM is present. .DESCRIPTION This function is used to test for a valid encoding without BOM. .PARAMETER Path Full or relative path to existing file. .INPUTS [None] .OUTPUTS [bool] .EXAMPLE Test-FileBOM -Path './Testfile.txt' .NOTES #> [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [ValidateScript( { Test-Path -Path $_ } )] [string]$Path ) begin { } process { try { $contents = [byte[]]::new(3) $stream = [System.IO.File]::OpenRead($Path) $stream.Read($contents, 0, 3) | Out-Null $stream.Close() } catch { Write-Error -Message 'Could not read the given file!' -ErrorAction 'Stop' } Write-Debug -Message ('BOM Content was: {0}' -f ([System.BitConverter]::ToString($contents))) if ( $contents[0] -eq 0xEF -and $contents[1] -eq 0xBB -and $contents -eq 0xBF ) { Write-Output $false } else { Write-Output $true } } end { } } # srcFile: /drone/src/src/FileLinter/Test-FileEncoding.ps1 function Test-FileEncoding { <# .SYNOPSIS Returns true if the given file is written in a valid encoding .DESCRIPTION Test the given file against the encoding regex and returns true or false .PARAMETER Path Relative or full path to an existing file. .PARAMETER Encoding Optional custom encoding regex string. Default is (utf8|ascii|xml). .INPUTS [none] .OUTPUTS [bool] .EXAMPLE Test-FileEncoding -Path './testfile.txt' .NOTES #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSAvoidUsingInvokeExpression', '', Justification = 'static input without user manipulation' )] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [ValidateScript( { Test-Path -Path $_ } )] [string]$Path, [Parameter(Mandatory = $false)] [string]$Encoding = '(utf8|utf-8|ascii|xml)' ) begin { } process { try { Get-Command -Name 'file' -ErrorAction 'Stop' | Out-Null } catch { Write-Error -Message "Could not find command called 'file'!" -ErrorAction 'Stop' } $Res = Invoke-Expression -Command ("file '{0}' " -f $Path) # Remove the file from matching. Use the latest array element if split doesn't work. $ParsedResult = ($Res -split ':')[-1] Write-Debug -Message ('Encoding: Raw file output {0}' -f $Res) Write-Debug -Message ('Parsed match string: {0}' -f $ParsedResult) if ($ParsedResult -match $Encoding) { Write-Output $true } else { Write-Output $false } } end { } } # srcFile: /drone/src/src/FileLinter/Test-FileEOF.ps1 function Test-FileEOF { <# .SYNOPSIS Returns false if EOF isn't an empty line. .DESCRIPTION Test the given file against the EOF standard (final empty/blank line + CRLF) and returns true or false. .PARAMETER Path Relative or full path to an existing file. .INPUTS [none] .OUTPUTS [bool] .EXAMPLE Test-FileEOF -Path './testfile.txt' .NOTES #> [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [string]$Path ) begin { } process { if (-not (Test-FileEOL -Path $Path)) { Write-Warning -Message ('The given file does not use CRLF! ({0})' -f $Path) Write-Output $false } $content = Get-Content -Path $Path -Raw -Encoding 'utf8' $lastLine = ($content -split "`r`n")[-1].Length # Test for multiple lines without content on EOF $perLine = ($content -split "`r`n")[-2].Length if (($lastLine -eq 0) -and ($perLine -ne 0)) { Write-Debug -Message ('EOF: LastLine {0}; PenultimateLine {1} -> true' -f $lastLine, $perLine) Write-Output $true } else { Write-Debug -Message ('EOF: LastLine {0}; PenultimateLine {1} -> false' -f $lastLine, $perLine) Write-Output $false } } end { } } # srcFile: /drone/src/src/FileLinter/Test-FileEOL.ps1 function Test-FileEOL { <# .SYNOPSIS Returns false if EOL isn't CRLF .DESCRIPTION Tests given file against valid EOL. Returns true if CRLF is used. .PARAMETER Path Relative or full path to an existing file. .INPUTS [None] .OUTPUTS [bool] .EXAMPLE Test-FileEOL -Path './TestFile.txt' .NOTES #> [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [ValidateScript( { Test-Path -Path $_ } )] [string]$Path ) begin { } process { $content = Get-Content -Path $Path -Raw -Encoding 'utf8' $CRLFCount = ([regex]::Matches($content, "`r`n$")).Count $LFCount = ([regex]::Matches($content, "`n$")).Count if ($CRLFCount -eq $LFCount) { Write-Debug -Message 'EOL: CRLFCount = LFCount -> true' Write-Output $true } elseif ($CRLFCount -gt $LFCount) { Write-Debug -Message 'EOL: CRLFCount > LFCount -> false' Write-Output $false } elseif ($LFCount -gt $CRLFCount) { Write-Debug -Message 'EOL: CRLFCount < LFCount -> false' Write-Output $false } } end { } } # srcFile: /drone/src/src/FileLinter/Test-FileTab.ps1 function Test-FileTab { <# .SYNOPSIS Returns false if tab char is used in file. .DESCRIPTION Test the given file if tabs are used. Returns false if any tabs were found. .PARAMETER Path elative or full path to an existing file. .INPUTS [none] .OUTPUTS [bool] .EXAMPLE Test-FileTab -Path './testfile.txt' .NOTES #> [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [ValidateScript( { Test-Path -Path $_ } )] [string]$Path ) begin { } process { $content = Get-Content -Path $Path -Raw -Encoding 'utf8' $Tabs = ([regex]::Matches($content, "`t")).Count if ($Tabs -ne 0 ) { Write-Debug -Message ('Tabs: {0} -> false' -f $Tabs) Write-Output $false } else { Write-Debug -Message ('Tabs: {0} -> true' -f $Tabs) Write-Output $true } } end { } } # srcFile: /drone/src/src/FileLinter/Test-FileTailingWhitespace.ps1 function Test-FileTailingWhitespace { <# .SYNOPSIS Returns false if there are any tailing whitespace in lines. .DESCRIPTION Tests the given file for tailing whitespace. Returns true if not found. .PARAMETER Path Relative or full path to an existing file. .INPUTS [none] .OUTPUTS [bool] .EXAMPLE Test-FileTailingWhitespace.ps1 -Path './testfile.txt' .NOTES #> [CmdletBinding()] [OutputType([Bool])] param ( [Parameter(Mandatory = $true)] [ValidateScript( { Test-Path -Path $_ } )] [string]$Path ) begin { } process { $content = Get-Content -Path $Path -Encoding 'utf8' $WhiteSpace = 0 foreach ($line in $content) { $c = ([regex]::Matches($line, "\s+$")).Count if ( $c -gt 0 ) { $WhiteSpace++ } } if ($WhiteSpace -ne 0 ) { Write-Debug -Message ('WhiteSpace: {0} -> false' -f $WhiteSpace) Write-Output $false } else { Write-Debug -Message ('WhiteSpace: {0} -> true' -f $WhiteSpace) Write-Output $true } } end { } } # srcFile: /drone/src/src/Deploy/Invoke-Publish.ps1 function Invoke-Publish { <# .SYNOPSIS Publishes powershell module to internal Nexus repository. .DESCRIPTION This Cmdlet is used to publish the module via Drone pipeline. .INPUTS [None] No pipeline input. .OUTPUTS [None] No pipeline output. .EXAMPLE Invoke-Publish #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseConsistentWhitespace', '', Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1' )] param () process { $Repo = Get-RepoPath $ExpandParams = @{ Path = $Repo.Bin.ArtifactPath DestinationPath = $Repo.Bin.ExpandPath Force = $true ErrorAction = 'Stop' Verbose = $VerbosePreference } Expand-Archive @ExpandParams $PublishParams = @{ Repository = 'PSGallery' Path = $Repo.Bin.ExpandPath NuGetApiKey = $Env:NuGetToken Verbose = $VerbosePreference ErrorAction = 'Stop' } Publish-Module @PublishParams } } # srcFile: /drone/src/src/Reports/Format-FileLinterReport.ps1 function Format-FileLinterReport { <# .SYNOPSIS Private helper function used by Write-ResultFile. #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [PSTypeName('DroneHelper.FileLinter.Report')]$InputObject ) begin { } process { $Output = @() if ($InputObject.Success) { $Output += ':heavy_check_mark: No FileLinter violations in {0} files found.' -f $InputObject.FilesCount } else { $Output += "| Result | File | Failed |" $Output += "| :----: | :--- | -----: |" foreach ($file in $InputObject.Files) { if ($file.FailedCount -gt 0) { $failedTestNames = ( $file.Tests.GetEnumerator() | Where-Object { $_.Value -eq $false } | Select-Object -ExpandProperty 'Name' ) -join ', ' $Output += "| :heavy_exclamation_mark: | ``{0}`` | ``{1}`` |" -f $file.Name, $failedTestNames } } } Write-Output $Output } end { } } # srcFile: /drone/src/src/Reports/Format-PesterReport.ps1 function Format-PesterReport { <# .SYNOPSIS Private helper function used by Write-ResultFile. #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true)] [PSCustomObject]$InputObject, [Parameter(Mandatory = $false)] [ValidateSet('Normal', 'Detailed')] [string]$Verbosity = 'Normal' ) begin { } process { $Output = @() $Output += "| Result | Test | Duration |" $Output += "| :----: | :--- | -------: |" foreach ($Result in $InputObject.Tests) { switch ($Result.Result) { 'Passed' { if ($Verbosity -eq 'Detailed') { $RawString = "| :heavy_check_mark: | ``{0}`` | *{1}ms* |" $Output += $RawString -f $Result.ExpandedPath, $Result.UserDuration.Milliseconds } } 'Failed' { $RawString = "| :heavy_exclamation_mark: | ``{0}`` | *{1}ms* |" $Output += $RawString -f $Result.ExpandedPath, $Result.UserDuration.Milliseconds $Parsed = $Result.ErrorRecord.Exception.Message -split "`n" | Select-Object -First 1 $Output += "| :fire: | **{0}** | :fire: |" -f $Parsed } 'NotRun' { $RawString = "| :trident: | ``{0}`` | *n/a* |" $Output += $RawString -f $Result.ExpandedPath } Default { $RawString = "| :warning: | ``{0}`` | *{1}ms* |" $Output += $RawString -f $Result.ExpandedPath, $Result.UserDuration.Milliseconds } } } $Output += [Environment]::NewLine # Writing test result summary $Output += @( ':test_tube: **{0}** Total Tests (' -f $InputObject.TotalCount + ':heavy_check_mark: ``{0} Passed`` :white_small_square:' -f $InputObject.PassedCount + ':trident: ``{0} Skipped / NotRun`` :white_small_square: ' -f ( $InputObject.SkippedCount + $InputObject.NotRunCount ) + ':warning: ``Unknown`` :white_small_square: ' + ':heavy_exclamation_mark: ``{0} Failed``)' -f $InputObject.FailedCount ) # Writing code coverage summary # Covered 37,38% / 75%. 610 analyzed Commands in 26 Files. $Output += @( ':bookmark_tabs: Covered **{0}%** / ' -f [Math]::Round($InputObject.CodeCoverage.CoveragePercent, 2) + '{0}%. (' -f $InputObject.CodeCoverage.CoveragePercentTarget + ':bookmark: ``{0} analyzed Commands`` ' -f $InputObject.CodeCoverage.CommandsAnalyzedCount + ':page_facing_up: ``in {0} Files``)' -f $InputObject.CodeCoverage.FilesAnalyzedCount ) Write-Output $Output } end { } } # srcFile: /drone/src/src/Reports/Format-ScriptAnalyzerReport.ps1 function Format-ScriptAnalyzerReport { <# .SYNOPSIS Private helper function used by Write-ResultFile. #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true)] [PSCustomObject]$InputObject ) begin { } process { $Output = @() $Output += "| Severity | ScriptName | Line | RuleName | Message |" $Output += "| :------: | :--------- | :--: | :------- | :------ |" foreach ( $v in $InputObject ) { switch ($v.Severity) { 'Warning' { $Emoji = ':warning:' } 'Error' { $Emoji = ':heavy_exclamation_mark:' } 'Information' { $Emoji = ':mag:' } Default { $Emoji = ':fried_egg:' } } $RawString = "| {0} | {1} | {2} | {3} | {4} |" $Output += $RawString -f $Emoji, $v.ScriptName, $v.Line, $v.RuleName, $v.Message } $RuleURL = 'https://github.com/PowerShell/PSScriptAnalyzer/tree/master/RuleDocumentation' $Output += "`n> See [RuleDocumentation]({0}) for additional help.`n" -f $RuleURL Write-Output $Output } end { } } # srcFile: /drone/src/src/Deps/Install-ModuleDependency.ps1 function Install-ModuleDependency { <# .SYNOPSIS Install required modules of the module manifest file. .DESCRIPTION Use this cmdlet to install required modules of the module manifest file. .INPUTS [None] .OUTPUTS [None] .EXAMPLE Install-ModuleDependency .NOTES #> [CmdletBinding()] #[OutputType([String])] param () begin { } process { $Repo = Get-RepoPath $ManifestContent = Import-PowerShellDataFile -Path $Repo.Src.Manifest.Item.FullName if ($ManifestContent.RequiredModules) { foreach ($Module in $ManifestContent.RequiredModules) { if ($Module.RequiredVersion) { $ParamsInstallModule = @{ Name = $Module.ModuleName Scope = 'AllUsers' RequiredVersion = $Module.RequiredVersion Force = $true AllowClobber = $true Verbose = $VerbosePreference ErrorAction = 'Stop' } } else { $ParamsInstallModule = @{ Name = $Module.ModuleName Scope = 'AllUsers' MinimumVersion = $Module.ModuleVersion Force = $true AllowClobber = $true Verbose = $VerbosePreference ErrorAction = 'Stop' } } try { Install-Module @ParamsInstallModule $Message = 'Module <{0}> successfully installed' -f $Module.ModuleName Write-Verbose -Message $Message } catch { $Message = 'Module <{0}> could not be installed! ' -f $Module.ModuleName $Message += $_.Exception.Message Write-Error -Message $Message -ErrorAction 'Stop' } } } else { Write-Verbose -Message 'no required modules found...' } } end { } } # srcFile: /drone/src/src/Deps/Invoke-InstallDependency.ps1 function Invoke-InstallDependency { <# .SYNOPSIS Install required modules for executing the DroneHelper pipeline helpers. .DESCRIPTION This can be used in drone.io docker pipeline if the modules are not integrated in the build image. .INPUTS [None] No Input required. .OUTPUTS [None] No Output .EXAMPLE Import-Module -Name DroneHelper; Invoke-Install-Dependency #> [CmdletBinding()] [OutputType()] param () process { try { $PSScriptParams = @{ Name = 'PSScriptAnalyzer' Scope = 'CurrentUser' RequiredVersion = '1.20.0' Force = $true SkipPublisherCheck = $true AllowClobber = $true Verbose = $VerbosePreference ErrorAction = 'Stop' } Install-Module @PSScriptParams $PesterParams = @{ Name = 'Pester' Scope = 'CurrentUser' RequiredVersion = '5.3.1' Force = $true SkipPublisherCheck = $true AllowClobber = $true Verbose = $VerbosePreference ErrorAction = 'Stop' } Install-Module @PesterParams $PoshParams = @{ Name = 'posh-git' Scope = 'CurrentUser' RequiredVersion = '1.1.0' Force = $true SkipPublisherCheck = $true AllowClobber = $true Verbose = $VerbosePreference ErrorAction = 'Stop' } Install-Module @PoshParams $PsdKitParams = @{ Name = 'PsdKit' Scope = 'CurrentUser' RequiredVersion = '0.6.2' Force = $true SkipPublisherCheck = $true AllowClobber = $true AllowPrerelease = $true Verbose = $VerbosePreference ErrorAction = 'Stop' } Install-Module @PsdKitParams } catch { $ExecParams = @{ Exception = [System.Exception]::new( 'Could not install required build dependencies!', $PSItem.Exception ) ErrorAction = 'Stop' } Write-Error @ExecParams } } } # srcFile: /drone/src/src/PRComment/Send-PRComment.ps1 function Send-PRComment { <# .SYNOPSIS Sends build report as Gitea PR comment. .DESCRIPTION Send-PRComment is used to report the build details from drone.io pipeline. .PARAMETER Mode Sets the report mode. Default is 'Renew'. This mode deletes the old pr comments and creates a new onw. Also available: - 'Add' -> simply adds new pr comments. - 'Edit' -> Edits the last known pr comment. Doesn't clean old ones. .INPUTS [None]. .OUTPUTS [None] .EXAMPLE Send-PRComment Depends on Drone.IO injected environment vars. Doesn't work locally on dev systems. .NOTES #> [CmdletBinding()] #[OutputType([string])] param ( [Parameter(Mandatory = $false, HelpMessage = 'HelpMessage')] [ValidateSet('Add', 'Edit', 'Renew')] [string]$Mode = 'Renew', [Parameter(Mandatory = $false, HelpMessage = 'Gitea user for drone bot')] [ValidateNotNullOrEmpty()] [string]$GiteaUser = 'drone-bot' ) begin { # workaround for false positive PSReviewUnusedParameter $null = $GiteaUser } process { $Repo = Get-RepoPath $Workspace = $Repo.Root Write-Debug -Message ('Workspace: {0}' -f $Workspace) $PRCommentFile = Join-Path -Path $Workspace -ChildPath 'pr_comment.log' Write-Debug -Message ('PRCommentFile: {0}' -f $PRCommentFile) $PipelineStateFile = Join-Path -Path $Workspace -ChildPath 'failure.log' Write-Debug -Message ('PipelineStateFile: {0}' -f $PipelineStateFile) Write-Debug -Message ('CUSTOM_PIPELINE_STATE: {0}' -f $Env:CUSTOM_PIPELINE_STATE) if ($Env:CUSTOM_PIPELINE_STATE -eq $true) { if (Test-Path $PipelineStateFile) { Write-Debug -Message ('Setting custom pipeline status to failed') $PipelineState = 'failed' } else { Write-Debug -Message ('Setting custom pipeline status to success') $PipelineState = 'success' } } else { Write-Debug -Message ('Setting global drone status {0}' -f $Env:DRONE_BUILD_STATUS) $PipelineState = $Env:DRONE_BUILD_STATUS } if ($Env:GITEA_BASE) { $GiteaBase = $Env:GITEA_BASE } else { $GiteaBase = 'https://gitea.ocram85.com' } $APIHeaders = @{ accept = 'application/json' 'Content-Type' = 'application/json' } # Can be used with POST method to add new comment. Used with GET method returns all comments. $CommentAPICall = ('{0}/api/v1/repos/{1}/{2}/issues/{3}/comments?access_token={4}' -f $GiteaBase, $Env:DRONE_REPO_OWNER, $Env:DRONE_REPO_NAME, $Env:DRONE_PULL_REQUEST, $Env:GITEA_TOKEN ) # Update Comment API endpoint: 0 - GiteaBase, 1 - Owner, 2- Repo, 3 - PR, 4 - Token # Method Delete - removes the given comment. Patch - updates the given comment. $UpdateAPICall = '{0}/api/v1/repos/{1}/{2}/issues/comments/{3}?access_token={4}' if ($Mode -eq 'Renew') { $Comments = Invoke-RestMethod -Method 'Get' -Headers $APIHeaders -Uri $CommentAPICall $DroneComments = $Comments | Where-Object { $_.user.login -eq $GiteaUser } | Select-Object -ExpandProperty 'id' Write-Debug -Message ('Found Drone comments: {0}.' -f ($DroneComments -join ', ')) foreach ($id in $DroneComments) { $ExtAPI = $UpdateAPICall -f @( $GiteaBase, $Env:DRONE_REPO_OWNER, $Env:DRONE_REPO_NAME, $id, $Env:GITEA_TOKEN ) Write-Debug -Message ('Exec API Call: {0}' -f $ExtAPI) Invoke-RestMethod -Method 'Delete' -Headers $APIHeaders -Uri $ExtAPI } } if ($Mode -eq 'Edit') { $Comments = Invoke-RestMethod -Method 'Get' -Headers $APIHeaders -Uri $CommentAPICall $DroneComments = $Comments | Where-Object { $_.user.login -eq 'drone' } | Select-Object -ExpandProperty 'id' Write-Debug -Message ('Found Drone comments: {0}.' -f ($DroneComments -join ', ')) $EditId = $DroneComments | Sort-Object | Select-Object -Last 1 Write-Debug -Message ('Edit Comment with id {0}' -f $EditId) } $PRCommentHeader = ('> Drone.io PR Build No. [#{0}]({1}://{2}/{3}/{4}): ``{5}``' -f $Env:DRONE_BUILD_NUMBER, $Env:DRONE_SYSTEM_PROTO, $Env:DRONE_SYSTEM_HOST, $Env:DRONE_REPO, $Env:DRONE_BUILD_NUMBER, $PipelineState ) $PRCommentHeader | Out-File -FilePath $PRCommentFile -Encoding 'utf8' $LogFiles = (Get-ChildItem -Path $Env:LOG_FILES -File).FullName foreach ($file in $LogFiles) { if (Test-Path -Path $file) { ('#### ``{0}``' -f $file) | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8' $fileContent = Get-Content -Path $file -Raw -Encoding utf8 $fileContent | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8' [Environment]::NewLine | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8' -NoNewline } else { Write-Warning -Message ('Given file {0} not found!' -f $file) ('##### ``{0}`` not found!' -f $file) | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8' } } if ($Mode -eq 'Edit') { 'Last mod: {0}' -f (Get-Date -Format 'u') | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8' } 'end.' | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8' $PRCommentJSON = ConvertTo-Json -InputObject @{ Body = Get-Content -Path $PRCommentFile -Encoding utf8 -Raw } Write-Debug -Message ('PR JSON body has a size of {0} chars' -f $PRCommentJSON.length) if ($Mode -ne 'Edit') { Write-Debug -Message 'Adding new Comment.' Invoke-RestMethod -Method 'Post' -Headers $APIHeaders -Uri $CommentAPICall -Body $PRCommentJSON } else { $ExtAPI = $UpdateAPICall -f @( $GiteaBase, $Env:DRONE_REPO_OWNER, $Env:DRONE_REPO_NAME, $EditId, $Env:GITEA_TOKEN ) Write-Debug -Message 'Edit last comment.' Invoke-RestMethod -Method 'Patch' -Headers $APIHeaders -Uri $ExtAPI -Body $PRCommentJSON } } end { } } # srcFile: /drone/src/src/Pester/Invoke-UnitTest.ps1 function Invoke-UnitTest { <# .SYNOPSIS Runs all Pester tests within this repo. .DESCRIPTION This Cmdlet is used in Drone pipeline to perform the Pester based unit tests. .PARAMETER CoverageFormat Pester provides the formats JaCoCo ans CoverageGutters. Default is JaCoCo. These are the known use cases: - JaCoCo -> Used as standard coverage report used by sonar - CoverageGutters -> Custom Format to show coverage in VSCode. .PARAMETER Verbosity This parameter sets the Pester detail level. Default is 'Normal.' Available values are: 'None', 'Normal', 'Detailed', 'Diagnostic' .PARAMETER PassThru Tells Invoke-UnitTest to write back the Pester results into your variable / output. .PARAMETER Tag Pester build in tag filter as string array. .PARAMETER ExcludeTag Pester build in exclude filter for tests as string array. .INPUTS [None] No pipeline input. .OUTPUTS [None] No pipeline output. .EXAMPLE Invoke-UnitTest #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseConsistentWhitespace', '', Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1' )] param ( [Parameter( Mandatory = $false )] [ValidateSet('JaCoCo', 'CoverageGutters')] [string]$CoverageFormat = 'JaCoCo', [Parameter(Mandatory = $false)] [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] [string]$Verbosity = 'Normal', [Parameter(Mandatory = $false)] [switch]$PassThru, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string[]]$Tag, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string[]]$ExcludeTag ) process { $Repo = Get-RepoPath Write-Verbose -Message '===== Running Pester =====' -Verbose:$VerbosePreference $PesterConf = New-PesterConfiguration $PesterConf.Run.Path = (Resolve-Path -Path './src').Path $PesterConf.Run.Exit = $false $PesterConf.Run.PassThru = $true $PesterConf.CodeCoverage.Enabled = $true $PesterConf.CodeCoverage.OutputFormat = $CoverageFormat $PesterConf.TestResult.Enabled = $true $CovFiles = Get-ChildItem -Path $Repo.Src.PS1Filter -Recurse | Where-Object { $_.BaseName -notmatch '.Tests' } | Select-Object -ExpandProperty 'FullName' $PesterConf.CodeCoverage.Path = $CovFiles $PesterConf.Output.Verbosity = $Verbosity # Set Tags if given if ($Tag) { $PesterConf.Filter.Tag = $Tag } if ($ExcludeTag) { $PesterConf.Filter.ExcludeTag = $ExcludeTag } $TestResults = Invoke-Pester -Configuration $PesterConf -ErrorAction 'Stop' try { $ResFileParams = @{ InputObject = $TestResults Path = $Repo.Build.PesterLogPath Type = 'Pester' ErrorAction = 'Stop' } Write-ResultFile @ResFileParams } catch { Write-FailureStateFile -StepName 'Pester' throw ('{0} tests failed!' -f $TestResults.FailedCount) } if ($TestResults.FailedCount -gt 0) { Write-FailureStateFile -StepName 'Pester' throw ('{0} tests failed!' -f $TestResults.FailedCount) } if ($PassThru.IsPresent) { Write-Output -InputObject $TestResults } } } # srcFile: /drone/src/src/Build/Merge-ModuleRoot.ps1 function Merge-ModuleRoot { <# .SYNOPSIS Merges single ps1 files into one module script file. .DESCRIPTION This Cmdlet is used in build pipeline to reduce the file load and import performance to the target module. .INPUTS [None] No pipeline input. .OUTPUTS [None] No pipeline output. .EXAMPLE Import-Module -Name DroneHelper; Merge-ModuleRoot #> [CmdletBinding()] param () process { $Repo = Get-RepoPath $srcFiles = Get-ChildItem -Path $Repo.Src.Path -Recurse -File | Where-Object { ($_.Name -notmatch '.Tests.') -and ($_.Name -match '.ps1') -and ($_.Name -notmatch '.ps1xml') } $Output = @() foreach ($psFile in $srcFiles) { $fileContent = Get-Content -Path $psFile.FullName -Raw -Encoding 'utf8' $Output += '# srcFile: {0}' -f $psFile.FullName $Output += $fileContent.TrimEnd() $Output += '{0}' -f [Environment]::NewLine } try { $Output | Out-File -FilePath $Repo.Bin.ScriptModuleName -Encoding 'utf8' -Force -ErrorAction Stop } catch { Write-FailureStateFile -StepName 'MergeModuleRoot' throw 'Could not write the final module root script file!' } } } # srcFile: /drone/src/src/Build/New-BuildPackage.ps1 function New-BuildPackage { <# .SYNOPSIS Creates a new module package as compressed archive. .DESCRIPTION This function is used in build pipeline to create an uploadable module version for the Gitea release page. .PARAMETER AdditionalPath You can provide additional paths to add files or folders in published module. .INPUTS [None] No pipeline input. .OUTPUTS [None] No pipeline output. .EXAMPLE Import-Module -Name DroneHelper; New-BuildPackage #> [CmdletBinding()] [OutputType()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseConsistentWhitespace', '', Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'system state does not change permanent in temp build clients.' )] param ( [Parameter(Mandatory = $false)] [String[]]$AdditionalPath ) process { $Repo = Get-RepoPath $res = @() foreach ($item in $AdditionalPath) { try { $res += Resolve-Path -Path $item -ErrorAction Stop } catch { Write-Error -Message ('The given additional path does not exist! ({0})' -f $item) -ErrorAction Stop } } Merge-ModuleRoot -ErrorAction Stop $CompressParams = @{ Path = @( # psm1 file $Repo.Bin.ScriptModuleName # psd1 file $Repo.Src.Manifest.Item.FullName # Formats/ folder $Repo.Src.Formats.Path ) DestinationPath = $Repo.Bin.ArtifactPath Force = $true ErrorAction = 'Stop' Verbose = $VerbosePreference } $CompressParams.Path += $res try { Compress-Archive @CompressParams } catch { Write-FailureStateFile -StepName 'BuildPackage' throw $_.Exception.Message } } } # srcFile: /drone/src/src/Build/Update-ModuleMeta.ps1 function Update-ModuleMeta { <# .SYNOPSIS Updates the module manifest file fields to prepare the new build. .DESCRIPTION Replaces the version fields in the manifest file. Uses Drone env vars populated by pushed tags. .INPUTS [None] No pipeline input. .OUTPUTS [None] No pipeline output. .EXAMPLE Import-Module -Name DroneHelper; Update-ModuleMeta #> [CmdletBinding()] [OutputType()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseConsistentWhitespace', '', Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'system state does not change permanent in temp build clients.' )] param ( [Parameter(Mandatory = $false)] [ValidateScript( { if (Test-Path -Path $_) { return $true } else { throw 'Could not find file: {0}' -f $_ } } )] [ValidateNotNullOrEmpty()] [string]$Path ) process { if (!$Path) { $Repo = Get-RepoPath $ManifestFilePath = $Repo.Src.Manifest.Item.FullName } else { $ManifestFilePath = $Path } if ($Env:DRONE) { if ($Env:DRONE_BUILD_EVENT -eq 'tag') { if ($null -ne $Env:DRONE_SEMVER) { $nVersion = $Env:DRONE_SEMVER_SHORT if ($null -ne $Env:DRONE_SEMVER_PRERELEASE) { $nPreRelease = $Env:DRONE_SEMVER_PRERELEASE } $DataParams = @{ Path = $ManifestFilePath ErrorAction = 'Stop' } # Getting the module manifest as imported object try { $ModManifestData = Import-PowerShellDataFile @DataParams } catch { $_.Exception.Message | Write-Debug $ErrorParams = @{ Message = "Could not import the module manifest file." ErrorAction = 'Stop' } Write-Error @ErrorParams } # Updating the new module version $ModManifestData.ModuleVersion = $nVersion # Updating the prerelease property if there is one if ($nPreRelease) { $ModManifestData.PrivateData.PSData.Prerelease = $nPreRelease } $ManifestData = Test-ModuleManifest -Path $ManifestFilePath if ( ($nVersion -ne $ManifestData.Version) -or ($nPreRelease -ne $ManifestData.PrivateData.PSData.Prerelease) ) { $OutputFileParams = @{ Path = $ManifestFilePath #PassThru = $true Encoding = 'utf8NoBom' Force = $true Verbose = $VerbosePreference } try { $ModManifestData | ConvertTo-Psd | Set-Content @OutputFileParams } catch { $_.Exception.Message | Write-Debug $ErrorParams = @{ Message = "Failed to update the module manifest file" ErrorAction = 'Stop' } Write-Error @ErrorParams } } else { Write-Warning -Message 'Identical version given. Skipping update.' } } else { Write-Warning -Message 'Could not read the new Tag / Semver!' } } else { Write-Warning -Message 'This pipeline was not triggered by a tag.' } } else { Write-Warning -Message 'Running outside of drone.io pipeline. Skipping module update!' } } } # srcFile: /drone/src/src/Changelog/Update-Changelog.ps1 function Update-Changelog { <# .SYNOPSIS Updates the changelog file with recent commits .DESCRIPTION This helper function is used to insert recent changes for an upcoming release. .Parameter NewVersion Provide a valid semver based version tag for the upcoming release like: - `v0.0.1-dev1` - `v1.0.0` .Parameter SkipCleanup You can skip the tag update and additional test. .INPUTS [None] No pipeline input. .OUTPUTS [None] no pipeline putput. .EXAMPLE Import-Module -Name DroneHelper; Update-Changelog -NewVersion '0.0.1-dev5' #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSAvoidUsingInvokeExpression', '', Justification = 'raw git commands needed' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'system state does not change permanent in temp build clients.' )] param ( [Parameter(Mandatory = $true)] [ValidatePattern('^(v)?([0-9]+)\.([0-9]+)\.([0-9]+)(-([A-Za-z]+)([0-9]+))?$')] [String]$NewVersion, [Parameter(Mandatory = $false)] [Switch]$SkipCleanup ) process { if (-not $SkipCleanup.IsPresent) { Invoke-Expression -Command 'git tag -d $(git tag -l)' | Write-Verbose Invoke-Expression -Command 'git fetch --tags --prune' | Write-Verbose $GitState = Get-GitStatus if ($GitState.Branch -eq 'master') { Write-Error -Message 'You can nor update the changelog within the master branch!' -ErrorAction Stop } if ( ($GitState.BehindBy -ne 0) -or ($GitState.AheadBy -ne 0) -or ($GitState.HasUntracked -ne $false) -or ($GitState.HasWorking -ne $false) ) { Write-Error -Message 'Your branch is a mess! Cleanup and try it again.' -ErrorAction Stop } } $Repo = Get-RepoPath $Tags = Invoke-Expression -Command 'git tag' $NormalizedTags = $Tags | Where-Object { $_ -notmatch '-' } | ForEach-Object { [PSCustomObject]@{ tag = $_ rawVersion = ( ($_ -split '-')[0] -replace 'v', '' ) } } $LTag = $NormalizedTags | Sort-Object -Property 'rawVersion' | Select-Object -ExpandProperty 'tag' -Last 1 Write-Debug -Message ('Last tag: {0}' -f $LTag) if ($null -eq $LTag) { Write-Error -Message 'No tags found!' -ErrorAction 'Stop' } $Expr = "git log {0}..HEAD --format='- (%h) %s'" -f $LTag $Res = Invoke-Expression -Command $Expr Write-Debug -Message ('New Changelog: {0}' -f $Res) if ($Repo.Changelog.Exists) { $Content = Get-Content -Path $Repo.Changelog.Item.FullName $Content[2] += "{0}## ``{2}``{0}{0}{1}" -f [Environment]::NewLine, ($Res | Out-String), $NewVersion $Content | Out-File -FilePath $Repo.Changelog.Item.FullName -Encoding utf8 Set-EOL -Path $Repo.Changelog.Item.FullName } else { Write-Error -Message 'Changelog file does not exist!' -ErrorAction Stop } } } |