add channel-aware distributable installers and web launcher
Follows the config-driven-badge decision through to packaging: a single channel-parameterized builder emits two distinct installers (stable from main, edge from dev-be) rather than two drifting scripts. Each package ships the image bundle, the airgap+channel compose templates, a pre-filled .env, and a one-command site installer. Secrets ship as CHANGE_ME_* sentinels and Install-ShopDB.ps1 refuses to start until every one is rotated, so a distributable can never come up on shipped credentials. docker-compose.airgap.yml now threads SHOPDB_ENV_LABEL through the shared app-env anchor, otherwise a distributed edge stack would run the badge-capable image but never light the badge. get.ps1 is the web launcher that makes the iex one-liners work: dev-bootstrap.ps1 operates on a local clone, so get.ps1 clones/updates the channel branch and hands off. docs/DEV-BUILD-TEST.md is the short reviewer handout with the two one-liners for Cameron to try.
This commit is contained in:
@@ -32,6 +32,10 @@ x-app-env: &app-env
|
|||||||
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
||||||
ZABBIX_URL: ${ZABBIX_URL:-}
|
ZABBIX_URL: ${ZABBIX_URL:-}
|
||||||
ZABBIX_TOKEN: ${ZABBIX_TOKEN:-}
|
ZABBIX_TOKEN: ${ZABBIX_TOKEN:-}
|
||||||
|
# Channel label for a distributable installer. Empty on the stable (main)
|
||||||
|
# bundle, 'DEV' on the edge (dev-be) bundle, which lights up the sidebar
|
||||||
|
# badge. Sourced from .env, which build-channel-installer.ps1 pre-fills.
|
||||||
|
SHOPDB_ENV_LABEL: ${SHOPDB_ENV_LABEL:-}
|
||||||
|
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
|
|||||||
26
docs/DEV-BUILD-TEST.md
Normal file
26
docs/DEV-BUILD-TEST.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# ShopDB dev build - quick test
|
||||||
|
|
||||||
|
Two dev builds you can stand up on a Windows box with Docker Desktop. Each is a
|
||||||
|
single PowerShell line: it clones the repo, checks your dependencies (offers to
|
||||||
|
winget-install anything missing), then builds and starts the stack in Docker.
|
||||||
|
|
||||||
|
## Stable (main)
|
||||||
|
|
||||||
|
$env:SHOPDB_CHANNEL='stable'; iex (irm https://raw.githubusercontent.com/ge-aero/shopdb-flask/wilm/scripts/get.ps1)
|
||||||
|
|
||||||
|
Comes up at https://localhost:5001. No DEV badge.
|
||||||
|
|
||||||
|
## Bleeding edge (dev-be)
|
||||||
|
|
||||||
|
$env:SHOPDB_CHANNEL='edge'; iex (irm https://raw.githubusercontent.com/ge-aero/shopdb-flask/wilm/scripts/get.ps1)
|
||||||
|
|
||||||
|
Comes up at https://localhost:5011. Red DEV badge in the sidebar.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- You need Git and Docker Desktop. If Git is missing, install it first
|
||||||
|
(`winget install Git.Git`); the script handles the rest with a prompt.
|
||||||
|
- Both builds can run at the same time - they use different ports and separate
|
||||||
|
Docker volumes, so they do not collide and each keeps its own database.
|
||||||
|
- First page load shows a self-signed cert warning (expected for a dev box).
|
||||||
|
- Stop a stack: `docker compose -p shopdb-stable down` (or `shopdb-edge`).
|
||||||
346
scripts/build-channel-installer.ps1
Normal file
346
scripts/build-channel-installer.ps1
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Build a channel-aware ShopDB distributable installer (stable or edge).
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Produces ONE self-contained installer package for the requested channel:
|
||||||
|
|
||||||
|
stable -> built from 'main', no DEV badge (SHOPDB_ENV_LABEL empty).
|
||||||
|
edge -> built from 'dev-be', red DEV badge (SHOPDB_ENV_LABEL=DEV).
|
||||||
|
|
||||||
|
This is the distributable counterpart to scripts/dev-bootstrap.ps1 (which
|
||||||
|
stands a dev box up FROM the repo). Here the operator has no repo: the script
|
||||||
|
builds the app image from the channel branch on a connected build box, saves
|
||||||
|
it plus MySQL into a loadable tarball (reusing the exact bundle mechanics of
|
||||||
|
build-offline-bundle.ps1), and drops a runnable package:
|
||||||
|
|
||||||
|
dist/shopdb-<channel>-<version>/
|
||||||
|
shopdb-stack-<version>.tar.gz # docker load input
|
||||||
|
shopdb-stack-<version>.tar.gz.sha256
|
||||||
|
docker-compose.airgap.yml # image-only run template
|
||||||
|
docker-compose.channel.yml # SHOPDB_ENV_LABEL overlay
|
||||||
|
.env # channel-filled, secrets as CHANGE_ME_* sentinels
|
||||||
|
Install-ShopDB.ps1 # site-side one-command installer
|
||||||
|
README.txt
|
||||||
|
|
||||||
|
Keeping stable and edge as ONE parameterized builder (not two scripts) is the
|
||||||
|
same anti-drift decision as the config-driven badge: the channels differ only
|
||||||
|
by branch, image tag, and label - all data, not code.
|
||||||
|
|
||||||
|
.PARAMETER Channel
|
||||||
|
'stable' (main) or 'edge' (dev-be). Required.
|
||||||
|
|
||||||
|
.PARAMETER Version
|
||||||
|
Image tag / bundle version. Defaults to a channel-prefixed date stamp so two
|
||||||
|
channels built the same day never collide (e.g. 'stable-2024.06.01').
|
||||||
|
|
||||||
|
.PARAMETER Ref
|
||||||
|
Override the git ref to build. Defaults to 'main' for stable, 'dev-be' for
|
||||||
|
edge. Lets a release engineer cut an installer from a tag or fix branch.
|
||||||
|
|
||||||
|
.PARAMETER MysqlImage
|
||||||
|
MySQL image the stack runs (default mysql:8.4). Must match db.image in
|
||||||
|
docker-compose.airgap.yml.
|
||||||
|
|
||||||
|
.PARAMETER OutDir
|
||||||
|
Root for the generated package (default ./dist).
|
||||||
|
|
||||||
|
.PARAMETER SkipBuild
|
||||||
|
Reuse an already-built shopdb-flask:<Version> image; only save + package.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
pwsh scripts/build-channel-installer.ps1 -Channel stable
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
pwsh scripts/build-channel-installer.ps1 -Channel edge -Version edge-preview3
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
ASCII only, no emoji or smart punctuation, per repo style. PowerShell 5.1+.
|
||||||
|
Requires Docker Desktop and (unless -SkipBuild) clean access to build the
|
||||||
|
image. Secrets ship as CHANGE_ME_* placeholders and the generated installer
|
||||||
|
refuses to start until they are rotated, so a package can never go live on
|
||||||
|
shipped credentials.
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[ValidateSet('stable', 'edge')]
|
||||||
|
[string]$Channel,
|
||||||
|
|
||||||
|
[string]$Version,
|
||||||
|
|
||||||
|
[string]$Ref,
|
||||||
|
|
||||||
|
[string]$MysqlImage = 'mysql:8.4',
|
||||||
|
|
||||||
|
[string]$OutDir = 'dist',
|
||||||
|
|
||||||
|
[switch]$SkipBuild
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Channel -> (branch, badge label). The single source of channel truth, mirrored
|
||||||
|
# from dev-bootstrap.ps1 so the two entry points never disagree about what a
|
||||||
|
# channel means.
|
||||||
|
$channels = @{
|
||||||
|
stable = @{ Ref = 'main'; EnvLabel = '' }
|
||||||
|
edge = @{ Ref = 'dev-be'; EnvLabel = 'DEV' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-LastExit([string]$What) {
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "$What failed (exit $LASTEXITCODE)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
|
||||||
|
throw 'docker not found on PATH. Install Docker Desktop and retry.'
|
||||||
|
}
|
||||||
|
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||||
|
throw 'git not found on PATH. Install Git and retry.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = $channels[$Channel]
|
||||||
|
$branch = if ($Ref) { $Ref } else { $config.Ref }
|
||||||
|
if (-not $Version) { $Version = "$Channel-$(Get-Date -Format 'yyyy.MM.dd')" }
|
||||||
|
|
||||||
|
$appImage = "shopdb-flask:$Version"
|
||||||
|
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
|
||||||
|
Write-Host "ShopDB channel installer builder" -ForegroundColor White
|
||||||
|
Write-Host " Channel : $Channel" -ForegroundColor White
|
||||||
|
Write-Host " Branch : $branch" -ForegroundColor White
|
||||||
|
Write-Host " Version : $Version" -ForegroundColor White
|
||||||
|
Write-Host " Label : $(if ($config.EnvLabel) { $config.EnvLabel } else { '(none)' })" -ForegroundColor White
|
||||||
|
|
||||||
|
Push-Location $repoRoot
|
||||||
|
try {
|
||||||
|
# --- 1. Build the image from the channel branch --------------------------
|
||||||
|
|
||||||
|
if (-not $SkipBuild) {
|
||||||
|
# Build from the channel branch WITHOUT switching the operator's working
|
||||||
|
# tree: create a throwaway worktree at the ref, build there, remove it.
|
||||||
|
# This keeps a release build reproducible and side-effect-free even when
|
||||||
|
# the operator has local edits checked out.
|
||||||
|
$worktree = Join-Path ([System.IO.Path]::GetTempPath()) "shopdb-build-$Version"
|
||||||
|
Write-Host "==> Fetching origin/$branch" -ForegroundColor Cyan
|
||||||
|
git fetch origin $branch
|
||||||
|
Assert-LastExit 'git fetch'
|
||||||
|
|
||||||
|
if (Test-Path $worktree) {
|
||||||
|
git worktree remove --force $worktree 2>$null
|
||||||
|
Remove-Item $worktree -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
Write-Host "==> Adding build worktree at $worktree" -ForegroundColor Cyan
|
||||||
|
git worktree add --detach $worktree "origin/$branch"
|
||||||
|
Assert-LastExit 'git worktree add'
|
||||||
|
|
||||||
|
try {
|
||||||
|
Write-Host "==> Building $appImage from $branch" -ForegroundColor Cyan
|
||||||
|
docker build -t $appImage $worktree
|
||||||
|
Assert-LastExit 'docker build'
|
||||||
|
} finally {
|
||||||
|
Write-Host "==> Removing build worktree" -ForegroundColor Cyan
|
||||||
|
git worktree remove --force $worktree 2>$null
|
||||||
|
Remove-Item $worktree -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "==> Skipping build; reusing $appImage" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "==> Pulling $MysqlImage" -ForegroundColor Cyan
|
||||||
|
docker pull $MysqlImage
|
||||||
|
Assert-LastExit 'docker pull'
|
||||||
|
|
||||||
|
# --- 2. Save image bundle into the package dir ---------------------------
|
||||||
|
|
||||||
|
$pkgName = "shopdb-$Channel-$Version"
|
||||||
|
$pkgDir = Join-Path $OutDir $pkgName
|
||||||
|
if (Test-Path $pkgDir) { Remove-Item $pkgDir -Recurse -Force }
|
||||||
|
New-Item -ItemType Directory -Path $pkgDir -Force | Out-Null
|
||||||
|
|
||||||
|
$stem = "shopdb-stack-$Version"
|
||||||
|
$tar = Join-Path $pkgDir "$stem.tar"
|
||||||
|
$gz = Join-Path $pkgDir "$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()
|
||||||
|
"$hash $stem.tar.gz" | Set-Content -Path "$gz.sha256" -NoNewline
|
||||||
|
|
||||||
|
# --- 3. Copy the run templates into the package -------------------------
|
||||||
|
|
||||||
|
Copy-Item (Join-Path $repoRoot 'docker-compose.airgap.yml') $pkgDir
|
||||||
|
Copy-Item (Join-Path $repoRoot 'docker-compose.channel.yml') $pkgDir
|
||||||
|
|
||||||
|
# --- 4. Channel-filled .env ---------------------------------------------
|
||||||
|
# Secrets ship as explicit CHANGE_ME_* sentinels, NOT generated values. The
|
||||||
|
# site-side installer refuses to start until every sentinel is replaced, so a
|
||||||
|
# distributable can never silently go live on shipped credentials. Ports,
|
||||||
|
# image tag, and the channel label are real because they are not secrets.
|
||||||
|
|
||||||
|
$apiPort = if ($Channel -eq 'edge') { 5011 } else { 5001 }
|
||||||
|
$mysqlPort = if ($Channel -eq 'edge') { 3316 } else { 3306 }
|
||||||
|
|
||||||
|
$envLines = @(
|
||||||
|
"# ShopDB $Channel installer .env (built $(Get-Date -Format s)).",
|
||||||
|
"# REQUIRED: replace every CHANGE_ME_* value below before installing.",
|
||||||
|
"# Generate strong secrets, e.g.:",
|
||||||
|
"# python -c ""import secrets; print(secrets.token_urlsafe(64))""",
|
||||||
|
"FLASK_APP=wsgi.py",
|
||||||
|
"FLASK_ENV=production",
|
||||||
|
"SECRET_KEY=CHANGE_ME_SECRET_KEY",
|
||||||
|
"JWT_SECRET_KEY=CHANGE_ME_JWT_SECRET_KEY",
|
||||||
|
"DATABASE_URL=mysql+pymysql://shopdb:CHANGE_ME_DB_PASSWORD@db:3306/shopdb_flask?charset=utf8mb4",
|
||||||
|
"CORS_ORIGINS=https://localhost:$apiPort",
|
||||||
|
"LOG_LEVEL=INFO",
|
||||||
|
"MYSQL_ROOT_PASSWORD=CHANGE_ME_ROOT_PASSWORD",
|
||||||
|
"MYSQL_PASSWORD=CHANGE_ME_DB_PASSWORD",
|
||||||
|
"MYSQL_PORT=$mysqlPort",
|
||||||
|
"API_PORT=$apiPort",
|
||||||
|
"IMAGE_TAG=$Version",
|
||||||
|
"SHOPDB_ENV_LABEL=$($config.EnvLabel)"
|
||||||
|
)
|
||||||
|
Set-Content -Path (Join-Path $pkgDir '.env') -Value $envLines -Encoding ASCII
|
||||||
|
|
||||||
|
# --- 5. Site-side installer script --------------------------------------
|
||||||
|
# One command at the site: verify checksum, load the image, bring the stack
|
||||||
|
# up with the airgap template + channel overlay. Mirrors the run steps in
|
||||||
|
# docs/DEPLOY-AIRGAP.md but wraps them so an operator runs a single file.
|
||||||
|
|
||||||
|
$installer = @'
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Install / start this ShopDB stack on the target host (one command).
|
||||||
|
.DESCRIPTION
|
||||||
|
Verifies the image-bundle checksum, docker-loads it, and brings the stack up
|
||||||
|
from the bundled compose templates and .env. Re-runnable: on later runs it
|
||||||
|
skips the load if the image is already present and just applies the compose.
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param([switch]$Recreate)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Set-Location $PSScriptRoot
|
||||||
|
|
||||||
|
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
|
||||||
|
throw 'docker not found. Install Docker Desktop / Engine and retry.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle = Get-ChildItem -Filter 'shopdb-stack-*.tar.gz' | Select-Object -First 1
|
||||||
|
if (-not $bundle) { throw 'No shopdb-stack-*.tar.gz found next to this script.' }
|
||||||
|
|
||||||
|
# Refuse to start on shipped placeholder secrets. A distributable must never go
|
||||||
|
# live on credentials that ship in the box; the operator has to rotate every
|
||||||
|
# CHANGE_ME_* in .env first. This is a hard gate, not a warning.
|
||||||
|
$sentinels = Select-String -Path '.env' -Pattern 'CHANGE_ME_' -SimpleMatch
|
||||||
|
if ($sentinels) {
|
||||||
|
Write-Host 'Refusing to start: .env still has placeholder secrets.' -ForegroundColor Red
|
||||||
|
Write-Host 'Replace every CHANGE_ME_* value with a real secret, then re-run:'
|
||||||
|
foreach ($line in $sentinels) { Write-Host " $($line.Line)" -ForegroundColor Yellow }
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'Generate strong values, e.g.:'
|
||||||
|
Write-Host ' python -c "import secrets; print(secrets.token_urlsafe(64))"'
|
||||||
|
throw 'Placeholder secrets present in .env.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$sumFile = "$($bundle.Name).sha256"
|
||||||
|
if (Test-Path $sumFile) {
|
||||||
|
Write-Host "==> Verifying $($bundle.Name) checksum" -ForegroundColor Cyan
|
||||||
|
$expected = ((Get-Content $sumFile -Raw).Trim() -split '\s+')[0].ToLower()
|
||||||
|
$actual = (Get-FileHash $bundle.FullName -Algorithm SHA256).Hash.ToLower()
|
||||||
|
if ($expected -ne $actual) { throw "Checksum mismatch. Expected $expected, got $actual." }
|
||||||
|
Write-Host " OK" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "==> No checksum file; skipping verification" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "==> Loading image bundle" -ForegroundColor Cyan
|
||||||
|
docker load -i $bundle.FullName
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "docker load failed ($LASTEXITCODE)." }
|
||||||
|
|
||||||
|
$composeArgs = @(
|
||||||
|
'compose', '--env-file', '.env',
|
||||||
|
'-f', 'docker-compose.airgap.yml',
|
||||||
|
'-f', 'docker-compose.channel.yml'
|
||||||
|
)
|
||||||
|
if ($Recreate) { $composeArgs += @('up', '-d', '--force-recreate') }
|
||||||
|
else { $composeArgs += @('up', '-d') }
|
||||||
|
|
||||||
|
Write-Host "==> Starting the stack" -ForegroundColor Cyan
|
||||||
|
& docker @composeArgs
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "docker compose up failed ($LASTEXITCODE)." }
|
||||||
|
|
||||||
|
$apiPort = (Select-String -Path '.env' -Pattern '^API_PORT=(.+)$').Matches.Groups[1].Value
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host "ShopDB is up: https://localhost:$apiPort" -ForegroundColor Green
|
||||||
|
Write-Host "Seed the first admin (once):"
|
||||||
|
Write-Host " docker compose --env-file .env -f docker-compose.airgap.yml exec api flask seed admin <user> <email> <password>"
|
||||||
|
'@
|
||||||
|
Set-Content -Path (Join-Path $pkgDir 'Install-ShopDB.ps1') -Value $installer -Encoding ASCII
|
||||||
|
|
||||||
|
# --- 6. README ----------------------------------------------------------
|
||||||
|
|
||||||
|
$badgeLine = if ($config.EnvLabel) {
|
||||||
|
"This is the EDGE (bleeding-edge) build from '$branch'. The sidebar shows a red '$($config.EnvLabel)' badge."
|
||||||
|
} else {
|
||||||
|
"This is the STABLE build from '$branch'. No DEV badge."
|
||||||
|
}
|
||||||
|
$readme = @(
|
||||||
|
"ShopDB $Channel installer ($Version)",
|
||||||
|
"==================================================",
|
||||||
|
"",
|
||||||
|
$badgeLine,
|
||||||
|
"",
|
||||||
|
"Contents:",
|
||||||
|
" shopdb-stack-$Version.tar.gz Docker image bundle (app + MySQL)",
|
||||||
|
" shopdb-stack-$Version.tar.gz.sha256 Checksum for the bundle",
|
||||||
|
" docker-compose.airgap.yml Image-only run template",
|
||||||
|
" docker-compose.channel.yml Channel label overlay",
|
||||||
|
" .env Pre-filled config (ROTATE SECRETS)",
|
||||||
|
" Install-ShopDB.ps1 One-command installer",
|
||||||
|
"",
|
||||||
|
"Install (Windows, from this folder):",
|
||||||
|
" 1. Edit .env: replace every CHANGE_ME_* value with a real secret.",
|
||||||
|
" 2. powershell -ExecutionPolicy Bypass -File .\Install-ShopDB.ps1",
|
||||||
|
"",
|
||||||
|
"The installer REFUSES to start while any CHANGE_ME_* placeholder remains,",
|
||||||
|
"so the stack can never come up on shipped credentials.",
|
||||||
|
"",
|
||||||
|
"The API listens on https://localhost:$apiPort (self-signed cert on first",
|
||||||
|
"run). Adjust the port or CORS origins in .env if needed. Both channels can",
|
||||||
|
"run on one host at once because stable and edge use different ports and",
|
||||||
|
"compose project state.",
|
||||||
|
"",
|
||||||
|
"Generate strong secrets with:",
|
||||||
|
" python -c ""import secrets; print(secrets.token_urlsafe(64))"""
|
||||||
|
)
|
||||||
|
Set-Content -Path (Join-Path $pkgDir 'README.txt') -Value $readme -Encoding ASCII
|
||||||
|
|
||||||
|
# --- 7. Summary ---------------------------------------------------------
|
||||||
|
|
||||||
|
$sizeMb = [math]::Round((Get-Item $gz).Length / 1MB, 1)
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host "Installer package ready:" -ForegroundColor Green
|
||||||
|
Write-Host " $pkgDir"
|
||||||
|
Write-Host " bundle : $stem.tar.gz ($sizeMb MB)"
|
||||||
|
Write-Host " sha256 : $hash"
|
||||||
|
Write-Host " label : $(if ($config.EnvLabel) { $config.EnvLabel } else { '(none, stable)' })"
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host "Ship the folder; the operator runs Install-ShopDB.ps1."
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
67
scripts/get.ps1
Normal file
67
scripts/get.ps1
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
One-line web launcher for a ShopDB dev workstation.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
The piece that makes `iex (irm .../get.ps1)` work. dev-bootstrap.ps1 operates
|
||||||
|
on a local clone, so it cannot be run straight from the web. This launcher
|
||||||
|
fills that gap: it clones (or updates) the repo at the channel branch into a
|
||||||
|
stable local path, then hands off to scripts/dev-bootstrap.ps1 for the real
|
||||||
|
dependency-check / build / up work.
|
||||||
|
|
||||||
|
Channel is chosen by an environment variable so a pure `iex` pipe (which
|
||||||
|
cannot pass parameters) still selects stable vs edge:
|
||||||
|
|
||||||
|
# Stable (main):
|
||||||
|
$env:SHOPDB_CHANNEL='stable'; iex (irm https://raw.githubusercontent.com/ge-aero/shopdb-flask/wilm/scripts/get.ps1)
|
||||||
|
|
||||||
|
# Bleeding edge (dev-be):
|
||||||
|
$env:SHOPDB_CHANNEL='edge'; iex (irm https://raw.githubusercontent.com/ge-aero/shopdb-flask/wilm/scripts/get.ps1)
|
||||||
|
|
||||||
|
SHOPDB_DIR overrides the clone location (default %USERPROFILE%\shopdb-dev).
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
ASCII only, no emoji or smart punctuation, per repo style. PowerShell 5.1+.
|
||||||
|
Runs git-checkout of the channel branch, so it needs Git; dev-bootstrap.ps1
|
||||||
|
offers to winget-install anything else that is missing.
|
||||||
|
#>
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$repoUrl = 'https://github.com/ge-aero/shopdb-flask.git'
|
||||||
|
$channel = if ($env:SHOPDB_CHANNEL) { $env:SHOPDB_CHANNEL } else { 'stable' }
|
||||||
|
if ($channel -notin @('stable', 'edge')) {
|
||||||
|
throw "SHOPDB_CHANNEL must be 'stable' or 'edge' (got '$channel')."
|
||||||
|
}
|
||||||
|
$branch = if ($channel -eq 'edge') { 'dev-be' } else { 'main' }
|
||||||
|
$target = if ($env:SHOPDB_DIR) { $env:SHOPDB_DIR } else { Join-Path $env:USERPROFILE 'shopdb-dev' }
|
||||||
|
|
||||||
|
Write-Host "ShopDB web launcher" -ForegroundColor White
|
||||||
|
Write-Host " Channel : $channel ($branch)" -ForegroundColor White
|
||||||
|
Write-Host " Target : $target" -ForegroundColor White
|
||||||
|
|
||||||
|
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||||
|
throw 'git is required to clone the repo. Install Git (winget install Git.Git) and re-run.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path (Join-Path $target '.git')) {
|
||||||
|
Write-Host "==> Updating existing clone" -ForegroundColor Cyan
|
||||||
|
git -C $target fetch origin $branch
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "git fetch failed ($LASTEXITCODE)." }
|
||||||
|
git -C $target checkout $branch
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "git checkout failed ($LASTEXITCODE)." }
|
||||||
|
git -C $target pull --ff-only origin $branch
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "git pull failed ($LASTEXITCODE)." }
|
||||||
|
} else {
|
||||||
|
Write-Host "==> Cloning $branch" -ForegroundColor Cyan
|
||||||
|
git clone --branch $branch $repoUrl $target
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "git clone failed ($LASTEXITCODE)." }
|
||||||
|
}
|
||||||
|
|
||||||
|
$bootstrap = Join-Path $target 'scripts\dev-bootstrap.ps1'
|
||||||
|
if (-not (Test-Path $bootstrap)) {
|
||||||
|
throw "dev-bootstrap.ps1 not found at $bootstrap. Is the branch correct?"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "==> Handing off to dev-bootstrap.ps1" -ForegroundColor Cyan
|
||||||
|
& $bootstrap -Channel $channel
|
||||||
Reference in New Issue
Block a user