diff --git a/Public/Invoke-DotNetReleaseBuild.ps1 b/Public/Invoke-DotNetReleaseBuild.ps1 new file mode 100644 index 00000000..56edfc54 --- /dev/null +++ b/Public/Invoke-DotNetReleaseBuild.ps1 @@ -0,0 +1,122 @@ +function Invoke-DotNetReleaseBuild { + <# + .SYNOPSIS + Builds a .NET project in Release configuration and prepares release artefacts. + + .DESCRIPTION + Wrapper around the build, pack and signing process typically used for publishing + .NET projects. The function cleans the Release directory, builds the project, + signs DLLs and NuGet packages when a certificate is provided, compresses the + build output and returns details about the generated files. + + .PARAMETER ProjectPath + Path to the folder containing the project (*.csproj) file. + + .PARAMETER CertificateThumbprint + Optional certificate thumbprint used to sign the built assemblies and NuGet + packages. When omitted no signing is performed. + + .PARAMETER LocalStore + Certificate store used when searching for the signing certificate. Defaults + to 'CurrentUser'. + + .PARAMETER TimeStampServer + Timestamp server URL used while signing. + + .OUTPUTS + PSCustomObject with properties Version, ReleasePath and ZipPath. + + .EXAMPLE + Invoke-DotNetReleaseBuild -ProjectPath 'C:\Git\MyProject' -CertificateThumbprint '483292C9E317AA13B07BB7A96AE9D1A5ED9E7703' + Builds and signs the project located in C:\Git\MyProject and returns paths to + the release output. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$ProjectPath, + [Parameter()] + [string]$CertificateThumbprint, + [string]$LocalStore = 'CurrentUser', + [string]$TimeStampServer = 'http://timestamp.digicert.com' + ) + $result = [ordered]@{ + Success = $false + Version = $null + ReleasePath = $null + ZipPath = $null + Packages = @() + ErrorMessage = $null + } + + if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) { + $result.ErrorMessage = 'dotnet CLI is not available.' + return [PSCustomObject]$result + } + if (-not (Test-Path -LiteralPath $ProjectPath)) { + $result.ErrorMessage = "Project path '$ProjectPath' not found." + return [PSCustomObject]$result + } + $csproj = Get-ChildItem -Path $ProjectPath -Filter '*.csproj' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $csproj) { + $result.ErrorMessage = "No csproj found in $ProjectPath" + return [PSCustomObject]$result + } + try { + [xml]$xml = Get-Content -LiteralPath $csproj.FullName -Raw -ErrorAction Stop + } catch { + $result.ErrorMessage = "Failed to read '$($csproj.FullName)' as XML: $_" + return [PSCustomObject]$result + } + $version = $xml.Project.PropertyGroup.VersionPrefix + if (-not $version) { + $result.ErrorMessage = "VersionPrefix not found in '$($csproj.FullName)'" + return [PSCustomObject]$result + } + $releasePath = Join-Path -Path $csproj.Directory.FullName -ChildPath 'bin/Release' + if (Test-Path -LiteralPath $releasePath) { + try { + Get-ChildItem -Path $releasePath -Recurse -File | Remove-Item -Force + Get-ChildItem -Path $releasePath -Recurse -Filter '*.nupkg' | Remove-Item -Force + Get-ChildItem -Path $releasePath -Directory | Remove-Item -Force -Recurse + } catch { + $result.ErrorMessage = "Failed to clean $releasePath: $_" + return [PSCustomObject]$result + } + } else { + $null = New-Item -ItemType Directory -Path $releasePath -Force + } + + dotnet build $csproj.FullName --configuration Release + if ($LASTEXITCODE -ne 0) { + $result.ErrorMessage = 'dotnet build failed.' + return [PSCustomObject]$result + } + if ($CertificateThumbprint) { + Register-Certificate -Path $releasePath -LocalStore $LocalStore -Include @('*.dll') -TimeStampServer $TimeStampServer -Thumbprint $CertificateThumbprint + } + $zipPath = Join-Path -Path $releasePath -ChildPath ("{0}.{1}.zip" -f $csproj.BaseName, $version) + Compress-Archive -Path (Join-Path $releasePath '*') -DestinationPath $zipPath -Force + + dotnet pack $csproj.FullName --configuration Release --no-restore --no-build + if ($LASTEXITCODE -ne 0) { + $result.ErrorMessage = 'dotnet pack failed.' + return [PSCustomObject]$result + } + $nupkgs = Get-ChildItem -Path $releasePath -Recurse -Filter '*.nupkg' -ErrorAction SilentlyContinue + if ($CertificateThumbprint -and $nupkgs) { + foreach ($pkg in $nupkgs) { + dotnet nuget sign $pkg.FullName --certificate-fingerprint $CertificateThumbprint --timestamper $TimeStampServer --overwrite + if ($LASTEXITCODE -ne 0) { + Write-Warning "Invoke-DotNetReleaseBuild - Failed to sign $($pkg.FullName)" + } + } + } + $result.Success = $true + $result.Version = $version + $result.ReleasePath = $releasePath + $result.ZipPath = $zipPath + $result.Packages = $nupkgs.FullName + return [PSCustomObject]$result +} diff --git a/Public/Publish-GitHubReleaseAsset.ps1 b/Public/Publish-GitHubReleaseAsset.ps1 new file mode 100644 index 00000000..8d6040e7 --- /dev/null +++ b/Public/Publish-GitHubReleaseAsset.ps1 @@ -0,0 +1,92 @@ +function Publish-GitHubReleaseAsset { + <# + .SYNOPSIS + Publishes a release asset to GitHub. + + .DESCRIPTION + Uses `Send-GitHubRelease` to create or update a GitHub release based on the + project version and upload the generated zip archive. + + .PARAMETER ProjectPath + Path to the project folder containing the *.csproj file. + + .PARAMETER GitHubUsername + GitHub account name owning the repository. + + .PARAMETER GitHubRepositoryName + Name of the GitHub repository. + + .PARAMETER GitHubAccessToken + Personal access token used for authentication. + + .PARAMETER IsPreRelease + Publish the release as a pre-release. + + .EXAMPLE + Publish-GitHubReleaseAsset -ProjectPath 'C:\Git\MyProject' -GitHubUsername 'EvotecIT' -GitHubRepositoryName 'MyRepo' -GitHubAccessToken $Token + Uploads the current project zip to the specified GitHub repository. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$ProjectPath, + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$GitHubUsername, + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$GitHubRepositoryName, + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$GitHubAccessToken, + [switch]$IsPreRelease + ) + $result = [ordered]@{ + Success = $false + TagName = $null + ZipPath = $null + ReleaseUrl = $null + ErrorMessage = $null + } + + if (-not (Test-Path -LiteralPath $ProjectPath)) { + $result.ErrorMessage = "Project path '$ProjectPath' not found." + return [PSCustomObject]$result + } + $csproj = Get-ChildItem -Path $ProjectPath -Filter '*.csproj' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $csproj) { + $result.ErrorMessage = "No csproj found in $ProjectPath" + return [PSCustomObject]$result + } + try { + [xml]$xml = Get-Content -LiteralPath $csproj.FullName -Raw -ErrorAction Stop + } catch { + $result.ErrorMessage = "Failed to read '$($csproj.FullName)' as XML: $_" + return [PSCustomObject]$result + } + $version = $xml.Project.PropertyGroup.VersionPrefix + if (-not $version) { + $result.ErrorMessage = "VersionPrefix not found in '$($csproj.FullName)'" + return [PSCustomObject]$result + } + $zipPath = Join-Path -Path $csproj.Directory.FullName -ChildPath ("bin/Release/{0}.{1}.zip" -f $csproj.BaseName, $version) + if (-not (Test-Path -LiteralPath $zipPath)) { + $result.ErrorMessage = "Zip file '$zipPath' not found." + return [PSCustomObject]$result + } + $tagName = "v$version" + $result.TagName = $tagName + $result.ZipPath = $zipPath + try { + $statusGithub = Send-GitHubRelease -GitHubUsername $GitHubUsername -GitHubRepositoryName $GitHubRepositoryName -GitHubAccessToken $GitHubAccessToken -TagName $tagName -AssetFilePaths $zipPath -IsPreRelease:$IsPreRelease.IsPresent + $result.Success = $statusGithub.Succeeded + $result.ReleaseUrl = $statusGithub.ReleaseUrl + if (-not $statusGithub.Succeeded) { + $result.ErrorMessage = $statusGithub.ErrorMessage + } + } catch { + $result.ErrorMessage = $_.Exception.Message + } + return [PSCustomObject]$result +} diff --git a/Public/Publish-NugetPackage.ps1 b/Public/Publish-NugetPackage.ps1 new file mode 100644 index 00000000..686fc7df --- /dev/null +++ b/Public/Publish-NugetPackage.ps1 @@ -0,0 +1,61 @@ +function Publish-NugetPackage { + <# + .SYNOPSIS + Pushes NuGet packages to a feed. + + .DESCRIPTION + Finds all *.nupkg files in the specified path and uploads them using + `dotnet nuget push` with the provided API key and feed URL. + + .PARAMETER Path + Directory to search for NuGet packages. + + .PARAMETER ApiKey + API key used to authenticate against the NuGet feed. + + .PARAMETER Source + NuGet feed URL. Defaults to https://api.nuget.org/v3/index.json. + + .EXAMPLE + Publish-NugetPackage -Path 'C:\Git\Project\bin\Release' -ApiKey $MyKey + Uploads all packages in the Release folder to NuGet.org. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Path, + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$ApiKey, + [string]$Source = 'https://api.nuget.org/v3/index.json' + ) + $result = [ordered]@{ + Success = $true + Pushed = @() + Failed = @() + ErrorMessage = $null + } + + if (-not (Test-Path -LiteralPath $Path)) { + $result.Success = $false + $result.ErrorMessage = "Path '$Path' not found." + return [PSCustomObject]$result + } + $packages = Get-ChildItem -Path $Path -Recurse -Filter '*.nupkg' -ErrorAction SilentlyContinue + if (-not $packages) { + $result.Success = $false + $result.ErrorMessage = "No packages found in $Path" + return [PSCustomObject]$result + } + foreach ($pkg in $packages) { + dotnet nuget push $pkg.FullName --api-key $ApiKey --source $Source + if ($LASTEXITCODE -eq 0) { + $result.Pushed += $pkg.FullName + } else { + $result.Failed += $pkg.FullName + $result.Success = $false + } + } + return [PSCustomObject]$result +}