Round five. 47 -> 34 -> 15 -> 7 -> 6 raw findings, and this round's sharpest was the loop's own blind spot: **the branch did not pass the gate.** `prettier --check` is one of its eight lanes and I had been running the other seven, so an unformatted `onError` from round one sat on the branch for four rounds — `siftlode publish` would have refused it, with no --skip flag to get past. **The restart hint could still strand the restored data.** Round four said the scripts no longer print a command they cannot resolve; they did. The COMPOSE_FILE fallback only fires when `docker-compose.yml` is ABSENT, so in a source checkout it never fires and the hint expanded to a bare, pasteable `docker compose up -d` — which reads the other file, whose volumes are `pgdata`/`downloads` rather than `siftlode_pgdata`/`siftlode_downloads`, and recreates the database on an empty one. When the file is unresolved the scripts now print `-f <the compose file you started with>`: not runnable, which is the point. **A comment naming the wrong owner.** `formats.py` still said a zero budget is "a refusal for enqueue to make" — the premise round one disproved, three files away from the two docstrings that now say the opposite. Anyone acting on it would delete the worker's refusal and re-open the original inversion, so it now says where the refusal actually lives. Also: the reset-flag rule was rewritten in three consecutive rounds and pinned by nothing — two tests now cover a fragment losing its token, both by a pasted verify link and by navigating away entirely; `backup.ps1` deletes a partially-copied dump before throwing, the guard round four gave the docs but not the script it mirrors; and one comment that read as if the asset were untouched by a line four below it. Gate green, all eight lanes: ruff, ruff format, 3x tsc, eslint, prettier, knip, vitest 687, pytest 950.
76 lines
4.8 KiB
PowerShell
76 lines
4.8 KiB
PowerShell
# Restores a Siftlode Postgres dump created by backup.ps1 into the running db container.
|
|
# Usage: .\scripts\restore.ps1 backups\siftlode-YYYYMMDD-HHMMSS.dump
|
|
#
|
|
# Restoring the database does NOT restore the secrets that decrypt it. If this dump is from an
|
|
# instance with a different TOKEN_ENCRYPTION_KEY than the current .env, the encrypted sysconfig
|
|
# values and refresh tokens will not be readable. See docs/self-hosting.md § Backups.
|
|
param([Parameter(Mandatory = $true)][string]$DumpFile)
|
|
$ErrorActionPreference = "Stop"
|
|
$WarningPreference = "Continue"
|
|
|
|
# See backup.ps1: the dump travels with `docker compose cp`, never through the PowerShell pipeline,
|
|
# which corrupts binary on 5.1 and cannot express `-Encoding Byte` at all on 7.
|
|
$remote = "/tmp/siftlode-restore.dump"
|
|
|
|
# See backup.ps1: on Windows PowerShell 5.1 a redirected native stderr is a TERMINATING error under
|
|
# $ErrorActionPreference = "Stop", so without the catch a cleanup step could kill the script after
|
|
# the restore already succeeded — and the obvious next move, re-running it, wipes the database again.
|
|
function Remove-RemoteDump($path) {
|
|
try { docker compose exec -T db rm -f $path 2>&1 | Out-Null } catch { }
|
|
}
|
|
|
|
if (-not $env:COMPOSE_FILE -and -not (Test-Path "docker-compose.yml") -and (Test-Path "docker-compose.selfhost.yml")) {
|
|
$env:COMPOSE_FILE = "docker-compose.selfhost.yml"
|
|
}
|
|
|
|
$user = if ($env:POSTGRES_USER) { $env:POSTGRES_USER } else { "siftlode" }
|
|
$dbname = if ($env:POSTGRES_DB) { $env:POSTGRES_DB } else { "siftlode" }
|
|
if (-not (Test-Path $DumpFile)) { throw "Dump file not found: $DumpFile" }
|
|
|
|
docker compose cp $DumpFile "db:$remote"
|
|
$cpCode = $LASTEXITCODE
|
|
if ($cpCode -ne 0) { throw "RESTORE FAILED — could not copy the dump into the container (exit $cpCode)." }
|
|
|
|
# Stop the app before dropping its tables. `--clean` DROPs each object in its own transaction, and a
|
|
# running api (with its scheduler) holds locks that make those DROPs queue behind it — abort the wait
|
|
# and the database is left half-dropped. It also keeps the app from writing into a schema that is
|
|
# only partly back.
|
|
#
|
|
# It is NOT restarted automatically, on purpose. Every start runs `alembic upgrade head`, so on the
|
|
# documented rollback path (restore a pre-upgrade dump, THEN pin the older IMAGE_TAG) an automatic
|
|
# restart would re-apply the very migrations the restore just undid. On a FAILED restore it would
|
|
# put the app back on a half-restored schema, which is the second thing the stop exists to prevent.
|
|
# The operator starts it when they are ready — with `up -d`, not `start`: `start` relaunches the
|
|
# EXISTING container, so it ignores a newly pinned IMAGE_TAG and comes back on the image being
|
|
# rolled back FROM.
|
|
docker compose stop api worker
|
|
$stopCode = $LASTEXITCODE
|
|
if ($stopCode -ne 0) { throw "Could not stop api/worker (exit $stopCode) — refusing to restore while the app may still be writing." }
|
|
|
|
docker compose exec -T db pg_restore -U $user -d $dbname --clean --if-exists $remote
|
|
$restoreCode = $LASTEXITCODE
|
|
Remove-RemoteDump $remote
|
|
|
|
# pg_restore exits non-zero for ignorable warnings too (--clean --if-exists on a fresh database
|
|
# reports every missing object it tried to drop), so a non-zero code is reported as "check this",
|
|
# not swallowed and not dressed up as success.
|
|
if ($restoreCode -ne 0) {
|
|
# Write-Warning, not Write-Output: `>` redirects only the SUCCESS stream, so on stdout this
|
|
# explanation would be filed into the log while the bare exception stayed on screen. (The
|
|
# warning stream is its own stream, not the error stream — $WarningPreference is pinned at the
|
|
# top so an inherited "SilentlyContinue" cannot drop it.)
|
|
Write-Warning "api and worker are left STOPPED: the database may be half-restored. Fix the dump and re-run before starting them."
|
|
throw "pg_restore reported errors while restoring $DumpFile (exit $restoreCode) — read the output above before treating this instance as restored."
|
|
}
|
|
Write-Output "Restored $DumpFile"
|
|
# Deliberately not a runnable line when we could not resolve the file: both compose files can be
|
|
# present (a source checkout), and the two declare DIFFERENT volume names — so a pasted bare
|
|
# 'docker compose up -d' would recreate the database on an empty volume and strand the restore.
|
|
$cf = if ($env:COMPOSE_FILE) { $env:COMPOSE_FILE } else { "<the compose file you started with>" }
|
|
Write-Output "api and worker are stopped. Start them again with:"
|
|
Write-Output " docker compose -f $cf up -d"
|
|
Write-Output "It must be the SAME file you started with: 'up -d' recreates from whatever file it reads,"
|
|
Write-Output "and a different one points at different volumes. Rolling a release back? Pin the older"
|
|
Write-Output "IMAGE_TAG in .env FIRST: every start re-runs the migrations, and 'start' would relaunch the"
|
|
Write-Output "existing container on the image you are rolling back FROM."
|