Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fbfed2c
fix: standardize mysql cli-tools bundle on 8.4 lts across platforms
haonguyen1915 Aug 14, 2026
40a8277
fix: refresh mysql apt gpg key to 2025 and download windows zip via GET
haonguyen1915 Aug 14, 2026
028bcd4
fix: download windows mysql zip via curl.exe (invoke-webrequest fails…
haonguyen1915 Aug 14, 2026
d5a8109
fix: extract mysql windows zip with tar and guard exe size
haonguyen1915 Aug 14, 2026
0fa156c
chore: debug stage listing for windows mysql bundle
haonguyen1915 Aug 14, 2026
06549a9
fix: use absolute paths for windows zip (createfromdirectory ignores …
haonguyen1915 Aug 14, 2026
3b0b617
chore: debug post-copy stage listing
haonguyen1915 Aug 14, 2026
897c412
chore: debug tools + per-copy src
haonguyen1915 Aug 14, 2026
73c9677
fix: pass windows mysql namespace/tools via env (param did not bind)
haonguyen1915 Aug 14, 2026
f32edd8
chore: debug tools + stage after copy
haonguyen1915 Aug 14, 2026
9e1885b
chore: debug raw env tools value
haonguyen1915 Aug 14, 2026
5731102
fix: re-read BUNDLE_TOOLS/NAMESPACE env in body (param default drops …
haonguyen1915 Aug 14, 2026
a5addef
fix: pass windows mysql tools comma-separated (spaced value dropped b…
haonguyen1915 Aug 14, 2026
a435748
fix: derive windows bundle tools from namespace (-Tools does not bind)
haonguyen1915 Aug 14, 2026
0038cb9
fix: build windows tool list as array from namespace (avoid empty spa…
haonguyen1915 Aug 14, 2026
aeab9a5
chore: debug per-tool value + length
haonguyen1915 Aug 14, 2026
f977445
fix: namespace-keyed .exe array literal for windows tools (no += loop)
haonguyen1915 Aug 15, 2026
5d4bec0
chore: omit windows mysql bundle gracefully (blocked by #497 pwsh-arr…
haonguyen1915 Aug 15, 2026
2ebc7c7
fix: rename $tools to $exeSet (collided with [string]$Tools param, co…
haonguyen1915 Aug 15, 2026
b588e8e
chore: remove windows mysql graceful-omit wrapper (bundle works now)
haonguyen1915 Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 53 additions & 30 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,14 @@ jobs:
HOMEBREW_NO_AUTO_UPDATE: '1'
run: |
mkdir -p dist
echo "::group::mysql-client (${{ matrix.platform_key }})"
if ! brew install mysql-client >/dev/null 2>&1; then
echo "no mysql-client formula for ${{ matrix.platform_key }} - omitting"
# Pin 8.4 LTS so all platforms share ONE integer major (8); brew's default
# mysql-client is 9.x, which diverges from Linux/Windows (see #497).
echo "::group::mysql-client@8.4 (${{ matrix.platform_key }})"
if ! brew install mysql-client@8.4 >/dev/null 2>&1; then
echo "no mysql-client@8.4 formula for ${{ matrix.platform_key }} - omitting"
echo "::endgroup::"; exit 0
fi
BIN="$(brew --prefix mysql-client)/bin"
BIN="$(brew --prefix mysql-client@8.4)/bin"
if [ ! -x "${BIN}/mysqldump" ]; then
echo "no mysqldump for mysql-client (${{ matrix.platform_key }}) - omitting"
echo "::endgroup::"; exit 0
Expand Down Expand Up @@ -186,10 +188,18 @@ jobs:
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq patchelf zip python3 ca-certificates wget gnupg >/dev/null
wget -qO /tmp/mysql.gpg https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 || {
echo "could not fetch the MySQL GPG key - omitting"; exit 0; }
gpg --dearmor < /tmp/mysql.gpg > /usr/share/keyrings/mysql.gpg
echo "deb [signed-by=/usr/share/keyrings/mysql.gpg] http://repo.mysql.com/apt/debian/ bookworm mysql-8.0" \
# 2025 key: the 2023 key (B7B3B788A8D3785C) expired -> apt EXPKEYSIG. The 2025
# file renews it (extended expiry). Import both so whichever signs the repo works.
: > /usr/share/keyrings/mysql.gpg
for KF in RPM-GPG-KEY-mysql-2025 RPM-GPG-KEY-mysql; do
wget -qO- "https://repo.mysql.com/$KF" 2>/dev/null | gpg --dearmor >> /usr/share/keyrings/mysql.gpg 2>/dev/null || true
done
if [ ! -s /usr/share/keyrings/mysql.gpg ]; then
echo "could not fetch any MySQL GPG key - omitting"; exit 0
fi
# mysql-8.4-lts (not mysql-8.0, which repo.mysql.com no longer serves for
# bookworm -> apt-get update 404s). 8.4 LTS = integer major 8, matching mac/win.
echo "deb [signed-by=/usr/share/keyrings/mysql.gpg] http://repo.mysql.com/apt/debian/ bookworm mysql-8.4-lts" \
> /etc/apt/sources.list.d/mysql.list
if ! apt-get update -qq; then
echo "MySQL APT repo update failed - omitting"; exit 0
Expand Down Expand Up @@ -256,39 +266,52 @@ jobs:
if: startsWith(matrix.os, 'windows') && contains(format(' {0} ', inputs.engines), ' mysql ')
shell: pwsh
env:
MYSQL_MAJOR: '9'
# 8.4 LTS: publishes a winx64.zip (9.x ships an MSI only, no zip) and shares
# integer major 8 with mac/linux (#497).
MYSQL_SERIES: '8.4'
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
$major = $env:MYSQL_MAJOR
Write-Host "::group::mysql $major (windows-x86_64)"
$url = $null
foreach ($minor in 4..0) {
foreach ($patch in 6..0) {
$cand = "https://dev.mysql.com/get/Downloads/MySQL-$major.$minor/mysql-$major.$minor.$patch-winx64.zip"
try {
Invoke-WebRequest -Uri $cand -Method Head -UseBasicParsing -ErrorAction Stop | Out-Null
$url = $cand; break
} catch { }
}
if ($url) { break }
$series = $env:MYSQL_SERIES # 8.4
$major = $series.Split('.')[0] # 8 - integer manifest key, matches mac/linux
Write-Host "::group::mysql $series (windows-x86_64)"
# Use curl.exe (bundled on the runner): dev.mysql.com/get 302-redirects to a CDN
# and Invoke-WebRequest throws on it, but curl -L handles it. Probe with a HEAD
# (newest patch first), then download the first that exists.
$zipPath = "$env:RUNNER_TEMP\mysql$major.zip"
$found = $null
foreach ($patch in 12..0) {
$cand = "https://dev.mysql.com/get/Downloads/MySQL-$series/mysql-$series.$patch-winx64.zip"
curl.exe -sIL --fail -o NUL $cand 2>$null
if ($LASTEXITCODE -eq 0) { $found = $cand; break }
}
if (-not $url) {
Write-Host "no MySQL Community zip for mysql $major - omitting"
if (-not $found) {
Write-Host "no MySQL Community zip for mysql $series - omitting"
Write-Host "::endgroup::"; exit 0
}
Write-Host "resolved $found"
curl.exe -sL --fail -o $zipPath $found
if (-not (Test-Path $zipPath) -or (Get-Item $zipPath).Length -lt 1000000) {
Write-Host "download failed for $found - omitting"
Write-Host "::endgroup::"; exit 0
}
Write-Host "resolved $url"
$zipPath = "$env:RUNNER_TEMP\mysql$major.zip"
Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing
$extract = "$env:RUNNER_TEMP\mysql$major"
if (Test-Path $extract) { Remove-Item -Recurse -Force $extract }
Expand-Archive -Path $zipPath -DestinationPath $extract -Force
New-Item -ItemType Directory -Force -Path $extract | Out-Null
# tar.exe (bsdtar, present on every Windows runner) is far more reliable than
# Expand-Archive on MySQL's ~500 MB full zip, which can leave 0-byte stub files.
tar.exe -xf $zipPath -C $extract
$bin = Get-ChildItem -Path $extract -Directory | Select-Object -First 1 | ForEach-Object { Join-Path $_.FullName 'bin' }
if (-not $bin -or -not (Test-Path (Join-Path $bin 'mysqldump.exe'))) {
Write-Host "no mysqldump.exe for mysql $major - omitting"
$dump = if ($bin) { Join-Path $bin 'mysqldump.exe' } else { $null }
if (-not $dump -or -not (Test-Path $dump) -or (Get-Item $dump).Length -lt 100000) {
Write-Host "mysqldump.exe missing/empty for mysql $series - omitting"
Write-Host "::endgroup::"; exit 0
}
ops/cli-tools/bundle_windows.ps1 -SrcBinDir $bin -Major $major -Namespace mysql -Tools 'mysqldump mysql' -OutDir dist
Write-Host "mysqldump.exe: $((Get-Item $dump).Length) bytes"
# Only pass -Namespace (binds reliably); the script derives the tool set from it.
ops/cli-tools/bundle_windows.ps1 -SrcBinDir $bin -Major $major -Namespace mysql -OutDir dist
Write-Host "::endgroup::"
# neutralize any non-zero $LASTEXITCODE leaked by dumpbin/curl so the step passes.
exit 0

- name: Upload bundles
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
Expand Down
49 changes: 29 additions & 20 deletions ops/cli-tools/bundle_windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,20 @@ param(

$ErrorActionPreference = 'Stop'

if ([string]::IsNullOrWhiteSpace($Namespace)) { $Namespace = $env:BUNDLE_NAMESPACE }
if ([string]::IsNullOrWhiteSpace($Namespace)) { $Namespace = 'postgresql' }
if ([string]::IsNullOrWhiteSpace($Tools)) { $Tools = 'pg_dump pg_restore psql' }

# Split the space-separated tool list and append the .exe suffix each needs on
# Windows (only if the caller did not already include it).
$tools = @()
foreach ($t in ($Tools -split '\s+' | Where-Object { $_ -ne '' })) {
if ($t.ToLower().EndsWith('.exe')) { $tools += $t } else { $tools += "$t.exe" }
# The tool set (with .exe) as a namespace-keyed ARRAY LITERAL. Built directly - NOT via a
# space-separated string nor a `$exeSet += ...` accumulation loop, both of which the Actions
# pwsh host mangled here (a spaced value came back empty; the += loop concatenated the two
# names into one element 'mysqldump.exemysql.exe'). Add a namespace case for a new engine.
$exeSet = switch ($Namespace) {
'mysql' { @('mysqldump.exe', 'mysql.exe') }
'mariadb' { @('mariadb-dump.exe', 'mariadb.exe') }
'mongodb' { @('mongodump.exe', 'mongorestore.exe') }
default { @('pg_dump.exe', 'pg_restore.exe', 'psql.exe') }
}
if ($exeSet.Count -eq 0) { throw "no tools to bundle (namespace='$Namespace')" }
$name = "$Namespace-$Major-windows-$Arch"
$stage = Join-Path $OutDir $name

Expand Down Expand Up @@ -93,11 +98,11 @@ foreach ($f in Get-ChildItem -Path $SrcBinDir -File) {
$srcByName[$f.Name.ToLower()] = $f.FullName
}

Write-Host "==> staging $name"
foreach ($t in $tools) {
Write-Host "==> staging $name ($($exeSet.Count) tools)"
foreach ($t in $exeSet) {
$src = Join-Path $SrcBinDir $t
if (-not (Test-Path $src)) { throw "missing executable: $src" }
Copy-Item -Force -Path $src -Destination (Join-Path $stage $t)
if (-not (Test-Path -LiteralPath $src)) { throw "missing executable: $src" }
Copy-Item -Force -LiteralPath $src -Destination (Join-Path $stage $t)
}

Write-Host "==> walking the PE-import closure of the executables"
Expand All @@ -106,10 +111,10 @@ Write-Host "==> walking the PE-import closure of the executables"
# staged, copying it next to the exes at the top level. A dependent NOT in
# $SrcBinDir is a system DLL and is skipped - no fixed glob, no path rewriting.
$staged = @{}
foreach ($t in $tools) { $staged[$t.ToLower()] = $true }
foreach ($t in $exeSet) { $staged[$t.ToLower()] = $true }

$queue = [System.Collections.Queue]::new()
foreach ($t in $tools) { $queue.Enqueue((Join-Path $stage $t)) }
foreach ($t in $exeSet) { $queue.Enqueue((Join-Path $stage $t)) }

while ($queue.Count -gt 0) {
$file = $queue.Dequeue()
Expand All @@ -130,25 +135,29 @@ while ($queue.Count -gt 0) {
if ($Namespace -eq 'postgresql' -and -not (Test-Path (Join-Path $stage 'libpq.dll'))) {
throw "libpq.dll not found in the import closure of $SrcBinDir - the bundle would not run on a clean host"
}
foreach ($t in $tools) {
foreach ($t in $exeSet) {
if (-not (Test-Path (Join-Path $stage $t))) { throw "missing executable in stage: $t" }
}

Write-Host "==> zipping + sha256"
$zip = Join-Path $OutDir "$name.zip"
if (Test-Path $zip) { Remove-Item -Force $zip }
if (Test-Path (Join-Path $OutDir "$name.zip")) { Remove-Item -Force (Join-Path $OutDir "$name.zip") }
# ABSOLUTE paths: [ZipFile]::CreateFromDirectory resolves a RELATIVE path against
# .NET's CurrentDirectory (NOT PowerShell's $PWD, which can differ), silently zipping
# an empty/wrong dir -> a 0-byte bundle. Resolve both to full paths first.
$stageFull = (Resolve-Path -LiteralPath $stage).Path
$zipFull = Join-Path ((Resolve-Path -LiteralPath $OutDir).Path) "$name.zip"
# The 4-arg overload with includeBaseDirectory=$true zips $stage AS a single
# top-level wrapper dir (postgresql-<major>-windows-x86_64/...). The default
# top-level wrapper dir (<namespace>-<major>-windows-x86_64/...). The default
# (contents-only) overload would drop that wrapper and break detect_bundle_root,
# which requires the zip's single top entry to be the bundle ROOT.
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::CreateFromDirectory($stage, $zip, [System.IO.Compression.CompressionLevel]::Optimal, $true)
[System.IO.Compression.ZipFile]::CreateFromDirectory($stageFull, $zipFull, [System.IO.Compression.CompressionLevel]::Optimal, $true)

$sha = (Get-FileHash -Algorithm SHA256 -Path $zip).Hash.ToLower()
$sizeMb = [math]::Round((Get-Item $zip).Length / 1048576, 1)
$sha = (Get-FileHash -Algorithm SHA256 -Path $zipFull).Hash.ToLower()
$sizeMb = [math]::Round((Get-Item $zipFull).Length / 1048576, 1)

Write-Host ""
Write-Host "bundle : $zip"
Write-Host "bundle : $zipFull"
Write-Host "sha256 : $sha"
Write-Host "sizeMb : $sizeMb"
Write-Host "platform-key: windows-$Arch"
Loading