Eleven commits from the sister site: a reversible Maximo/WINC machine-adoption import, a versioned agent binary store with upload and rollback, remote-BuildKit CI, tray version reporting, and UI fixes. Applied as a patch again - the published mirror rewrites hashes, so the branches have unrelated histories and git refuses to merge them. Reviewed by three agents before merging. Four things fixed here. THE PUBLISH WORKFLOW WAS DEAD. The new remote-BuildKit step in publish-images.yml sits at column 0 instead of indented under steps:, so the whole file is invalid YAML and GitHub would refuse it. Their entire GHCR distribution channel - which Start-ShopDBDev.ps1 and the compose files consume - was broken at the source and clearly never run. Indented; all five workflows now parse. THE IMPORT'S APPROVAL CHECK COULD NOT FAIL. commit() built the plan and the approval packet from the same upload and then asked the writer whether they matched, so packet_matches_plan compared a hash against itself. The documented contract - "a human approves a specific, frozen plan, and the writer can prove nothing drifted" - did not exist in the web path: an admin could preview file A and commit file B, gated only by the needs-create group NAMES they ticked. The preview's planhash is now required at commit and checked against a freshly rebuilt plan. Their tests committed without previewing, which is what let this through; they go through the real flow now, and three new tests pin it - all confirmed to fail with the check stubbed out. endpoints.agentpackage QUIETLY GREW. It used to mean "wrap the staged exe in a ZIP"; with the upload routes it also means "choose the executable every future package installs and runs as SYSTEM". Before this, staging needed filesystem access to the server. The permission description still said only "generate and download a package", so an admin granting it could not see what they were granting. Reworded. NOTHING RECORDED WHAT WAS STAGED, OR BY WHOM. The manifest held version, size and timestamp; re-uploading a version replaces the bytes in place under the same label. A sha256 of the stored file is now recorded, and the upload is audit-logged with it - after an incident there was previously no answer to who put that binary on the fleet. Also documented the nine new routes in docs/api-inventory.json (that gate cannot run on the mirror, so it falls to us at promotion time again), regenerated the spec and the project map, and kept our own docs where they conflicted: docs/ is scrubbed from the mirror, so their DEV-BUILD-TEST.md still had the dev-be channel mapping we corrected, and their PLUGIN-HOOKS.md was a new 45-line file rather than our 737-line canonical one. Their journal section is appended to ours. Contract version comes across as 0.30.0 for a genuinely additive surface. Suite 2369, frontend 324, gate green. Scope: product
192 lines
8.5 KiB
PowerShell
192 lines
8.5 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Save-and-run launcher for a ShopDB dev workstation (stable or edge).
|
|
|
|
.DESCRIPTION
|
|
Download this one file and run it. It clones (or updates) the repo at the
|
|
channel branch into a local path, then hands off to scripts/dev-bootstrap.ps1
|
|
for the real dependency-check / build / up work.
|
|
|
|
ge-aero/shopdb-flask is a PRIVATE repo, but this needs no token pasting: the
|
|
clone uses a plain https URL and Git Credential Manager supplies your stored
|
|
GitHub credential automatically - the same way `git clone` works in the wiki
|
|
DEVELOPMENT-SETUP procedure. If you have ever cloned this repo or signed in to
|
|
GitHub in Visual Studio / VS Code on this box, GCM already has what it needs.
|
|
|
|
This is a downloaded FILE, not a web `iex` one-liner, precisely because a
|
|
private repo cannot be fetched anonymously from raw.githubusercontent.com. A
|
|
saved script sidesteps that entirely - only the git operations need auth, and
|
|
GCM handles those.
|
|
|
|
.PARAMETER Channel
|
|
'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
|
|
from any repo you already have open.
|
|
|
|
.EXAMPLE
|
|
# Bleeding edge:
|
|
powershell -ExecutionPolicy Bypass -File .\Start-ShopDBDev.ps1 -Channel edge
|
|
|
|
.EXAMPLE
|
|
# Stable:
|
|
powershell -ExecutionPolicy Bypass -File .\Start-ShopDBDev.ps1 -Channel stable
|
|
|
|
.NOTES
|
|
ASCII only, no emoji or smart punctuation, per repo style. PowerShell 5.1+.
|
|
Needs Git (with Credential Manager, the Git-for-Windows default);
|
|
dev-bootstrap.ps1 offers to winget-install anything else that is missing.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[ValidateSet('stable', 'edge')]
|
|
[string]$Channel = 'stable',
|
|
|
|
[string]$TargetDir
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# Reset leftovers from an interrupted or timed-out prior run so the retry starts
|
|
# clean. Proxy bottlenecks can kill a build mid-flight, leaving orphaned
|
|
# containers holding the channel ports, dangling build cache from the partial
|
|
# build, and sometimes a half-written clone. This is scoped to the channel and
|
|
# is non-destructive to data: the compose down keeps named volumes (the DB), and
|
|
# only dangling build cache is pruned - never tagged images or volumes.
|
|
function Clear-InterruptedRun {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Project,
|
|
[Parameter(Mandatory)][string]$Target
|
|
)
|
|
|
|
if (Get-Command docker -ErrorAction SilentlyContinue) {
|
|
# docker/compose write warnings to stderr (e.g. "No resource found to
|
|
# remove"), which would trip $ErrorActionPreference='Stop' and abort the
|
|
# whole run. These cleanup steps are best-effort, so soften the preference
|
|
# and swallow all streams for their duration.
|
|
$previous = $ErrorActionPreference
|
|
$ErrorActionPreference = 'Continue'
|
|
try {
|
|
# Only try compose down when the engine is actually responding, otherwise
|
|
# skip quietly - a fresh box may not have Docker started yet.
|
|
docker info *> $null
|
|
if ($LASTEXITCODE -eq 0) {
|
|
Write-Host "==> Cleaning up any interrupted '$Project' stack" -ForegroundColor Cyan
|
|
# --remove-orphans clears containers left by a killed build; volumes are
|
|
# intentionally kept so the DB survives a retry.
|
|
docker compose -p $Project down --remove-orphans *> $null
|
|
# Reclaim only dangling build cache from failed/partial builds.
|
|
docker builder prune -f *> $null
|
|
}
|
|
}
|
|
finally {
|
|
$ErrorActionPreference = $previous
|
|
}
|
|
}
|
|
|
|
# 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
|
|
# "detected dubious ownership" guard and every git op fails with exit 128.
|
|
# That stale clone is effectively unusable for this user, so detect the
|
|
# condition and remove it here for a clean re-clone rather than surfacing a
|
|
# misleading auth error later.
|
|
if (Test-Path (Join-Path $Target '.git')) {
|
|
$previous = $ErrorActionPreference
|
|
$ErrorActionPreference = 'Continue'
|
|
try { $probe = git -C $Target rev-parse --is-inside-work-tree 2>&1 } catch { $probe = "$_" }
|
|
finally { $ErrorActionPreference = $previous }
|
|
if ($probe -match 'dubious ownership') {
|
|
# 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.')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Plain https URL. No token in the URL: Git Credential Manager injects the
|
|
# 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'
|
|
# 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
|
|
Write-Host " Channel : $Channel ($branch)" -ForegroundColor White
|
|
Write-Host " Target : $target" -ForegroundColor White
|
|
|
|
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
|
throw 'git is required. Install Git for Windows (winget install Git.Git) and re-run.'
|
|
}
|
|
|
|
$project = if ($Channel -eq 'edge') { 'shopdb-edge' } else { 'shopdb-stable' }
|
|
Clear-InterruptedRun -Project $project -Target $target
|
|
|
|
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). If this is an auth error, clone the repo once in Visual Studio / VS Code so Credential Manager stores your GitHub login." }
|
|
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). If this is an auth error, sign in to GitHub once in Visual Studio / VS Code (or run 'git clone $repoUrl' manually) so Credential Manager stores your login, then re-run." }
|
|
}
|
|
|
|
$bootstrap = Join-Path $target 'scripts\dev-bootstrap.ps1'
|
|
if (-not (Test-Path $bootstrap)) {
|
|
throw "dev-bootstrap.ps1 not found at $bootstrap. Is the '$branch' branch correct?"
|
|
}
|
|
|
|
Write-Host "==> Handing off to dev-bootstrap.ps1" -ForegroundColor Cyan
|
|
& $bootstrap -Channel $Channel
|