(chore): migrate to .slnx and refine release scripts/docs

This commit is contained in:
Maksym Sadovnychyy 2026-02-27 18:51:26 +01:00
parent 91adc78690
commit 64953f4ca1
7 changed files with 95 additions and 184 deletions

View File

@ -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

View File

@ -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`.

View File

@ -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

4
src/MaksIT.Results.slnx Normal file
View File

@ -0,0 +1,4 @@
<Solution>
<Project Path="MaksIT.Results.Tests/MaksIT.Results.Tests.csproj" />
<Project Path="MaksIT.Results/MaksIT.Results.csproj" />
</Solution>

View File

@ -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

View File

@ -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,13 +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
$zipNamePattern = $settings.release.zipNamePattern
# Release naming pattern
$releaseTitlePattern = $settings.release.releaseTitlePattern
# Branch configuration
@ -249,21 +246,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 +306,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/(?<owner>[^/]+)/(?<repo>[^/]+?)(?:\.git)?/?$') {
return "$($Matches['owner'])/$($Matches['repo'])"
}
if ($value -match '^(?<owner>[^/]+)/(?<repo>[^/]+)$') {
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 "[:/](?<owner>[^/]+)/(?<repo>[^/.]+)(\.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,75 +426,13 @@ 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
@ -573,7 +457,20 @@ 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. 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 +483,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
# 10. 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
@ -596,7 +506,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)
# 11. Check if tag is pushed to remote (skip on dev branch)
if (-not $isDevBranch) {
@ -618,6 +528,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,13 +540,8 @@ if (-not $isDevBranch) {
exit 1
}
# gh release subcommands do not support custom auth headers.
# Scope GH_TOKEN to this block so all gh commands authenticate with the configured token.
$previousGhToken = $env:GH_TOKEN
$env:GH_TOKEN = $githubToken
try {
$authTest = & gh api user 2>$null
$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."
@ -640,9 +549,15 @@ if (-not $isDevBranch) {
}
Write-Log -Level "OK" -Message " GitHub CLI authenticated."
# 18. Create or update GitHub release
# 12. 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,
@ -665,8 +580,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 +645,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."

View File

@ -25,14 +25,12 @@
"..\\..\\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",
"releaseTitlePattern": "Release {version}"
},
@ -54,13 +52,11 @@
"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."
},
"release": {
"zipNamePattern": "Archive name pattern. Supports {version} placeholder.",
"releaseTitlePattern": "GitHub release title pattern. Supports {version} placeholder."
}
}