Skip to content

chore(release): 發佈 v1.0.0.22 #25

chore(release): 發佈 v1.0.0.22

chore(release): 發佈 v1.0.0.22 #25

Workflow file for this run

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