mirror of https://github.com/raandree/NTFSSecurity
Browse Source
ci: publish releases from CI on a version tag, starting with 5.0.0-rc1pull/99/head 5.0.0-rc1
committed by
GitHub
17 changed files with 907 additions and 82 deletions
@ -0,0 +1,119 @@ |
|||||
|
<# |
||||
|
.SYNOPSIS |
||||
|
Returns the version of the module and its release notes from CHANGELOG.md. |
||||
|
|
||||
|
.DESCRIPTION |
||||
|
Reads ModuleVersion and the prerelease label (PrivateData.PSData.Prerelease) from the module manifest and returns |
||||
|
the version to release, such as 5.0.0 or 5.0.0-rc1, with its release notes: |
||||
|
|
||||
|
- A release without a prerelease label takes the notes of the section "## [<version>] - <yyyy-MM-dd>". The |
||||
|
section must exist and have a release date. |
||||
|
- A prerelease takes the notes of the section "## [Unreleased]", which must not be empty. CHANGELOG.md must not |
||||
|
have a section for the version yet; it gets one with the final release. |
||||
|
|
||||
|
The notes are the text below the heading up to the next section or up to the link definitions at the end of the |
||||
|
file, with line feeds as line breaks. |
||||
|
|
||||
|
.PARAMETER ManifestPath |
||||
|
Specifies the module manifest. |
||||
|
|
||||
|
.PARAMETER ChangelogPath |
||||
|
Specifies CHANGELOG.md. |
||||
|
|
||||
|
.EXAMPLE |
||||
|
.\.github\scripts\Get-ReleaseInfo.ps1 -ManifestPath .\NTFSSecurity\NTFSSecurity.psd1 -ChangelogPath .\CHANGELOG.md |
||||
|
|
||||
|
Returns the version, whether it is a prerelease, the release date, and the release notes. |
||||
|
#> |
||||
|
[CmdletBinding()] |
||||
|
[OutputType([pscustomobject])] |
||||
|
param ( |
||||
|
[Parameter(Mandatory)] |
||||
|
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })] |
||||
|
[string] |
||||
|
$ManifestPath, |
||||
|
|
||||
|
[Parameter(Mandatory)] |
||||
|
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })] |
||||
|
[string] |
||||
|
$ChangelogPath |
||||
|
) |
||||
|
|
||||
|
$ErrorActionPreference = 'Stop' |
||||
|
|
||||
|
function Get-ChangelogSection { |
||||
|
<# |
||||
|
Returns the date and the notes of the section with the given name, such as 5.0.0 or Unreleased, or nothing if |
||||
|
CHANGELOG.md has no such section. |
||||
|
#> |
||||
|
param ( |
||||
|
[Parameter(Mandatory)] |
||||
|
[string] |
||||
|
$Changelog, |
||||
|
|
||||
|
[Parameter(Mandatory)] |
||||
|
[string] |
||||
|
$Name |
||||
|
) |
||||
|
|
||||
|
$pattern = '(?ms)^## \[{0}\](?:[ \t]+-[ \t]+(?<Date>\S+))?[ \t]*$(?<Notes>.*?)(?=^## |^\[[^\]]+\]:[ \t]|\z)' -f |
||||
|
[regex]::Escape($Name) |
||||
|
$match = [regex]::Match($Changelog, $pattern) |
||||
|
if ($match.Success) { |
||||
|
[pscustomobject]@{ |
||||
|
Date = $match.Groups['Date'].Value |
||||
|
Notes = $match.Groups['Notes'].Value.Trim() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$manifest = Import-PowerShellDataFile -LiteralPath $ManifestPath |
||||
|
$moduleVersion = "$($manifest.ModuleVersion)" |
||||
|
if ($moduleVersion -notmatch '^\d+\.\d+\.\d+$') { |
||||
|
throw "The module version '$moduleVersion' must have three parts, such as 5.0.0." |
||||
|
} |
||||
|
|
||||
|
$prerelease = "$($manifest.PrivateData.PSData.Prerelease)" |
||||
|
if ($prerelease -and $prerelease -notmatch '^[A-Za-z][0-9A-Za-z-]*$') { |
||||
|
throw ("The prerelease label '$prerelease' isn't valid in the PowerShell Gallery. Use letters, digits, and " + |
||||
|
'hyphens only, starting with a letter, such as rc1.') |
||||
|
} |
||||
|
|
||||
|
$changelog = (Get-Content -LiteralPath $ChangelogPath -Raw -Encoding UTF8) -replace '\r\n', "`n" |
||||
|
$versionSection = Get-ChangelogSection -Changelog $changelog -Name $moduleVersion |
||||
|
|
||||
|
if ($prerelease) { |
||||
|
if ($versionSection) { |
||||
|
throw ("CHANGELOG.md already has a section for $moduleVersion, so $moduleVersion is released. A prerelease " + |
||||
|
'needs a higher module version.') |
||||
|
} |
||||
|
|
||||
|
$section = Get-ChangelogSection -Changelog $changelog -Name 'Unreleased' |
||||
|
if (-not $section -or -not $section.Notes) { |
||||
|
throw "The [Unreleased] section of CHANGELOG.md is empty. It holds the notes of $moduleVersion-$prerelease." |
||||
|
} |
||||
|
$date = $null |
||||
|
} else { |
||||
|
if (-not $versionSection) { |
||||
|
throw ("CHANGELOG.md has no section for $moduleVersion. Rename the [Unreleased] section to " + |
||||
|
"[$moduleVersion] and add the release date.") |
||||
|
} |
||||
|
if ($versionSection.Date -notmatch '^\d{4}-\d{2}-\d{2}$') { |
||||
|
throw "The section of $moduleVersion in CHANGELOG.md has no release date in the format yyyy-MM-dd." |
||||
|
} |
||||
|
if (-not $versionSection.Notes) { |
||||
|
throw "The section of $moduleVersion in CHANGELOG.md is empty." |
||||
|
} |
||||
|
|
||||
|
$section = $versionSection |
||||
|
$date = [datetime]::ParseExact($versionSection.Date, 'yyyy-MM-dd', [cultureinfo]::InvariantCulture) |
||||
|
} |
||||
|
|
||||
|
[pscustomobject]@{ |
||||
|
Version = if ($prerelease) { "$moduleVersion-$prerelease" } else { $moduleVersion } |
||||
|
ModuleVersion = $moduleVersion |
||||
|
Prerelease = $prerelease |
||||
|
IsPrerelease = [bool] $prerelease |
||||
|
Date = $date |
||||
|
Notes = $section.Notes |
||||
|
} |
||||
@ -0,0 +1,148 @@ |
|||||
|
<# |
||||
|
.SYNOPSIS |
||||
|
Builds the packages of a release from the build output of the module. |
||||
|
|
||||
|
.DESCRIPTION |
||||
|
Copies the files that the FileList of the module manifest names from BuildPath into the folder NTFSSecurity in |
||||
|
DestinationPath, so that the packages contain no debug symbols or other build output, and checks the copy with |
||||
|
Test-ModuleManifest. Then it creates the NuGet package for the PowerShell Gallery with Compress-PSResource, named |
||||
|
after the version and the prerelease label, and adds the command tags that the PowerShell Gallery uses to list the |
||||
|
cmdlets of the module. Last, it creates NTFSSecurity.zip, which contains the folder NTFSSecurity, for the GitHub |
||||
|
release. |
||||
|
|
||||
|
The script needs Compress-PSResource from Microsoft.PowerShell.PSResourceGet, which comes with PowerShell 7.4 and |
||||
|
later. |
||||
|
|
||||
|
.PARAMETER BuildPath |
||||
|
Specifies the build output folder, such as NTFSSecurity\bin\Release. |
||||
|
|
||||
|
.PARAMETER DestinationPath |
||||
|
Specifies the folder for the module folder and the packages. The script replaces the module folder and the |
||||
|
packages that an earlier run created there. |
||||
|
|
||||
|
.EXAMPLE |
||||
|
.\.github\scripts\New-ModulePackage.ps1 -BuildPath .\NTFSSecurity\bin\Release -DestinationPath .\out |
||||
|
|
||||
|
Creates the folder .\out\NTFSSecurity and the files .\out\NTFSSecurity.<version>.nupkg and .\out\NTFSSecurity.zip. |
||||
|
#> |
||||
|
[CmdletBinding()] |
||||
|
[OutputType([pscustomobject])] |
||||
|
param ( |
||||
|
[Parameter(Mandatory)] |
||||
|
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] |
||||
|
[string] |
||||
|
$BuildPath, |
||||
|
|
||||
|
[Parameter(Mandatory)] |
||||
|
[ValidateNotNullOrEmpty()] |
||||
|
[string] |
||||
|
$DestinationPath |
||||
|
) |
||||
|
|
||||
|
$ErrorActionPreference = 'Stop' |
||||
|
$moduleName = 'NTFSSecurity' |
||||
|
|
||||
|
if (-not (Get-Command -Name Compress-PSResource -ErrorAction SilentlyContinue)) { |
||||
|
throw 'Compress-PSResource was not found. Run the script in PowerShell 7.4 or later, which includes PSResourceGet.' |
||||
|
} |
||||
|
|
||||
|
$buildRoot = (Resolve-Path -LiteralPath $BuildPath).ProviderPath |
||||
|
$manifestPath = Join-Path -Path $buildRoot -ChildPath "$moduleName.psd1" |
||||
|
if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { |
||||
|
throw "The build output in '$buildRoot' has no module manifest $moduleName.psd1." |
||||
|
} |
||||
|
|
||||
|
$manifest = Import-PowerShellDataFile -LiteralPath $manifestPath |
||||
|
$version = "$($manifest.ModuleVersion)" |
||||
|
if ($manifest.PrivateData.PSData.Prerelease) { |
||||
|
$version = '{0}-{1}' -f $version, $manifest.PrivateData.PSData.Prerelease |
||||
|
} |
||||
|
|
||||
|
$missingFiles = @($manifest.FileList | Where-Object -FilterScript { |
||||
|
-not (Test-Path -LiteralPath (Join-Path -Path $buildRoot -ChildPath $_) -PathType Leaf) |
||||
|
}) |
||||
|
if ($missingFiles) { |
||||
|
throw "The build output in '$buildRoot' lacks these files of the FileList: $($missingFiles -join ', ')." |
||||
|
} |
||||
|
|
||||
|
$destinationRoot = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath) |
||||
|
$modulePath = Join-Path -Path $destinationRoot -ChildPath $moduleName |
||||
|
$packagePath = Join-Path -Path $destinationRoot -ChildPath "$moduleName.$version.nupkg" |
||||
|
$zipPath = Join-Path -Path $destinationRoot -ChildPath "$moduleName.zip" |
||||
|
foreach ($path in $modulePath, $packagePath, $zipPath) { |
||||
|
if (Test-Path -LiteralPath $path) { |
||||
|
Remove-Item -LiteralPath $path -Recurse -Force |
||||
|
} |
||||
|
} |
||||
|
New-Item -ItemType Directory -Path $modulePath -Force | Out-Null |
||||
|
|
||||
|
foreach ($file in $manifest.FileList) { |
||||
|
$target = Join-Path -Path $modulePath -ChildPath $file |
||||
|
$targetFolder = Split-Path -Path $target -Parent |
||||
|
if (-not (Test-Path -LiteralPath $targetFolder)) { |
||||
|
New-Item -ItemType Directory -Path $targetFolder | Out-Null |
||||
|
} |
||||
|
Copy-Item -LiteralPath (Join-Path -Path $buildRoot -ChildPath $file) -Destination $target |
||||
|
} |
||||
|
|
||||
|
$testParameters = @{ |
||||
|
Path = Join-Path -Path $modulePath -ChildPath "$moduleName.psd1" |
||||
|
ErrorAction = 'Stop' |
||||
|
WarningVariable = 'manifestWarnings' |
||||
|
WarningAction = 'SilentlyContinue' |
||||
|
} |
||||
|
$null = Test-ModuleManifest @testParameters |
||||
|
if ($manifestWarnings) { |
||||
|
throw "Test-ModuleManifest reported warnings for the module in '$modulePath': $($manifestWarnings -join ' ')" |
||||
|
} |
||||
|
|
||||
|
Compress-PSResource -Path $modulePath -DestinationPath $destinationRoot |
||||
|
if (-not (Test-Path -LiteralPath $packagePath -PathType Leaf)) { |
||||
|
throw "Compress-PSResource didn't create the package '$packagePath'." |
||||
|
} |
||||
|
|
||||
|
<# |
||||
|
PSResourceGet doesn't add the command tags that PowerShellGet 2 added when it published 4.2.6. The PowerShell |
||||
|
Gallery lists the cmdlets of a module from these tags, and Find-Command searches them, so add the missing ones to |
||||
|
the nuspec in the package. |
||||
|
#> |
||||
|
$commandTags = @('PSIncludes_Cmdlet') + @($manifest.CmdletsToExport | Sort-Object -Unique | |
||||
|
ForEach-Object -Process { "PSCmdlet_$_"; "PSCommand_$_" }) |
||||
|
$archive = [IO.Compression.ZipFile]::Open($packagePath, [IO.Compression.ZipArchiveMode]::Update) |
||||
|
try { |
||||
|
$stream = $archive.GetEntry("$moduleName.nuspec").Open() |
||||
|
try { |
||||
|
$nuspec = New-Object -TypeName 'System.Xml.XmlDocument' |
||||
|
$nuspec.Load($stream) |
||||
|
$tagsNode = $nuspec.SelectSingleNode("/*[local-name()='package']/*[local-name()='metadata']/*[local-name()='tags']") |
||||
|
if (-not $tagsNode) { |
||||
|
throw "The nuspec in the package '$packagePath' has no tags element." |
||||
|
} |
||||
|
$tags = @($tagsNode.InnerText -split '\s+' | Where-Object -FilterScript { $_ }) |
||||
|
$tagsNode.InnerText = ($tags + @($commandTags | Where-Object -FilterScript { $_ -notin $tags })) -join ' ' |
||||
|
|
||||
|
$stream.SetLength(0) |
||||
|
$settings = New-Object -TypeName 'System.Xml.XmlWriterSettings' |
||||
|
$settings.Encoding = New-Object -TypeName 'System.Text.UTF8Encoding' -ArgumentList $false |
||||
|
$settings.Indent = $true |
||||
|
$writer = [Xml.XmlWriter]::Create($stream, $settings) |
||||
|
try { |
||||
|
$nuspec.Save($writer) |
||||
|
} finally { |
||||
|
$writer.Dispose() |
||||
|
} |
||||
|
} finally { |
||||
|
$stream.Dispose() |
||||
|
} |
||||
|
} finally { |
||||
|
$archive.Dispose() |
||||
|
} |
||||
|
|
||||
|
[IO.Compression.ZipFile]::CreateFromDirectory($modulePath, $zipPath, [IO.Compression.CompressionLevel]::Optimal, $true) |
||||
|
|
||||
|
[pscustomobject]@{ |
||||
|
Version = $version |
||||
|
ModulePath = $modulePath |
||||
|
PackagePath = $packagePath |
||||
|
ZipPath = $zipPath |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
--- |
||||
|
status: accepted |
||||
|
date: 2026-10-04 |
||||
|
last-verified: 2026-10-04 |
||||
|
owner: shared |
||||
|
source: maintainer decisions after #97 (release first, prerelease first) |
||||
|
--- |
||||
|
|
||||
|
# Decision 12: Releases are built and published by CI on a version tag |
||||
|
|
||||
|
- Choice: Pushing a tag such as `5.0.0` or `5.0.0-rc1` on `master` runs the |
||||
|
`release` job of `.github/workflows/ci.yml`. It publishes the package that |
||||
|
the `build` job built and tested to the PowerShell Gallery |
||||
|
(`Publish-PSResource -NupkgPath`) and creates the GitHub release with |
||||
|
`NTFSSecurity.zip`, marked as a prerelease for a prerelease tag. The job |
||||
|
checks that the tag equals the manifest version (with the prerelease |
||||
|
label) and points to a commit on `master`, and it skips a version the |
||||
|
Gallery or GitHub already has, so a rerun is safe. |
||||
|
- Package: `New-ModulePackage.ps1` copies only the `FileList` files of the |
||||
|
Release build, so no `.pdb`, XML documentation, or |
||||
|
`System.Management.Automation.dll` ships. `Compress-PSResource` builds the |
||||
|
nupkg; the script adds the command tags (`PSIncludes_Cmdlet`, |
||||
|
`PSCmdlet_<name>`, `PSCommand_<name>`) that PowerShellGet 2 added and |
||||
|
PSResourceGet 1.2 doesn't, because the Gallery lists cmdlets and |
||||
|
`Find-Command` searches by them. Every CI run builds the packages, so pull |
||||
|
requests test them. |
||||
|
- Secret: `PSGALLERY_API_KEY` belongs to the GitHub environment |
||||
|
`powershell-gallery`, which only the `release` job uses; the maintainer |
||||
|
creates the key, the environment, and the secret, and may require a |
||||
|
reviewer. |
||||
|
- Process: a prerelease first (maintainer, 2026-10-04: "no full release |
||||
|
without proper testing"), starting with `5.0.0-rc1`; the final release |
||||
|
removes the label and dates the changelog section. Steps for maintainers |
||||
|
are in `Docs/Contributing/05-Releasing.md`. |
||||
|
- Rationale: Releases so far were local Debug builds published by hand |
||||
|
with the whole output folder; tags carried the previous version. |
||||
|
- Rejected: publishing with PowerShellGet 2 `Publish-Module` (repackages at |
||||
|
publish time, so the tested package isn't the published one), and adding |
||||
|
the command tags to the shipped manifest. |
||||
@ -0,0 +1,110 @@ |
|||||
|
# Release a new version |
||||
|
|
||||
|
This page explains how a maintainer releases a new version of NTFSSecurity. |
||||
|
The CI workflow does the work: when you push a version tag, it builds and |
||||
|
tests the module, publishes the package to the PowerShell Gallery, and creates |
||||
|
the GitHub release with `NTFSSecurity.zip`. |
||||
|
|
||||
|
## Prepare the repository once |
||||
|
|
||||
|
1. In the PowerShell Gallery, [create an API key][api-key] for the account |
||||
|
that owns NTFSSecurity. Select the scope **Push new versions of existing |
||||
|
packages**, enter `NTFSSecurity` as the glob pattern, and choose an |
||||
|
expiration. |
||||
|
2. In the settings of the repository on GitHub, under **Environments**, |
||||
|
create the environment `powershell-gallery`. To approve every release |
||||
|
before it's published, add yourself as a required reviewer. Under |
||||
|
**Deployment branches and tags**, allow only tags such as |
||||
|
`[0-9]*.[0-9]*.[0-9]*`. |
||||
|
3. Add the API key as the secret `PSGALLERY_API_KEY` of the environment. |
||||
|
The command asks for the key: |
||||
|
|
||||
|
```powershell |
||||
|
gh secret set PSGALLERY_API_KEY --env powershell-gallery --repo raandree/NTFSSecurity |
||||
|
``` |
||||
|
|
||||
|
Renew the API key before it expires, and update the secret. |
||||
|
|
||||
|
## Versions |
||||
|
|
||||
|
- `ModuleVersion` in `NTFSSecurity\NTFSSecurity.psd1` and the |
||||
|
`AssemblyVersion` and `AssemblyFileVersion` of the `NTFSSecurity`, |
||||
|
`Security2`, and `PrivilegeControl` projects carry the same version. The |
||||
|
tests in `Tests\Manifest.Tests.ps1` check this. |
||||
|
- A prerelease, such as `5.0.0-rc1`, has its label in `Prerelease` under |
||||
|
`PrivateData.PSData` of the module manifest. Its release notes are the |
||||
|
`[Unreleased]` section of `CHANGELOG.md`. The PowerShell Gallery installs a |
||||
|
prerelease only when you add `-AllowPrerelease`. |
||||
|
- A release, such as `5.0.0`, has no `Prerelease` value. Its release notes |
||||
|
are the section `## [5.0.0] - <date>` of `CHANGELOG.md`. |
||||
|
|
||||
|
The tests in `Tests\Release.Tests.ps1` check that `CHANGELOG.md` has the |
||||
|
release notes for the version of the module manifest. |
||||
|
|
||||
|
## Publish a prerelease |
||||
|
|
||||
|
1. Set the version and the `Prerelease` label, such as `rc1`, and make sure |
||||
|
that the `[Unreleased]` section of `CHANGELOG.md` describes the changes. |
||||
|
Merge the change into `master`. |
||||
|
2. Tag the commit on `master` with the version and push the tag: |
||||
|
|
||||
|
```powershell |
||||
|
git switch master |
||||
|
git pull |
||||
|
git tag 5.0.0-rc1 |
||||
|
git push origin 5.0.0-rc1 |
||||
|
``` |
||||
|
|
||||
|
3. Watch the CI run of the tag. The **Release** job checks that the tag |
||||
|
matches the version in the module manifest and points to a commit on |
||||
|
`master`. Then it publishes the package and creates a GitHub prerelease. |
||||
|
If the environment requires a reviewer, approve the deployment. |
||||
|
4. Install the prerelease in a test environment and test it: |
||||
|
|
||||
|
```powershell |
||||
|
Install-Module -Name NTFSSecurity -AllowPrerelease -Scope CurrentUser |
||||
|
``` |
||||
|
|
||||
|
For another prerelease, increase the label, such as `rc2`. The PowerShell |
||||
|
Gallery compares labels as text, so `rc10` sorts before `rc2`. |
||||
|
|
||||
|
## Publish a release |
||||
|
|
||||
|
1. Remove the `Prerelease` value from the module manifest. |
||||
|
2. In `CHANGELOG.md`, rename `## [Unreleased]` to the version with the |
||||
|
release date, such as `## [5.0.0] - 2026-10-31`, add an empty |
||||
|
`## [Unreleased]` section above it, and update the links at the end of the |
||||
|
file: |
||||
|
|
||||
|
```markdown |
||||
|
[Unreleased]: https://github.com/raandree/NTFSSecurity/compare/5.0.0...HEAD |
||||
|
[5.0.0]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...5.0.0 |
||||
|
``` |
||||
|
|
||||
|
3. Merge the change into `master`. Then tag the commit with the version, such |
||||
|
as `5.0.0`, and push the tag, as for a prerelease. The **Release** job |
||||
|
warns if the date in `CHANGELOG.md` isn't the day of the release. |
||||
|
|
||||
|
## If a release fails |
||||
|
|
||||
|
The **Release** job skips what's already done: a version that the PowerShell |
||||
|
Gallery already has, and a GitHub release that already exists. If the |
||||
|
failure doesn't need a change in the repository, fix the cause and rerun the |
||||
|
failed job. |
||||
|
|
||||
|
If the fix needs a change in the repository and the PowerShell Gallery |
||||
|
doesn't have the version yet, delete the tag, merge the fix, and tag the new |
||||
|
commit: |
||||
|
|
||||
|
```powershell |
||||
|
git push origin --delete 5.0.0-rc1 |
||||
|
git tag --delete 5.0.0-rc1 |
||||
|
``` |
||||
|
|
||||
|
The PowerShell Gallery never accepts the same version twice. To replace a |
||||
|
published version, publish the next one, such as `5.0.0-rc2`, and |
||||
|
[unlist][unlist] the faulty version. |
||||
|
|
||||
|
<!-- External URLs --> |
||||
|
[api-key]: https://learn.microsoft.com/powershell/gallery/how-to/managing-profile/creating-apikeys |
||||
|
[unlist]: https://learn.microsoft.com/powershell/gallery/how-to/publishing-packages/unlisting-packages |
||||
@ -0,0 +1,255 @@ |
|||||
|
<# |
||||
|
Tests the release scripts in .github\scripts: Get-ReleaseInfo.ps1, which reads the version of the module and its |
||||
|
release notes, and New-ModulePackage.ps1, which builds the packages from the module built in |
||||
|
NTFSSecurity\bin\Release. |
||||
|
#> |
||||
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
||||
|
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' |
||||
|
)] |
||||
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
||||
|
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The test helpers write only to TestDrive.' |
||||
|
)] |
||||
|
param () |
||||
|
|
||||
|
BeforeDiscovery { |
||||
|
# Compress-PSResource is part of Microsoft.PowerShell.PSResourceGet, which comes with PowerShell 7.4 and later. |
||||
|
$canPackage = [bool](Get-Command -Name Compress-PSResource -ErrorAction SilentlyContinue) |
||||
|
} |
||||
|
|
||||
|
Describe 'Get-ReleaseInfo.ps1' { |
||||
|
BeforeAll { |
||||
|
$scriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\.github\scripts\Get-ReleaseInfo.ps1' |
||||
|
$manifestPath = Join-Path -Path $TestDrive -ChildPath 'Module.psd1' |
||||
|
|
||||
|
function Set-TestManifest { |
||||
|
param ( |
||||
|
[Parameter(Mandatory)] |
||||
|
[string] |
||||
|
$Version, |
||||
|
|
||||
|
[Parameter()] |
||||
|
[string] |
||||
|
$Prerelease |
||||
|
) |
||||
|
|
||||
|
$psData = if ($Prerelease) { "Prerelease = '$Prerelease'" } else { '' } |
||||
|
Set-Content -LiteralPath $manifestPath -Value "@{ ModuleVersion = '$Version'; PrivateData = @{ PSData = @{ $psData } } }" |
||||
|
} |
||||
|
|
||||
|
$changelogPath = Join-Path -Path $TestDrive -ChildPath 'CHANGELOG.md' |
||||
|
Set-Content -LiteralPath $changelogPath -Value @' |
||||
|
# Changelog |
||||
|
|
||||
|
## [Unreleased] |
||||
|
|
||||
|
### Fixed |
||||
|
|
||||
|
- Fix a thing |
||||
|
|
||||
|
## [2.0.0] - 2026-01-02 |
||||
|
|
||||
|
### Changed |
||||
|
|
||||
|
- Change a thing |
||||
|
|
||||
|
## [1.0.0] |
||||
|
|
||||
|
### Added |
||||
|
|
||||
|
- Add a thing |
||||
|
|
||||
|
[Unreleased]: https://example.com/compare/2.0.0...HEAD |
||||
|
[2.0.0]: https://example.com/compare/1.0.0...2.0.0 |
||||
|
'@ |
||||
|
|
||||
|
$releasedChangelogPath = Join-Path -Path $TestDrive -ChildPath 'CHANGELOG-released.md' |
||||
|
Set-Content -LiteralPath $releasedChangelogPath -Value @' |
||||
|
# Changelog |
||||
|
|
||||
|
## [Unreleased] |
||||
|
|
||||
|
## [2.0.0] - 2026-01-02 |
||||
|
|
||||
|
### Changed |
||||
|
|
||||
|
- Change a thing |
||||
|
|
||||
|
[Unreleased]: https://example.com/compare/2.0.0...HEAD |
||||
|
[2.0.0]: https://example.com/compare/1.0.0...2.0.0 |
||||
|
'@ |
||||
|
} |
||||
|
|
||||
|
Context 'When the module manifest has no prerelease label' { |
||||
|
It 'Should return the version, the release date, and the notes of its section' { |
||||
|
Set-TestManifest -Version '2.0.0' |
||||
|
|
||||
|
$release = & $scriptPath -ManifestPath $manifestPath -ChangelogPath $changelogPath |
||||
|
|
||||
|
$release.Version | Should -BeExactly '2.0.0' |
||||
|
$release.IsPrerelease | Should -BeFalse |
||||
|
$release.Date | Should -Be ([datetime] '2026-01-02') |
||||
|
$release.Notes | Should -BeExactly "### Changed`n`n- Change a thing" |
||||
|
} |
||||
|
|
||||
|
It 'Should not include the link definitions after the last section in the notes' { |
||||
|
Set-TestManifest -Version '2.0.0' |
||||
|
|
||||
|
$release = & $scriptPath -ManifestPath $manifestPath -ChangelogPath $releasedChangelogPath |
||||
|
|
||||
|
$release.Notes | Should -BeExactly "### Changed`n`n- Change a thing" |
||||
|
} |
||||
|
|
||||
|
It 'Should fail if the section of the version has no release date' { |
||||
|
Set-TestManifest -Version '1.0.0' |
||||
|
|
||||
|
{ & $scriptPath -ManifestPath $manifestPath -ChangelogPath $changelogPath } | Should -Throw '*1.0.0*date*' |
||||
|
} |
||||
|
|
||||
|
It 'Should fail if CHANGELOG.md has no section for the version' { |
||||
|
Set-TestManifest -Version '3.0.0' |
||||
|
|
||||
|
{ & $scriptPath -ManifestPath $manifestPath -ChangelogPath $changelogPath } | Should -Throw '*3.0.0*' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Context 'When the module manifest has a prerelease label' { |
||||
|
It 'Should return the prerelease version and the notes of the Unreleased section' { |
||||
|
Set-TestManifest -Version '3.0.0' -Prerelease 'rc1' |
||||
|
|
||||
|
$release = & $scriptPath -ManifestPath $manifestPath -ChangelogPath $changelogPath |
||||
|
|
||||
|
$release.Version | Should -BeExactly '3.0.0-rc1' |
||||
|
$release.IsPrerelease | Should -BeTrue |
||||
|
$release.Date | Should -BeNullOrEmpty |
||||
|
$release.Notes | Should -BeExactly "### Fixed`n`n- Fix a thing" |
||||
|
} |
||||
|
|
||||
|
It 'Should fail if the Unreleased section is empty' { |
||||
|
Set-TestManifest -Version '3.0.0' -Prerelease 'rc1' |
||||
|
|
||||
|
{ & $scriptPath -ManifestPath $manifestPath -ChangelogPath $releasedChangelogPath } | |
||||
|
Should -Throw '*Unreleased*' |
||||
|
} |
||||
|
|
||||
|
It 'Should fail if CHANGELOG.md already has a section for the version' { |
||||
|
Set-TestManifest -Version '2.0.0' -Prerelease 'rc1' |
||||
|
|
||||
|
{ & $scriptPath -ManifestPath $manifestPath -ChangelogPath $changelogPath } | Should -Throw '*2.0.0*' |
||||
|
} |
||||
|
|
||||
|
It 'Should fail if the label is not valid in the PowerShell Gallery' { |
||||
|
Set-TestManifest -Version '3.0.0' -Prerelease 'rc.1' |
||||
|
|
||||
|
{ & $scriptPath -ManifestPath $manifestPath -ChangelogPath $changelogPath } | Should -Throw '*rc.1*' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Context 'When it reads the module manifest and CHANGELOG.md of the repository' { |
||||
|
It 'Should return release notes for the version of the module manifest' { |
||||
|
$repositoryPath = Join-Path -Path $PSScriptRoot -ChildPath '..' |
||||
|
$sourceManifestPath = Join-Path -Path $repositoryPath -ChildPath 'NTFSSecurity\NTFSSecurity.psd1' |
||||
|
$manifest = Import-PowerShellDataFile -LiteralPath $sourceManifestPath |
||||
|
|
||||
|
$release = & $scriptPath -ManifestPath $sourceManifestPath -ChangelogPath (Join-Path -Path $repositoryPath -ChildPath 'CHANGELOG.md') |
||||
|
|
||||
|
$release.Version | Should -BeLike "$($manifest.ModuleVersion)*" |
||||
|
$release.Notes | Should -Not -BeNullOrEmpty |
||||
|
$release.Notes | Should -Not -Match '(?m)^\[[^\]]+\]: ' |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Describe 'New-ModulePackage.ps1' -Skip:(-not $canPackage) { |
||||
|
BeforeAll { |
||||
|
$scriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\.github\scripts\New-ModulePackage.ps1' |
||||
|
$buildPath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release' |
||||
|
$manifest = Import-PowerShellDataFile -LiteralPath (Join-Path -Path $buildPath -ChildPath 'NTFSSecurity.psd1') |
||||
|
$version = $manifest.ModuleVersion |
||||
|
if ($manifest.PrivateData.PSData.Prerelease) { |
||||
|
$version = '{0}-{1}' -f $version, $manifest.PrivateData.PSData.Prerelease |
||||
|
} |
||||
|
$expectedFiles = @($manifest.FileList | ForEach-Object -Process { $_ -replace '\\', '/' } | Sort-Object) |
||||
|
|
||||
|
function Get-ZipEntryName { |
||||
|
param ( |
||||
|
[Parameter(Mandatory)] |
||||
|
[string] |
||||
|
$Path |
||||
|
) |
||||
|
|
||||
|
$zip = [IO.Compression.ZipFile]::OpenRead($Path) |
||||
|
try { |
||||
|
$zip.Entries | Where-Object -Property Name | ForEach-Object -Process { $_.FullName } |
||||
|
} finally { |
||||
|
$zip.Dispose() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$package = & $scriptPath -BuildPath $buildPath -DestinationPath (Join-Path -Path $TestDrive -ChildPath 'out') |
||||
|
} |
||||
|
|
||||
|
It 'Should copy exactly the files of the FileList into the module folder' { |
||||
|
$files = Get-ChildItem -LiteralPath $package.ModulePath -Recurse -File | |
||||
|
ForEach-Object -Process { $_.FullName.Substring($package.ModulePath.Length + 1) -replace '\\', '/' } |
||||
|
|
||||
|
($files | Sort-Object) -join ', ' | Should -BeExactly ($expectedFiles -join ', ') |
||||
|
} |
||||
|
|
||||
|
It 'Should name the NuGet package after the version, including the prerelease label' { |
||||
|
Split-Path -Path $package.PackagePath -Leaf | Should -BeExactly "NTFSSecurity.$version.nupkg" |
||||
|
$package.Version | Should -BeExactly $version |
||||
|
} |
||||
|
|
||||
|
It 'Should put exactly the module files into the NuGet package' { |
||||
|
$entries = Get-ZipEntryName -Path $package.PackagePath | |
||||
|
Where-Object -FilterScript { $_ -notmatch '^(_rels/|package/|\[Content_Types\]\.xml$|NTFSSecurity\.nuspec$)' } |
||||
|
|
||||
|
($entries | Sort-Object) -join ', ' | Should -BeExactly ($expectedFiles -join ', ') |
||||
|
} |
||||
|
|
||||
|
It 'Should set the version and the link to the release notes in the NuGet package' { |
||||
|
$zip = [IO.Compression.ZipFile]::OpenRead($package.PackagePath) |
||||
|
try { |
||||
|
$reader = New-Object -TypeName 'System.IO.StreamReader' -ArgumentList $zip.GetEntry('NTFSSecurity.nuspec').Open() |
||||
|
$nuspec = [xml] $reader.ReadToEnd() |
||||
|
$reader.Dispose() |
||||
|
} finally { |
||||
|
$zip.Dispose() |
||||
|
} |
||||
|
|
||||
|
$nuspec.package.metadata.version | Should -BeExactly $version |
||||
|
$nuspec.package.metadata.releaseNotes | Should -Match 'https://github\.com/raandree/NTFSSecurity/blob/master/CHANGELOG\.md' |
||||
|
} |
||||
|
|
||||
|
It 'Should tag the NuGet package with its cmdlets, which the PowerShell Gallery lists and Find-Command searches' { |
||||
|
$zip = [IO.Compression.ZipFile]::OpenRead($package.PackagePath) |
||||
|
try { |
||||
|
$reader = New-Object -TypeName 'System.IO.StreamReader' -ArgumentList $zip.GetEntry('NTFSSecurity.nuspec').Open() |
||||
|
$tags = ([xml] $reader.ReadToEnd()).package.metadata.tags -split '\s+' |
||||
|
$reader.Dispose() |
||||
|
} finally { |
||||
|
$zip.Dispose() |
||||
|
} |
||||
|
$expectedTags = @('PSModule', 'PSIncludes_Cmdlet') + $manifest.PrivateData.PSData.Tags + |
||||
|
@($manifest.CmdletsToExport | ForEach-Object -Process { "PSCmdlet_$_"; "PSCommand_$_" }) |
||||
|
|
||||
|
$expectedTags | Where-Object -FilterScript { $_ -notin $tags } | Should -BeNullOrEmpty |
||||
|
$tags | Group-Object | Where-Object -Property Count -GT -Value 1 | ForEach-Object -Process { $_.Name } | |
||||
|
Should -BeNullOrEmpty |
||||
|
} |
||||
|
|
||||
|
It 'Should put the module folder into NTFSSecurity.zip' { |
||||
|
$entries = Get-ZipEntryName -Path $package.ZipPath | Sort-Object |
||||
|
|
||||
|
$entries -join ', ' | Should -BeExactly (($expectedFiles | ForEach-Object -Process { "NTFSSecurity/$_" }) -join ', ') |
||||
|
} |
||||
|
|
||||
|
It 'Should fail if a file of the FileList is missing from the build output' { |
||||
|
$incompletePath = Join-Path -Path $TestDrive -ChildPath 'incomplete' |
||||
|
Copy-Item -LiteralPath $buildPath -Destination $incompletePath -Recurse |
||||
|
Remove-Item -LiteralPath (Join-Path -Path $incompletePath -ChildPath 'en-US\NTFSSecurity.dll-Help.xml') |
||||
|
|
||||
|
{ & $scriptPath -BuildPath $incompletePath -DestinationPath (Join-Path -Path $TestDrive -ChildPath 'out-incomplete') } | |
||||
|
Should -Throw '*NTFSSecurity.dll-Help.xml*' |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue