diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a51fb3e..72f10f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 @@ -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 @@ -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 diff --git a/ops/cli-tools/bundle_windows.ps1 b/ops/cli-tools/bundle_windows.ps1 index 18d9fa3..1b70561 100644 --- a/ops/cli-tools/bundle_windows.ps1 +++ b/ops/cli-tools/bundle_windows.ps1 @@ -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 @@ -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" @@ -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() @@ -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--windows-x86_64/...). The default +# top-level wrapper dir (--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"