Merge origin/main into align-wilm-main (align wilm with Cameron's latest)
Bring main's endpoint hardening, operator-report validation, launch-path policy, and theme-variable CSS into wilm. Channel model reconciled to this repo's shipping (stable=release tag, edge=main trunk) while keeping wilm's remote-BuildKit publish step and GHCR pull-based launcher.
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
GCM handles those.
|
||||
|
||||
.PARAMETER Channel
|
||||
'stable' (main) or 'edge' (dev-be). Defaults to 'stable'.
|
||||
'stable' (the newest release tag) or 'edge' (main, the trunk). Defaults to 'stable'.
|
||||
|
||||
.PARAMETER TargetDir
|
||||
Where to clone. Defaults to %USERPROFILE%\shopdb-dev, a separate working copy
|
||||
@@ -85,12 +85,28 @@ function Clear-InterruptedRun {
|
||||
}
|
||||
}
|
||||
|
||||
# A directory that exists but has no valid .git is a broken/partial clone;
|
||||
# remove it so the clone below can start fresh instead of failing.
|
||||
if ((Test-Path $Target) -and -not (Test-Path (Join-Path $Target '.git'))) {
|
||||
# A directory that exists but has no valid .git is a broken/partial clone -
|
||||
# USUALLY. It is also what -TargetDir pointed at somebody's Documents folder
|
||||
# looks like, and this ran Remove-Item -Recurse -Force on it without asking.
|
||||
# For a script distributed as "save it anywhere and run it", a mistyped
|
||||
# -TargetDir must not be able to delete a user's files.
|
||||
#
|
||||
# Only reclaim a directory this script can show it owns: empty, or carrying
|
||||
# the marker written after a successful clone. Anything else stops.
|
||||
if ((Test-Path $Target) -and -not (Test-Path (Join-Path $Target '.git'))) {
|
||||
$ours = Test-Path (Join-Path $Target $script:CloneMarker)
|
||||
$empty = -not (Get-ChildItem -LiteralPath $Target -Force |
|
||||
Select-Object -First 1)
|
||||
if ($ours -or $empty) {
|
||||
Write-Host "==> Removing incomplete clone at $Target" -ForegroundColor Cyan
|
||||
Remove-Item -Path $Target -Recurse -Force
|
||||
}
|
||||
else {
|
||||
throw ("$Target already exists, is not empty, and was not created " +
|
||||
"by this launcher. Refusing to delete it. Pass -TargetDir " +
|
||||
'pointing somewhere else, or remove it yourself if you are sure.')
|
||||
}
|
||||
}
|
||||
|
||||
# A clone created by an elevated (Administrator) run is owned by
|
||||
# BUILTIN\Administrators; a later non-elevated run then trips git's
|
||||
@@ -104,8 +120,18 @@ function Clear-InterruptedRun {
|
||||
try { $probe = git -C $Target rev-parse --is-inside-work-tree 2>&1 } catch { $probe = "$_" }
|
||||
finally { $ErrorActionPreference = $previous }
|
||||
if ($probe -match 'dubious ownership') {
|
||||
Write-Host "==> Removing clone with mismatched ownership at $Target (was it created from an elevated prompt?)" -ForegroundColor Cyan
|
||||
Remove-Item -Path $Target -Recurse -Force
|
||||
# Same rule as above: a .git directory proves this is a clone, but
|
||||
# not that it is OUR clone, and re-cloning discards whatever is in
|
||||
# it. Only reclaim one this launcher created.
|
||||
if (Test-Path (Join-Path $Target $script:CloneMarker)) {
|
||||
Write-Host "==> Removing clone with mismatched ownership at $Target (was it created from an elevated prompt?)" -ForegroundColor Cyan
|
||||
Remove-Item -Path $Target -Recurse -Force
|
||||
}
|
||||
else {
|
||||
throw ("The git clone at $Target has mismatched ownership and " +
|
||||
'was not created by this launcher, so it is not being ' +
|
||||
'deleted. Remove it yourself, or pass -TargetDir elsewhere.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,7 +140,21 @@ function Clear-InterruptedRun {
|
||||
# stored GitHub credential for github.com, so clone/fetch authenticate against
|
||||
# the private repo without anything sensitive touching this script or disk.
|
||||
$repoUrl = 'https://github.com/ge-aero/shopdb-flask.git'
|
||||
$branch = if ($Channel -eq 'edge') { 'dev-be' } else { 'main' }
|
||||
# Written into a clone this launcher made, so a later run can tell its own
|
||||
# working directory apart from one that happens to sit at the same path.
|
||||
$script:CloneMarker = '.shopdb-launcher'
|
||||
# Channels follow how this repo ships: a release is a TAG, the trunk is main.
|
||||
# Upstream this read 'dev-be' for edge, which is that repo's trunk branch and
|
||||
# does not exist here - the launcher would have cloned a missing branch.
|
||||
$branch = 'main'
|
||||
if ($Channel -ne 'edge') {
|
||||
# Newest release tag, so 'stable' means a thing that was released rather
|
||||
# than whatever the trunk happens to be today.
|
||||
$tag = (git ls-remote --tags --refs --sort='-v:refname' $repoUrl 'v*' |
|
||||
Select-Object -First 1) -replace '.*refs/tags/', ''
|
||||
if ($tag) { $branch = $tag }
|
||||
else { Write-Warning "No v* tag found; falling back to main for 'stable'." }
|
||||
}
|
||||
$target = if ($TargetDir) { $TargetDir } else { Join-Path $env:USERPROFILE 'shopdb-dev' }
|
||||
|
||||
Write-Host "ShopDB dev launcher" -ForegroundColor White
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
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).
|
||||
edge -> built from 'main' (the trunk), 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
|
||||
@@ -28,14 +28,14 @@
|
||||
by branch, image tag, and label - all data, not code.
|
||||
|
||||
.PARAMETER Channel
|
||||
'stable' (main) or 'edge' (dev-be). Required.
|
||||
'stable' (the newest release tag) or 'edge' (main, the trunk). 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
|
||||
Override the git ref to build. Defaults to the newest v* tag for stable, 'main' for
|
||||
edge. Lets a release engineer cut an installer from a tag or fix branch.
|
||||
|
||||
.PARAMETER MysqlImage
|
||||
@@ -80,12 +80,28 @@ param(
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Channel -> (branch, badge label). The single source of channel truth, mirrored
|
||||
# Channel -> (ref, 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.
|
||||
#
|
||||
# stable has NO fixed ref: a release here is a tag, not a branch, so it is
|
||||
# resolved below from the newest v* tag. Writing 'main' here would build the
|
||||
# trunk and hand it to a site under a word that promises a release - the same
|
||||
# mislabel this repo's channel mapping was corrected for, missed in this file
|
||||
# the first time round.
|
||||
$channels = @{
|
||||
stable = @{ Ref = 'main'; EnvLabel = '' }
|
||||
edge = @{ Ref = 'dev-be'; EnvLabel = 'DEV' }
|
||||
stable = @{ Ref = $null; EnvLabel = '' }
|
||||
edge = @{ Ref = 'main'; EnvLabel = 'DEV' }
|
||||
}
|
||||
|
||||
function Resolve-StableRef {
|
||||
# Newest release tag by version sort. Falls back to main with a warning
|
||||
# rather than silently building something and calling it stable.
|
||||
$tag = (git tag --list 'v*' --sort='-v:refname' | Select-Object -First 1)
|
||||
if ($tag) { return $tag }
|
||||
Write-Warning 'No v* tag in this repo; building stable from main.'
|
||||
return 'main'
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-LastExit([string]$What) {
|
||||
@@ -100,7 +116,11 @@ if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||
}
|
||||
|
||||
$config = $channels[$Channel]
|
||||
$branch = if ($Ref) { $Ref } else { $config.Ref }
|
||||
# An explicit -Ref always wins. Otherwise the channel decides: edge is the
|
||||
# trunk, stable resolves the newest release tag.
|
||||
$branch = if ($Ref) { $Ref }
|
||||
elseif ($config.Ref) { $config.Ref }
|
||||
else { Resolve-StableRef }
|
||||
if (-not $Version) { $Version = "$Channel-$(Get-Date -Format 'yyyy.MM.dd')" }
|
||||
|
||||
$appImage = "shopdb-flask:$Version"
|
||||
@@ -271,8 +291,15 @@ Write-Host "==> Loading image bundle" -ForegroundColor Cyan
|
||||
docker load -i $bundle.FullName
|
||||
if ($LASTEXITCODE -ne 0) { throw "docker load failed ($LASTEXITCODE)." }
|
||||
|
||||
# -p is NOT optional here. Without it compose names the project after the
|
||||
# containing folder, which is shopdb-<channel>-<version> - so installing the
|
||||
# next version creates a NEW project with new empty named volumes, and the site
|
||||
# comes up looking wiped. The old data is still there under the old project's
|
||||
# volumes, but nothing carries it across. Pin the project to the channel so an
|
||||
# upgrade lands on the same volumes.
|
||||
$composeArgs = @(
|
||||
'compose', '--env-file', '.env',
|
||||
'-p', 'shopdb-__CHANNEL__',
|
||||
'-f', 'docker-compose.airgap.yml',
|
||||
'-f', 'docker-compose.channel.yml'
|
||||
)
|
||||
@@ -289,6 +316,11 @@ 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>"
|
||||
'@
|
||||
# The here-string above is single-quoted so nothing in it interpolates - that
|
||||
# is deliberate, the installer is full of $ that must survive verbatim. The
|
||||
# compose project name is the one value that has to be baked per channel, so
|
||||
# substitute it here rather than switching the whole block to interpolation.
|
||||
$installer = $installer.Replace('__CHANNEL__', $Channel)
|
||||
Set-Content -Path (Join-Path $pkgDir 'Install-ShopDB.ps1') -Value $installer -Encoding ASCII
|
||||
|
||||
# --- 6. README ----------------------------------------------------------
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
passed, because the app itself runs entirely in the container.
|
||||
2. Picks a channel:
|
||||
stable -> pulls the 'stable' image (built from main), no DEV badge.
|
||||
edge -> pulls the 'edge' image (built from dev-be), red DEV badge.
|
||||
edge -> pulls the 'edge' image (built from main), red DEV badge.
|
||||
The two channels use different compose project names, host ports, and
|
||||
named volumes, so they run side by side without colliding.
|
||||
3. Generates a .env for the channel (dev-grade secrets, NOT for production)
|
||||
@@ -25,10 +25,10 @@
|
||||
installer - see deploy/windows/installer/shopdb-install.ps1 for that.
|
||||
|
||||
.PARAMETER Channel
|
||||
'stable' (main) or 'edge' (dev-be). Defaults to 'stable'.
|
||||
'stable' (the newest release tag) or 'edge' (main, the trunk). Defaults to 'stable'.
|
||||
|
||||
.PARAMETER Ref
|
||||
Override the git ref to build. Defaults to 'main' for stable, 'dev-be' for
|
||||
Override the git ref to build. Defaults to the newest v* tag for stable, 'main' for
|
||||
edge. Retained for compatibility; the pull flow selects the image by channel
|
||||
tag, so this now only affects which branch the working tree is prepared on.
|
||||
|
||||
@@ -74,7 +74,7 @@ $channels = @{
|
||||
ImageTag = 'stable'
|
||||
}
|
||||
edge = @{
|
||||
Ref = 'dev-be'
|
||||
Ref = 'main'
|
||||
Project = 'shopdb-edge'
|
||||
ApiPort = 5011
|
||||
MysqlPort = 3316
|
||||
@@ -282,10 +282,20 @@ try {
|
||||
} else {
|
||||
# Dev-grade secrets: generated locally, never for production. A real
|
||||
# deploy fills .env from the wiki runbook, not from this script.
|
||||
$secret = [Convert]::ToBase64String((1..48 | ForEach-Object { Get-Random -Maximum 256 }))
|
||||
$jwt = [Convert]::ToBase64String((1..48 | ForEach-Object { Get-Random -Maximum 256 }))
|
||||
$dbPass = [Convert]::ToBase64String((1..18 | ForEach-Object { Get-Random -Maximum 256 })) -replace '[^A-Za-z0-9]', 'x'
|
||||
$rootPass = [Convert]::ToBase64String((1..18 | ForEach-Object { Get-Random -Maximum 256 })) -replace '[^A-Za-z0-9]', 'x'
|
||||
#
|
||||
# Get-Random is System.Random: a 32-bit seed, and all four values came
|
||||
# off one stream, so recovering the seed from any one of them recovers
|
||||
# the other three. These sign JWTs for a stack a pilot may point at a
|
||||
# real imported dataset, so they come from the CSPRNG instead. "Dev
|
||||
# grade" is about rotation and provenance, not about being guessable.
|
||||
$secret = [Convert]::ToBase64String(
|
||||
[System.Security.Cryptography.RandomNumberGenerator]::GetBytes(48))
|
||||
$jwt = [Convert]::ToBase64String(
|
||||
[System.Security.Cryptography.RandomNumberGenerator]::GetBytes(48))
|
||||
$dbPass = ([Convert]::ToBase64String(
|
||||
[System.Security.Cryptography.RandomNumberGenerator]::GetBytes(18))) -replace '[^A-Za-z0-9]', 'x'
|
||||
$rootPass = ([Convert]::ToBase64String(
|
||||
[System.Security.Cryptography.RandomNumberGenerator]::GetBytes(18))) -replace '[^A-Za-z0-9]', 'x'
|
||||
$apiPort = $config.ApiPort
|
||||
$mysqlPort = $config.MysqlPort
|
||||
$image = "$imageRepo`:$($config.ImageTag)"
|
||||
|
||||
Reference in New Issue
Block a user