Repository navigation
chore(release): 發佈 v1.0.0.22 #25
Workflow file for this run
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 | |
| needs: build-cross-platform-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 | |
| env: | |
| RELEASE_EVENT_NAME: ${{ github.event_name }} | |
| RELEASE_INPUT_VERSION: ${{ inputs.version }} | |
| RELEASE_REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| if ($env:RELEASE_EVENT_NAME -eq "workflow_dispatch") { | |
| $rawVersion = $env:RELEASE_INPUT_VERSION | |
| } else { | |
| $rawVersion = $env:RELEASE_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: Download verified cross-platform packages | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: mySQLPunk-${{ steps.release_version.outputs.version }}-* | |
| path: dist/cross-platform | |
| merge-multiple: true | |
| - name: Verify cross-platform package set and SHA-256 | |
| shell: pwsh | |
| run: | | |
| $version = "${{ steps.release_version.outputs.version }}" | |
| $packageRoot = ".\dist\cross-platform" | |
| $expectedPackages = @( | |
| "mySQLPunk-$version-linux-x64.tar.gz", | |
| "mySQLPunk-$version-linux-arm64.tar.gz", | |
| "mySQLPunk-$version-osx-x64.app.zip", | |
| "mySQLPunk-$version-osx-arm64.app.zip" | |
| ) | |
| foreach ($packageName in $expectedPackages) { | |
| $packagePath = Join-Path $packageRoot $packageName | |
| $hashPath = "$packagePath.sha256" | |
| if (-not (Test-Path -LiteralPath $packagePath -PathType Leaf) -or | |
| -not (Test-Path -LiteralPath $hashPath -PathType Leaf)) { | |
| throw "Missing cross-platform package or SHA-256: $packageName" | |
| } | |
| $expectedHash = ((Get-Content -LiteralPath $hashPath -Raw -Encoding UTF8).Trim() -split '\s+')[0].ToLowerInvariant() | |
| $actualHash = (Get-FileHash -LiteralPath $packagePath -Algorithm SHA256).Hash.ToLowerInvariant() | |
| if ($expectedHash -ne $actualHash) { | |
| throw "Cross-platform SHA-256 mismatch for $packageName. Expected $expectedHash, got $actualHash." | |
| } | |
| } | |
| - 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/cross-platform/* | |
| 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" -File) | |
| @(Get-ChildItem -LiteralPath ".\dist\cross-platform" -File) | |
| ) | |
| if ($assets.Count -ne 9) { | |
| throw "Exactly one Windows setup and eight cross-platform asset/hash files are required, but found $($assets.Count)." | |
| } | |
| # Every required platform asset is ready before the public Release is changed. | |
| 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) | |
| $label = if ($asset.Name.EndsWith(".sha256", [System.StringComparison]::OrdinalIgnoreCase)) { | |
| "SHA-256 完整性檢查" | |
| } elseif ($asset.Name -like "*-linux-x64.tar.gz") { | |
| "Linux x64 self-contained 預覽版" | |
| } elseif ($asset.Name -like "*-linux-arm64.tar.gz") { | |
| "Linux ARM64 self-contained 預覽版" | |
| } elseif ($asset.Name -like "*-osx-x64.app.zip") { | |
| "macOS Intel self-contained 預覽版" | |
| } elseif ($asset.Name -like "*-osx-arm64.app.zip") { | |
| "macOS Apple Silicon self-contained 預覽版" | |
| } else { | |
| "Windows x64 單一安裝程式" | |
| } | |
| $assetLabel = [uri]::EscapeDataString($label) | |
| $uploadUrl = "${uploadBaseUrl}?name=$assetName&label=$assetLabel" | |
| Invoke-RestMethod -Method Post -Uri $uploadUrl -Headers $headers -InFile $asset.FullName -ContentType "application/octet-stream" | Out-Null | |
| } | |
| build-cross-platform-release: | |
| name: Publish ${{ matrix.name }} release packages | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: Linux x64 | |
| os: ubuntu-24.04 | |
| runtimes: linux-x64 | |
| artifact_name: linux-x64 | |
| - name: Linux ARM64 | |
| os: ubuntu-24.04-arm | |
| runtimes: linux-arm64 | |
| artifact_name: linux-arm64 | |
| - name: macOS Intel | |
| os: macos-15-intel | |
| runtimes: osx-x64 | |
| artifact_name: macos-x64 | |
| - name: macOS Apple Silicon | |
| os: macos-15 | |
| runtimes: osx-arm64 | |
| artifact_name: macos-arm64 | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Setup .NET 8 | |
| uses: actions/setup-dotnet@v6 | |
| with: | |
| dotnet-version: "8.0.x" | |
| - name: Resolve and verify release version | |
| id: release_version | |
| shell: bash | |
| env: | |
| RELEASE_EVENT_NAME: ${{ github.event_name }} | |
| RELEASE_INPUT_VERSION: ${{ inputs.version }} | |
| RELEASE_REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| if [[ "$RELEASE_EVENT_NAME" == "workflow_dispatch" ]]; then | |
| raw_version="$RELEASE_INPUT_VERSION" | |
| else | |
| raw_version="$RELEASE_REF_NAME" | |
| fi | |
| version="${raw_version#v}" | |
| version="${version#V}" | |
| if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then | |
| echo "Release version must be n.n.n or n.n.n.n. Received: $raw_version" >&2 | |
| exit 1 | |
| fi | |
| assembly_version=$(sed -n 's/.*AssemblyFileVersion("\([0-9.]*\)").*/\1/p' mySQLPunk/Properties/AssemblyInfo.cs | tail -n 1) | |
| normalized_version="$version" | |
| normalized_assembly="$assembly_version" | |
| if [[ "$normalized_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| normalized_version="$normalized_version.0" | |
| fi | |
| if [[ "$normalized_assembly" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| normalized_assembly="$normalized_assembly.0" | |
| fi | |
| if [[ "$normalized_version" != "$normalized_assembly" ]]; then | |
| echo "AssemblyFileVersion $assembly_version does not match release version $version." >&2 | |
| exit 1 | |
| fi | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "tag_name=v$version" >> "$GITHUB_OUTPUT" | |
| - name: Build self-contained packages | |
| shell: bash | |
| run: | | |
| for runtime in ${{ matrix.runtimes }}; do | |
| ./scripts/package-cross-platform.sh \ | |
| --version "${{ steps.release_version.outputs.version }}" \ | |
| --runtime "$runtime" \ | |
| --output dist/cross-platform | |
| done | |
| - name: Smoke test Linux install, startup and uninstall | |
| if: runner.os == 'Linux' | |
| env: | |
| MYSQLPUNK_PACKAGE_RUN_UI: "1" | |
| run: ./tests/Test-LinuxCrossPlatformPackage.sh dist/cross-platform/*-${{ matrix.runtimes }}.tar.gz | |
| - name: Smoke test Linux safe update apply and rollback | |
| if: runner.os == 'Linux' | |
| run: ./tests/Test-LinuxUpdateApply.sh dist/cross-platform/*-${{ matrix.runtimes }}.tar.gz | |
| - name: Verify macOS app archives and safe update apply | |
| if: runner.os == 'macOS' | |
| run: | | |
| ./tests/Test-MacCrossPlatformPackage.sh dist/cross-platform/*.app.zip | |
| ./tests/Test-MacUpdateApply.sh dist/cross-platform/*-${{ matrix.runtimes }}.app.zip | |
| - name: Upload workflow artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: mySQLPunk-${{ steps.release_version.outputs.version }}-${{ matrix.artifact_name }} | |
| path: dist/cross-platform/* | |
| compression-level: 0 |