Compare commits
2 Commits
3f29c71995
...
1816f76736
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1816f76736 | ||
|
|
e390b42dce |
@ -1,711 +0,0 @@
|
||||
<#
|
||||
.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