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}
|
||||
ZABBIX_URL: ${ZABBIX_URL:-}
|
||||
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:
|
||||
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