Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/scripts/bump-version.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<#
.SYNOPSIS
Computes (and if needed increments) the release version stored in Directory.Build.props.

.DESCRIPTION
Directory.Build.props holds RELEASE_MAJOR / RELEASE_MINOR / RELEASE_REVISION for the
most recently released or pending-release version. This script:

1. Reads the three parts and computes RELEASE_VERSION.
2. Checks whether that version has already been released (a git tag named
v<version> or <version> exists; older releases were tagged without the v).
3. If it was released, increments the requested part (patch by default),
writes the new values back to Directory.Build.props and reports changed=true.
If it was NOT released yet (e.g. a previous release run failed after the bump
was committed), it keeps the version as-is and reports changed=false, so
re-running the release never skips or double-increments a version.

Results are written to $env:GITHUB_OUTPUT when running in GitHub Actions.

.EXAMPLE
./.github/scripts/bump-version.ps1 -Bump patch
#>
[CmdletBinding()]
param(
[ValidateSet('patch', 'minor', 'major')]
[string] $Bump = 'patch',
[string] $PropsPath = 'Directory.Build.props'
)

$ErrorActionPreference = 'Stop'

$content = Get-Content -Path $PropsPath -Raw

function Get-Part([string] $name) {
$m = [regex]::Match($content, "<$name\b[^>]*>\s*(\d+)\s*</$name>")
if (-not $m.Success) { throw "Could not find <$name> in $PropsPath" }
return [int] $m.Groups[1].Value
}

function Set-Part([string] $name, [int] $value) {
$pattern = "(<$name\b[^>]*>)\s*\d+\s*(</$name>)"
$script:content = [regex]::Replace($script:content, $pattern, "`${1}$value`${2}")
}

function Test-Released([string] $version) {
foreach ($tag in @("v$version", $version)) {
& git rev-parse -q --verify "refs/tags/$tag" *> $null
if ($LASTEXITCODE -eq 0) { return $true }
}
return $false
}

$major = Get-Part 'RELEASE_MAJOR'
$minor = Get-Part 'RELEASE_MINOR'
$revision = Get-Part 'RELEASE_REVISION'
$previous = "$major.$minor.$revision"

if (Test-Released $previous) {
switch ($Bump) {
'major' { $major++; $minor = 0; $revision = 0 }
'minor' { $minor++; $revision = 0 }
default { $revision++ }
}
$changed = $true
Write-Host "Version $previous is already released; applying '$Bump' increment."
}
else {
$changed = $false
Write-Host "::notice::Version $previous has not been released yet; releasing it without incrementing."
}

$version = "$major.$minor.$revision"
if ($changed -and (Test-Released $version)) {
throw "Computed version $version is already tagged. Fix RELEASE_* in $PropsPath before releasing."
}

if ($changed) {
Set-Part 'RELEASE_MAJOR' $major
Set-Part 'RELEASE_MINOR' $minor
Set-Part 'RELEASE_REVISION' $revision
# Preserve the file's existing line endings / encoding (no BOM).
[System.IO.File]::WriteAllText((Resolve-Path $PropsPath), $content, [System.Text.UTF8Encoding]::new($false))
}

Write-Host "RELEASE_VERSION: $previous -> $version (changed=$changed)"

if ($env:GITHUB_OUTPUT) {
@(
"previous=$previous"
"version=$version"
"major=$major"
"minor=$minor"
"revision=$revision"
"changed=$($changed.ToString().ToLowerInvariant())"
) | Add-Content -Path $env:GITHUB_OUTPUT
}
108 changes: 108 additions & 0 deletions .github/scripts/verify-package.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<#
.SYNOPSIS
Verifies a packed NullFX.CRC NuGet package.

.DESCRIPTION
Opens the .nupkg produced by `dotnet pack` and checks that:
- the package id and version match what we expect
- README.md is packed and referenced from the nuspec
- the MIT license expression is present
- every target framework in the library project has an assembly in lib/
- no replace-tokens placeholders (#{...}#) survived
- no test/benchmark assemblies or bin/obj paths were packed
Writes a short summary to $env:GITHUB_STEP_SUMMARY when available.

.EXAMPLE
./.github/scripts/verify-package.ps1 -PackageDirectory ./artifacts/package -Version 0.0.0
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $PackageDirectory,
[Parameter(Mandatory)] [string] $Version,
[string] $PackageId = 'NullFX.CRC',
[string] $Project = 'NullFX.CRC/NullFX.CRC.csproj',
# Optional override; by default the list is read from the project.
[string[]] $TargetFrameworks
)

$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.IO.Compression.FileSystem

$failures = [System.Collections.Generic.List[string]]::new()
function Fail([string] $message) { $failures.Add($message); Write-Host "::error::$message" }

$nupkgPath = Join-Path $PackageDirectory "$PackageId.$Version.nupkg"
if (-not (Test-Path $nupkgPath)) {
$found = (Get-ChildItem $PackageDirectory -Filter '*.nupkg' | Select-Object -ExpandProperty Name) -join ', '
throw "Expected package '$nupkgPath' was not found. Found: $found"
}

# Target frameworks come from the project so this list never drifts.
if ($TargetFrameworks) {
$expectedTfms = $TargetFrameworks
} else {
$tfmRaw = (& dotnet msbuild $Project -getProperty:TargetFrameworks | Out-String).Trim()
$expectedTfms = $tfmRaw.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries)
}
if ($expectedTfms.Count -eq 0) { throw "Could not read TargetFrameworks from $Project" }

$zip = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $nupkgPath))
try {
$entries = $zip.Entries | ForEach-Object { $_.FullName }

function Read-Entry([string] $name) {
$entry = $zip.Entries | Where-Object { $_.FullName -eq $name } | Select-Object -First 1
if (-not $entry) { return $null }
$reader = [System.IO.StreamReader]::new($entry.Open())
try { return $reader.ReadToEnd() } finally { $reader.Dispose() }
}

$nuspecName = $entries | Where-Object { $_ -like '*.nuspec' } | Select-Object -First 1
if (-not $nuspecName) { throw 'No .nuspec found inside the package.' }
[xml] $nuspec = Read-Entry $nuspecName
$meta = $nuspec.package.metadata

if ($meta.id -ne $PackageId) { Fail "Package id is '$($meta.id)', expected '$PackageId'." }
if ($meta.version -ne $Version) { Fail "Package version is '$($meta.version)', expected '$Version'." }
if ($meta.readme -ne 'README.md') { Fail "nuspec <readme> is '$($meta.readme)', expected 'README.md'." }
if ($meta.license.'#text' -ne 'MIT') { Fail "nuspec license is '$($meta.license.'#text')', expected 'MIT'." }
if ($entries -notcontains 'README.md') { Fail 'README.md is not packed at the package root.' }
if ($meta.repository -and -not $meta.repository.commit) { Write-Host '::warning::Repository commit is missing from the nuspec (SourceLink).' }

foreach ($tfm in $expectedTfms) {
if ($entries -notcontains "lib/$tfm/NullFX.CRC.dll") { Fail "Missing lib/$tfm/NullFX.CRC.dll" }
}

foreach ($name in @($nuspecName, 'README.md')) {
$text = Read-Entry $name
if ($text -and $text -match '#\{[A-Z_]+\}#') { Fail "Unreplaced token found in $name." }
}

$unexpected = $entries | Where-Object {
$_ -match '(?i)(^|/)(bin|obj)/' -or $_ -match '(?i)\.Tests\.dll$' -or $_ -match '(?i)Benchmarks' -or $_ -match '(?i)\.snk$'
}
foreach ($u in $unexpected) { Fail "Unexpected file in package: $u" }

$summary = @(
"### NuGet package: ``$PackageId $($meta.version)``",
'',
"| Check | Result |",
"|---|---|",
"| Version | ``$($meta.version)`` |",
"| Target frameworks | $($expectedTfms -join ', ') |",
"| README | $([bool]($entries -contains 'README.md')) |",
"| License | $($meta.license.'#text') |",
"| Files | $($entries.Count) |",
"| Problems | $($failures.Count) |"
) -join "`n"
Write-Host $summary
if ($env:GITHUB_STEP_SUMMARY) { Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $summary }
}
finally {
$zip.Dispose()
}

if ($failures.Count -gt 0) {
throw "Package verification failed with $($failures.Count) problem(s)."
}
Write-Host "Package $nupkgPath verified."
Loading
Loading