Files
shopdb-flask/scripts/build-offline-bundle.ps1
cproudlock c6c806667e
Some checks failed
CI / backend (push) Failing after 7m18s
CI / naming (push) Failing after 7m14s
CI / frontend (push) Failing after 7m13s
CI / migrations-mysql (push) Failing after 7m10s
Run the database version the rest of the product already recommends
INSTALL-WINDOWS-IIS.md has said MySQL 8.4 LTS is standard for new installs since
8.0 reached end of life in April 2026, while both compose files and the offline
bundler still pinned 8.0. A site reading the Windows runbook and a site reading
the Docker one were being told to run different servers, and the migration page
written this week sent people onto the dead one.

Verified against a real server rather than by editing a tag: 8.4.11, core chain
plus five plugin chains applied clean, 66 tables at a single utf8mb4_unicode_ci
collation, six alembic version tables. The image's PyMySQL authenticates against
8.4's caching_sha2_password, which is what requirements.in already pins
cryptography for.

Existing servers need one thing done FIRST: 8.4 removes mysql_native_password,
so an account created on 5.6 or 5.7 must be moved to caching_sha2_password
before the upgrade or it cannot authenticate afterwards. In-place also has no
downgrade path, and 5.7 cannot reach 8.4 in one hop. For databases this size a
dump into a fresh 8.4 server is the better trade: same outage, and the old
server stays as the rollback.

Air-gapped sites need a fresh offline bundle, because the tarball carries the
MySQL image alongside the app image.

Also here, found by having it bite during that verification: the db healthcheck
pinged over the unix socket, and the entrypoint's init pass answers on the
socket while running the server with --skip-networking. The probe therefore
reported healthy DURING init, which is what `depends_on: service_healthy` gates
api and migrate on. A ping passed at 8 seconds and the next query failed because
the server was mid-restart. Probing 127.0.0.1 keeps it red until the real server
is listening.
2026-08-19 19:57:40 -04:00

109 lines
3.9 KiB
PowerShell

<#
.SYNOPSIS
Build the shopdb-flask image on a CONNECTED box and bundle it (plus the MySQL
image) into one tarball for transfer to an air-gapped site.
.DESCRIPTION
Air-gapped sites cannot pip install / npm ci / docker pull. This builds the
fully self-contained application image where connectivity exists, pulls the
MySQL image the stack needs, and `docker save`s both into a single gzipped
tarball with a SHA-256 checksum. Carry the .tar.gz to the site and follow
docs/DEPLOY-AIRGAP.md (docker load + docker-compose.airgap.yml).
Run from the repo root. Requires Docker Desktop. The connected build box needs
clean access to Docker Hub + PyPI + npm. If it is behind GE Zscaler and the
build fails on pip/npm SSL (CERTIFICATE_VERIFY_FAILED), build from a box with
clean internet, or trust the corp root CA in the build - see
docs/DEPLOY-AIRGAP.md "Building behind Zscaler".
.PARAMETER Version
Image tag for shopdb-flask (default 0.7.0). This is the IMAGE_TAG the site
sets in its .env so docker-compose.airgap.yml runs the matching image.
.PARAMETER MysqlImage
MySQL image the stack runs (default mysql:8.4). Must match db.image in
docker-compose.airgap.yml.
.PARAMETER OutDir
Where to write the tarball + checksum (default the repo root).
.PARAMETER SkipBuild
Reuse an already-built shopdb-flask:<Version> image; only pull + save.
.EXAMPLE
pwsh scripts/build-offline-bundle.ps1 -Version 0.7.0
#>
[CmdletBinding()]
param(
[string]$Version = '0.7.0',
[string]$MysqlImage = 'mysql:8.4',
[string]$OutDir = '.',
[switch]$SkipBuild
)
$ErrorActionPreference = 'Stop'
function Assert-LastExit([string]$What) {
if ($LASTEXITCODE -ne 0) { throw "$What failed (exit $LASTEXITCODE)" }
}
# Fail early if docker is missing rather than midway through a long build.
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
throw 'docker not found on PATH. Install Docker Desktop and retry.'
}
$appImage = "shopdb-flask:$Version"
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
Push-Location $repoRoot
try {
if (-not $SkipBuild) {
Write-Host "==> Building $appImage" -ForegroundColor Cyan
docker build -t $appImage .
Assert-LastExit 'docker build'
} else {
Write-Host "==> Skipping build; reusing $appImage" -ForegroundColor Yellow
}
Write-Host "==> Pulling $MysqlImage" -ForegroundColor Cyan
docker pull $MysqlImage
Assert-LastExit 'docker pull'
if (-not (Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null }
$stem = "shopdb-stack-$Version"
$tar = Join-Path $OutDir "$stem.tar"
$gz = Join-Path $OutDir "$stem.tar.gz"
Write-Host "==> Saving $appImage + $MysqlImage" -ForegroundColor Cyan
docker save -o $tar $appImage $MysqlImage
Assert-LastExit 'docker save'
Write-Host "==> Compressing to $gz" -ForegroundColor Cyan
$inStream = [System.IO.File]::OpenRead($tar)
$outStream = [System.IO.File]::Create($gz)
$gzip = New-Object System.IO.Compression.GzipStream($outStream, [System.IO.Compression.CompressionMode]::Compress)
try {
$inStream.CopyTo($gzip)
} finally {
$gzip.Dispose(); $outStream.Dispose(); $inStream.Dispose()
}
Remove-Item $tar
$hash = (Get-FileHash $gz -Algorithm SHA256).Hash.ToLower()
$sumFile = "$gz.sha256"
"$hash $stem.tar.gz" | Set-Content -Path $sumFile -NoNewline
$sizeMb = [math]::Round((Get-Item $gz).Length / 1MB, 1)
Write-Host ''
Write-Host "Bundle ready: $gz ($sizeMb MB)" -ForegroundColor Green
Write-Host "SHA-256: $hash"
Write-Host "Checksum: $sumFile"
Write-Host ''
Write-Host 'At the air-gapped site (verify the checksum first):'
Write-Host " docker load -i $stem.tar.gz"
Write-Host " # set IMAGE_TAG=$Version in .env, then:"
Write-Host ' docker compose -f docker-compose.airgap.yml up -d'
}
finally {
Pop-Location
}