Compare commits
2 Commits
1816f76736
...
3f29c71995
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f29c71995 | ||
|
|
b6b5340197 |
711
utils/Release-NuGetPackage/Release-NuGetPackage.ps1
Normal file
711
utils/Release-NuGetPackage/Release-NuGetPackage.ps1
Normal file
@ -0,0 +1,711 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Release script for MaksIT.Core NuGet package and GitHub release.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This script automates the release process for MaksIT.Core library.
|
||||||
|
The script is IDEMPOTENT - you can safely re-run it if any step fails.
|
||||||
|
It will skip already-completed steps (NuGet and GitHub) and only create what's missing.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Validates environment and prerequisites
|
||||||
|
- Checks if version already exists on NuGet.org (skips if released)
|
||||||
|
- Checks if GitHub release exists (skips if released)
|
||||||
|
- Scans for vulnerable packages (security check)
|
||||||
|
- Builds and tests the project (Windows + Linux via Docker)
|
||||||
|
- Collects code coverage with Coverlet (threshold enforcement optional)
|
||||||
|
- Generates test result artifacts (TRX format) and coverage reports
|
||||||
|
- Displays test results with pass/fail counts and coverage percentage
|
||||||
|
- Publishes to NuGet.org
|
||||||
|
- Creates a GitHub release with changelog and NuGet package assets
|
||||||
|
- Shows timing summary for all steps
|
||||||
|
|
||||||
|
.REQUIREMENTS
|
||||||
|
Environment Variables:
|
||||||
|
- NUGET_MAKS_IT : NuGet.org API key for publishing packages
|
||||||
|
- GITHUB_MAKS_IT_COM : GitHub Personal Access Token (needs 'repo' scope)
|
||||||
|
|
||||||
|
Tools (Required):
|
||||||
|
- dotnet CLI : For building, testing, and packing
|
||||||
|
- git : For version control operations
|
||||||
|
- gh (GitHub CLI) : For creating GitHub releases
|
||||||
|
- docker : For cross-platform Linux testing
|
||||||
|
|
||||||
|
.WORKFLOW
|
||||||
|
1. VALIDATION PHASE
|
||||||
|
- Check required environment variables (NuGet key, GitHub token)
|
||||||
|
- Check required tools are installed (dotnet, git, gh, docker)
|
||||||
|
- Verify no uncommitted changes in working directory
|
||||||
|
- Authenticate GitHub CLI
|
||||||
|
|
||||||
|
2. VERSION & RELEASE CHECK PHASE (Idempotent)
|
||||||
|
- Read latest version from CHANGELOG.md
|
||||||
|
- Find commit with matching version tag
|
||||||
|
- Validate tag is on configured release branch (from scriptsettings.json)
|
||||||
|
- Check if already released on NuGet.org (mark for skip if yes)
|
||||||
|
- Check if GitHub release exists (mark for skip if yes)
|
||||||
|
- Read target framework from MaksIT.Core.csproj
|
||||||
|
- Extract release notes from CHANGELOG.md for current version
|
||||||
|
|
||||||
|
3. SECURITY SCAN
|
||||||
|
- Check for vulnerable packages (dotnet list package --vulnerable)
|
||||||
|
- Fail or warn based on $failOnVulnerabilities setting
|
||||||
|
|
||||||
|
4. BUILD & TEST PHASE
|
||||||
|
- Clean previous builds (delete bin/obj folders)
|
||||||
|
- Restore NuGet packages
|
||||||
|
- Windows: Build main project -> Build test project -> Run tests with coverage
|
||||||
|
- Analyze code coverage (fail if below threshold when configured)
|
||||||
|
- Linux (Docker): Build main project -> Build test project -> Run tests (TRX report)
|
||||||
|
- Rebuild for Windows (Docker may overwrite bin/obj)
|
||||||
|
- Create NuGet package (.nupkg) and symbols (.snupkg)
|
||||||
|
- All steps are timed for performance tracking
|
||||||
|
|
||||||
|
5. CONFIRMATION PHASE
|
||||||
|
- Display release summary
|
||||||
|
- Prompt user for confirmation before proceeding
|
||||||
|
|
||||||
|
6. NUGET RELEASE PHASE (Idempotent)
|
||||||
|
- Skip if version already exists on NuGet.org
|
||||||
|
- Otherwise, push package to NuGet.org
|
||||||
|
|
||||||
|
7. GITHUB RELEASE PHASE (Idempotent)
|
||||||
|
- Skip if release already exists
|
||||||
|
- Push tag to remote if not already there
|
||||||
|
- Create GitHub release with:
|
||||||
|
* Release notes from CHANGELOG.md
|
||||||
|
* .nupkg and .snupkg as downloadable assets
|
||||||
|
|
||||||
|
8. COMPLETION PHASE
|
||||||
|
- Display timing summary for all steps
|
||||||
|
- Display test results summary
|
||||||
|
- Display success summary with links
|
||||||
|
- Open NuGet and GitHub release pages in browser
|
||||||
|
- TODO: Email notification (template provided)
|
||||||
|
- TODO: Package signing (template provided)
|
||||||
|
|
||||||
|
.USAGE
|
||||||
|
Before running:
|
||||||
|
1. Ensure Docker Desktop is running (for Linux tests)
|
||||||
|
2. Update version in MaksIT.Core.csproj
|
||||||
|
3. Run .\Generate-Changelog.ps1 to update CHANGELOG.md and LICENSE.md
|
||||||
|
4. Review and commit all changes
|
||||||
|
5. Create version tag: git tag v1.x.x
|
||||||
|
6. Run: .\Release-NuGetPackage.ps1
|
||||||
|
|
||||||
|
Note: The script finds the commit with the tag matching CHANGELOG.md version.
|
||||||
|
You can run it from any branch/commit - it releases the tagged commit.
|
||||||
|
|
||||||
|
Re-run release (idempotent - skips NuGet/GitHub if already released):
|
||||||
|
.\Release-NuGetPackage.ps1
|
||||||
|
|
||||||
|
Generate changelog and update LICENSE year:
|
||||||
|
.\Generate-Changelog.ps1
|
||||||
|
|
||||||
|
.CONFIGURATION
|
||||||
|
All settings are stored in scriptsettings.json:
|
||||||
|
- qualityGates: Coverage threshold, vulnerability checks
|
||||||
|
- packageSigning: Code signing certificate configuration
|
||||||
|
- emailNotification: SMTP settings for release notifications
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
Author: Maksym Sadovnychyy (MAKS-IT)
|
||||||
|
Repository: https://github.com/MAKS-IT-COM/maksit-core
|
||||||
|
#>
|
||||||
|
|
||||||
|
# No parameters - behavior is controlled by current branch (configured in scriptsettings.json):
|
||||||
|
# - dev branch -> Local build only (no tag required, uncommitted changes allowed)
|
||||||
|
# - release branch -> Full release to GitHub (tag required, clean working directory)
|
||||||
|
|
||||||
|
# Get the directory of the current script (for loading settings and relative paths)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
|
||||||
|
#region Import Modules
|
||||||
|
|
||||||
|
# Import TestRunner module
|
||||||
|
$utilsDir = Split-Path $scriptDir -Parent
|
||||||
|
|
||||||
|
$testRunnerModulePath = Join-Path $utilsDir "TestRunner.psm1"
|
||||||
|
if (-not (Test-Path $testRunnerModulePath)) {
|
||||||
|
Write-Error "TestRunner module not found at: $testRunnerModulePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module $testRunnerModulePath -Force
|
||||||
|
|
||||||
|
# Import ScriptConfig module
|
||||||
|
$scriptConfigModulePath = Join-Path $utilsDir "ScriptConfig.psm1"
|
||||||
|
if (-not (Test-Path $scriptConfigModulePath)) {
|
||||||
|
Write-Error "ScriptConfig module not found at: $scriptConfigModulePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module $scriptConfigModulePath -Force
|
||||||
|
|
||||||
|
# Import Logging module
|
||||||
|
$loggingModulePath = Join-Path $utilsDir "Logging.psm1"
|
||||||
|
if (-not (Test-Path $loggingModulePath)) {
|
||||||
|
Write-Error "Logging module not found at: $loggingModulePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module $loggingModulePath -Force
|
||||||
|
|
||||||
|
|
||||||
|
# Import GitTools module
|
||||||
|
$gitToolsModulePath = Join-Path $utilsDir "GitTools.psm1"
|
||||||
|
if (-not (Test-Path $gitToolsModulePath)) {
|
||||||
|
Write-Error "GitTools module not found at: $gitToolsModulePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module $gitToolsModulePath -Force
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Load Settings
|
||||||
|
$settings = Get-ScriptSettings -ScriptDir $scriptDir
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Configuration
|
||||||
|
|
||||||
|
# GitHub configuration
|
||||||
|
$githubReleseEnabled = $settings.github.enabled
|
||||||
|
$githubTokenEnvVar = $settings.github.githubToken
|
||||||
|
$githubToken = [System.Environment]::GetEnvironmentVariable($githubTokenEnvVar)
|
||||||
|
|
||||||
|
# NuGet configuration
|
||||||
|
$nugetReleseEnabled = $settings.nuget.enabled
|
||||||
|
$nugetApiKeyEnvVar = $settings.nuget.nugetApiKey
|
||||||
|
$nugetApiKey = [System.Environment]::GetEnvironmentVariable($nugetApiKeyEnvVar)
|
||||||
|
$nugetSource = if ($settings.nuget.source) { $settings.nuget.source } else { "https://api.nuget.org/v3/index.json" }
|
||||||
|
|
||||||
|
# Paths from settings (resolve relative to script directory)
|
||||||
|
$csprojPaths = @()
|
||||||
|
$rawCsprojPaths = @()
|
||||||
|
|
||||||
|
if ($settings.paths.csprojPaths) {
|
||||||
|
if ($settings.paths.csprojPaths -is [System.Collections.IEnumerable] -and -not ($settings.paths.csprojPaths -is [string])) {
|
||||||
|
$rawCsprojPaths += $settings.paths.csprojPaths
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$rawCsprojPaths += $settings.paths.csprojPaths
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Error "No csproj path configured. Set 'paths.csprojPaths' (preferred) or 'paths.csprojPath' in scriptsettings.json."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($path in $rawCsprojPaths) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($path)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $path))
|
||||||
|
$csprojPaths += $resolvedPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($csprojPaths.Count -eq 0) {
|
||||||
|
Write-Error "No valid csproj paths configured in scriptsettings.json."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$testResultsDir = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.testResultsDir))
|
||||||
|
$releaseDir = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.releaseDir))
|
||||||
|
$changelogPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.changelogPath))
|
||||||
|
$testProjectPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.testProject))
|
||||||
|
|
||||||
|
# Release naming pattern
|
||||||
|
$zipNamePattern = $settings.release.zipNamePattern
|
||||||
|
$releaseTitlePattern = $settings.release.releaseTitlePattern
|
||||||
|
|
||||||
|
# Branch configuration
|
||||||
|
$releaseBranch = $settings.branches.release
|
||||||
|
$devBranch = $settings.branches.dev
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
# Helper: extract a csproj property (first match)
|
||||||
|
function Get-CsprojPropertyValue {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][xml]$csproj,
|
||||||
|
[Parameter(Mandatory=$true)][string]$propertyName
|
||||||
|
)
|
||||||
|
|
||||||
|
$propNode = $csproj.Project.PropertyGroup |
|
||||||
|
Where-Object { $_.$propertyName } |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if ($propNode) {
|
||||||
|
return $propNode.$propertyName
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: check for uncommitted changes
|
||||||
|
function Assert-WorkingTreeClean {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[bool]$IsReleaseBranch
|
||||||
|
)
|
||||||
|
|
||||||
|
$gitStatus = Get-GitStatusShort
|
||||||
|
if ($gitStatus) {
|
||||||
|
if ($IsReleaseBranch) {
|
||||||
|
Write-Error "Working directory has uncommitted changes. Commit or stash them before releasing."
|
||||||
|
Write-Log -Level "WARN" -Message "Uncommitted files:"
|
||||||
|
$gitStatus | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "WARN" -Message " Uncommitted changes detected (allowed on dev branch)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "OK" -Message " Working directory is clean."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: read versions from csproj files
|
||||||
|
function Get-CsprojVersions {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string[]]$CsprojPaths
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message "Reading version(s) from csproj(s)..."
|
||||||
|
$projectVersions = @{}
|
||||||
|
|
||||||
|
foreach ($projPath in $CsprojPaths) {
|
||||||
|
if (-not (Test-Path $projPath -PathType Leaf)) {
|
||||||
|
Write-Error "Csproj file not found at: $projPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([System.IO.Path]::GetExtension($projPath) -ne ".csproj") {
|
||||||
|
Write-Error "Configured path is not a .csproj file: $projPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[xml]$csproj = Get-Content $projPath
|
||||||
|
$version = Get-CsprojPropertyValue -csproj $csproj -propertyName "Version"
|
||||||
|
|
||||||
|
if (-not $version) {
|
||||||
|
Write-Error "Version not found in $projPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$projectVersions[$projPath] = $version
|
||||||
|
Write-Log -Level "OK" -Message " $([System.IO.Path]::GetFileName($projPath)): $version"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $projectVersions
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Validate CLI Dependencies
|
||||||
|
|
||||||
|
Assert-Command dotnet
|
||||||
|
Assert-Command git
|
||||||
|
Assert-Command docker
|
||||||
|
# gh command check deferred until after branch detection (only needed on release branch)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Main
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "=================================================="
|
||||||
|
Write-Log -Level "STEP" -Message "RELEASE BUILD"
|
||||||
|
Write-Log -Level "STEP" -Message "=================================================="
|
||||||
|
|
||||||
|
#region Preflight
|
||||||
|
|
||||||
|
$isDevBranch = $false
|
||||||
|
$isReleaseBranch = $false
|
||||||
|
|
||||||
|
# 1. Detect current branch and determine release mode
|
||||||
|
$currentBranch = Get-CurrentBranch
|
||||||
|
|
||||||
|
$isDevBranch = $currentBranch -eq $devBranch
|
||||||
|
$isReleaseBranch = $currentBranch -eq $releaseBranch
|
||||||
|
|
||||||
|
if (-not $isDevBranch -and -not $isReleaseBranch) {
|
||||||
|
Write-Error "Releases can only be created from '$releaseBranch' or '$devBranch' branches. Current branch: $currentBranch"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Check for uncommitted changes (required on release branch, allowed on dev)
|
||||||
|
Assert-WorkingTreeClean -IsReleaseBranch:$isReleaseBranch
|
||||||
|
|
||||||
|
# 3. Get version from csproj (source of truth)
|
||||||
|
$projectVersions = Get-CsprojVersions -CsprojPaths $csprojPaths
|
||||||
|
|
||||||
|
# Use the first project's version as the release version
|
||||||
|
$version = $projectVersions[$csprojPaths[0]]
|
||||||
|
|
||||||
|
# 4. Handle tag based on branch
|
||||||
|
if ($isReleaseBranch) {
|
||||||
|
# Release branch: tag is required and must match version
|
||||||
|
$tag = Get-CurrentCommitTag -Version $version
|
||||||
|
|
||||||
|
if ($tag -notmatch '^v(\d+\.\d+\.\d+)$') {
|
||||||
|
Write-Error "Tag '$tag' does not match expected format 'vX.Y.Z' (e.g., v$version)."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$tagVersion = $Matches[1]
|
||||||
|
|
||||||
|
if ($tagVersion -ne $version) {
|
||||||
|
Write-Error "Tag version ($tagVersion) does not match csproj version ($version)."
|
||||||
|
Write-Log -Level "WARN" -Message " Either update the tag or the csproj version."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Tag found: $tag (matches csproj)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# Dev branch: no tag required, use version from csproj
|
||||||
|
$tag = "v$version"
|
||||||
|
Write-Log -Level "INFO" -Message " Using version from csproj (no tag required on dev)."
|
||||||
|
}
|
||||||
|
|
||||||
|
# 5. Verify CHANGELOG.md has matching version entry
|
||||||
|
Write-Log -Level "INFO" -Message "Verifying CHANGELOG.md..."
|
||||||
|
if (-not (Test-Path $changelogPath)) {
|
||||||
|
Write-Error "CHANGELOG.md not found at: $changelogPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$changelog = Get-Content $changelogPath -Raw
|
||||||
|
|
||||||
|
if ($changelog -notmatch '##\s+v(\d+\.\d+\.\d+)') {
|
||||||
|
Write-Error "No version entry found in CHANGELOG.md"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$changelogVersion = $Matches[1]
|
||||||
|
|
||||||
|
if ($changelogVersion -ne $version) {
|
||||||
|
Write-Error "Csproj version ($version) does not match latest CHANGELOG.md version ($changelogVersion)."
|
||||||
|
Write-Log -Level "WARN" -Message " Update CHANGELOG.md or the csproj version."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " CHANGELOG.md version matches: v$changelogVersion"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message "All pre-flight checks passed!"
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Test
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Running tests..."
|
||||||
|
|
||||||
|
# Run tests using TestRunner module
|
||||||
|
$testResult = Invoke-TestsWithCoverage -TestProjectPath $testProjectPath -ResultsDirectory $testResultsDir -Silent
|
||||||
|
|
||||||
|
if (-not $testResult.Success) {
|
||||||
|
Write-Error "Tests failed. Release aborted."
|
||||||
|
Write-Log -Level "ERROR" -Message " Error: $($testResult.Error)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " All tests passed!"
|
||||||
|
Write-Log -Level "INFO" -Message " Line Coverage: $($testResult.LineRate)%"
|
||||||
|
Write-Log -Level "INFO" -Message " Branch Coverage: $($testResult.BranchRate)%"
|
||||||
|
Write-Log -Level "INFO" -Message " Method Coverage: $($testResult.MethodRate)%"
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Build And Publish
|
||||||
|
|
||||||
|
# 7. Prepare release directory
|
||||||
|
if (!(Test-Path $releaseDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $releaseDir | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 8. Pack NuGet package and resolve produced .nupkg/.snupkg files
|
||||||
|
$packageProjectPath = $csprojPaths[0]
|
||||||
|
Write-Log -Level "STEP" -Message "Packing NuGet package..."
|
||||||
|
dotnet pack $packageProjectPath -c Release -o $releaseDir --nologo `
|
||||||
|
-p:IncludeSymbols=true `
|
||||||
|
-p:SymbolPackageFormat=snupkg
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "dotnet pack failed for $packageProjectPath."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$packageFile = Get-ChildItem -Path $releaseDir -Filter "*.nupkg" |
|
||||||
|
Where-Object {
|
||||||
|
$_.Name -like "*$version*.nupkg" -and
|
||||||
|
$_.Name -notlike "*.symbols.nupkg" -and
|
||||||
|
$_.Name -notlike "*.snupkg"
|
||||||
|
} |
|
||||||
|
Sort-Object LastWriteTime -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $packageFile) {
|
||||||
|
Write-Error "Could not locate generated NuGet package for version $version in: $releaseDir"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Package ready: $($packageFile.FullName)"
|
||||||
|
|
||||||
|
# Find the symbols package if available
|
||||||
|
$symbolsPackageFile = Get-ChildItem -Path $releaseDir -Filter "*.snupkg" |
|
||||||
|
Where-Object { $_.Name -like "*$version*.snupkg" } |
|
||||||
|
Sort-Object LastWriteTime -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if ($symbolsPackageFile) {
|
||||||
|
Write-Log -Level "OK" -Message " Symbols package ready: $($symbolsPackageFile.FullName)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "WARN" -Message " Symbols package (.snupkg) not found for version $version."
|
||||||
|
}
|
||||||
|
|
||||||
|
# 9. Create release archive with NuGet package artifacts
|
||||||
|
Write-Log -Level "STEP" -Message "Creating release archive..."
|
||||||
|
$resolvedZipNamePattern = if ([string]::IsNullOrWhiteSpace($zipNamePattern)) { "release-{version}.zip" } else { $zipNamePattern }
|
||||||
|
$zipFileName = $resolvedZipNamePattern -replace '\{version\}', $version
|
||||||
|
$zipPath = Join-Path $releaseDir $zipFileName
|
||||||
|
|
||||||
|
if (Test-Path $zipPath) {
|
||||||
|
Remove-Item $zipPath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$archiveArtifacts = @($packageFile.FullName)
|
||||||
|
if ($symbolsPackageFile) {
|
||||||
|
$archiveArtifacts += $symbolsPackageFile.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
Compress-Archive -Path $archiveArtifacts -DestinationPath $zipPath -CompressionLevel Optimal -Force
|
||||||
|
|
||||||
|
if (-not (Test-Path $zipPath)) {
|
||||||
|
Write-Error "Failed to create release archive at: $zipPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Release archive ready: $zipPath"
|
||||||
|
|
||||||
|
# 10. Extract release notes from CHANGELOG.md
|
||||||
|
Write-Log -Level "STEP" -Message "Extracting release notes..."
|
||||||
|
$pattern = "(?ms)^##\s+v$([regex]::Escape($version))\b.*?(?=^##\s+v\d+\.\d+\.\d+|\Z)"
|
||||||
|
$match = [regex]::Match($changelog, $pattern)
|
||||||
|
|
||||||
|
if (-not $match.Success) {
|
||||||
|
Write-Error "Changelog entry for version $version not found."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$releaseNotes = $match.Value.Trim()
|
||||||
|
Write-Log -Level "OK" -Message " Release notes extracted."
|
||||||
|
|
||||||
|
# 11. Get repository info
|
||||||
|
$remoteUrl = git config --get remote.origin.url
|
||||||
|
if ($LASTEXITCODE -ne 0 -or -not $remoteUrl) {
|
||||||
|
Write-Error "Could not determine git remote origin URL."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($remoteUrl -match "[:/](?<owner>[^/]+)/(?<repo>[^/.]+)(\.git)?$") {
|
||||||
|
$owner = $matches['owner']
|
||||||
|
$repoName = $matches['repo']
|
||||||
|
$repo = "$owner/$repoName"
|
||||||
|
} else {
|
||||||
|
Write-Error "Could not parse GitHub repo from remote URL: $remoteUrl"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$releaseName = $releaseTitlePattern -replace '\{version\}', $version
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Release Summary:"
|
||||||
|
Write-Log -Level "INFO" -Message " Repository: $repo"
|
||||||
|
Write-Log -Level "INFO" -Message " Tag: $tag"
|
||||||
|
Write-Log -Level "INFO" -Message " Title: $releaseName"
|
||||||
|
|
||||||
|
# 12. Check if tag is pushed to remote (skip on dev branch)
|
||||||
|
|
||||||
|
if (-not $isDevBranch) {
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Verifying tag is pushed to remote..."
|
||||||
|
$remoteTagExists = Test-RemoteTagExists -Tag $tag -Remote "origin"
|
||||||
|
if (-not $remoteTagExists) {
|
||||||
|
Write-Log -Level "WARN" -Message " Tag $tag not found on remote. Pushing..."
|
||||||
|
Push-TagToRemote -Tag $tag -Remote "origin"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "OK" -Message " Tag exists on remote."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Release to GitHub
|
||||||
|
if ($githubReleseEnabled) {
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message " Release branch ($releaseBranch) - will publish to GitHub."
|
||||||
|
Assert-Command gh
|
||||||
|
|
||||||
|
$ghApiAuthArgs = @(
|
||||||
|
"-H", "Authorization: token $githubToken"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6. Check GitHub authentication
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message "Checking GitHub authentication..."
|
||||||
|
if (-not $githubToken) {
|
||||||
|
Write-Error "GitHub token is not set. Set '$githubTokenEnvVar' and rerun."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$authArgs = @("api", "user") + $ghApiAuthArgs
|
||||||
|
$authTest = & gh @authArgs 2>$null
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0 -or -not $authTest) {
|
||||||
|
Write-Error "GitHub CLI authentication failed. GitHub token may be invalid or missing repo scope."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message " GitHub CLI authenticated."
|
||||||
|
|
||||||
|
# 13. Create or update GitHub release
|
||||||
|
Write-Log -Level "STEP" -Message "Creating GitHub release..."
|
||||||
|
|
||||||
|
# gh release subcommands do not support custom auth headers.
|
||||||
|
# Scope GH_TOKEN to this block so commands authenticate with the configured token.
|
||||||
|
$previousGhToken = $env:GH_TOKEN
|
||||||
|
$env:GH_TOKEN = $githubToken
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Check if release already exists
|
||||||
|
$releaseViewArgs = @(
|
||||||
|
"release", "view", $tag,
|
||||||
|
"--repo", $repo
|
||||||
|
)
|
||||||
|
& gh @releaseViewArgs 2>$null
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Log -Level "WARN" -Message " Release $tag already exists. Deleting..."
|
||||||
|
$releaseDeleteArgs = @("release", "delete", $tag, "--repo", $repo, "--yes")
|
||||||
|
& gh @releaseDeleteArgs
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "Failed to delete existing release $tag."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create release using the existing tag
|
||||||
|
# Write release notes to a temp file to avoid shell interpretation issues with special characters
|
||||||
|
$notesFilePath = Join-Path $releaseDir "release-notes-temp.md"
|
||||||
|
[System.IO.File]::WriteAllText($notesFilePath, $releaseNotes, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
|
||||||
|
$releaseAssets = @($packageFile.FullName)
|
||||||
|
if ($symbolsPackageFile) {
|
||||||
|
$releaseAssets += $symbolsPackageFile.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
$createReleaseArgs = @("release", "create", $tag) + $releaseAssets + @(
|
||||||
|
"--repo", $repo
|
||||||
|
"--title", $releaseName
|
||||||
|
"--notes-file", $notesFilePath
|
||||||
|
)
|
||||||
|
& gh @createReleaseArgs
|
||||||
|
|
||||||
|
$ghExitCode = $LASTEXITCODE
|
||||||
|
|
||||||
|
# Cleanup temp notes file
|
||||||
|
if (Test-Path $notesFilePath) {
|
||||||
|
Remove-Item $notesFilePath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ghExitCode -ne 0) {
|
||||||
|
Write-Error "Failed to create GitHub release for tag $tag."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if ($null -ne $previousGhToken) {
|
||||||
|
$env:GH_TOKEN = $previousGhToken
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Remove-Item Env:GH_TOKEN -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " GitHub release created successfully."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "WARN" -Message "Skipping GitHub release (disabled)."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Release to NuGet
|
||||||
|
|
||||||
|
if ($nugetReleseEnabled) {
|
||||||
|
Write-Log -Level "STEP" -Message "Pushing to NuGet.org..."
|
||||||
|
dotnet nuget push $packageFile.FullName -k $nugetApiKey -s $nugetSource --skip-duplicate
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "Failed to push the package to NuGet."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " NuGet push completed."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "WARN" -Message "Skipping NuGet publish (disabled)."
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "WARN" -Message "Skipping remote tag verification and GitHub release (dev branch)."
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Cleanup
|
||||||
|
if (Test-Path $testResultsDir) {
|
||||||
|
Remove-Item $testResultsDir -Recurse -Force
|
||||||
|
Write-Log -Level "INFO" -Message " Cleaned up test results directory."
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-ChildItem -Path $releaseDir -File |
|
||||||
|
Where-Object { $_.Name -like "*$version*.nupkg" -or $_.Name -like "*$version*.snupkg" } |
|
||||||
|
Remove-Item -Force -ErrorAction SilentlyContinue
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Summary
|
||||||
|
Write-Log -Level "OK" -Message "=================================================="
|
||||||
|
if ($isDevBranch) {
|
||||||
|
Write-Log -Level "OK" -Message "DEV BUILD COMPLETE"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "OK" -Message "RELEASE COMPLETE"
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "=================================================="
|
||||||
|
|
||||||
|
if (-not $isDevBranch) {
|
||||||
|
Write-Log -Level "STEP" -Message "Release URL: https://github.com/$repo/releases/tag/$tag"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message "Artifacts location: $releaseDir"
|
||||||
|
|
||||||
|
if ($isDevBranch) {
|
||||||
|
Write-Log -Level "WARN" -Message "To publish to GitHub, switch to '$releaseBranch', merge dev, tag, and run this script again:"
|
||||||
|
Write-Log -Level "WARN" -Message " git checkout $releaseBranch"
|
||||||
|
Write-Log -Level "WARN" -Message " git merge dev"
|
||||||
|
Write-Log -Level "WARN" -Message " git tag v$version"
|
||||||
|
Write-Log -Level "WARN" -Message " .\Release-NuGetPackage.ps1"
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#endregion
|
||||||
Loading…
Reference in New Issue
Block a user