(feature): update repo utils
This commit is contained in:
parent
e390b42dce
commit
1816f76736
@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Branch Coverage: 50%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Branch Coverage: 50%">
|
||||||
<title>Branch Coverage: 50%</title>
|
<title>Branch Coverage: 50%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 62.1%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 62.1%">
|
||||||
<title>Line Coverage: 62.1%</title>
|
<title>Line Coverage: 62.1%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Method Coverage: 60%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Method Coverage: 60%">
|
||||||
<title>Method Coverage: 60%</title>
|
<title>Method Coverage: 60%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,3 +1,3 @@
|
|||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Force-AmendTaggedCommit.ps1"
|
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Force-AmendTaggedCommit.ps1"
|
||||||
pause
|
pause
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Amends the latest commit, recreates its associated tag, and force pushes both to remote.
|
Amends the latest commit, recreates its associated tag, and force pushes both to remote.
|
||||||
@ -16,10 +19,10 @@
|
|||||||
If specified, shows what would be done without making changes.
|
If specified, shows what would be done without making changes.
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\Force-AmendTaggedCommit.ps1
|
pwsh -File .\Force-AmendTaggedCommit.ps1
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\Force-AmendTaggedCommit.ps1 -DryRun
|
pwsh -File .\Force-AmendTaggedCommit.ps1 -DryRun
|
||||||
|
|
||||||
.NOTES
|
.NOTES
|
||||||
CONFIGURATION (scriptsettings.json):
|
CONFIGURATION (scriptsettings.json):
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Generate-CoverageBadges.ps1"
|
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Generate-CoverageBadges.ps1"
|
||||||
pause
|
pause
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Runs tests, collects coverage, and generates SVG badges for README.
|
Generates SVG coverage badges for README.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
This script runs unit tests via TestRunner.psm1, then generates shields.io-style
|
This script runs unit tests via TestRunner.psm1, then generates shields.io-style
|
||||||
SVG badges for line, branch, and method coverage.
|
SVG badges for line, branch, and method coverage.
|
||||||
Optional HTML report generation is controlled by scriptsettings.json (openReport).
|
|
||||||
|
|
||||||
Configuration is stored in scriptsettings.json:
|
Configuration is stored in scriptsettings.json:
|
||||||
- openReport : Generate and open full HTML report (true/false)
|
- openReport : Generate and open full HTML report (true/false)
|
||||||
@ -21,7 +23,7 @@
|
|||||||
dotnet tool install -g dotnet-reportgenerator-globaltool
|
dotnet tool install -g dotnet-reportgenerator-globaltool
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\Generate-CoverageBadges.ps1
|
pwsh -File .\Generate-CoverageBadges.ps1
|
||||||
Runs tests and generates coverage badges (and optionally HTML report if configured).
|
Runs tests and generates coverage badges (and optionally HTML report if configured).
|
||||||
|
|
||||||
.OUTPUTS
|
.OUTPUTS
|
||||||
@ -186,7 +188,7 @@ foreach ($badge in $Settings.badges) {
|
|||||||
$color = Get-BadgeColor $metricValue
|
$color = Get-BadgeColor $metricValue
|
||||||
$svg = New-Badge -label $badge.label -value "$metricValue%" -color $color
|
$svg = New-Badge -label $badge.label -value "$metricValue%" -color $color
|
||||||
$path = Join-Path $BadgesDir $badge.name
|
$path = Join-Path $BadgesDir $badge.name
|
||||||
$svg | Out-File -FilePath $path -Encoding utf8
|
$svg | Out-File -FilePath $path -Encoding utf8NoBOM
|
||||||
Write-Log -Level "OK" -Message "$($badge.name): $($badge.label) = $metricValue%"
|
Write-Log -Level "OK" -Message "$($badge.name): $($badge.label) = $metricValue%"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
#
|
#
|
||||||
# Shared Git helpers for utility scripts.
|
# Shared Git helpers for utility scripts.
|
||||||
#
|
#
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
function Get-LogTimestampInternal {
|
function Get-LogTimestampInternal {
|
||||||
return (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
|
return (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
@echo off
|
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Release-NuGetPackage.ps1"
|
|
||||||
pause
|
|
||||||
@ -1,747 +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
|
|
||||||
$configuredGithubRepository = $settings.github.repository
|
|
||||||
$repoSource = $null
|
|
||||||
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($configuredGithubRepository)) {
|
|
||||||
$repoSource = $configuredGithubRepository.Trim()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$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
|
|
||||||
}
|
|
||||||
|
|
||||||
$repoSource = $remoteUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($repoSource -match "(?i)github\.com[:/](?<owner>[^/]+)/(?<repo>[^/.]+)(\.git)?$") {
|
|
||||||
$owner = $matches['owner']
|
|
||||||
$repoName = $matches['repo']
|
|
||||||
$repo = "$owner/$repoName"
|
|
||||||
}
|
|
||||||
elseif ($repoSource -match "^(?<owner>[^/]+)/(?<repo>[^/]+)$") {
|
|
||||||
$owner = $matches['owner']
|
|
||||||
$repoName = $matches['repo']
|
|
||||||
$repo = "$owner/$repoName"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Error "Could not parse GitHub repo from source: $repoSource. Use 'github.repository' in scriptsettings.json (owner/repo or github URL)."
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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 {
|
|
||||||
$ghVersion = & gh --version 2>&1
|
|
||||||
if ($ghVersion) {
|
|
||||||
Write-Log -Level "INFO" -Message " gh version: $($ghVersion[0])"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log -Level "INFO" -Message " Auth env var: $githubTokenEnvVar (set)"
|
|
||||||
Write-Log -Level "INFO" -Message " Target repository: $repo"
|
|
||||||
|
|
||||||
# Validate that the provided token can access the target repository.
|
|
||||||
$authArgs = @("api", "repos/$repo", "--jq", ".full_name")
|
|
||||||
Write-Log -Level "INFO" -Message " Running: gh $($authArgs -join ' ')"
|
|
||||||
$authOutput = & gh @authArgs 2>&1
|
|
||||||
$authExitCode = $LASTEXITCODE
|
|
||||||
|
|
||||||
if ($authExitCode -ne 0 -or [string]::IsNullOrWhiteSpace(($authOutput | Out-String))) {
|
|
||||||
Write-Log -Level "WARN" -Message " gh auth check failed (exit code: $authExitCode)."
|
|
||||||
if ($authOutput) {
|
|
||||||
Write-Log -Level "WARN" -Message " gh api output:"
|
|
||||||
$authOutput | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
|
|
||||||
}
|
|
||||||
|
|
||||||
$authStatus = & gh auth status --hostname github.com 2>&1
|
|
||||||
if ($authStatus) {
|
|
||||||
Write-Log -Level "WARN" -Message " gh auth status output:"
|
|
||||||
$authStatus | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Error "GitHub CLI authentication failed for repository '$repo'. Ensure '$githubTokenEnvVar' is valid and has access to this repository."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log -Level "OK" -Message " GitHub token validated for repository: $($authOutput | Select-Object -First 1)"
|
|
||||||
|
|
||||||
# 13. Create or update GitHub release
|
|
||||||
Write-Log -Level "STEP" -Message "Creating GitHub release..."
|
|
||||||
|
|
||||||
# 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
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json-schema.org/draft-07/schema",
|
|
||||||
"title": "Release NuGet Package Script Settings",
|
|
||||||
"description": "Configuration file for Release-NuGetPackage.ps1 script.",
|
|
||||||
|
|
||||||
"github": {
|
|
||||||
"enabled": true,
|
|
||||||
"githubToken": "GITHUB_MAKS_IT_COM",
|
|
||||||
"repository": "https://github.com/MAKS-IT-COM/maksit-dapr"
|
|
||||||
},
|
|
||||||
|
|
||||||
"nuget": {
|
|
||||||
"enabled": true,
|
|
||||||
"nugetApiKey": "NUGET_MAKS_IT",
|
|
||||||
"source": "https://api.nuget.org/v3/index.json"
|
|
||||||
},
|
|
||||||
|
|
||||||
"branches": {
|
|
||||||
"release": "main",
|
|
||||||
"dev": "dev"
|
|
||||||
},
|
|
||||||
|
|
||||||
"paths": {
|
|
||||||
"csprojPaths": [
|
|
||||||
"..\\..\\src\\MaksIT.Dapr\\MaksIT.Dapr.csproj"
|
|
||||||
],
|
|
||||||
"testResultsDir": "..\\..\\testResults",
|
|
||||||
"releaseDir": "..\\..\\release",
|
|
||||||
"changelogPath": "..\\..\\CHANGELOG.md",
|
|
||||||
"testProject": "..\\..\\src\\MaksIT.Dapr.Tests"
|
|
||||||
},
|
|
||||||
|
|
||||||
"release": {
|
|
||||||
"zipNamePattern": "maksit.dapr-{version}.zip",
|
|
||||||
"releaseTitlePattern": "Release {version}"
|
|
||||||
},
|
|
||||||
|
|
||||||
"_comments": {
|
|
||||||
"github": {
|
|
||||||
"enabled": "Enable/disable GitHub release creation.",
|
|
||||||
"githubToken": "Environment variable name containing GitHub token used by gh CLI.",
|
|
||||||
"repository": "GitHub repository override used for releases (supports owner/repo or full GitHub URL)."
|
|
||||||
},
|
|
||||||
"nuget": {
|
|
||||||
"enabled": "Enable/disable NuGet publish step.",
|
|
||||||
"nugetApiKey": "Environment variable name containing NuGet API key.",
|
|
||||||
"source": "NuGet feed URL passed to dotnet nuget push."
|
|
||||||
},
|
|
||||||
"branches": {
|
|
||||||
"release": "Branch that requires tag and allows full publish flow.",
|
|
||||||
"dev": "Branch for local/dev build flow (no tag required)."
|
|
||||||
},
|
|
||||||
"paths": {
|
|
||||||
"csprojPaths": "List of project files used for version discovery and publish output.",
|
|
||||||
"testResultsDir": "Directory where test artifacts are written.",
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
121
utils/Release-Package/CorePlugins/CleanupArtifacts.psm1
Normal file
121
utils/Release-Package/CorePlugins/CleanupArtifacts.psm1
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Cleanup plugin for removing generated artifacts after pipeline completion.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin removes files from the configured artifacts directory using
|
||||||
|
glob patterns. It is typically placed at the end of the Release stage so
|
||||||
|
cleanup becomes explicit and opt-in per repository.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-CleanupPatternsInternal {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
$ConfiguredPatterns
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $ConfiguredPatterns) {
|
||||||
|
return @('*.nupkg', '*.snupkg')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ConfiguredPatterns -is [System.Collections.IEnumerable] -and -not ($ConfiguredPatterns -is [string])) {
|
||||||
|
return @($ConfiguredPatterns | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$ConfiguredPatterns)) {
|
||||||
|
return @('*.nupkg', '*.snupkg')
|
||||||
|
}
|
||||||
|
|
||||||
|
return @([string]$ConfiguredPatterns)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExcludePatternsInternal {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
$ConfiguredPatterns
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $ConfiguredPatterns) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ConfiguredPatterns -is [System.Collections.IEnumerable] -and -not ($ConfiguredPatterns -is [string])) {
|
||||||
|
return @($ConfiguredPatterns | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$ConfiguredPatterns)) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
return @([string]$ConfiguredPatterns)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$artifactsDirectory = $sharedSettings.ArtifactsDirectory
|
||||||
|
$patterns = Get-CleanupPatternsInternal -ConfiguredPatterns $pluginSettings.includePatterns
|
||||||
|
$excludePatterns = Get-ExcludePatternsInternal -ConfiguredPatterns $pluginSettings.excludePatterns
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||||
|
throw "CleanupArtifacts plugin requires an artifacts directory in the shared context."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $artifactsDirectory -PathType Container)) {
|
||||||
|
Write-Log -Level "WARN" -Message " Artifacts directory not found: $artifactsDirectory"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Cleaning generated artifacts..."
|
||||||
|
|
||||||
|
$itemsToRemove = @()
|
||||||
|
foreach ($pattern in $patterns) {
|
||||||
|
$matchedItems = @(
|
||||||
|
Get-ChildItem -Path $artifactsDirectory -Force -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.Name -like $pattern }
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($excludePatterns.Count -gt 0) {
|
||||||
|
$matchedItems = @(
|
||||||
|
$matchedItems |
|
||||||
|
Where-Object {
|
||||||
|
$item = $_
|
||||||
|
-not ($excludePatterns | Where-Object { $item.Name -like $_ } | Select-Object -First 1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemsToRemove += @($matchedItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemsToRemove = @($itemsToRemove | Sort-Object FullName -Unique)
|
||||||
|
|
||||||
|
if ($itemsToRemove.Count -eq 0) {
|
||||||
|
Write-Log -Level "INFO" -Message " No artifacts matched cleanup rules."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($item in $itemsToRemove) {
|
||||||
|
Remove-Item -Path $item.FullName -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Log -Level "OK" -Message " Removed: $($item.Name)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
93
utils/Release-Package/CorePlugins/CreateArchive.psm1
Normal file
93
utils/Release-Package/CorePlugins/CreateArchive.psm1
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Creates a release zip from prepared build artifacts.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin compresses the release artifact inputs prepared by an earlier
|
||||||
|
producer plugin (for example DotNetPack or DotNetPublish) into a zip file
|
||||||
|
and exposes the resulting release assets for later publisher plugins.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$artifactsDirectory = $sharedSettings.ArtifactsDirectory
|
||||||
|
$version = $sharedSettings.Version
|
||||||
|
$archiveInputs = @()
|
||||||
|
|
||||||
|
if ($sharedSettings.PSObject.Properties['ReleaseArchiveInputs'] -and $sharedSettings.ReleaseArchiveInputs) {
|
||||||
|
$archiveInputs = @($sharedSettings.ReleaseArchiveInputs)
|
||||||
|
}
|
||||||
|
elseif ($sharedSettings.PSObject.Properties['PackageFile'] -and $sharedSettings.PackageFile) {
|
||||||
|
$archiveInputs = @($sharedSettings.PackageFile.FullName)
|
||||||
|
if ($sharedSettings.PSObject.Properties['SymbolsPackageFile'] -and $sharedSettings.SymbolsPackageFile) {
|
||||||
|
$archiveInputs += $sharedSettings.SymbolsPackageFile.FullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($archiveInputs.Count -eq 0) {
|
||||||
|
throw "CreateArchive plugin requires prepared artifacts. Run a producer plugin (for example DotNetPack or DotNetPublish) first."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||||
|
throw "CreateArchive plugin requires an artifacts directory in the shared context."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $artifactsDirectory -PathType Container)) {
|
||||||
|
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$zipNamePattern = if ($pluginSettings.PSObject.Properties['zipNamePattern'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.zipNamePattern)) {
|
||||||
|
[string]$pluginSettings.zipNamePattern
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
"release-{version}.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
$zipFileName = $zipNamePattern -replace '\{version\}', $version
|
||||||
|
$zipPath = Join-Path $artifactsDirectory $zipFileName
|
||||||
|
|
||||||
|
if (Test-Path $zipPath) {
|
||||||
|
Remove-Item -Path $zipPath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Creating release archive..."
|
||||||
|
Compress-Archive -Path $archiveInputs -DestinationPath $zipPath -CompressionLevel Optimal -Force
|
||||||
|
|
||||||
|
if (-not (Test-Path $zipPath -PathType Leaf)) {
|
||||||
|
throw "Failed to create release archive at: $zipPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Release archive ready: $zipPath"
|
||||||
|
|
||||||
|
$releaseAssetPaths = @($zipPath)
|
||||||
|
if ($sharedSettings.PSObject.Properties['PackageFile'] -and $sharedSettings.PackageFile) {
|
||||||
|
$releaseAssetPaths += $sharedSettings.PackageFile.FullName
|
||||||
|
}
|
||||||
|
if ($sharedSettings.PSObject.Properties['SymbolsPackageFile'] -and $sharedSettings.SymbolsPackageFile) {
|
||||||
|
$releaseAssetPaths += $sharedSettings.SymbolsPackageFile.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName ReleaseDir -NotePropertyValue $artifactsDirectory -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName ReleaseArchivePath -NotePropertyValue $zipPath -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName ReleaseAssetPaths -NotePropertyValue $releaseAssetPaths -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
99
utils/Release-Package/CorePlugins/DotNetPack.psm1
Normal file
99
utils/Release-Package/CorePlugins/DotNetPack.psm1
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
.NET pack plugin for producing package artifacts.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin creates package output for the release pipeline.
|
||||||
|
It packs the configured .NET project, resolves the generated
|
||||||
|
package artifacts, and publishes them into shared runtime context
|
||||||
|
for later plugins.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
# Load this globally only as a fallback. Re-importing PluginSupport in its own execution path
|
||||||
|
# can invalidate commands already resolved by the release engine.
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||||
|
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$projectFiles = $sharedSettings.ProjectFiles
|
||||||
|
$artifactsDirectory = $sharedSettings.ArtifactsDirectory
|
||||||
|
$version = $sharedSettings.Version
|
||||||
|
$packageProjectPath = $null
|
||||||
|
$releaseArchiveInputs = @()
|
||||||
|
|
||||||
|
Assert-Command dotnet
|
||||||
|
|
||||||
|
if (-not $sharedSettings.PSObject.Properties['ProjectFiles'] -or $projectFiles.Count -eq 0) {
|
||||||
|
throw "DotNetPack plugin requires project files in the shared context."
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputDir = $artifactsDirectory
|
||||||
|
|
||||||
|
if (!(Test-Path $outputDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $outputDir | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# The release context guarantees ProjectFiles is an array, so index 0 is the first project path,
|
||||||
|
# not the first character of a string.
|
||||||
|
$packageProjectPath = $projectFiles[0]
|
||||||
|
Write-Log -Level "STEP" -Message "Packing NuGet package..."
|
||||||
|
dotnet pack $packageProjectPath -c Release -o $outputDir --nologo `
|
||||||
|
-p:IncludeSymbols=true `
|
||||||
|
-p:SymbolPackageFormat=snupkg
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "dotnet pack failed for $packageProjectPath."
|
||||||
|
}
|
||||||
|
|
||||||
|
# dotnet pack can leave older packages in the artifacts directory.
|
||||||
|
# Pick the newest file matching the current version rather than assuming a clean folder.
|
||||||
|
$packageFile = Get-ChildItem -Path $outputDir -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) {
|
||||||
|
throw "Could not locate generated NuGet package for version $version in: $outputDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Package ready: $($packageFile.FullName)"
|
||||||
|
$releaseArchiveInputs = @($packageFile.FullName)
|
||||||
|
|
||||||
|
$symbolsPackageFile = Get-ChildItem -Path $outputDir -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)"
|
||||||
|
$releaseArchiveInputs += $symbolsPackageFile.FullName
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "WARN" -Message " Symbols package (.snupkg) not found for version $version."
|
||||||
|
}
|
||||||
|
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName PackageFile -NotePropertyValue $packageFile -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName SymbolsPackageFile -NotePropertyValue $symbolsPackageFile -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName ReleaseArchiveInputs -NotePropertyValue $releaseArchiveInputs -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
71
utils/Release-Package/CorePlugins/DotNetPublish.psm1
Normal file
71
utils/Release-Package/CorePlugins/DotNetPublish.psm1
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
.NET publish plugin for producing application release artifacts.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin publishes the configured .NET project into a release output
|
||||||
|
directory and exposes that published directory to the shared release
|
||||||
|
context so later release-stage plugins can archive and publish it.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||||
|
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$projectFiles = $sharedSettings.ProjectFiles
|
||||||
|
$artifactsDirectory = $sharedSettings.ArtifactsDirectory
|
||||||
|
$publishProjectPath = $null
|
||||||
|
|
||||||
|
Assert-Command dotnet
|
||||||
|
|
||||||
|
if (-not $sharedSettings.PSObject.Properties['ProjectFiles'] -or $projectFiles.Count -eq 0) {
|
||||||
|
throw "DotNetPublish plugin requires project files in the shared context."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(Test-Path $artifactsDirectory)) {
|
||||||
|
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# The first configured project remains the canonical release artifact source.
|
||||||
|
$publishProjectPath = $projectFiles[0]
|
||||||
|
$publishDir = Join-Path $artifactsDirectory ([System.IO.Path]::GetFileNameWithoutExtension($publishProjectPath))
|
||||||
|
|
||||||
|
if (Test-Path $publishDir) {
|
||||||
|
Remove-Item -Path $publishDir -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Publishing release artifact..."
|
||||||
|
dotnet publish $publishProjectPath -c Release -o $publishDir --nologo
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "dotnet publish failed for $publishProjectPath."
|
||||||
|
}
|
||||||
|
|
||||||
|
$publishedItems = @(Get-ChildItem -Path $publishDir -Force -ErrorAction SilentlyContinue)
|
||||||
|
if ($publishedItems.Count -eq 0) {
|
||||||
|
throw "dotnet publish completed, but no files were produced in: $publishDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Published artifact ready: $publishDir"
|
||||||
|
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName PackageFile -NotePropertyValue $null -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName SymbolsPackageFile -NotePropertyValue $null -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName ReleaseArchiveInputs -NotePropertyValue @($publishDir) -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
72
utils/Release-Package/CorePlugins/DotNetTest.psm1
Normal file
72
utils/Release-Package/CorePlugins/DotNetTest.psm1
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
.NET test plugin for executing automated tests.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin resolves the configured .NET test project and optional
|
||||||
|
results directory, runs tests through TestRunner, and stores
|
||||||
|
the resulting test metrics in shared runtime context.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
# Same fallback pattern as the other plugins: use the existing shared module if it is already loaded.
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
Import-PluginDependency -ModuleName "TestRunner" -RequiredCommand "Invoke-TestsWithCoverage"
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$testProjectSetting = $pluginSettings.project
|
||||||
|
$testResultsDirSetting = $pluginSettings.resultsDir
|
||||||
|
$scriptDir = $sharedSettings.ScriptDir
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($testProjectSetting)) {
|
||||||
|
throw "DotNetTest plugin requires 'project' in scriptsettings.json."
|
||||||
|
}
|
||||||
|
|
||||||
|
$testProjectPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $testProjectSetting))
|
||||||
|
$testResultsDir = $null
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($testResultsDirSetting)) {
|
||||||
|
$testResultsDir = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $testResultsDirSetting))
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Running tests..."
|
||||||
|
|
||||||
|
# Build a splatted hashtable so optional arguments can be added without duplicating the call site.
|
||||||
|
$invokeTestParams = @{
|
||||||
|
TestProjectPath = $testProjectPath
|
||||||
|
Silent = $true
|
||||||
|
}
|
||||||
|
if ($testResultsDir) {
|
||||||
|
$invokeTestParams.ResultsDirectory = $testResultsDir
|
||||||
|
}
|
||||||
|
|
||||||
|
$testResult = Invoke-TestsWithCoverage @invokeTestParams
|
||||||
|
|
||||||
|
if (-not $testResult.Success) {
|
||||||
|
throw "Tests failed. $($testResult.Error)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName TestResult -NotePropertyValue $testResult -Force
|
||||||
|
|
||||||
|
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)%"
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
232
utils/Release-Package/CorePlugins/GitHub.psm1
Normal file
232
utils/Release-Package/CorePlugins/GitHub.psm1
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
GitHub release plugin.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin validates GitHub CLI access, resolves the target
|
||||||
|
repository, and creates the configured GitHub release using the
|
||||||
|
shared release artifacts and extracted release notes.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-GitHubRepositoryInternal {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[string]$ConfiguredRepository
|
||||||
|
)
|
||||||
|
|
||||||
|
$repoSource = $ConfiguredRepository
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($repoSource)) {
|
||||||
|
$repoSource = git config --get remote.origin.url
|
||||||
|
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($repoSource)) {
|
||||||
|
throw "Could not determine git remote origin URL."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$repoSource = $repoSource.Trim()
|
||||||
|
|
||||||
|
if ($repoSource -match "(?i)github\.com[:/](?<owner>[^/]+)/(?<repo>[^/.]+)(\.git)?$") {
|
||||||
|
return "$($matches['owner'])/$($matches['repo'])"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($repoSource -match "^(?<owner>[^/]+)/(?<repo>[^/]+)$") {
|
||||||
|
return "$($matches['owner'])/$($matches['repo'])"
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Could not parse GitHub repo from source: $repoSource. Configure Plugins[].repository with 'owner/repo' or a GitHub URL."
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ReleaseNotesInternal {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ReleaseNotesFile,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Version
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message "Verifying release notes source..."
|
||||||
|
if (-not (Test-Path $ReleaseNotesFile -PathType Leaf)) {
|
||||||
|
throw "Release notes source file not found at: $ReleaseNotesFile"
|
||||||
|
}
|
||||||
|
|
||||||
|
$releaseNotesContent = Get-Content $ReleaseNotesFile -Raw
|
||||||
|
if ($releaseNotesContent -notmatch '##\s+v(\d+\.\d+\.\d+)') {
|
||||||
|
throw "No version entry found in the configured release notes source."
|
||||||
|
}
|
||||||
|
|
||||||
|
$releaseNotesVersion = $Matches[1]
|
||||||
|
if ($releaseNotesVersion -ne $Version) {
|
||||||
|
throw "Project version ($Version) does not match the latest release notes version ($releaseNotesVersion)."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Release notes version matches: v$releaseNotesVersion"
|
||||||
|
|
||||||
|
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($releaseNotesContent, $pattern)
|
||||||
|
|
||||||
|
if (-not $match.Success) {
|
||||||
|
throw "Release notes entry for version $Version not found."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Release notes extracted."
|
||||||
|
return $match.Value.Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$githubTokenEnvVar = $pluginSettings.githubToken
|
||||||
|
$configuredRepository = $pluginSettings.repository
|
||||||
|
$releaseNotesFileSetting = $pluginSettings.releaseNotesFile
|
||||||
|
$releaseTitlePatternSetting = $pluginSettings.releaseTitlePattern
|
||||||
|
$scriptDir = $sharedSettings.ScriptDir
|
||||||
|
$version = $sharedSettings.Version
|
||||||
|
$tag = $sharedSettings.Tag
|
||||||
|
$releaseDir = $sharedSettings.ReleaseDir
|
||||||
|
$releaseAssetPaths = @()
|
||||||
|
|
||||||
|
Assert-Command gh
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($githubTokenEnvVar)) {
|
||||||
|
throw "GitHub plugin requires 'githubToken' in scriptsettings.json."
|
||||||
|
}
|
||||||
|
|
||||||
|
$githubToken = [System.Environment]::GetEnvironmentVariable($githubTokenEnvVar)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($githubToken)) {
|
||||||
|
throw "GitHub token is not set. Set '$githubTokenEnvVar' and rerun."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
|
||||||
|
throw "GitHub plugin requires 'releaseNotesFile' in scriptsettings.json."
|
||||||
|
}
|
||||||
|
|
||||||
|
$releaseNotesFile = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $releaseNotesFileSetting))
|
||||||
|
$releaseNotes = Get-ReleaseNotesInternal -ReleaseNotesFile $releaseNotesFile -Version $version
|
||||||
|
|
||||||
|
if ($sharedSettings.PSObject.Properties['ReleaseAssetPaths'] -and $sharedSettings.ReleaseAssetPaths) {
|
||||||
|
$releaseAssetPaths = @($sharedSettings.ReleaseAssetPaths)
|
||||||
|
}
|
||||||
|
elseif ($sharedSettings.PSObject.Properties['PackageFile'] -and $sharedSettings.PackageFile) {
|
||||||
|
$releaseAssetPaths = @($sharedSettings.PackageFile.FullName)
|
||||||
|
if ($sharedSettings.PSObject.Properties['SymbolsPackageFile'] -and $sharedSettings.SymbolsPackageFile) {
|
||||||
|
$releaseAssetPaths += $sharedSettings.SymbolsPackageFile.FullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($releaseAssetPaths.Count -eq 0) {
|
||||||
|
throw "GitHub release requires at least one prepared release asset."
|
||||||
|
}
|
||||||
|
|
||||||
|
$repo = Get-GitHubRepositoryInternal -ConfiguredRepository $configuredRepository
|
||||||
|
$releaseTitlePattern = if ([string]::IsNullOrWhiteSpace($releaseTitlePatternSetting)) {
|
||||||
|
"Release {version}"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$releaseTitlePatternSetting
|
||||||
|
}
|
||||||
|
$releaseName = $releaseTitlePattern -replace '\{version\}', $version
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message " GitHub repository: $repo"
|
||||||
|
Write-Log -Level "INFO" -Message " GitHub tag: $tag"
|
||||||
|
Write-Log -Level "INFO" -Message " GitHub title: $releaseName"
|
||||||
|
|
||||||
|
$previousGhToken = $env:GH_TOKEN
|
||||||
|
$env:GH_TOKEN = $githubToken
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ghVersion = & gh --version 2>&1
|
||||||
|
if ($ghVersion) {
|
||||||
|
Write-Log -Level "INFO" -Message " gh version: $($ghVersion[0])"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message " Auth env var: $githubTokenEnvVar (set)"
|
||||||
|
|
||||||
|
$authArgs = @("api", "repos/$repo", "--jq", ".full_name")
|
||||||
|
$authOutput = & gh @authArgs 2>&1
|
||||||
|
$authExitCode = $LASTEXITCODE
|
||||||
|
|
||||||
|
if ($authExitCode -ne 0 -or [string]::IsNullOrWhiteSpace(($authOutput | Out-String))) {
|
||||||
|
Write-Log -Level "WARN" -Message " gh auth check failed (exit code: $authExitCode)."
|
||||||
|
if ($authOutput) {
|
||||||
|
$authOutput | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$authStatus = & gh auth status --hostname github.com 2>&1
|
||||||
|
if ($authStatus) {
|
||||||
|
$authStatus | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "GitHub CLI authentication failed for repository '$repo'. Ensure '$githubTokenEnvVar' is valid and has access to this repository."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " GitHub token validated for repository: $($authOutput | Select-Object -First 1)"
|
||||||
|
Write-Log -Level "STEP" -Message "Creating GitHub release..."
|
||||||
|
|
||||||
|
$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) {
|
||||||
|
throw "Failed to delete existing release $tag."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$notesFilePath = Join-Path $releaseDir ("release-notes-{0}.md" -f $version)
|
||||||
|
|
||||||
|
try {
|
||||||
|
[System.IO.File]::WriteAllText($notesFilePath, $releaseNotes, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
|
||||||
|
$createReleaseArgs = @("release", "create", $tag) + $releaseAssetPaths + @(
|
||||||
|
"--repo", $repo,
|
||||||
|
"--title", $releaseName,
|
||||||
|
"--notes-file", $notesFilePath
|
||||||
|
)
|
||||||
|
& gh @createReleaseArgs
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Failed to create GitHub release for tag $tag."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (Test-Path $notesFilePath) {
|
||||||
|
Remove-Item $notesFilePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " GitHub release created successfully."
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName PublishCompleted -NotePropertyValue $true -Force
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if ($null -ne $previousGhToken) {
|
||||||
|
$env:GH_TOKEN = $previousGhToken
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Remove-Item Env:GH_TOKEN -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
67
utils/Release-Package/CorePlugins/NuGet.psm1
Normal file
67
utils/Release-Package/CorePlugins/NuGet.psm1
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
NuGet publish plugin.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin publishes the package artifact from shared runtime
|
||||||
|
context to the configured NuGet feed using the configured API key.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$nugetApiKeyEnvVar = $pluginSettings.nugetApiKey
|
||||||
|
$packageFile = $sharedSettings.PackageFile
|
||||||
|
|
||||||
|
Assert-Command dotnet
|
||||||
|
|
||||||
|
if (-not $packageFile) {
|
||||||
|
throw "NuGet plugin requires a NuGet package artifact. Ensure DotNetPack produced a .nupkg before running NuGet."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($nugetApiKeyEnvVar)) {
|
||||||
|
throw "NuGet plugin requires 'nugetApiKey' in scriptsettings.json."
|
||||||
|
}
|
||||||
|
|
||||||
|
$nugetApiKey = [System.Environment]::GetEnvironmentVariable($nugetApiKeyEnvVar)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($nugetApiKey)) {
|
||||||
|
throw "NuGet API key is not set. Set '$nugetApiKeyEnvVar' and rerun."
|
||||||
|
}
|
||||||
|
|
||||||
|
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {
|
||||||
|
"https://api.nuget.org/v3/index.json"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$pluginSettings.source
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Pushing to NuGet.org..."
|
||||||
|
dotnet nuget push $packageFile.FullName -k $nugetApiKey -s $nugetSource --skip-duplicate
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Failed to push the package to NuGet."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " NuGet push completed."
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName PublishCompleted -NotePropertyValue $true -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
119
utils/Release-Package/CorePlugins/QualityGate.psm1
Normal file
119
utils/Release-Package/CorePlugins/QualityGate.psm1
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Quality gate plugin for validating release readiness.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin evaluates quality constraints using shared test
|
||||||
|
results and project files. It enforces coverage thresholds
|
||||||
|
and checks for vulnerable packages before release plugins run.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-VulnerablePackagesInternal {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string[]]$ProjectFiles
|
||||||
|
)
|
||||||
|
|
||||||
|
$findings = @()
|
||||||
|
|
||||||
|
foreach ($projectPath in $ProjectFiles) {
|
||||||
|
Write-Log -Level "STEP" -Message "Checking vulnerable packages: $([System.IO.Path]::GetFileName($projectPath))"
|
||||||
|
|
||||||
|
$output = & dotnet list $projectPath package --vulnerable --include-transitive 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "dotnet list package --vulnerable failed for $projectPath."
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputText = ($output | Out-String)
|
||||||
|
if ($outputText -match "(?im)\bhas the following vulnerable packages\b" -or $outputText -match "(?im)^\s*>\s+[A-Za-z0-9_.-]+\s") {
|
||||||
|
$findings += [pscustomobject]@{
|
||||||
|
Project = $projectPath
|
||||||
|
Output = $outputText.Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $findings
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$coverageThresholdSetting = $pluginSettings.coverageThreshold
|
||||||
|
$failOnVulnerabilitiesSetting = $pluginSettings.failOnVulnerabilities
|
||||||
|
$projectFiles = $sharedSettings.ProjectFiles
|
||||||
|
$testResult = $null
|
||||||
|
if ($sharedSettings.PSObject.Properties['TestResult']) {
|
||||||
|
$testResult = $sharedSettings.TestResult
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $testResult) {
|
||||||
|
throw "QualityGate plugin requires test results. Run the DotNetTest plugin first."
|
||||||
|
}
|
||||||
|
|
||||||
|
$coverageThreshold = 0
|
||||||
|
if ($null -ne $coverageThresholdSetting) {
|
||||||
|
$coverageThreshold = [double]$coverageThresholdSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($coverageThreshold -gt 0) {
|
||||||
|
Write-Log -Level "STEP" -Message "Checking coverage threshold..."
|
||||||
|
if ([double]$testResult.LineRate -lt $coverageThreshold) {
|
||||||
|
throw "Line coverage $($testResult.LineRate)% is below the configured threshold of $coverageThreshold%."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Coverage threshold met: $($testResult.LineRate)% >= $coverageThreshold%"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "WARN" -Message "Skipping coverage threshold check (disabled)."
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Command dotnet
|
||||||
|
|
||||||
|
$failOnVulnerabilities = $true
|
||||||
|
if ($null -ne $failOnVulnerabilitiesSetting) {
|
||||||
|
$failOnVulnerabilities = [bool]$failOnVulnerabilitiesSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
$vulnerabilities = Test-VulnerablePackagesInternal -ProjectFiles $projectFiles
|
||||||
|
|
||||||
|
if ($vulnerabilities.Count -eq 0) {
|
||||||
|
Write-Log -Level "OK" -Message " No vulnerable packages detected."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($finding in $vulnerabilities) {
|
||||||
|
Write-Log -Level "WARN" -Message " Vulnerable packages detected in $([System.IO.Path]::GetFileName($finding.Project))"
|
||||||
|
$finding.Output -split "`r?`n" | ForEach-Object {
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($_)) {
|
||||||
|
Write-Log -Level "WARN" -Message " $_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($failOnVulnerabilities) {
|
||||||
|
throw "Vulnerable packages were detected and failOnVulnerabilities is enabled."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "WARN" -Message "Vulnerable packages detected, but failOnVulnerabilities is disabled."
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
1
utils/Release-Package/CustomPlugins/.gitkeep
Normal file
1
utils/Release-Package/CustomPlugins/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
110
utils/Release-Package/DotNetProjectSupport.psm1
Normal file
110
utils/Release-Package/DotNetProjectSupport.psm1
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
|
||||||
|
$loggingModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Logging.psm1"
|
||||||
|
if (Test-Path $loggingModulePath -PathType Leaf) {
|
||||||
|
Import-Module $loggingModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-Command Get-PluginPathListSetting -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path $PSScriptRoot "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-DotNetProjectPropertyValue {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[xml]$Csproj,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PropertyName
|
||||||
|
)
|
||||||
|
|
||||||
|
# SDK-style .csproj files can have multiple PropertyGroup nodes.
|
||||||
|
# Use the first group that defines the requested property.
|
||||||
|
$propNode = $Csproj.Project.PropertyGroup |
|
||||||
|
Where-Object { $_.$PropertyName } |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if ($propNode) {
|
||||||
|
return $propNode.$PropertyName
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-DotNetProjectVersions {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string[]]$ProjectFiles
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message "Reading version(s) from .NET project files..."
|
||||||
|
$projectVersions = @{}
|
||||||
|
|
||||||
|
foreach ($projectPath in $ProjectFiles) {
|
||||||
|
if (-not (Test-Path $projectPath -PathType Leaf)) {
|
||||||
|
Write-Error "Project file not found at: $projectPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([System.IO.Path]::GetExtension($projectPath) -ne ".csproj") {
|
||||||
|
Write-Error "Configured project file is not a .csproj file: $projectPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[xml]$csproj = Get-Content $projectPath
|
||||||
|
$version = Get-DotNetProjectPropertyValue -Csproj $csproj -PropertyName "Version"
|
||||||
|
|
||||||
|
if (-not $version) {
|
||||||
|
Write-Error "Version not found in $projectPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$projectVersions[$projectPath] = $version
|
||||||
|
Write-Log -Level "OK" -Message " $([System.IO.Path]::GetFileName($projectPath)): $version"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $projectVersions
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-DotNetReleaseContext {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$Plugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ScriptDir
|
||||||
|
)
|
||||||
|
|
||||||
|
# The array wrapper is intentional: without it, one configured project can collapse to a string,
|
||||||
|
# and later indexing [0] would return only the first character of the path.
|
||||||
|
$projectFiles = @(Get-PluginPathListSetting -Plugins $Plugins -PropertyName "projectFiles" -BasePath $ScriptDir)
|
||||||
|
$artifactsDirectory = Get-PluginPathSetting -Plugins $Plugins -PropertyName "artifactsDir" -BasePath $ScriptDir
|
||||||
|
|
||||||
|
if ($projectFiles.Count -eq 0) {
|
||||||
|
Write-Error "No .NET project files configured in plugin settings. Add 'projectFiles' to a relevant plugin."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||||
|
Write-Error "No artifacts directory configured in plugin settings. Add 'artifactsDir' to a relevant plugin."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$projectVersions = Get-DotNetProjectVersions -ProjectFiles $projectFiles
|
||||||
|
# The first configured project is treated as the canonical version source for the release.
|
||||||
|
$version = $projectVersions[$projectFiles[0]]
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
ProjectFiles = $projectFiles
|
||||||
|
ArtifactsDirectory = $artifactsDirectory
|
||||||
|
Version = $version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Get-DotNetProjectPropertyValue, Get-DotNetProjectVersions, New-DotNetReleaseContext
|
||||||
165
utils/Release-Package/EngineSupport.psm1
Normal file
165
utils/Release-Package/EngineSupport.psm1
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
|
||||||
|
$loggingModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Logging.psm1"
|
||||||
|
if (Test-Path $loggingModulePath -PathType Leaf) {
|
||||||
|
Import-Module $loggingModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-Command Get-CurrentBranch -ErrorAction SilentlyContinue)) {
|
||||||
|
$gitToolsModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "GitTools.psm1"
|
||||||
|
if (Test-Path $gitToolsModulePath -PathType Leaf) {
|
||||||
|
Import-Module $gitToolsModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-Command Get-PluginStage -ErrorAction SilentlyContinue) -or -not (Get-Command Test-IsPublishPlugin -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path $PSScriptRoot "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-Command New-DotNetReleaseContext -ErrorAction SilentlyContinue)) {
|
||||||
|
$dotNetProjectSupportModulePath = Join-Path $PSScriptRoot "DotNetProjectSupport.psm1"
|
||||||
|
if (Test-Path $dotNetProjectSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $dotNetProjectSupportModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "WARN" -Message " Uncommitted changes detected (allowed on dev branch)."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Working directory is clean."
|
||||||
|
}
|
||||||
|
|
||||||
|
function Initialize-ReleaseStageContext {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$RemainingPlugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$SharedSettings,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ArtifactsDirectory,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Version
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Verifying tag is pushed to remote..."
|
||||||
|
$remoteTagExists = Test-RemoteTagExists -Tag $SharedSettings.Tag -Remote "origin"
|
||||||
|
if (-not $remoteTagExists) {
|
||||||
|
Write-Log -Level "WARN" -Message " Tag $($SharedSettings.Tag) not found on remote. Pushing..."
|
||||||
|
Push-TagToRemote -Tag $SharedSettings.Tag -Remote "origin"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "OK" -Message " Tag exists on remote."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $SharedSettings.PSObject.Properties['ReleaseDir'] -or [string]::IsNullOrWhiteSpace([string]$SharedSettings.ReleaseDir)) {
|
||||||
|
$SharedSettings | Add-Member -NotePropertyName ReleaseDir -NotePropertyValue $ArtifactsDirectory -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-EngineContext {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$Plugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ScriptDir,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$UtilsDir
|
||||||
|
)
|
||||||
|
|
||||||
|
$dotNetContext = New-DotNetReleaseContext -Plugins $Plugins -ScriptDir $ScriptDir
|
||||||
|
|
||||||
|
$currentBranch = Get-CurrentBranch
|
||||||
|
$releaseBranches = @(
|
||||||
|
$Plugins |
|
||||||
|
Where-Object { Test-IsPublishPlugin -Plugin $_ } |
|
||||||
|
ForEach-Object { Get-PluginBranches -Plugin $_ } |
|
||||||
|
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||||
|
Select-Object -Unique
|
||||||
|
)
|
||||||
|
|
||||||
|
$isReleaseBranch = $releaseBranches -contains $currentBranch
|
||||||
|
$isNonReleaseBranch = -not $isReleaseBranch
|
||||||
|
|
||||||
|
Assert-WorkingTreeClean -IsReleaseBranch:$isReleaseBranch
|
||||||
|
|
||||||
|
$version = $dotNetContext.Version
|
||||||
|
|
||||||
|
if ($isReleaseBranch) {
|
||||||
|
$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 the project version ($version)."
|
||||||
|
Write-Log -Level "WARN" -Message " Either update the tag or the project version."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Tag found: $tag (matches project version)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$tag = "v$version"
|
||||||
|
Write-Log -Level "INFO" -Message " Using version from the package project (no tag required on non-release branches)."
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
ScriptDir = $ScriptDir
|
||||||
|
UtilsDir = $UtilsDir
|
||||||
|
CurrentBranch = $currentBranch
|
||||||
|
Version = $version
|
||||||
|
Tag = $tag
|
||||||
|
ProjectFiles = $dotNetContext.ProjectFiles
|
||||||
|
ArtifactsDirectory = $dotNetContext.ArtifactsDirectory
|
||||||
|
IsReleaseBranch = $isReleaseBranch
|
||||||
|
IsNonReleaseBranch = $isNonReleaseBranch
|
||||||
|
ReleaseBranches = $releaseBranches
|
||||||
|
NonReleaseBranches = @()
|
||||||
|
PublishCompleted = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PreferredReleaseBranch {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$EngineContext
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($EngineContext.ReleaseBranches.Count -gt 0) {
|
||||||
|
return $EngineContext.ReleaseBranches[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return "main"
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Assert-WorkingTreeClean, Initialize-ReleaseStageContext, New-EngineContext, Get-PreferredReleaseBranch
|
||||||
368
utils/Release-Package/PluginSupport.psm1
Normal file
368
utils/Release-Package/PluginSupport.psm1
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
|
||||||
|
$loggingModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Logging.psm1"
|
||||||
|
if (Test-Path $loggingModulePath -PathType Leaf) {
|
||||||
|
Import-Module $loggingModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Import-PluginDependency {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ModuleName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$RequiredCommand
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Get-Command $RequiredCommand -ErrorAction SilentlyContinue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$moduleRoot = Split-Path $PSScriptRoot -Parent
|
||||||
|
$modulePath = Join-Path $moduleRoot "$ModuleName.psm1"
|
||||||
|
if (Test-Path $modulePath -PathType Leaf) {
|
||||||
|
# Import into the global session so the calling plugin can see the exported commands.
|
||||||
|
# Importing only into this module's scope would make the dependency invisible to the plugin.
|
||||||
|
Import-Module $modulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-Command $RequiredCommand -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "Required command '$RequiredCommand' is still unavailable after importing module '$ModuleName'."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ConfiguredPlugins {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Settings.PSObject.Properties['Plugins'] -or $null -eq $Settings.Plugins) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
# JSON can deserialize a single plugin as one object or multiple plugins as an array.
|
||||||
|
# Always return an array so the engine can loop without special-case logic.
|
||||||
|
if ($Settings.Plugins -is [System.Collections.IEnumerable] -and -not ($Settings.Plugins -is [string])) {
|
||||||
|
return @($Settings.Plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($Settings.Plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PluginStage {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Plugin.PSObject.Properties['Stage'] -or [string]::IsNullOrWhiteSpace([string]$Plugin.Stage)) {
|
||||||
|
return "Release"
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$Plugin.Stage
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PluginBranches {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Plugin.PSObject.Properties['branches'] -or $null -eq $Plugin.branches) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Strings are also IEnumerable in PowerShell, so exclude them or we would split into characters.
|
||||||
|
if ($Plugin.branches -is [System.Collections.IEnumerable] -and -not ($Plugin.branches -is [string])) {
|
||||||
|
return @($Plugin.branches | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$Plugin.branches)) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
return @([string]$Plugin.branches)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-IsPublishPlugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Plugin -or [string]::IsNullOrWhiteSpace([string]$Plugin.Name)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return @('GitHub', 'NuGet') -contains ([string]$Plugin.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PluginSettingValue {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$Plugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PropertyName
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($plugin in $Plugins) {
|
||||||
|
if ($null -eq $plugin -or [string]::IsNullOrWhiteSpace($plugin.Name)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $plugin.PSObject.Properties[$PropertyName]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $plugin.$PropertyName
|
||||||
|
if ($null -eq $value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value -is [string] -and [string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PluginPathListSetting {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$Plugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PropertyName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$BasePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$rawPaths = @()
|
||||||
|
$value = Get-PluginSettingValue -Plugins $Plugins -PropertyName $PropertyName
|
||||||
|
|
||||||
|
if ($null -eq $value) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Same rule as above: treat a string as one path, not a char-by-char sequence.
|
||||||
|
if ($value -is [System.Collections.IEnumerable] -and -not ($value -is [string])) {
|
||||||
|
$rawPaths += $value
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$rawPaths += $value
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedPaths = @()
|
||||||
|
foreach ($path in $rawPaths) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$path)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedPaths += [System.IO.Path]::GetFullPath((Join-Path $BasePath ([string]$path)))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wrap again to stop PowerShell from unrolling a single-item array into a bare string.
|
||||||
|
return @($resolvedPaths)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PluginPathSetting {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$Plugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PropertyName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$BasePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$value = Get-PluginSettingValue -Plugins $Plugins -PropertyName $PropertyName
|
||||||
|
if ($null -eq $value -or [string]::IsNullOrWhiteSpace([string]$value)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return [System.IO.Path]::GetFullPath((Join-Path $BasePath ([string]$value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ArchiveNamePattern {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$Plugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$CurrentBranch
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($plugin in $Plugins) {
|
||||||
|
if ($null -eq $plugin -or [string]::IsNullOrWhiteSpace($plugin.Name)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $plugin.Enabled) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedBranches = Get-PluginBranches -Plugin $plugin
|
||||||
|
if ($allowedBranches.Count -gt 0 -and -not ($allowedBranches -contains $CurrentBranch)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($plugin.PSObject.Properties['zipNamePattern'] -and -not [string]::IsNullOrWhiteSpace([string]$plugin.zipNamePattern)) {
|
||||||
|
return [string]$plugin.zipNamePattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "release-{version}.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-PluginModulePath {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PluginsDirectory
|
||||||
|
)
|
||||||
|
|
||||||
|
$pluginFileName = "{0}.psm1" -f $Plugin.Name
|
||||||
|
$candidatePaths = @(
|
||||||
|
(Join-Path $PluginsDirectory $pluginFileName),
|
||||||
|
(Join-Path (Join-Path (Split-Path $PluginsDirectory -Parent) "CustomPlugins") $pluginFileName)
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($candidatePath in $candidatePaths) {
|
||||||
|
if (Test-Path $candidatePath -PathType Leaf) {
|
||||||
|
return $candidatePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $candidatePaths[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-PluginRunnable {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$SharedSettings,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PluginsDirectory,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[bool]$WriteLogs = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Plugin -or [string]::IsNullOrWhiteSpace($Plugin.Name)) {
|
||||||
|
if ($WriteLogs) {
|
||||||
|
Write-Log -Level "WARN" -Message "Skipping plugin entry with no Name."
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $Plugin.Enabled) {
|
||||||
|
if ($WriteLogs) {
|
||||||
|
Write-Log -Level "WARN" -Message "Skipping plugin '$($Plugin.Name)' (disabled)."
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-IsPublishPlugin -Plugin $Plugin) {
|
||||||
|
$allowedBranches = Get-PluginBranches -Plugin $Plugin
|
||||||
|
if ($allowedBranches.Count -eq 0) {
|
||||||
|
if ($WriteLogs) {
|
||||||
|
Write-Log -Level "WARN" -Message "Skipping plugin '$($Plugin.Name)' because no publish branches are configured."
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not ($allowedBranches -contains $SharedSettings.CurrentBranch)) {
|
||||||
|
if ($WriteLogs) {
|
||||||
|
Write-Log -Level "WARN" -Message "Skipping plugin '$($Plugin.Name)' on branch '$($SharedSettings.CurrentBranch)'."
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -PluginsDirectory $PluginsDirectory
|
||||||
|
if (-not (Test-Path $pluginModulePath -PathType Leaf)) {
|
||||||
|
if ($WriteLogs) {
|
||||||
|
Write-Log -Level "ERROR" -Message "Plugin module not found: $pluginModulePath"
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-PluginInvocationSettings {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$SharedSettings
|
||||||
|
)
|
||||||
|
|
||||||
|
$properties = @{}
|
||||||
|
foreach ($property in $Plugin.PSObject.Properties) {
|
||||||
|
$properties[$property.Name] = $property.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
# Plugins receive their own config plus a shared Context object that carries runtime artifacts.
|
||||||
|
$properties['Context'] = $SharedSettings
|
||||||
|
return [pscustomobject]$properties
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-ConfiguredPlugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$SharedSettings,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PluginsDirectory,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[bool]$ContinueOnError = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-PluginRunnable -Plugin $Plugin -SharedSettings $SharedSettings -PluginsDirectory $PluginsDirectory -WriteLogs:$true)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -PluginsDirectory $PluginsDirectory
|
||||||
|
Write-Log -Level "STEP" -Message "Running plugin '$($Plugin.Name)'..."
|
||||||
|
|
||||||
|
try {
|
||||||
|
$moduleInfo = Import-Module $pluginModulePath -Force -PassThru -ErrorAction Stop
|
||||||
|
# Resolve Invoke-Plugin from the imported module explicitly so we call the plugin we just loaded,
|
||||||
|
# not some command with the same name from another module already in session.
|
||||||
|
$invokeCommand = Get-Command -Name "Invoke-Plugin" -Module $moduleInfo.Name -ErrorAction Stop
|
||||||
|
$pluginSettings = New-PluginInvocationSettings -Plugin $Plugin -SharedSettings $SharedSettings
|
||||||
|
|
||||||
|
& $invokeCommand -Settings $pluginSettings
|
||||||
|
Write-Log -Level "OK" -Message " Plugin '$($Plugin.Name)' completed."
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Log -Level "ERROR" -Message " Plugin '$($Plugin.Name)' failed: $($_.Exception.Message)"
|
||||||
|
if (-not $ContinueOnError) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Import-PluginDependency, Get-ConfiguredPlugins, Get-PluginStage, Get-PluginBranches, Test-IsPublishPlugin, Get-PluginSettingValue, Get-PluginPathListSetting, Get-PluginPathSetting, Get-ArchiveNamePattern, Resolve-PluginModulePath, Test-PluginRunnable, New-PluginInvocationSettings, Invoke-ConfiguredPlugin
|
||||||
3
utils/Release-Package/Release-Package.bat
Normal file
3
utils/Release-Package/Release-Package.bat
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Release-Package.ps1"
|
||||||
|
pause
|
||||||
182
utils/Release-Package/Release-Package.ps1
Normal file
182
utils/Release-Package/Release-Package.ps1
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Plugin-driven release engine.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This script is the orchestration layer for release automation.
|
||||||
|
It loads scriptsettings.json, evaluates the configured plugins in order,
|
||||||
|
builds shared execution context, and invokes each plugin's Invoke-Plugin
|
||||||
|
entrypoint with that plugin's own settings object plus runtime context.
|
||||||
|
|
||||||
|
The engine is intentionally generic:
|
||||||
|
- It does not embed release-provider-specific logic
|
||||||
|
- It preserves plugin execution order from scriptsettings.json
|
||||||
|
- It isolates plugin failures according to the stage/runtime policy
|
||||||
|
- It keeps shared orchestration helpers in dedicated support modules
|
||||||
|
|
||||||
|
.REQUIREMENTS
|
||||||
|
Tools (Required):
|
||||||
|
- Shared support modules required by the engine
|
||||||
|
- Any commands required by configured plugins or support helpers
|
||||||
|
|
||||||
|
.WORKFLOW
|
||||||
|
1. Load and normalize plugin configuration
|
||||||
|
2. Determine branch mode from configured plugin metadata
|
||||||
|
3. Validate repository state and resolve the release version
|
||||||
|
4. Build shared execution context
|
||||||
|
5. Execute plugins one by one in configured order
|
||||||
|
6. Initialize release-stage shared artifacts only when needed
|
||||||
|
7. Report completion summary
|
||||||
|
|
||||||
|
.USAGE
|
||||||
|
Configure plugin order and plugin settings in scriptsettings.json, then run:
|
||||||
|
pwsh -File .\Release-Package.ps1
|
||||||
|
|
||||||
|
.CONFIGURATION
|
||||||
|
All settings are stored in scriptsettings.json:
|
||||||
|
- Plugins: Ordered plugin definitions and plugin-specific settings
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
Plugin-specific behavior belongs in the plugin modules, not in this engine.
|
||||||
|
#>
|
||||||
|
|
||||||
|
# No parameters - behavior is controlled by configured plugin metadata:
|
||||||
|
# - non-release branches -> Run only the plugins allowed for those branches
|
||||||
|
# - release branches -> Require a matching tag and allow release-stage plugins
|
||||||
|
|
||||||
|
# Get the directory of the current script (for loading settings and relative paths)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
|
||||||
|
#region Import Modules
|
||||||
|
|
||||||
|
$utilsDir = Split-Path $scriptDir -Parent
|
||||||
|
|
||||||
|
# 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 PluginSupport module
|
||||||
|
$pluginSupportModulePath = Join-Path $scriptDir "PluginSupport.psm1"
|
||||||
|
if (-not (Test-Path $pluginSupportModulePath)) {
|
||||||
|
Write-Error "PluginSupport module not found at: $pluginSupportModulePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module $pluginSupportModulePath -Force
|
||||||
|
|
||||||
|
# Import DotNetProjectSupport module
|
||||||
|
$dotNetProjectSupportModulePath = Join-Path $scriptDir "DotNetProjectSupport.psm1"
|
||||||
|
if (-not (Test-Path $dotNetProjectSupportModulePath)) {
|
||||||
|
Write-Error "DotNetProjectSupport module not found at: $dotNetProjectSupportModulePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module $dotNetProjectSupportModulePath -Force
|
||||||
|
|
||||||
|
# Import EngineSupport module
|
||||||
|
$engineSupportModulePath = Join-Path $scriptDir "EngineSupport.psm1"
|
||||||
|
if (-not (Test-Path $engineSupportModulePath)) {
|
||||||
|
Write-Error "EngineSupport module not found at: $engineSupportModulePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module $engineSupportModulePath -Force
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Load Settings
|
||||||
|
$settings = Get-ScriptSettings -ScriptDir $scriptDir
|
||||||
|
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Configuration
|
||||||
|
|
||||||
|
$pluginsDir = Join-Path $scriptDir "CorePlugins"
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Main
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "=================================================="
|
||||||
|
Write-Log -Level "STEP" -Message "RELEASE ENGINE"
|
||||||
|
Write-Log -Level "STEP" -Message "=================================================="
|
||||||
|
|
||||||
|
#region Preflight
|
||||||
|
|
||||||
|
$plugins = $configuredPlugins
|
||||||
|
$engineContext = New-EngineContext -Plugins $plugins -ScriptDir $scriptDir -UtilsDir $utilsDir
|
||||||
|
Write-Log -Level "OK" -Message "All pre-flight checks passed!"
|
||||||
|
$sharedPluginSettings = $engineContext
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Plugin Execution
|
||||||
|
|
||||||
|
$releaseStageInitialized = $false
|
||||||
|
|
||||||
|
if ($plugins.Count -eq 0) {
|
||||||
|
Write-Log -Level "WARN" -Message "No plugins configured in scriptsettings.json."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for ($pluginIndex = 0; $pluginIndex -lt $plugins.Count; $pluginIndex++) {
|
||||||
|
$plugin = $plugins[$pluginIndex]
|
||||||
|
$pluginStage = Get-PluginStage -Plugin $plugin
|
||||||
|
|
||||||
|
if ((Test-IsPublishPlugin -Plugin $plugin) -and -not $releaseStageInitialized) {
|
||||||
|
if (Test-PluginRunnable -Plugin $plugin -SharedSettings $sharedPluginSettings -PluginsDirectory $pluginsDir -WriteLogs:$false) {
|
||||||
|
$remainingPlugins = @($plugins[$pluginIndex..($plugins.Count - 1)])
|
||||||
|
Initialize-ReleaseStageContext -RemainingPlugins $remainingPlugins -SharedSettings $sharedPluginSettings -ArtifactsDirectory $engineContext.ArtifactsDirectory -Version $engineContext.Version
|
||||||
|
$releaseStageInitialized = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$continueOnError = $pluginStage -eq "Release"
|
||||||
|
Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -PluginsDirectory $pluginsDir -ContinueOnError:$continueOnError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $releaseStageInitialized) {
|
||||||
|
Write-Log -Level "WARN" -Message "No release plugins executed for branch '$($engineContext.CurrentBranch)'."
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Summary
|
||||||
|
Write-Log -Level "OK" -Message "=================================================="
|
||||||
|
if ($engineContext.IsNonReleaseBranch) {
|
||||||
|
Write-Log -Level "OK" -Message "NON-RELEASE RUN COMPLETE"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "OK" -Message "RELEASE COMPLETE"
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "=================================================="
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message "Artifacts location: $($engineContext.ArtifactsDirectory)"
|
||||||
|
|
||||||
|
if ($engineContext.IsNonReleaseBranch) {
|
||||||
|
$preferredReleaseBranch = Get-PreferredReleaseBranch -EngineContext $engineContext
|
||||||
|
Write-Log -Level "WARN" -Message "To execute release-stage plugins, rerun from an allowed release branch such as '$preferredReleaseBranch'."
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#endregion
|
||||||
92
utils/Release-Package/scriptsettings.json
Normal file
92
utils/Release-Package/scriptsettings.json
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft-07/schema",
|
||||||
|
"title": "Release Package Script Settings",
|
||||||
|
"description": "Configuration file for Release-Package.ps1 script.",
|
||||||
|
"Plugins": [
|
||||||
|
{
|
||||||
|
"Name": "DotNetTest",
|
||||||
|
"Stage": "Test",
|
||||||
|
"Enabled": true,
|
||||||
|
"project": "..\\..\\src\\MaksIT.Dapr.Tests",
|
||||||
|
"resultsDir": "..\\..\\testResults"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "QualityGate",
|
||||||
|
"Stage": "QualityGate",
|
||||||
|
"Enabled": true,
|
||||||
|
"coverageThreshold": 0,
|
||||||
|
"failOnVulnerabilities": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "DotNetPack",
|
||||||
|
"Stage": "Build",
|
||||||
|
"Enabled": true,
|
||||||
|
"projectFiles": [
|
||||||
|
"..\\..\\src\\MaksIT.Dapr\\MaksIT.Dapr.csproj"
|
||||||
|
],
|
||||||
|
"artifactsDir": "..\\..\\release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "CreateArchive",
|
||||||
|
"Stage": "Build",
|
||||||
|
"Enabled": true,
|
||||||
|
"zipNamePattern": "maksit.dapr-{version}.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "GitHub",
|
||||||
|
"Stage": "Release",
|
||||||
|
"Enabled": true,
|
||||||
|
"branches": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"githubToken": "GITHUB_MAKS_IT_COM",
|
||||||
|
"repository": "https://github.com/MAKS-IT-COM/maksit-dapr",
|
||||||
|
"releaseNotesFile": "..\\..\\CHANGELOG.md",
|
||||||
|
"releaseTitlePattern": "Release {version}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "NuGet",
|
||||||
|
"Stage": "Release",
|
||||||
|
"Enabled": true,
|
||||||
|
"branches": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"nugetApiKey": "NUGET_MAKS_IT",
|
||||||
|
"source": "https://api.nuget.org/v3/index.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "CleanupArtifacts",
|
||||||
|
"Stage": "Release",
|
||||||
|
"Enabled": true,
|
||||||
|
"includePatterns": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"excludePatterns": [
|
||||||
|
"*.zip"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"_comments": {
|
||||||
|
"Plugins": {
|
||||||
|
"Name": "Plugin module file name in CorePlugins (for example, DotNetPack -> CorePlugins/DotNetPack.psm1).",
|
||||||
|
"Stage": "Execution phase. Supported values are Test, QualityGate, Build, and Release.",
|
||||||
|
"Enabled": "If true, the plugin is imported and Invoke-Plugin is called in the configured order.",
|
||||||
|
"branches": "Used only by publish plugins such as GitHub and NuGet. When the current branch is not listed, publishing is skipped.",
|
||||||
|
"project": "DotNetTest plugin only. Path to the test project directory, relative to the script folder.",
|
||||||
|
"resultsDir": "DotNetTest plugin only. Optional results directory path, relative to the script folder.",
|
||||||
|
"projectFiles": "DotNetPack, DotNetPublish, or another producer plugin can define the project files used for version discovery and artifact creation.",
|
||||||
|
"artifactsDir": "DotNetPack, DotNetPublish, or another producer plugin can define the artifacts output directory, relative to the script folder.",
|
||||||
|
"coverageThreshold": "QualityGate plugin only. Coverage threshold percent (0 disables threshold check).",
|
||||||
|
"failOnVulnerabilities": "QualityGate plugin only. If true, fail when vulnerable packages are detected.",
|
||||||
|
"githubToken": "GitHub plugin only. Environment variable name containing the GitHub token used by gh CLI.",
|
||||||
|
"repository": "GitHub plugin only. Optional owner/repo or GitHub remote URL. Leave empty to use remote.origin.url.",
|
||||||
|
"releaseNotesFile": "GitHub plugin (or another notes consumer plugin) can define the release notes source file, relative to the script folder.",
|
||||||
|
"releaseTitlePattern": "GitHub plugin only. Release title pattern. Supports {version} placeholder.",
|
||||||
|
"zipNamePattern": "CreateArchive plugin only. Archive name pattern for packaged release assets. Supports {version} placeholder.",
|
||||||
|
"nugetApiKey": "NuGet plugin only. Environment variable name containing the NuGet API key.",
|
||||||
|
"source": "NuGet plugin only. Feed URL passed to dotnet nuget push.",
|
||||||
|
"includePatterns": "CleanupArtifacts plugin only. File patterns to remove from artifactsDir (for example ['*.nupkg','*.snupkg']).",
|
||||||
|
"excludePatterns": "CleanupArtifacts plugin only. File patterns to keep even when includePatterns match (for example ['*.zip'])."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
function Get-ScriptSettings {
|
function Get-ScriptSettings {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
PowerShell module for running tests with code coverage.
|
PowerShell module for running tests with code coverage.
|
||||||
@ -8,7 +11,7 @@
|
|||||||
|
|
||||||
.NOTES
|
.NOTES
|
||||||
Author: MaksIT
|
Author: MaksIT
|
||||||
Usage: Import-Module .\TestRunner.psm1
|
Usage: pwsh -Command "Import-Module .\TestRunner.psm1"
|
||||||
#>
|
#>
|
||||||
|
|
||||||
function Import-LoggingModuleInternal {
|
function Import-LoggingModuleInternal {
|
||||||
|
|||||||
3
utils/Update-RepoUtils/Update-RepoUtils.bat
Normal file
3
utils/Update-RepoUtils/Update-RepoUtils.bat
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Update-RepoUtils.ps1"
|
||||||
|
pause
|
||||||
325
utils/Update-RepoUtils/Update-RepoUtils.ps1
Normal file
325
utils/Update-RepoUtils/Update-RepoUtils.ps1
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Refreshes a local maksit-repoutils copy from GitHub.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This script clones the configured repository into a temporary directory,
|
||||||
|
refreshes the parent directory of this script, preserves existing
|
||||||
|
scriptsettings.json files in subfolders, and copies the cloned source
|
||||||
|
contents into that parent directory.
|
||||||
|
|
||||||
|
All configuration is stored in scriptsettings.json.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
pwsh -File .\Update-RepoUtils.ps1
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
CONFIGURATION (scriptsettings.json):
|
||||||
|
- dryRun: If true, logs the planned update without modifying files
|
||||||
|
- repository.url: Git repository to clone
|
||||||
|
- repository.sourceSubdirectory: Folder copied into the target directory
|
||||||
|
- repository.preserveFileName: Existing file name to preserve in subfolders
|
||||||
|
- repository.cloneDepth: Depth used for git clone
|
||||||
|
- repository.skippedRelativeDirectories: Relative directories to exclude from phase-two refresh
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[switch]$ContinueAfterSelfUpdate,
|
||||||
|
[string]$TargetDirectoryOverride,
|
||||||
|
[string]$ClonedSourceDirectoryOverride,
|
||||||
|
[string]$TemporaryRootOverride
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Get the directory of the current script (for loading settings and relative paths)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$utilsDir = Split-Path $scriptDir -Parent
|
||||||
|
|
||||||
|
# Refresh the parent directory that contains the shared modules and sibling tools.
|
||||||
|
$targetDirectory = if ([string]::IsNullOrWhiteSpace($TargetDirectoryOverride)) {
|
||||||
|
Split-Path $scriptDir -Parent
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[System.IO.Path]::GetFullPath($TargetDirectoryOverride)
|
||||||
|
}
|
||||||
|
$currentScriptPath = [System.IO.Path]::GetFullPath($MyInvocation.MyCommand.Path)
|
||||||
|
$selfUpdateDirectory = 'Update-RepoUtils'
|
||||||
|
|
||||||
|
#region Import Modules
|
||||||
|
|
||||||
|
$scriptConfigModulePath = Join-Path $utilsDir "ScriptConfig.psm1"
|
||||||
|
if (-not (Test-Path $scriptConfigModulePath)) {
|
||||||
|
Write-Error "ScriptConfig module not found at: $scriptConfigModulePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$loggingModulePath = Join-Path $utilsDir "Logging.psm1"
|
||||||
|
if (-not (Test-Path $loggingModulePath)) {
|
||||||
|
Write-Error "Logging module not found at: $loggingModulePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module $scriptConfigModulePath -Force
|
||||||
|
Import-Module $loggingModulePath -Force
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Load Settings
|
||||||
|
|
||||||
|
$settings = Get-ScriptSettings -ScriptDir $scriptDir
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Configuration
|
||||||
|
|
||||||
|
$repositoryUrl = $settings.repository.url
|
||||||
|
$dryRun = if ($null -ne $settings.dryRun) { [bool]$settings.dryRun } else { $false }
|
||||||
|
$sourceSubdirectory = if ($settings.repository.sourceSubdirectory) { $settings.repository.sourceSubdirectory } else { 'src' }
|
||||||
|
$preserveFileName = if ($settings.repository.preserveFileName) { $settings.repository.preserveFileName } else { 'scriptsettings.json' }
|
||||||
|
$cloneDepth = if ($settings.repository.cloneDepth) { [int]$settings.repository.cloneDepth } else { 1 }
|
||||||
|
$skippedRelativeDirectories = if ($settings.repository.skippedRelativeDirectories) {
|
||||||
|
@(
|
||||||
|
$settings.repository.skippedRelativeDirectories |
|
||||||
|
ForEach-Object {
|
||||||
|
([string]$_).Replace('/', [System.IO.Path]::DirectorySeparatorChar).Replace('\', [System.IO.Path]::DirectorySeparatorChar)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
@([System.IO.Path]::Combine('Release-Package', 'CustomPlugins'))
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Validate CLI Dependencies
|
||||||
|
|
||||||
|
Assert-Command git
|
||||||
|
Assert-Command pwsh
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($repositoryUrl)) {
|
||||||
|
Write-Error "repository.url is required in scriptsettings.json."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Main
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message "========================================"
|
||||||
|
Write-Log -Level "INFO" -Message "Update RepoUtils Script"
|
||||||
|
Write-Log -Level "INFO" -Message "========================================"
|
||||||
|
Write-Log -Level "INFO" -Message "Target directory: $targetDirectory"
|
||||||
|
Write-Log -Level "INFO" -Message "Dry run: $dryRun"
|
||||||
|
|
||||||
|
$ownsTemporaryRoot = [string]::IsNullOrWhiteSpace($TemporaryRootOverride)
|
||||||
|
$temporaryRoot = if ($ownsTemporaryRoot) {
|
||||||
|
Join-Path ([System.IO.Path]::GetTempPath()) ("maksit-repoutils-update-" + [System.Guid]::NewGuid().ToString('N'))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[System.IO.Path]::GetFullPath($TemporaryRootOverride)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$clonedSourceDirectory = if ([string]::IsNullOrWhiteSpace($ClonedSourceDirectoryOverride)) {
|
||||||
|
Write-LogStep "Cloning latest repository snapshot..."
|
||||||
|
& git clone --depth $cloneDepth $repositoryUrl $temporaryRoot
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "git clone failed with exit code $LASTEXITCODE."
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "Repository cloned"
|
||||||
|
|
||||||
|
Join-Path $temporaryRoot $sourceSubdirectory
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[System.IO.Path]::GetFullPath($ClonedSourceDirectoryOverride)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $clonedSourceDirectory -PathType Container)) {
|
||||||
|
throw "The cloned repository does not contain the expected source directory: $clonedSourceDirectory"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $ContinueAfterSelfUpdate) {
|
||||||
|
if ($dryRun) {
|
||||||
|
Write-LogStep "Dry run self-update summary"
|
||||||
|
Write-Log -Level "INFO" -Message "Would refresh shared modules and $selfUpdateDirectory before relaunching the updater"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-LogStep "Refreshing updater files..."
|
||||||
|
$selfUpdateFiles = Get-ChildItem -Path $clonedSourceDirectory -Recurse -Force -File |
|
||||||
|
Where-Object {
|
||||||
|
$relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $_.FullName)
|
||||||
|
$isRootFile = -not $relativePath.Contains([System.IO.Path]::DirectorySeparatorChar)
|
||||||
|
$isUpdaterFile = $relativePath.StartsWith($selfUpdateDirectory + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
|
$_.Name -ne $preserveFileName -and
|
||||||
|
($isRootFile -or $isUpdaterFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($sourceFile in $selfUpdateFiles) {
|
||||||
|
$relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $sourceFile.FullName)
|
||||||
|
$destinationPath = Join-Path $targetDirectory $relativePath
|
||||||
|
$destinationDirectory = Split-Path -Parent $destinationPath
|
||||||
|
if (-not (Test-Path -Path $destinationDirectory -PathType Container)) {
|
||||||
|
New-Item -ItemType Directory -Path $destinationDirectory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -Path $sourceFile.FullName -Destination $destinationPath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message "Updater files refreshed"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
Write-LogStep "Dry run bootstrap completed"
|
||||||
|
Write-Log -Level "INFO" -Message "Continuing with phase two in the current process because no files were changed"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-LogStep "Relaunching the updated updater..."
|
||||||
|
& pwsh -File $currentScriptPath `
|
||||||
|
-ContinueAfterSelfUpdate `
|
||||||
|
-TargetDirectoryOverride $targetDirectory `
|
||||||
|
-ClonedSourceDirectoryOverride $clonedSourceDirectory `
|
||||||
|
-TemporaryRootOverride $temporaryRoot
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Relaunched updater failed with exit code $LASTEXITCODE."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message "Bootstrap phase completed"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$preservedFiles = @()
|
||||||
|
$updatePhaseSkippedDirectories = $skippedRelativeDirectories + $selfUpdateDirectory
|
||||||
|
$existingPreservedFiles = Get-ChildItem -Path $targetDirectory -Recurse -File -Filter $preserveFileName -ErrorAction SilentlyContinue
|
||||||
|
if ($existingPreservedFiles) {
|
||||||
|
foreach ($file in $existingPreservedFiles) {
|
||||||
|
$relativePath = [System.IO.Path]::GetRelativePath($targetDirectory, $file.FullName)
|
||||||
|
$backupPath = Join-Path $temporaryRoot ("preserved-" + ($relativePath -replace '[\\/:*?""<>|]', '_'))
|
||||||
|
$preservedFiles += [pscustomobject]@{
|
||||||
|
RelativePath = $relativePath
|
||||||
|
BackupPath = $backupPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $dryRun) {
|
||||||
|
Copy-Item -Path $file.FullName -Destination $backupPath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "Preserved $($preservedFiles.Count) existing $preserveFileName file(s)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "WARN" -Message "No existing $preserveFileName files found in subfolders"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
Write-LogStep "Dry run summary"
|
||||||
|
Write-Log -Level "INFO" -Message "Would remove all files under target except preserved $preserveFileName files"
|
||||||
|
Write-Log -Level "INFO" -Message "Would skip phase-two refresh for: $($updatePhaseSkippedDirectories -join ', ')"
|
||||||
|
Write-Log -Level "INFO" -Message "Would copy refreshed files from: $clonedSourceDirectory"
|
||||||
|
if ($preservedFiles.Count -gt 0) {
|
||||||
|
$preservedList = ($preservedFiles | ForEach-Object { $_.RelativePath }) -join ", "
|
||||||
|
Write-Log -Level "INFO" -Message "Would restore preserved files: $preservedList"
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "Dry run completed. No files were modified."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-LogStep "Cleaning target directory..."
|
||||||
|
$filesToRemove = Get-ChildItem -Path $targetDirectory -Recurse -Force -File |
|
||||||
|
Where-Object {
|
||||||
|
$relativePath = [System.IO.Path]::GetRelativePath($targetDirectory, $_.FullName)
|
||||||
|
$isInSkippedDirectory = $false
|
||||||
|
foreach ($skippedDirectory in $updatePhaseSkippedDirectories) {
|
||||||
|
if ($relativePath.StartsWith($skippedDirectory + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
$isInSkippedDirectory = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$_.Name -ne $preserveFileName -and
|
||||||
|
-not $isInSkippedDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($file in $filesToRemove) {
|
||||||
|
Remove-Item -Path $file.FullName -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$directoriesToRemove = Get-ChildItem -Path $targetDirectory -Recurse -Force -Directory |
|
||||||
|
Sort-Object { $_.FullName.Length } -Descending
|
||||||
|
|
||||||
|
foreach ($directory in $directoriesToRemove) {
|
||||||
|
$remainingItems = Get-ChildItem -Path $directory.FullName -Force -ErrorAction SilentlyContinue
|
||||||
|
if (-not $remainingItems) {
|
||||||
|
Remove-Item -Path $directory.FullName -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "Target directory cleaned"
|
||||||
|
|
||||||
|
Write-LogStep "Copying refreshed source files..."
|
||||||
|
$sourceFilesToCopy = Get-ChildItem -Path $clonedSourceDirectory -Recurse -Force -File |
|
||||||
|
Where-Object {
|
||||||
|
$relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $_.FullName)
|
||||||
|
$isInSkippedDirectory = $false
|
||||||
|
foreach ($skippedDirectory in $updatePhaseSkippedDirectories) {
|
||||||
|
if ($relativePath.StartsWith($skippedDirectory + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
$isInSkippedDirectory = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-not $isInSkippedDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($sourceFile in $sourceFilesToCopy) {
|
||||||
|
$relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $sourceFile.FullName)
|
||||||
|
$destinationPath = Join-Path $targetDirectory $relativePath
|
||||||
|
$destinationDirectory = Split-Path -Parent $destinationPath
|
||||||
|
if (-not (Test-Path -Path $destinationDirectory -PathType Container)) {
|
||||||
|
New-Item -ItemType Directory -Path $destinationDirectory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -Path $sourceFile.FullName -Destination $destinationPath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($skippedDirectory in $updatePhaseSkippedDirectories) {
|
||||||
|
$skippedSourcePath = Join-Path $clonedSourceDirectory $skippedDirectory
|
||||||
|
if (Test-Path -Path $skippedSourcePath) {
|
||||||
|
Write-Log -Level "INFO" -Message "Skipped refresh for $skippedDirectory"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "Source files copied"
|
||||||
|
|
||||||
|
if ($preservedFiles.Count -gt 0) {
|
||||||
|
foreach ($preservedFile in $preservedFiles) {
|
||||||
|
if (-not (Test-Path -Path $preservedFile.BackupPath -PathType Leaf)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$restorePath = Join-Path $targetDirectory $preservedFile.RelativePath
|
||||||
|
$restoreDirectory = Split-Path -Parent $restorePath
|
||||||
|
if (-not (Test-Path -Path $restoreDirectory -PathType Container)) {
|
||||||
|
New-Item -ItemType Directory -Path $restoreDirectory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -Path $preservedFile.BackupPath -Destination $restorePath -Force
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "$preserveFileName files restored"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message "========================================"
|
||||||
|
Write-Log -Level "OK" -Message "Update completed successfully!"
|
||||||
|
Write-Log -Level "OK" -Message "========================================"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if ($ownsTemporaryRoot -and (Test-Path -Path $temporaryRoot)) {
|
||||||
|
Remove-Item -Path $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
15
utils/Update-RepoUtils/scriptsettings.json
Normal file
15
utils/Update-RepoUtils/scriptsettings.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft-07/schema",
|
||||||
|
"title": "Update RepoUtils Script Settings",
|
||||||
|
"description": "Configuration for the Update-RepoUtils utility.",
|
||||||
|
"dryRun": true,
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/MAKS-IT-COM/maksit-repoutils.git",
|
||||||
|
"sourceSubdirectory": "src",
|
||||||
|
"preserveFileName": "scriptsettings.json",
|
||||||
|
"cloneDepth": 1,
|
||||||
|
"skippedRelativeDirectories": [
|
||||||
|
"Release-Package/CustomPlugins"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user