From 6b72b83487511ebd4a3b256bc4388b9665b99b92 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Fri, 27 Feb 2026 18:51:26 +0100 Subject: [PATCH] (chore): migrate to .slnx and refine release scripts/docs --- LICENSE.md | 2 +- README.md | 1 + src/MaksIT.Results.sln | 30 --- src/MaksIT.Results.slnx | 4 + .../Force-AmendTaggedCommit.ps1 | 34 ++- .../Release-NuGetPackage.ps1 | 231 +++++++----------- .../Release-NuGetPackage/scriptsettings.json | 4 +- 7 files changed, 123 insertions(+), 183 deletions(-) delete mode 100644 src/MaksIT.Results.sln create mode 100644 src/MaksIT.Results.slnx diff --git a/LICENSE.md b/LICENSE.md index edbe7fe..915cfc6 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 - 2025 Maksym Sadovnychyy (MAKS-IT) +Copyright (c) 2024 - 2026 Maksym Sadovnychyy (MAKS-IT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index eb166b1..91b1416 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ If you have any questions or need further assistance, feel free to reach out: - **Email**: [maksym.sadovnychyy@gmail.com](mailto:maksym.sadovnychyy@gmail.com) - **Reddit**: [MaksIT.Results: Streamline Your ASP.NET Core API Response Handling](https://www.reddit.com/r/MaksIT/comments/1f89ifn/maksitresults_streamline_your_aspnet_core_api/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button) + ## License See `LICENSE.md`. \ No newline at end of file diff --git a/src/MaksIT.Results.sln b/src/MaksIT.Results.sln deleted file mode 100644 index 4f2c276..0000000 --- a/src/MaksIT.Results.sln +++ /dev/null @@ -1,30 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.0.11222.15 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaksIT.Results", "MaksIT.Results\MaksIT.Results.csproj", "{E947F5FC-8FD9-4F1E-AA5F-29FED95B5A2D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaksIT.Results.Tests", "MaksIT.Results.Tests\MaksIT.Results.Tests.csproj", "{68D2F460-1550-5219-355F-BEDA6C1557AA}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {E947F5FC-8FD9-4F1E-AA5F-29FED95B5A2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E947F5FC-8FD9-4F1E-AA5F-29FED95B5A2D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E947F5FC-8FD9-4F1E-AA5F-29FED95B5A2D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E947F5FC-8FD9-4F1E-AA5F-29FED95B5A2D}.Release|Any CPU.Build.0 = Release|Any CPU - {68D2F460-1550-5219-355F-BEDA6C1557AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {68D2F460-1550-5219-355F-BEDA6C1557AA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {68D2F460-1550-5219-355F-BEDA6C1557AA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {68D2F460-1550-5219-355F-BEDA6C1557AA}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C3627A51-0642-40DB-96BC-07C627FF8ACC} - EndGlobalSection -EndGlobal diff --git a/src/MaksIT.Results.slnx b/src/MaksIT.Results.slnx new file mode 100644 index 0000000..4022567 --- /dev/null +++ b/src/MaksIT.Results.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1 b/utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1 index 3f1e001..bca2946 100644 --- a/utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1 +++ b/utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Amends the latest tagged commit and force-pushes updated branch and tag. + Amends the latest commit, recreates its associated tag, and force pushes both to remote. .DESCRIPTION This script performs the following operations: @@ -66,6 +66,29 @@ Import-Module $gitToolsModulePath -Force #endregion +#region Helpers + +function Select-PreferredHeadTag { + param( + [Parameter(Mandatory = $true)] + [string[]]$Tags + ) + + # Pick the latest tag on HEAD by git's own ordering (no tag-name parsing assumptions). + $ordered = (& git tag --points-at HEAD --sort=-creatordate 2>$null) + if ($LASTEXITCODE -eq 0 -and $ordered) { + $orderedTags = @($ordered | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() }) + if ($orderedTags.Count -gt 0) { + return $orderedTags[0] + } + } + + # Fallback: keep script functional even if sorting is unavailable. + return $Tags[0] +} + +#endregion + #region Load Settings $settings = Get-ScriptSettings -ScriptDir $scriptDir @@ -110,14 +133,17 @@ Write-Log -Level "INFO" -Message "Commit: $CommitHash - $CommitMessage" # 3. Ensure HEAD has at least one tag Write-LogStep "Finding tag on last commit..." -$tags = @(Get-HeadTags) +$tags = Get-HeadTags if ($tags.Count -eq 0) { Write-Error "No tag found on the last commit ($CommitHash). This script requires the last commit to have an associated tag." exit 1 } -# If multiple tags exist, use the first one returned by git. -$TagName = $tags[0] +# If multiple tags exist, choose the latest one on HEAD by git ordering. +if ($tags.Count -gt 1) { + Write-Log -Level "WARN" -Message "Multiple tags found on HEAD: $($tags -join ', ')" +} +$TagName = Select-PreferredHeadTag -Tags $tags Write-Log -Level "OK" -Message "Found tag: $TagName" # 4. Inspect pending changes before amend diff --git a/utils/Release-NuGetPackage/Release-NuGetPackage.ps1 b/utils/Release-NuGetPackage/Release-NuGetPackage.ps1 index 8ed7082..3556b1a 100644 --- a/utils/Release-NuGetPackage/Release-NuGetPackage.ps1 +++ b/utils/Release-NuGetPackage/Release-NuGetPackage.ps1 @@ -1,12 +1,11 @@ <# .SYNOPSIS - Builds, tests, packs, and publishes MaksIT.Core to NuGet and GitHub releases. + 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. - GitHub repository target can be configured explicitly in scriptsettings.json. Features: - Validates environment and prerequisites @@ -18,7 +17,7 @@ - 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 package assets + - Creates a GitHub release with changelog and NuGet package assets - Shows timing summary for all steps .REQUIREMENTS @@ -105,6 +104,7 @@ .CONFIGURATION All settings are stored in scriptsettings.json: + - qualityGates: Coverage threshold, vulnerability checks - packageSigning: Code signing certificate configuration - emailNotification: SMTP settings for release notifications @@ -174,7 +174,6 @@ $settings = Get-ScriptSettings -ScriptDir $scriptDir $githubReleseEnabled = $settings.github.enabled $githubTokenEnvVar = $settings.github.githubToken $githubToken = [System.Environment]::GetEnvironmentVariable($githubTokenEnvVar) -$githubRepositorySetting = $settings.github.repository # NuGet configuration $nugetReleseEnabled = $settings.nuget.enabled @@ -214,12 +213,11 @@ if ($csprojPaths.Count -eq 0) { } $testResultsDir = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.testResultsDir)) -$stagingDir = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.stagingDir)) $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 patterns +# Release naming pattern $zipNamePattern = $settings.release.zipNamePattern $releaseTitlePattern = $settings.release.releaseTitlePattern @@ -249,21 +247,6 @@ function Get-CsprojPropertyValue { return $null } -# Helper: resolve output assembly name for published exe -function Resolve-ProjectExeName { - param( - [Parameter(Mandatory=$true)][string]$projPath - ) - - [xml]$csproj = Get-Content $projPath - $assemblyName = Get-CsprojPropertyValue -csproj $csproj -propertyName "AssemblyName" - if ($assemblyName) { - return $assemblyName - } - - return [System.IO.Path]::GetFileNameWithoutExtension($projPath) -} - # Helper: check for uncommitted changes function Assert-WorkingTreeClean { param( @@ -324,42 +307,6 @@ function Get-CsprojVersions { return $projectVersions } -# Helper: resolve GitHub repository (owner/repo) from settings override or remote URL -function Resolve-GitHubRepository { - param( - [Parameter(Mandatory = $false)] - [string]$RepositorySetting - ) - - if (-not [string]::IsNullOrWhiteSpace($RepositorySetting)) { - $value = $RepositorySetting.Trim() - - if ($value -match '^https?://github\.com/(?[^/]+)/(?[^/]+?)(?:\.git)?/?$') { - return "$($Matches['owner'])/$($Matches['repo'])" - } - - if ($value -match '^(?[^/]+)/(?[^/]+)$') { - return "$($Matches['owner'])/$($Matches['repo'])" - } - - Write-Error "Invalid github.repository format '$value'. Use 'owner/repo' or 'https://github.com/owner/repo'." - exit 1 - } - - $remoteUrl = git config --get remote.origin.url - if ($LASTEXITCODE -ne 0 -or -not $remoteUrl) { - Write-Error "Could not determine git remote origin URL. Configure github.repository in scriptsettings.json." - exit 1 - } - - if ($remoteUrl -match "[:/](?[^/]+)/(?[^/.]+)(\.git)?$") { - return "$($Matches['owner'])/$($Matches['repo'])" - } - - Write-Error "Could not parse repository from remote URL: $remoteUrl. Configure github.repository in scriptsettings.json." - exit 1 -} - #endregion #region Validate CLI Dependencies @@ -480,78 +427,18 @@ Write-Log -Level "INFO" -Message " Method Coverage: $($testResult.MethodRate)%" #region Build And Publish -# 7. Prepare staging directory -Write-Log -Level "STEP" -Message "Preparing staging directory..." -if (Test-Path $stagingDir) { - Remove-Item $stagingDir -Recurse -Force -} - -New-Item -ItemType Directory -Path $stagingDir | Out-Null - -$binDir = Join-Path $stagingDir "bin" - -# 8. Publish the project to staging/bin - -Write-Log -Level "STEP" -Message "Publishing projects to bin folder..." -$publishSuccess = $true -$publishedProjects = @() - -foreach ($projPath in $csprojPaths) { - $projName = [System.IO.Path]::GetFileNameWithoutExtension($projPath) - $projBinDir = Join-Path $binDir $projName - - dotnet publish $projPath -c Release -o $projBinDir - if ($LASTEXITCODE -ne 0) { - Write-Error "dotnet publish failed for $projName." - $publishSuccess = $false - } - else { - $exeBaseName = Resolve-ProjectExeName -projPath $projPath - $publishedProjects += [PSCustomObject]@{ - ProjPath = $projPath - ProjName = $projName - BinDir = $projBinDir - ExeBaseName = $exeBaseName - } - - Write-Log -Level "OK" -Message " Published $projName successfully to: $projBinDir" - } -} - -if (-not $publishSuccess) { - exit 1 -} - - -# 12. Prepare release directory +# 7. Prepare release directory if (!(Test-Path $releaseDir)) { New-Item -ItemType Directory -Path $releaseDir | Out-Null } -# 13. Create zip file -$zipName = $zipNamePattern -$zipName = $zipName -replace '\{version\}', $version -$zipPath = Join-Path $releaseDir $zipName - -if (Test-Path $zipPath) { - Remove-Item $zipPath -Force -} - -Write-Log -Level "STEP" -Message "Creating archive $zipName..." -Compress-Archive -Path "$stagingDir\*" -DestinationPath $zipPath -Force - -if (-not (Test-Path $zipPath)) { - Write-Error "Failed to create archive $zipPath" - exit 1 -} - -Write-Log -Level "OK" -Message " Archive created: $zipPath" - -# 14. Pack NuGet package and resolve produced .nupkg file +# 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 +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 @@ -573,7 +460,44 @@ if (-not $packageFile) { Write-Log -Level "OK" -Message " Package ready: $($packageFile.FullName)" -# 15. Extract release notes from CHANGELOG.md +# 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) @@ -586,8 +510,21 @@ if (-not $match.Success) { $releaseNotes = $match.Value.Trim() Write-Log -Level "OK" -Message " Release notes extracted." -# 16. Resolve repository info for GitHub release -$repo = Resolve-GitHubRepository -RepositorySetting $githubRepositorySetting +# 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 "[:/](?[^/]+)/(?[^/.]+)(\.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 @@ -596,7 +533,7 @@ Write-Log -Level "INFO" -Message " Repository: $repo" Write-Log -Level "INFO" -Message " Tag: $tag" Write-Log -Level "INFO" -Message " Title: $releaseName" -# 17. Check if tag is pushed to remote (skip on dev branch) +# 12. Check if tag is pushed to remote (skip on dev branch) if (-not $isDevBranch) { @@ -618,6 +555,10 @@ if (-not $isDevBranch) { 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..." @@ -626,23 +567,24 @@ if (-not $isDevBranch) { 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 all gh commands authenticate with the configured token. + # Scope GH_TOKEN to this block so commands authenticate with the configured token. $previousGhToken = $env:GH_TOKEN $env:GH_TOKEN = $githubToken try { - $authTest = & gh api user 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." - - # 18. Create or update GitHub release - Write-Log -Level "STEP" -Message "Creating GitHub release..." - # Check if release already exists $releaseViewArgs = @( "release", "view", $tag, @@ -665,8 +607,12 @@ if (-not $isDevBranch) { $notesFilePath = Join-Path $releaseDir "release-notes-temp.md" [System.IO.File]::WriteAllText($notesFilePath, $releaseNotes, [System.Text.UTF8Encoding]::new($false)) - $createReleaseArgs = @( - "release", "create", $tag, $zipPath + $releaseAssets = @($packageFile.FullName) + if ($symbolsPackageFile) { + $releaseAssets += $symbolsPackageFile.FullName + } + + $createReleaseArgs = @("release", "create", $tag) + $releaseAssets + @( "--repo", $repo "--title", $releaseName "--notes-file", $notesFilePath @@ -726,11 +672,6 @@ else { #endregion #region Cleanup -if (Test-Path $stagingDir) { - Remove-Item $stagingDir -Recurse -Force - Write-Log -Level "INFO" -Message " Cleaned up staging directory." -} - if (Test-Path $testResultsDir) { Remove-Item $testResultsDir -Recurse -Force Write-Log -Level "INFO" -Message " Cleaned up test results directory." diff --git a/utils/Release-NuGetPackage/scriptsettings.json b/utils/Release-NuGetPackage/scriptsettings.json index cdb3111..c1c3e74 100644 --- a/utils/Release-NuGetPackage/scriptsettings.json +++ b/utils/Release-NuGetPackage/scriptsettings.json @@ -25,14 +25,13 @@ "..\\..\\src\\MaksIT.Results\\MaksIT.Results.csproj" ], "testResultsDir": "..\\..\\testResults", - "stagingDir": "..\\..\\staging", "releaseDir": "..\\..\\release", "changelogPath": "..\\..\\CHANGELOG.md", "testProject": "..\\..\\src\\MaksIT.Results.Tests" }, "release": { - "zipNamePattern": "maksit.results-{version}.zip", + "zipNamePattern": "maksit.dapr-{version}.zip", "releaseTitlePattern": "Release {version}" }, @@ -54,7 +53,6 @@ "paths": { "csprojPaths": "List of project files used for version discovery and publish output.", "testResultsDir": "Directory where test artifacts are written.", - "stagingDir": "Temporary staging directory before archive creation.", "releaseDir": "Output directory for release archives and artifacts.", "changelogPath": "Path to CHANGELOG.md used for version and release notes extraction.", "testProject": "Test project path used by TestRunner."