Repository navigation
Release #20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| - "V*" | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Release version, for example 1.0.1 or v1.0.1" | |
| required: true | |
| type: string | |
| permissions: | |
| contents: write | |
| jobs: | |
| build-release: | |
| name: Build and publish release | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Setup MSBuild | |
| uses: microsoft/setup-msbuild@v3 | |
| - name: Setup NuGet | |
| uses: NuGet/setup-nuget@v4 | |
| - name: Resolve release version | |
| id: release_version | |
| shell: pwsh | |
| run: | | |
| if ("${{ github.event_name }}" -eq "workflow_dispatch") { | |
| $rawVersion = "${{ inputs.version }}" | |
| } else { | |
| $rawVersion = "${{ github.ref_name }}" | |
| } | |
| $version = $rawVersion.Trim() | |
| if ($version.StartsWith("v", [System.StringComparison]::OrdinalIgnoreCase)) { | |
| $version = $version.Substring(1) | |
| } | |
| if ($version -notmatch '^\d+\.\d+\.\d+(\.\d+)?$') { | |
| throw "Release version must be n.n.n or n.n.n.n. Received: $rawVersion" | |
| } | |
| $tagName = "v$version" | |
| "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| "tag_name=$tagName" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| - name: Verify assembly version matches release | |
| shell: pwsh | |
| run: | | |
| $assemblyInfoPath = ".\mySQLPunk\Properties\AssemblyInfo.cs" | |
| $assemblyInfo = Get-Content -LiteralPath $assemblyInfoPath -Raw -Encoding UTF8 | |
| if ($assemblyInfo -notmatch 'AssemblyFileVersion\("([^"]+)"\)') { | |
| throw "AssemblyFileVersion was not found in $assemblyInfoPath" | |
| } | |
| $assemblyVersion = [version]$Matches[1] | |
| $releaseVersion = [version]"${{ steps.release_version.outputs.version }}" | |
| if ($releaseVersion.Revision -lt 0) { | |
| $releaseVersion = [version]::new($releaseVersion.Major, $releaseVersion.Minor, $releaseVersion.Build, 0) | |
| } | |
| if ($assemblyVersion.Revision -lt 0) { | |
| $assemblyVersion = [version]::new($assemblyVersion.Major, $assemblyVersion.Minor, $assemblyVersion.Build, 0) | |
| } | |
| if ($assemblyVersion -ne $releaseVersion) { | |
| throw "AssemblyFileVersion $assemblyVersion does not match release version $releaseVersion. Update AssemblyInfo.cs before publishing." | |
| } | |
| - name: Restore packages | |
| shell: pwsh | |
| run: nuget restore .\mySQLPunk.sln -NonInteractive | |
| - name: Setup Inno Setup | |
| shell: pwsh | |
| run: | | |
| $candidates = @( | |
| "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", | |
| "$env:ProgramFiles\Inno Setup 6\ISCC.exe", | |
| "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe" | |
| ) | |
| if ($candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1) { | |
| exit 0 | |
| } | |
| $installerPath = Join-Path $env:RUNNER_TEMP "innosetup-6.7.3.exe" | |
| $installerUrl = "https://github.com/jrsoftware/issrc/releases/download/is-6_7_3/innosetup-6.7.3.exe" | |
| $expectedSha256 = "9c73c3bae7ed48d44112a0f48e66742c00090bdb5bef71d9d3c056c66e97b732" | |
| Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath | |
| $actualSha256 = (Get-FileHash -LiteralPath $installerPath -Algorithm SHA256).Hash.ToLowerInvariant() | |
| if ($actualSha256 -ne $expectedSha256) { | |
| throw "Inno Setup installer SHA-256 mismatch. Expected $expectedSha256, got $actualSha256." | |
| } | |
| $process = Start-Process -FilePath $installerPath -ArgumentList "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART", "/CURRENTUSER" -Wait -PassThru | |
| if ($process.ExitCode -ne 0) { | |
| throw "Inno Setup installation failed with exit code $($process.ExitCode)." | |
| } | |
| $iscc = "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe" | |
| if (-not (Test-Path -LiteralPath $iscc)) { | |
| throw "ISCC.exe was not found after installing Inno Setup." | |
| } | |
| - name: Build and package | |
| shell: pwsh | |
| run: .\scripts\package-release.ps1 -Version "${{ steps.release_version.outputs.version }}" -Configuration Release -OutputRoot .\dist | |
| - name: Smoke test installer | |
| shell: pwsh | |
| run: | | |
| $installers = @(Get-ChildItem -LiteralPath ".\dist" -Filter "*-setup.exe" -File) | |
| if ($installers.Count -ne 1) { | |
| throw "Exactly one setup EXE is required for the installer smoke test." | |
| } | |
| $installRoot = Join-Path $env:RUNNER_TEMP "mysqlpunk-installer-smoke" | |
| $runnerRoot = [System.IO.Path]::GetFullPath($env:RUNNER_TEMP).TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar | |
| $resolvedInstallRoot = [System.IO.Path]::GetFullPath($installRoot).TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar | |
| if (-not $resolvedInstallRoot.StartsWith($runnerRoot, [System.StringComparison]::OrdinalIgnoreCase)) { | |
| throw "Installer smoke path must stay inside RUNNER_TEMP: $resolvedInstallRoot" | |
| } | |
| $appProcess = $null | |
| try { | |
| $install = Start-Process -FilePath $installers[0].FullName -ArgumentList @( | |
| "/VERYSILENT", | |
| "/SUPPRESSMSGBOXES", | |
| "/NORESTART", | |
| "/SP-", | |
| "/CURRENTUSER", | |
| "/NOICONS", | |
| "/DIR=`"$installRoot`"" | |
| ) -Wait -PassThru | |
| if ($install.ExitCode -ne 0) { | |
| throw "Installer smoke installation failed with exit code $($install.ExitCode)." | |
| } | |
| $appPath = Join-Path $installRoot "mySQLPunk.exe" | |
| $uninstallerPath = Join-Path $installRoot "unins000.exe" | |
| foreach ($requiredPath in @($appPath, $uninstallerPath, (Join-Path $installRoot "THIRD_PARTY_NOTICES.md"))) { | |
| if (-not (Test-Path -LiteralPath $requiredPath)) { | |
| throw "Installer smoke test is missing required file: $requiredPath" | |
| } | |
| } | |
| $appProcess = Start-Process -FilePath $appPath -WindowStyle Hidden -PassThru | |
| Start-Sleep -Seconds 5 | |
| $appProcess.Refresh() | |
| if ($appProcess.HasExited) { | |
| throw "Installed mySQLPunk exited during the startup smoke test with code $($appProcess.ExitCode)." | |
| } | |
| $null = $appProcess.CloseMainWindow() | |
| if (-not $appProcess.WaitForExit(5000)) { | |
| Stop-Process -Id $appProcess.Id -Force | |
| $appProcess.WaitForExit() | |
| } | |
| } | |
| finally { | |
| if ($null -ne $appProcess -and -not $appProcess.HasExited) { | |
| Stop-Process -Id $appProcess.Id -Force -ErrorAction SilentlyContinue | |
| $appProcess.WaitForExit() | |
| } | |
| $uninstallerPath = Join-Path $installRoot "unins000.exe" | |
| if (Test-Path -LiteralPath $uninstallerPath) { | |
| $uninstall = Start-Process -FilePath $uninstallerPath -ArgumentList @( | |
| "/VERYSILENT", | |
| "/SUPPRESSMSGBOXES", | |
| "/NORESTART" | |
| ) -Wait -PassThru | |
| if ($uninstall.ExitCode -ne 0) { | |
| throw "Installer smoke uninstall failed with exit code $($uninstall.ExitCode)." | |
| } | |
| } | |
| } | |
| if (Test-Path -LiteralPath (Join-Path $installRoot "mySQLPunk.exe")) { | |
| throw "Installer smoke uninstall left mySQLPunk.exe behind." | |
| } | |
| - name: Write release notes | |
| shell: pwsh | |
| run: .\scripts\New-ReleaseNotes.ps1 -Version "${{ steps.release_version.outputs.version }}" -OutputDirectory .\dist | |
| - name: Upload workflow artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: mySQLPunk-${{ steps.release_version.outputs.version }}-release-assets | |
| path: | | |
| dist/*-setup.exe | |
| dist/release-notes.md | |
| dist/release-title.txt | |
| - name: Create or update GitHub Release | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| REPOSITORY: ${{ github.repository }} | |
| TAG_NAME: ${{ steps.release_version.outputs.tag_name }} | |
| run: | | |
| $headers = @{ | |
| Authorization = "Bearer $env:GITHUB_TOKEN" | |
| Accept = "application/vnd.github+json" | |
| "X-GitHub-Api-Version" = "2022-11-28" | |
| } | |
| $releaseName = (Get-Content -LiteralPath ".\dist\release-title.txt" -Raw -Encoding UTF8).Trim() | |
| $releaseNotes = Get-Content -LiteralPath ".\dist\release-notes.md" -Raw -Encoding UTF8 | |
| $releaseByTagUrl = "https://api.github.com/repos/$env:REPOSITORY/releases/tags/$env:TAG_NAME" | |
| $release = $null | |
| try { | |
| $release = Invoke-RestMethod -Method Get -Uri $releaseByTagUrl -Headers $headers | |
| } catch { | |
| $statusCode = 0 | |
| if ($_.Exception.Response -ne $null) { | |
| $statusCode = [int]$_.Exception.Response.StatusCode | |
| } | |
| if ($statusCode -ne 404) { | |
| throw | |
| } | |
| $body = @{ | |
| tag_name = $env:TAG_NAME | |
| target_commitish = $env:GITHUB_SHA | |
| name = $releaseName | |
| body = $releaseNotes | |
| draft = $false | |
| prerelease = $false | |
| } | ConvertTo-Json -Depth 4 | |
| $release = Invoke-RestMethod -Method Post -Uri "https://api.github.com/repos/$env:REPOSITORY/releases" -Headers $headers -Body $body -ContentType "application/json" | |
| } | |
| if ($null -ne $release -and ($release.name -ne $releaseName -or [string]$release.body -ne $releaseNotes)) { | |
| $body = @{ | |
| name = $releaseName | |
| body = $releaseNotes | |
| draft = $false | |
| prerelease = $false | |
| } | ConvertTo-Json -Depth 4 | |
| $release = Invoke-RestMethod -Method Patch -Uri "https://api.github.com/repos/$env:REPOSITORY/releases/$($release.id)" -Headers $headers -Body $body -ContentType "application/json" | |
| } | |
| $uploadBaseUrl = $release.upload_url -replace '\{.*\}$', '' | |
| $assets = @(Get-ChildItem -LiteralPath ".\dist" -Filter "*-setup.exe") | |
| if ($assets.Count -ne 1) { | |
| throw "Exactly one setup EXE is required, but found $($assets.Count)." | |
| } | |
| # A release intentionally has one downloadable file. Remove assets left by older runs. | |
| foreach ($existingAsset in @($release.assets)) { | |
| Invoke-RestMethod -Method Delete -Uri $existingAsset.url -Headers $headers | Out-Null | |
| } | |
| foreach ($asset in $assets) { | |
| $assetName = [uri]::EscapeDataString($asset.Name) | |
| $assetLabel = [uri]::EscapeDataString("Windows x64 單一安裝程式") | |
| $uploadUrl = "${uploadBaseUrl}?name=$assetName&label=$assetLabel" | |
| Invoke-RestMethod -Method Post -Uri $uploadUrl -Headers $headers -InFile $asset.FullName -ContentType "application/octet-stream" | Out-Null | |
| } |