fix: run ops psql as trakrf-migrate, add psql-super and a QUERY argument (TRA-1105) - #164
Merged
mikestankavich merged 5 commits intoAug 19, 2026
Conversation
added 4 commits
August 19, 2026 12:50
infra ignored all of .claude/, so agent workflow config could not be
shared and every csw run fell back to built-in defaults: tracker 'none'
(no Linear), worktreeDir '.worktrees' (which CLAUDE.md explicitly calls a
fresh-clone footgun), and an empty validate command — a green run that
proved nothing.
Narrow the ignore to .claude/worktrees/, settings.local.json and *.lock,
matching trakrf/docs, and track:
.claude/csw.json tracker/base branch/validate, same shape as
the configs already in trakrf/{docs,platform}
.claude/csw-validate.sh local mirror of .github/workflows/ci.yml, so
CI failures surface before the PR
Audited .claude/ before flipping the rule: it held nothing but these two
files, so no scratch or credential material becomes trackable.
Refs: TRA-1105
just psql connected as the postgres superuser, so any DDL hand-run
through it created a postgres-owned object in the trakrf schema. Those
objects are permanently un-replaceable by the migrate role — CREATE OR
REPLACE, DROP and ALTER .. OWNER TO all require ownership — so a later
migration aborts partway and leaves the golang-migrate ledger dirty.
Because the migrate Job is an ArgoCD PreSync hook the Deployment is then
never updated: the old pod keeps serving while ArgoCD reports Healthy and
CI stays green. That is how preview wedged in TRA-1104.
New db_psql helper in ops-lib.sh runs psql through
env PGOPTIONS='-c role=<role>', which applies the equivalent of SET ROLE
at connect time. Connection auth is unchanged (in-pod peer auth as
postgres over the unix socket — the only credential available without
threading a password in); what changes is the role the session runs as,
and therefore who owns anything it creates.
just psql ENV [QUERY] role trakrf-migrate (default)
just psql-super ENV [QUERY] raw postgres superuser, opt-in
psql also gained the QUERY argument the v1.4.0 release needed: empty
opens an interactive shell, non-empty runs psql -c with ON_ERROR_STOP=1
and no tty. The banner moved to stderr so stdout stays pipeable. Without
it, docs/releasing.md step 0 could not be run as documented and operators
fell back to a raw kubectl exec — the superuser path this change closes.
QUERY reaches the shell via just's quote(), not "{{ QUERY }}": just
substitutes into the recipe body textually, so SQL containing a quoted
identifier would otherwise terminate the string and mangle the statement.
psql-super gates prod behind confirm_prod and warns on stderr in both
environments. It is a guardrail, not a boundary — session_user is still
postgres, so SET ROLE postgres escapes deliberately. Plain RESET ROLE
does not, since the role arrives in the startup packet and becomes the
session default.
Verified against a real postgres:16: PGOPTIONS yields
current_user=trakrf-migrate/usesuper=f, a function created through the
new path is owned by trakrf-migrate and CREATE OR REPLACE by the migrate
role succeeds, while one created through the old path is owned by
postgres and fails with 'must be owner of function' — the TRA-1104 wedge.
12 new ops-lib tests cover role selection, tty mode, ON_ERROR_STOP,
argument integrity for SQL with spaces and quotes, and arg validation.
Refs: TRA-1105
scripts/test-ops-lib.sh was never wired into CI — its own header asks you to run it by hand after changing ops-lib.sh, which is discipline rather than a gate. This change adds 12 tests to that file covering db_psql, so make them load-bearing. Pure bash with a stubbed kubectl: no cluster, no credentials, no matrix. Refs: TRA-1105
tofu init records provider hashes for the current platform into the tracked .terraform.lock.hcl files. CI does this in a throwaway checkout and never notices; run locally it left four modified lockfiles behind, which a later 'git add -A' would sweep into an unrelated commit — caught exactly that way on this branch. Note which lockfiles are clean before the tofu loop and restore those afterwards. A lockfile already edited on purpose is left alone. Refs: TRA-1105
The explanatory comment above the QUERY assignment in the psql recipe itself contained an interpolation of QUERY. just expands interpolations inside shebang-recipe comments exactly as it does anywhere else, so the value was pasted into the comment too. A single-line query stayed behind the leading # and nothing looked wrong; a multi-line one spilled past it and its remaining lines ran as shell: psql: line 1116: SELECT: command not found Found by running the real findOwnershipDrift sweep from platform through the new recipe — the exact use case the QUERY argument exists for, and one that no single-line test could have surfaced. Reword the comment so it never contains interpolation braces, and say so explicitly, since the failure is invisible until a value spans lines. Two regression tests per recipe, driven through and a stub kubectl on PATH: the rendered body must parse under , and the query must reach psql as one byte-for-byte intact argument. Verified by reintroducing the bug — both fail for psql and neither for psql-super. Both environments audit clean through the repaired path (0 drifted objects in schema trakrf on preview and prod). Refs: TRA-1105
mikestankavich
deleted the
fix/tra-1105-ops-psql-connects-as-superuser-which-sil
branch
August 19, 2026 18:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
just psqlconnected as thepostgressuperuser, so any DDL hand-run through it created a postgres-owned object in thetrakrfschema. Those objects are permanently un-replaceable by the migrate role —CREATE OR REPLACE,DROPandALTER … OWNER TOall require ownership — so a later migration aborts partway and leaves the golang-migrate ledger dirty. Because the migrate Job is an ArgoCD PreSync hook, the Deployment is then never updated: the old pod keeps serving while ArgoCD reports Healthy and CI stays green. That is how preview wedged in TRA-1104.What changed
New
db_psqlhelper inscripts/ops-lib.shruns psql throughenv PGOPTIONS='-c role=<role>', which applies the equivalent ofSET ROLEat connect time. Connection auth is unchanged (in-pod peer auth aspostgresover the unix socket — the only credential available without threading a password in). What changes is the role the session runs as, and therefore who owns anything it creates.just psql ENV [QUERY]trakrf-migratejust psql-super ENV [QUERY]postgressuperuserconfirm_prod, fails closed without a ttypsqlalso gained theQUERYargument the v1.4.0 release needed: empty opens an interactive shell, non-empty runspsql -cwithON_ERROR_STOP=1and no tty. The→ namespace/podbanner moved to stderr so stdout stays pipeable.QUERYreaches the shell via just'squote(), not"{{ QUERY }}"— just substitutes into the recipe body textually, so SQL containing a quoted identifier ("trakrf-migrate") would otherwise terminate the string and mangle the statement.Verification
Reproduced the TRA-1104 wedge against a throwaway
postgres:16, then confirmed the fix live on both environments.Ownership, the thing that actually matters:
A later migration running as
trakrf-migrate:Live against real CNPG clusters — the ticket's own table, inverted:
The
docs/releasing.mdstep 0 invocation, which previously failed withJustfile does not contain recipe `-c`:Also verified live: banner is on stderr (stdout clean under
2>/dev/null); SQL with quotes and a semicolon survives intact;psql-super previewyieldspostgres/usesuper=t;psql-super prodfails closed without a tty andYES=1bypasses as documented.Correction found by testing: I had documented
RESET ROLEas the escape back to superuser. It is not — the role arrives in the startup packet, so it becomes the session default thatRESETreturns to.SET ROLE postgresis the actual escape. Both the code comment anddocs/ops.mdsay so now. This makes the guardrail slightly stronger than written: an incidentalRESET ROLEwill not silently restore superuser.Commits
chore:narrow the.claude/gitignore (matching trakrf/docs) and track.claude/csw.json+.claude/csw-validate.sh. infra ignored all of.claude/, so csw fell back to defaults: trackernone, worktreeDir.worktrees(which CLAUDE.md calls a fresh-clone footgun), and an empty validate command..claude/was audited before flipping the rule — it held nothing but those two files.fix:the psql change above, plus 12 new ops-lib tests.ci:runscripts/test-ops-lib.sh. It was never wired into CI — its header asks you to run it by hand, which is discipline rather than a gate.chore:stop csw-validate mutating the terraform lockfiles.fix:stop a multi-line QUERY spilling out of a recipe comment (see below).The multi-line bug, found late
Running the real
findOwnershipDriftsweep through the new recipe — the exact use caseQUERYexists for — failed withpsql: line 1116: SELECT: command not found.Cause: the explanatory comment above the
QUERYassignment itself contained an interpolation of QUERY. just expands interpolations inside shebang-recipe comments like anywhere else, so the value was pasted into the comment too. A single-line query stayed behind the leading#and looked fine; a multi-line one spilled past it and ran as shell. Every earlier test passed because they were all single-line.Fixed by rewording the comment so it contains no interpolation braces, plus two regression tests per recipe (via
just --dry-runand a stub kubectl on PATH): the rendered body must parse underbash -n, and the query must reach psql as one byte-for-byte intact argument. Verified by reintroducing the bug — both fail forpsql, neither forpsql-super.With it repaired, the full ownership sweep runs through
just psqland both environments audit clean (0 drifted objects in schematrakrfon preview and prod). Note that sweep must run aspsql, notpsql-super: it filters onpg_has_role(CURRENT_USER, ...)and a superuser is implicitly a member of every role, so it would report a false clean.Scope notes
docs/releasing.mdstep 0 is a trakrf/platform change; re-dispatch there. A scope-split comment is on TRA-1105.docs/db-migration.md,docs/backups.mdanddocs/prod-cutover.mdstill contain rawpsql -U postgreskubectl calls. Left deliberately: those areDROP SCHEMArecovery paths that legitimately need superuser, not the hand-run-DDL hazard this closes.just psql→trakrf-migratewith atrakrf=>prompt;just psql-super→postgres). Note\conninforeportssession_userand so sayspostgresin both modes;SELECT current_user, or the=>vs=#prompt, is the check that distinguishes them.Refs TRA-1105