Files
shopdb-flask/scripts/build-offline-bundle.ps1
Cam Proudlock d267813999 Printer assignment end to end, and a container that keeps a site's files
Assign printers to a MACHINE and let whichever PC controls it install them, with
the observed half kept strictly apart from the assigned half: a collector report
can never write an assignment row. Adoption is explicit. Adds a singular
relationship type so defaultprinter can mean exactly one, client scripts that
converge a bay's queues and correct a drifted one in place, and a wave gate so a
fleet-wide driver rollout cannot stampede its own share.

On the container side: the compose stack never persisted the instance directory,
so a rebuild discarded plugins.json and every upload. It is a volume now, the
image ships instance/ owned by the app user so the mountpoint is writable, and
the database moves to MySQL 8.4 LTS, which the Windows runbook has recommended
since 8.0 went end of life. The image also could not build at all: the frontend
stage never copied the plugin staging script it runs.

Adds a topology migration guide covering IIS to Docker and back.
2026-08-20 07:46:35 -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
}