feat(instance): db upgrade-major — guarded Postgres major upgrade + real detection adapters + matrix reconcile (#129) - #137
Conversation
Assisted-by: Claude Code
…verify Assisted-by: Claude Code
📝 WalkthroughWalkthroughThe CLI adds ChangesPostgreSQL major upgrade workflow
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant CLI
participant runDbUpgradeMajor
participant pg-adapters
participant upgrade-major
participant upgrade-ledger-hook
participant DockerVolumes
CLI->>runDbUpgradeMajor: dispatch db.upgrade-major
runDbUpgradeMajor->>pg-adapters: probeVersion and readMarker
pg-adapters->>DockerVolumes: inspect container and PG_VERSION
runDbUpgradeMajor->>upgrade-major: planTransition
upgrade-major->>upgrade-ledger-hook: begin migration
upgrade-major->>DockerVolumes: dump, restore, verify, and cut over
upgrade-major->>upgrade-ledger-hook: commit or rollback
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
tests/upgrade-ledger-hook.test.mjs (1)
25-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDead cleanup: temp dirs are collected but never removed.
dirsis populated on everyledgerDir()call, butafterEach(() => {})is a no-op, so the created temp directories accumulate across runs. Either wire up removal or drop the unused bookkeeping.🧹 Proposed cleanup
-import { mkdtempSync, readFileSync } from "node:fs"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs";const dirs = []; function ledgerDir() { const d = freshDir(); dirs.push(d); return d; } -afterEach(() => {}); +afterEach(() => { + while (dirs.length) rmSync(dirs.pop(), { recursive: true, force: true }); +});🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/upgrade-ledger-hook.test.mjs` around lines 25 - 31, Fix the dead temp-directory cleanup in ledgerDir by either removing the unused dirs collection or implementing afterEach cleanup that removes every directory recorded during the test. Keep ledgerDir’s freshDir creation behavior unchanged and ensure cleanup runs after each test.tests/pg-adapters.test.mjs (1)
65-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCoverage looks solid. Note that Line 77 pins the probe argv to
-U postgres; if the superuser fix suggested insrc/pg-adapters.mjsis adopted, update this assertion and add a case covering a non-postgressuperuser (e.g.nango-db).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/pg-adapters.test.mjs` around lines 65 - 95, Update the makeProbeVersion test assertion to match the superuser argument used by the implementation in src/pg-adapters.mjs, and add a test case proving probing works when the configured superuser is a non-postgres value such as nango-db. Keep the existing running-container, non-running, and exec-failure coverage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/index.mjs`:
- Line 5727: Update the targetTag assignment to fall back to
`${ctx.plan.to}-alpine` when imageRefTagWithDigest(imageParts(ctx.targetImage))
returns null or another empty value, while preserving the existing fallback when
ctx.targetImage is absent.
In `@src/pg-adapters.mjs`:
- Around line 113-122: Update makeProbeVersion to accept the service-specific
superuser configuration and use it when constructing the dockerExec psql
arguments, defaulting to "postgres" when none is provided. Thread
PG_SERVICE_SUPERUSER from the existing caller in src/index.mjs into the probe
while preserving the current SHOW server_version and parseServerVersionMajor
flow.
In `@src/upgrade-ledger-hook.mjs`:
- Around line 181-184: Update the entrypoint guard around main() in
src/upgrade-ledger-hook.mjs to use a URL-safe comparison, such as
pathToFileURL(process.argv[1]).href against import.meta.url, or the supported
import.meta.main check. Preserve the existing behavior of invoking main() and
exiting with its returned code when executed directly, including paths
containing spaces or escaped characters.
---
Nitpick comments:
In `@tests/pg-adapters.test.mjs`:
- Around line 65-95: Update the makeProbeVersion test assertion to match the
superuser argument used by the implementation in src/pg-adapters.mjs, and add a
test case proving probing works when the configured superuser is a non-postgres
value such as nango-db. Keep the existing running-container, non-running, and
exec-failure coverage unchanged.
In `@tests/upgrade-ledger-hook.test.mjs`:
- Around line 25-31: Fix the dead temp-directory cleanup in ledgerDir by either
removing the unused dirs collection or implementing afterEach cleanup that
removes every directory recorded during the test. Keep ledgerDir’s freshDir
creation behavior unchanged and ensure cleanup runs after each test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4b2881cb-b27e-44c0-a843-8331f5c8c4f5
📒 Files selected for processing (12)
src/command-table.mjssrc/index.mjssrc/pg-adapters.mjssrc/upgrade-ledger-hook.mjssrc/upgrade-major.mjssrc/upgrade-matrix.mjstests/command-table.test.mjstests/pg-adapters.test.mjstests/upgrade-ledger-hook.test.mjstests/upgrade-major.test.mjstests/upgrade-matrix-reconcile.test.mjstests/upgrade-matrix.test.mjs
| const backupDir = ctx.backupDir ?? mkdtempSync(path.join(os.tmpdir(), `cinatra-pgupgrade-${ctx.service}-`)); | ||
| const ledgerDir = process.env.CINATRA_VERSION_LEDGER_DIR || defaultLedgerDir(); | ||
| const hookPath = fileURLToPath(new URL("./upgrade-ledger-hook.mjs", import.meta.url)); | ||
| const targetTag = ctx.targetImage ? imageRefTagWithDigest(imageParts(ctx.targetImage)) : `${ctx.plan.to}-alpine`; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the helper and the target usage, then print the relevant ranges with line numbers.
rg -n "imageRefTagWithDigest|targetTag|--to-tag|targetImage" src/index.mjs
echo '---'
# Print a focused window around the target assignment and nearby usage.
sed -n '5700,5785p' src/index.mjs
echo '---'
# Print the helper definition region.
start=$(rg -n "function imageRefTagWithDigest|const imageRefTagWithDigest" src/index.mjs | head -n1 | cut -d: -f1 || true)
if [ -n "${start}" ]; then
sed -n "${start},$((start+80))p" src/index.mjs
fiRepository: cinatra-ai/cinatra-cli
Length of output: 8740
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- serviceImage and imageParts references ---'
rg -n "serviceImage|imageParts\(" src/index.mjs
echo '--- relevant surrounding ranges ---'
# Print around the imageParts helper if present
imgparts_line=$(rg -n "function imageParts|const imageParts" src/index.mjs | head -n1 | cut -d: -f1 || true)
if [ -n "${imgparts_line}" ]; then
sed -n "${imgparts_line},$((imgparts_line+80))p" src/index.mjs
fi
echo '--- Node spawnSync null arg probe ---'
node - <<'JS'
const { spawnSync } = require('node:child_process');
for (const arg of [null, undefined, 0, false, '']) {
try {
const r = spawnSync(process.execPath, ['-e', 'console.log(JSON.stringify(process.argv.slice(2)))', arg], { encoding: 'utf8' });
console.log('ARG:', arg);
console.log('status:', r.status);
console.log('error:', r.error && r.error.message);
console.log('stdout:', JSON.stringify(r.stdout.trim()));
console.log('stderr:', JSON.stringify(r.stderr.trim()));
} catch (e) {
console.log('ARG:', arg, 'THREW:', e && e.message);
}
console.log('---');
}
JSRepository: cinatra-ai/cinatra-cli
Length of output: 1556
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the ranges where serviceImage is populated and where imageParts is defined.
sed -n '5388,5455p' src/index.mjs
echo '---'
sed -n '5600,5665p' src/index.mjs
echo '---'
sed -n '5750,5795p' src/index.mjsRepository: cinatra-ai/cinatra-cli
Length of output: 9032
🏁 Script executed:
#!/bin/bash
set -euo pipefail
node - <<'JS'
const { spawnSync } = require('node:child_process');
const args = ['-c', 'for i in "$@"; do printf "<%s>\n" "$i"; done', 'bash', '--to-tag', null, '--backup-dir', 'B'];
const r = spawnSync('bash', args, { encoding: 'utf8' });
console.log('status:', r.status);
console.log('stdout:\n' + r.stdout);
console.log('stderr:\n' + r.stderr);
JSRepository: cinatra-ai/cinatra-cli
Length of output: 225
🏁 Script executed:
#!/bin/bash
set -euo pipefail
node - <<'JS'
const { spawnSync } = require('node:child_process');
const args = [
'-c',
'printf "argc=%s\n" "$#"; i=1; for a in "$@"; do printf "%s=<%s>\n" "$i" "$a"; i=$((i+1)); done',
'bash',
'--service',
'svc',
'--from',
'1',
'--to',
'2',
'--from-tag',
'1-alpine',
'--to-tag',
null,
'--backup-dir',
'B',
'--superuser',
'postgres',
];
const r = spawnSync('bash', args, { encoding: 'utf8' });
console.log('status:', r.status);
console.log(r.stdout);
console.log(r.stderr);
JSRepository: cinatra-ai/cinatra-cli
Length of output: 341
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- imageParts helper ---'
rg -n "function imageParts|const imageParts|imageParts\s*=" src/index.mjs
echo '--- digest-style image refs in repo ---'
rg -n "`@sha256`:|sha256:" -g '!**/node_modules/**' .
echo '--- compose/image handling around serviceImage ---'
rg -n "serviceImage\.set\(|s\.image|image:" src/index.mjsRepository: cinatra-ai/cinatra-cli
Length of output: 186
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- all imageParts references in repo ---'
rg -n "imageParts" .
echo '--- imports near top of src/index.mjs ---'
sed -n '1,220p' src/index.mjsRepository: cinatra-ai/cinatra-cli
Length of output: 10714
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '180,235p' src/upgrade-matrix.mjs
echo '---'
sed -n '1,120p' tests/version-ledger-capture.test.mjsRepository: cinatra-ai/cinatra-cli
Length of output: 7014
Handle digest-only --to-tag values
At src/index.mjs:5727, imageRefTagWithDigest() can return null for a digest-only image ref, and spawnSync() forwards that as the literal string null. Fall back to ${ctx.plan.to}-alpine when the helper returns nothing.
Proposed change
- const targetTag = ctx.targetImage ? imageRefTagWithDigest(imageParts(ctx.targetImage)) : `${ctx.plan.to}-alpine`;
+ const targetTag = imageRefTagWithDigest(imageParts(ctx.targetImage)) ?? `${ctx.plan.to}-alpine`;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/index.mjs` at line 5727, Update the targetTag assignment to fall back to
`${ctx.plan.to}-alpine` when imageRefTagWithDigest(imageParts(ctx.targetImage))
returns null or another empty value, while preserving the existing fallback when
ctx.targetImage is absent.
| export function makeProbeVersion({ runningContainerFor, dockerExec }) { | ||
| return (service) => { | ||
| const container = runningContainerFor ? runningContainerFor(service) : null; | ||
| if (!container) return null; | ||
| // -tA: tuples-only, unaligned — bare "17.2". `SHOW server_version` is | ||
| // cheaper + more robust than parsing verbose version(). | ||
| const out = dockerExec(container, ["psql", "-U", "postgres", "-tArc", "SHOW server_version"]); | ||
| return parseServerVersionMajor(out); | ||
| }; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does the official postgres Docker image create a "postgres" superuser role when POSTGRES_USER is set to a different name?
💡 Result:
Yes, when you set the POSTGRES_USER environment variable to a different name, the official PostgreSQL Docker image creates that user as a superuser [1][2]. According to the official documentation, the POSTGRES_USER variable is used to define the database superuser [1][2]. If this variable is specified, the image creates that user with superuser privileges and automatically creates a database with the same name [1][2]. If POSTGRES_USER is not defined, the image defaults to creating the 'postgres' user as the superuser [1][2]. It is important to note that this behavior only applies when the container is initialized for the first time with an empty data directory [3]. If a pre-existing data directory is mounted to the container, the environment variables will not modify the existing database's users or roles [3]. Additionally, this configuration is distinct from the Linux system user (also named 'postgres') that runs the PostgreSQL daemon process inside the container [1][2].
Citations:
- 1: https://github.com/docker-library/docs/blob/master/postgres/content.md
- 2: https://hub.docker.com/_/postgres?ordering=last_updated&page=1&tab=description
- 3: Unable to override login credentials for already initialized database docker-library/postgres#1224
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant files and nearby context.
git ls-files src/pg-adapters.mjs src/index.mjs
echo "---- src/pg-adapters.mjs ----"
sed -n '1,220p' src/pg-adapters.mjs
echo "---- src/index.mjs ----"
sed -n '1,260p' src/index.mjs
# Search for probe/version wiring and any superuser plumbing.
echo "---- search ----"
rg -n "makeProbeVersion|PG_SERVICE_SUPERUSER|superuserFor|server_version|psql -U" srcRepository: cinatra-ai/cinatra-cli
Length of output: 19621
Thread the service superuser into the live probe
makeProbeVersion() hardcodes psql -U postgres, but src/index.mjs already tracks per-service superusers via PG_SERVICE_SUPERUSER (nango-db → nango, plane-db → plane). For those services, the live probe can’t authenticate, so it falls back to the raw marker every time.
Pass the service-specific superuser into the probe (defaulting to postgres) so the SHOW server_version path works for all supported services.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pg-adapters.mjs` around lines 113 - 122, Update makeProbeVersion to
accept the service-specific superuser configuration and use it when constructing
the dockerExec psql arguments, defaulting to "postgres" when none is provided.
Thread PG_SERVICE_SUPERUSER from the existing caller in src/index.mjs into the
probe while preserving the current SHOW server_version and
parseServerVersionMajor flow.
| // Executed directly by the shell frame (never imported there): run main + exit. | ||
| if (import.meta.url === `file://${process.argv[1]}`) { | ||
| main().then((code) => process.exit(code)); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
node import.meta.url equals file protocol process.argv[1] pathToFileURL idiom
💡 Result:
In Node.js, the pattern of comparing process.argv[1] to the file path of the current module is the historical idiom used to determine if a module is being executed directly as the entry point (the ESM equivalent of the CommonJS require.main === module check) [1][2][3]. Recent versions of Node.js have introduced more direct, built-in ways to achieve this, reducing the need for the fileURLToPath idiom [4]. Current methods for detecting the main module: 1. import.meta.main (Recommended): Available since Node.js v24.2.0 and v22.18.0. This is the modern, idiomatic way to check if a module is the main entry point [4]. if (import.meta.main) { // Module is being run directly } 2. import.meta.filename: Available since Node.js v20.11.0. You can compare this directly to process.argv[1] [4]. if (process.argv[5] === import.meta.filename) { // Module is being run directly } 3. The legacy idiom: For older versions of Node.js, developers use fileURLToPath from the node:url module to convert import.meta.url into a file system path, which is then compared to process.argv[1] [6][2][4]. import { fileURLToPath } from 'node:url'; if (process.argv[5] === fileURLToPath(import.meta.url)) { // Module is being run directly } Note that the legacy idiom has known limitations, such as potential issues with symbolic links or cases where the script is executed without its file extension [7][8][6]. For robust production code, using the newer built-in properties (import.meta.main) is preferred [4].
Citations:
- 1: doc: add esm alternative for require.main nodejs/node#31875
- 2: https://stackoverflow.com/questions/45136831/node-js-require-main-module
- 3: https://stackoverflow.com/questions/34842738/if-name-main-equivalent-in-javascript-es6-modules
- 4: https://stackoverflow.com/questions/4981891/node-js-equivalent-of-pythons-if-name-main
- 5: https://stackoverflow.com/questions/57838022/detect-whether-es-module-is-run-from-command-line-in-node
- 6: https://mbork.pl/2026-02-09_Node_modules_working_as_command-line_scripts_revisited
- 7: Provide an equivalent of require.main for ESM nodejs/node#57616
- 8: import.meta.main nodejs/node#49440
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## locate references\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' 'upgrade-ledger-hook\.mjs|upgrade ledger hook|ledger-hook' .
printf '\n## file outline\n'
ast-grep outline src/upgrade-ledger-hook.mjs --view expanded || true
printf '\n## relevant lines around guard\n'
nl -ba src/upgrade-ledger-hook.mjs | sed -n '150,210p'Repository: cinatra-ai/cinatra-cli
Length of output: 1323
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## guard in src/upgrade-ledger-hook.mjs\n'
sed -n '175,190p' src/upgrade-ledger-hook.mjs | cat -n
printf '\n## spawn site in src/index.mjs\n'
sed -n '5716,5745p' src/index.mjs | cat -n
printf '\n## tests referencing hook execution\n'
sed -n '1,220p' tests/upgrade-ledger-hook.test.mjs | cat -nRepository: cinatra-ai/cinatra-cli
Length of output: 12122
🌐 Web query:
Node ESM import.meta.url symlink realpath process.argv[1] direct execution guard
💡 Result:
To determine if an ES module is being executed directly in Node.js, the most authoritative and modern approach is using the import.meta.main property [1][2][3]. ### Preferred Approach: import.meta.main Starting in recent Node.js versions (e.g., v24.2.0+), import.meta.main provides a boolean indicating whether the module is the entry point of the current process [3]. if (import.meta.main) { // Executed directly } ### Robust Legacy / Compatibility Approach If you need to support older environments, or if you encounter issues with symlinks where process.argv[1] might point to a symlink path while import.meta.url resolves to the real path, use import.meta.filename or a manual comparison with realpathSync [4][5][1]. 1. Using import.meta.filename (Node.js v20.11.0+): import.meta.filename is the absolute path to the current module with symlinks resolved [4][5]. if (process.argv[4] === import.meta.filename) { // Executed directly } 2. Manual symlink-safe comparison: If import.meta.filename is unavailable, compare the file URL of the resolved process.argv[1] against import.meta.url [1][3]. import { fileURLToPath, pathToFileURL } from 'node:url'; import { realpathSync } from 'node:fs'; const isDirect = import.meta.url === pathToFileURL(realpathSync(process.argv[4])).href; if (isDirect) { // Executed directly } ### Key Considerations * Symlinks: import.meta.filename automatically resolves symlinks, making it highly reliable for identifying the file path [4][5]. Using realpathSync(process.argv[1]) ensures that you are comparing the actual target file path against the entry point, preventing mismatches caused by symlinks (e.g., in node_modules/.bin) [1][3]. * Command Line Arguments: process.argv[1] is the standard way to identify the entry module path, but relying on it directly can be fragile without resolving the path to its real canonical form [1][6]. * Comparison: Always convert path strings to file URLs before comparing them to import.meta.url to ensure the protocols and formatting match correctly [1][7][8].
Citations:
- 1: https://stackoverflow.com/questions/57838022/detect-whether-es-module-is-run-from-command-line-in-node
- 2: import.meta.main nodejs/node#57226
- 3: https://stackoverflow.com/questions/4981891/node-js-equivalent-of-pythons-if-name-main
- 4: https://r2.nodejs.org/docs/v22.17.1/api/esm.html
- 5: https://www.thenodebook.com/modules/import-meta-caching
- 6: Modules doc direct execution test invalid under "type": "module" nodejs/node#34882
- 7: https://stackoverflow.com/questions/59638994/how-to-check-if-node-esmodule-was-imported-or-ran-directly
- 8: https://stackoverflow.com/questions/45136831/node-js-require-main-module
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## UPGRADE_LEDGER_HOOK references\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' 'UPGRADE_LEDGER_HOOK|upgrade-ledger-hook' src scripts tests .
printf '\n## shell frame candidates\n'
fd -t f 'upgrade' .
fd -t f 'postgres-upgrade-major.sh' .Repository: cinatra-ai/cinatra-cli
Length of output: 2341
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## search for hook execution\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' 'UPGRADE_LEDGER_HOOK|execFileSync|spawnSync|spawn\(' src/upgrade-major.mjs src/index.mjs scripts tests
printf '\n## relevant section of src/upgrade-major.mjs\n'
sed -n '430,540p' src/upgrade-major.mjs | cat -nRepository: cinatra-ai/cinatra-cli
Length of output: 21150
Use a URL-safe entrypoint check in src/upgrade-ledger-hook.mjs
file://${process.argv[1]} is brittle here: a checkout path with spaces or other escaped characters makes the comparison false, so the hook skips main() and exits 0. Use pathToFileURL(process.argv[1]).href or import.meta.main so the ledger hook still runs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/upgrade-ledger-hook.mjs` around lines 181 - 184, Update the entrypoint
guard around main() in src/upgrade-ledger-hook.mjs to use a URL-safe comparison,
such as pathToFileURL(process.argv[1]).href against import.meta.url, or the
supported import.meta.main check. Preserve the existing behavior of invoking
main() and exiting with its returned code when executed directly, including
paths containing spaces or escaped characters.
cinatra-cli#129 —
cinatra instance db upgrade-major(guarded Postgres major upgrade)Guarded logical dump → fresh target-major volume → restore for the four Postgres instances (platform / nango / twenty / plane), driven by the supported matrix. This is the command the fail-closed preflight (cli#128) STOP messages point to.
What's here
cinatra instance db upgrade-major— matrix eligibility gate (fail-closed on downgrade / unsupported / unknown, before any mutation); the guarded-frame contract model + exit-code contract (0 ok / 2 usage / 3 refusal / 4 interrupted / 5 rolled-back); a dry-run plan by default,--yesexecutes. The destructive dump/restore/cutover MECHANICS are delegated to the product's guarded shell frame (scripts/upgrade/postgres-upgrade-major.sh, proven with real docker by the works-after upgrade-from arm in the paired PR) with the deployed-version ledger wired in as that frame'sUPGRADE_LEDGER_HOOK.PG_VERSIONmarker adapters (folds in cli#128 residual 1) — the marker reads from the deployment's ACTUAL data path, handling both pg layouts (the pg18 parent-mount move, ChangePGDATAin 18+ to/var/lib/postgresql/MAJOR/dockerdocker-library/postgres#1259).4e89bf4a) — removed a bogus nango general 16→17 hop the shipped copy carried; pinned the reconciled revision (cli#128 residual 3).src/upgrade-ledger-hook.mjs— the ledger transaction wiring: begin/commit/rollback under the per-slug lock, volume-identity-enforced, fail-closed.Coordinated PAIR with cinatra-ai/cinatra#1422 (the guarded shell mechanism + the works-after upgrade-from arm proving Case A platform 17→18 and Case B nango 15→17). Both pin matrix revision 2; neither is complete without its counterpart. Part of the upgrade-paths epic (cinatra-ai/cinatra#1417 / #1419).
Codex-converged (3 rounds; found + fixed 6 correctness/data-safety issues → MERGE-SAFE).
Not user-visible: a CLI command + detection adapters; no UI surface.
Closes #129
Summary by CodeRabbit
New Features
instance db upgrade-majorcommand for supported PostgreSQL major-version upgrades.Tests