maksit-core/utils/plugins/Platform/GitHub.psm1
2026-07-28 22:26:14 +02:00

294 lines
12 KiB
PowerShell

#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 release notes from CHANGELOG.md.
Release notes must use Keep a Changelog headers: ## [semver] - YYYY-MM-DD
(see ChangelogSupport.psm1).
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/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
$releaseNotesVersion = Get-LatestChangelogVersion -ReleaseNotesContent $releaseNotesContent
if ([string]::IsNullOrWhiteSpace($releaseNotesVersion)) {
throw "No version entry found in the configured release notes source. Expected Keep a Changelog header: ## [semver] - YYYY-MM-DD."
}
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..."
$section = Get-ChangelogReleaseNotesSection -ReleaseNotesContent $releaseNotesContent -Version $Version
if ([string]::IsNullOrWhiteSpace($section)) {
throw "Release notes entry for version $Version not found. Expected header: ## [$Version] - YYYY-MM-DD."
}
Write-Log -Level "OK" -Message " Release notes extracted."
return $section
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Get-LatestChangelogVersion"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Get-EngineFact"
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$githubSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'githubSecret'
$configuredRepository = $pluginSettings.repository
$releaseNotesFileSetting = $pluginSettings.releaseNotesFile
$releaseTitlePatternSetting = $pluginSettings.releaseTitlePattern
$scriptDir = $sharedSettings.scriptDir
$version = $sharedSettings.version
$tag = $sharedSettings.tag
$releaseDir = $sharedSettings.releaseDir
$releaseAssetPaths = @()
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
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 ($dryRun) {
$repo = Get-GitHubRepositoryInternal -ConfiguredRepository $configuredRepository
$releaseTitlePattern = if ([string]::IsNullOrWhiteSpace($releaseTitlePatternSetting)) {
"Release {version}"
}
else {
$releaseTitlePatternSetting
}
$releaseName = $releaseTitlePattern -replace '\{version\}', $version
Write-Log -Level "INFO" -Message "Dry run: would create GitHub release '$releaseName' ($tag) on $repo"
return
}
Assert-Command gh
if ([string]::IsNullOrWhiteSpace($githubSecret)) {
throw "GitHub plugin requires 'githubSecret' in scriptSettings.json (logical secret name, e.g. GitHub)."
}
$ghToken = Get-SecretEnvironmentValue -Name $githubSecret
if ([string]::IsNullOrWhiteSpace($ghToken)) {
throw "GitHub token is not set. Set environment variable '$githubSecret'."
}
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 (Get-Command Get-EngineFact -ErrorAction SilentlyContinue) {
$fromAssets = Get-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -LegacyProperty @('releaseAssetPaths')
if ($null -ne $fromAssets) {
$releaseAssetPaths = @($fromAssets)
}
else {
$packageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -LegacyProperty @('packageFile')
if ($null -eq $packageFile) {
$packageFile = Get-EngineFact -Context $sharedSettings -Namespace 'npm' -Name 'packageFile' -LegacyProperty @('packageFile')
}
if ($null -ne $packageFile) {
$releaseAssetPaths = @($packageFile.FullName)
$symbolsPackageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -LegacyProperty @('symbolsPackageFile')
if ($null -ne $symbolsPackageFile) {
$releaseAssetPaths += $symbolsPackageFile.FullName
}
}
}
}
elseif ($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
}
}
$requireReleaseAssets = $true
if ($null -ne $pluginSettings.requireReleaseAssets) {
$requireReleaseAssets = [bool]$pluginSettings.requireReleaseAssets
}
if ($releaseAssetPaths.Count -eq 0 -and $requireReleaseAssets) {
throw "GitHub release requires at least one prepared release asset (set requireReleaseAssets: false for notes-only npm releases)."
}
if ($releaseAssetPaths.Count -eq 0) {
Write-Log -Level "INFO" -Message " Notes-only GitHub release (requireReleaseAssets: false)."
}
$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 = $ghToken
try {
$ghVersion = & gh --version 2>&1
if ($ghVersion) {
Write-Log -Level "INFO" -Message " gh version: $($ghVersion[0])"
}
Write-Log -Level "INFO" -Message " Auth secret: $githubSecret"
$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 secret '$githubSecret' 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 {
if (-not [string]::IsNullOrWhiteSpace($releaseDir) -and -not (Test-Path -LiteralPath $releaseDir -PathType Container)) {
New-Item -ItemType Directory -Path $releaseDir -Force | Out-Null
}
[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."
Add-EnginePublishCompletion -Context $sharedSettings -Publisher 'GitHub'
}
finally {
if ($null -ne $previousGhToken) {
$env:GH_TOKEN = $previousGhToken
}
else {
Remove-Item Env:GH_TOKEN -ErrorAction SilentlyContinue
}
}
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $true }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata