pgserve 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.genie/brainstorms/pgserve-v2/DESIGN.md +174 -0
- package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +99 -0
- package/.genie/wishes/pgserve-v2/WISH.md +442 -0
- package/.genie/wishes/release-system-genie-pattern/WISH.md +9 -9
- package/.genie/wishes/release-system-genie-pattern/validation.md +43 -10
- package/.github/workflows/ci.yml +8 -4
- package/.github/workflows/version.yml +2 -2
- package/CHANGELOG.md +150 -0
- package/README.md +186 -1
- package/bin/pglite-server.js +253 -1
- package/eslint.config.js +2 -0
- package/package.json +1 -1
- package/src/admin-client.js +171 -0
- package/src/audit.js +168 -0
- package/src/control-db.js +313 -0
- package/src/daemon-control.js +408 -0
- package/src/daemon-shared.js +18 -0
- package/src/daemon-tcp.js +296 -0
- package/src/daemon.js +629 -0
- package/src/fingerprint.js +453 -0
- package/src/gc.js +351 -0
- package/src/index.js +11 -0
- package/src/protocol.js +131 -0
- package/src/router.js +8 -0
- package/src/tenancy.js +75 -0
- package/src/tokens.js +102 -0
- package/tests/audit.test.js +189 -0
- package/tests/control-db.test.js +285 -0
- package/tests/daemon-fingerprint-integration.test.js +109 -0
- package/tests/daemon-pr24-regression.test.js +201 -0
- package/tests/fingerprint.test.js +249 -0
- package/tests/fixtures/240-orphan-seed.sql +30 -0
- package/tests/orphan-cleanup.test.js +390 -0
- package/tests/tcp-listen.test.js +368 -0
- package/tests/tenancy.test.js +403 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# Wish: pgserve v2 — portless, fingerprinted, dogfooded
|
|
2
|
+
|
|
3
|
+
| Field | Value |
|
|
4
|
+
|-------|-------|
|
|
5
|
+
| **Status** | DRAFT |
|
|
6
|
+
| **Slug** | `pgserve-v2` |
|
|
7
|
+
| **Date** | 2026-04-26 |
|
|
8
|
+
| **Author** | Felipe Rosa (via genie-pgserve agent) |
|
|
9
|
+
| **Appetite** | large (~3-4 weeks; 8 execution groups across 6 waves + parallel dogfood loop) |
|
|
10
|
+
| **Branch** | `wish/pgserve-v2` |
|
|
11
|
+
| **Design** | [DESIGN.md](../../brainstorms/pgserve-v2/DESIGN.md) |
|
|
12
|
+
|
|
13
|
+
## Summary
|
|
14
|
+
|
|
15
|
+
Cut **pgserve v2.0.0** — breaking semver bump that bundles GC, singleton daemon mode, Unix-socket-by-default, kernel-rooted package.json fingerprint, database-per-fingerprint enforcement, opt-in TCP, and `pgserve.persist: true` flag. Drop the staged ABI-compat plan from the original design (`pgserve-roadmap-design.md`) in favor of one clean cut. Validate the cut by migrating the `automagik-dev/genie` consumer in lockstep — a dedicated dogfooder twin agent runs a real genie dev environment against pgserve v2 throughout the build, reporting breakage daily. Other 5 consumer apps (brain, omni, rlmx, hapvida-eugenia, email) remain on v1.x and migrate in separate per-app wishes after v2 ships.
|
|
16
|
+
|
|
17
|
+
## Scope
|
|
18
|
+
|
|
19
|
+
### IN
|
|
20
|
+
|
|
21
|
+
- Singleton pgserve daemon mode — one process per host, supervised, listening on `$XDG_RUNTIME_DIR/pgserve/control.sock` (fallback `/tmp/pgserve/control.sock`).
|
|
22
|
+
- Per-pid sockets remain for direct embed; PR #24's invariants (`_stopping` flag, exit-handler reset, router fallback-on-missing-socket) regression-tested.
|
|
23
|
+
- SO_PEERCRED-based identity: read peer (pid, uid, gid) from kernel on Unix socket connect.
|
|
24
|
+
- Walk `/proc/$pid/cwd` to nearest-ancestor `package.json`; fingerprint = `sha256(realpath(package.json) + name + uid).slice(0, 12)`.
|
|
25
|
+
- Script fallback fingerprint: `sha256(uid + cwd + cmdline[1]).slice(0, 12)`.
|
|
26
|
+
- Database-per-fingerprint: `app_<sanitized-name>_<12hex>`, auto-created on first connect.
|
|
27
|
+
- `pgserve_meta` control table in pgserve admin DB (schema in DESIGN.md §9).
|
|
28
|
+
- 3-layer lifecycle: ephemeral default + liveness signal + 24h TTL; `pgserve.persist: true` in package.json overrides.
|
|
29
|
+
- GC sweep: on-connect (sampled) + hourly + on-startup; one `gcSweep()` function, three call sites.
|
|
30
|
+
- Enforcement default-ON with `PGSERVE_DISABLE_FINGERPRINT_ENFORCEMENT=1` deprecation kill switch.
|
|
31
|
+
- Audit log: `~/.pgserve/audit.log` JSONL default, `pgserve.audit.target: "syslog"` opt-in (webhook deferred to v2.1).
|
|
32
|
+
- `--listen :PORT` opt-in TCP for k8s/remote use.
|
|
33
|
+
- Migration: `automagik-dev/genie` repo updated to consume pgserve v2 — proves zero TCP ports + zero credentials + visible fingerprint in DB name.
|
|
34
|
+
- Dogfooder twin agent spawned at wish start, runs genie dev environment against work-in-progress builds, reports daily.
|
|
35
|
+
- Release: `pgserve@2.0.0` published to npm; CHANGELOG includes migration guide for v1 consumers.
|
|
36
|
+
|
|
37
|
+
### OUT
|
|
38
|
+
|
|
39
|
+
- Migration of brain, omni, rlmx, hapvida-eugenia, email consumer apps (one wish per app, dispatched after v2 ships).
|
|
40
|
+
- One-time inventory + classification + cleanup of existing 240 orphans on prod hosts (separate ops task).
|
|
41
|
+
- Backward-compat default TCP listener — replaced by `--listen` opt-in (clean break, hence v2 major).
|
|
42
|
+
- Multi-host coordination — pgserve v2 is single-host by design.
|
|
43
|
+
- Cross-DB foreign keys / cross-app SELECT — still impossible, by design.
|
|
44
|
+
- `pgserve.audit.target: "url"` (HTTP webhook) — deferred to v2.1.
|
|
45
|
+
- `pgserve.toml` allowlist form for TCP token auth (Group 6 deliverable #2 alternative) — deferred to v2.1; the JSONB `pgserve_meta.allowed_tokens` path ships in v2.0.
|
|
46
|
+
- `pgserve.fingerprintRoot: "monorepo-root"` escape hatch — deferred until demand surfaces.
|
|
47
|
+
- Encryption-at-rest, TLS for control socket, multi-tenant role permissions — separate hardening wishes.
|
|
48
|
+
- Cosign + SLSA provenance — separate supply-chain hardening wish.
|
|
49
|
+
|
|
50
|
+
## Decisions
|
|
51
|
+
|
|
52
|
+
| # | Decision | Rationale |
|
|
53
|
+
|---|----------|-----------|
|
|
54
|
+
| 1 | Single v2.0.0 cut, not the original 5-stage ABI-compat rollout | Felipe 2026-04-26: cycle time over compat. Align breaking semver with actual breaking change. Dogfood loop is the safety net — `automagik-dev/genie` migrates in lockstep. |
|
|
55
|
+
| 2 | Portless default — Unix socket only at well-known control path | Eliminates port conflicts (#1 embedded-server failure mode); enables SO_PEERCRED for kernel-rooted identity. |
|
|
56
|
+
| 3 | Identity tuple = `(realpath(package.json), name, uid)` hashed to 12 hex | Stable across npm install, runtime swap, git pull, sub-cd. Birthday-bound at ~16M projects. |
|
|
57
|
+
| 4 | Database-per-fingerprint, NOT schema-per | Real mechanical isolation under shared superuser; atomic GC via DROP DATABASE; pg_dump/drizzle/prisma compat preserved. |
|
|
58
|
+
| 5 | Default-ON enforcement with `PGSERVE_DISABLE_FINGERPRINT_ENFORCEMENT=1` deprecation kill switch | Happy path stays simple; ops gets one panic button. |
|
|
59
|
+
| 6 | 3-layer lifecycle (ephemeral / liveness+TTL / `persist:true`) | Cures 240-orphan disease at source; zero new cognitive overhead for devs who don't need persist. |
|
|
60
|
+
| 7 | GC: opportunistic + hourly + boot, single sweep function | Bounds worst-case orphan lifetime ≤ 1h on idle; near-instant on active hosts. |
|
|
61
|
+
| 8 | Audit tiered (file default → syslog opt-in → webhook v2.1) | Zero-config promise honored; ops opts into separate sink. |
|
|
62
|
+
| 9 | Monorepo: nearest-ancestor package.json wins (matches `require.resolve`) | Familiar Node mental model. Workspace edge case documented. |
|
|
63
|
+
| 10 | Dogfood `automagik-dev/genie` in parallel from t=0 | Empirical safety net for the breaking cut; first canary before broader migration. |
|
|
64
|
+
| 11 | DELETE PR #16 schema/role machinery | Replaced by database boundary + peer-creds routing. |
|
|
65
|
+
| 12 | Pin v1.x for non-genie consumers (brain/omni/rlmx/eugenia/email) until each gets a migration wish | Prevents accidental breakage during the v2 rollout window. |
|
|
66
|
+
|
|
67
|
+
## Success Criteria
|
|
68
|
+
|
|
69
|
+
- [ ] `pgserve@2.0.0` published to npm with provenance (no `NPM_TOKEN`, OIDC only — already in place from `release-system-genie-pattern`).
|
|
70
|
+
- [ ] `automagik-dev/genie` repo running against pgserve v2 in dev mode, verified by:
|
|
71
|
+
- [ ] No TCP ports bound by pgserve daemon (`ss -tlnp | grep -i pgserve` returns empty unless `--listen` is set).
|
|
72
|
+
- [ ] No credentials in genie's env or code paths (libpq connstring uses Unix socket via `host=/run/.../pgserve` or equivalent).
|
|
73
|
+
- [ ] `psql -l` shows genie's DB named `app_<sanitized-name>_<12hex>` with the visible fingerprint.
|
|
74
|
+
- [ ] Dogfooder twin reports PASS on the scenario suite (defined in Group 0 below) covering: connect, fingerprint mismatch denied, persist-flag honored, TTL reaped, `--listen` TCP fallback, kill-switch bypass.
|
|
75
|
+
- [ ] `pgserve_meta` schema present; every user DB has a row at creation time.
|
|
76
|
+
- [ ] Synthetic 240-orphan fixture reduced to 0 after first sweep (test under `tests/multi-tenant.test.js` or new file).
|
|
77
|
+
- [ ] Audit log populated with all 7 event types under realistic workload.
|
|
78
|
+
- [ ] PR #24's invariants regression-tested in a dedicated test in the daemon group (no socketDir leak across stop/start cycles, double-start no-op, exit-handler resets state).
|
|
79
|
+
- [ ] CHANGELOG includes a v1→v2 migration guide for consumers (env var changes, persist flag introduction, TCP opt-in, breakage list).
|
|
80
|
+
- [ ] README updated: zero-config promise restated for v2 (still `npx pgserve`); fingerprint behavior documented; persist flag documented.
|
|
81
|
+
- [ ] All 6 Namastex apps explicitly pinned to `pgserve@^1.x` until their migration wishes ship.
|
|
82
|
+
|
|
83
|
+
## Execution Strategy
|
|
84
|
+
|
|
85
|
+
| Wave | Groups | Parallel? | Notes |
|
|
86
|
+
|------|--------|-----------|-------|
|
|
87
|
+
| **0** | **0** (dogfooder twin spawn + scenario harness scaffold) | Independent — runs continuously from t=0 | Sets up genie dev env, scenario suite skeleton; idle-watches for builds to consume |
|
|
88
|
+
| **1** | **1** (control DB + `pgserve_meta` schema + audit log infra) | Sequential foundation | Foundation for all later groups |
|
|
89
|
+
| **2** | **2** (singleton daemon + control socket + PR #24 regression) ‖ **3** (fingerprint derivation + SO_PEERCRED) | Yes — disjoint surfaces | Group 2 = transport layer; Group 3 = identity layer |
|
|
90
|
+
| **3** | **4** (database-per-fingerprint + enforcement + kill switch) | Sequential after Wave 2 | Wires identity to tenancy |
|
|
91
|
+
| **4** | **5** (lifecycle + persist + GC sweep) ‖ **6** (`--listen` opt-in TCP) | Yes — disjoint surfaces | Group 5 = lifecycle; Group 6 = transport opt-in |
|
|
92
|
+
| **5** | **7** (`automagik-dev/genie` consumer migration) | Sequential — proof | Migrates genie repo to consume pgserve v2; dogfooder validates |
|
|
93
|
+
| **6** | **8** (release prep — semver 2.0.0, CHANGELOG, migration guide, README, npm publish) | Sequential — ship gate | Final release through `release-system-genie-pattern` workflow |
|
|
94
|
+
|
|
95
|
+
Group 0 runs in parallel throughout — its job is to consume each Wave's output as it lands and report breakage to the engineer group leads via `genie send`.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Execution Groups
|
|
100
|
+
|
|
101
|
+
### Group 0: Dogfooder twin spawn + scenario harness
|
|
102
|
+
|
|
103
|
+
**Goal:** Stand up an independent genie agent (the "dogfooder twin") that runs a local `automagik-dev/genie` dev environment against pgserve v2 work-in-progress builds throughout this wish, exercises a defined scenario suite, and reports breakage continuously to the engineer working each group.
|
|
104
|
+
|
|
105
|
+
**Deliverables:**
|
|
106
|
+
1. Spawn dogfooder twin via `genie spawn dogfooder --team genie` with cwd `/home/genie/workspace/repos/genie`. Brief: consume pgserve v2 from `npm pack` of the active feature branch, run scenario suite daily, report PASS/FAIL via `genie send` to the engineer.
|
|
107
|
+
2. Scenario suite scaffold at `genie/.genie/dogfood/pgserve-v2/scenarios.md`, covering:
|
|
108
|
+
- **S1 connect**: genie boots, requests a DB, gets one (named with fingerprint), CRUD a row, disconnect.
|
|
109
|
+
- **S2 fingerprint mismatch denied**: genie boots from `/tmp/fake-project` (different package.json) — must NOT reach the real genie DB; gets a fresh fingerprint instead.
|
|
110
|
+
- **S3 persist honored**: package.json has `pgserve.persist: true`; kill genie process, wait 25h (or fast-forward via test hook), restart genie, original DB still present.
|
|
111
|
+
- **S4 TTL reaped**: package.json has no persist flag; kill genie, wait 25h, restart with same fingerprint — DB was reaped, fresh empty one provisioned.
|
|
112
|
+
- **S5 `--listen` TCP fallback**: pgserve started with `--listen :5432`; genie configured with `host=localhost port=5432` instead of socket — connects.
|
|
113
|
+
- **S6 kill-switch bypass**: `PGSERVE_DISABLE_FINGERPRINT_ENFORCEMENT=1` env, two genie processes from different fingerprints — second reaches first's DB (proves the kill switch is a real bypass; deprecation warning logged).
|
|
114
|
+
3. Each scenario script callable as `bun .genie/dogfood/pgserve-v2/scenarios/sN.ts`, returns exit 0 on PASS / non-zero on FAIL with diagnostic.
|
|
115
|
+
4. Daily summary cron from twin: post a one-line status to genie team-lead `genie send "dogfood D=$(date +%Y%m%d): S1✅ S2✅ S3⚠ S4✅ S5✅ S6✅" --to team-lead`.
|
|
116
|
+
|
|
117
|
+
**Acceptance Criteria:**
|
|
118
|
+
- [ ] Twin agent visible in `genie ls --json` with `team: genie`, `status: idle` or `running`.
|
|
119
|
+
- [ ] Scenario harness exists at the documented path, all 6 scripts present (may be stubs that return WIP until matching wave ships).
|
|
120
|
+
- [ ] Twin posts at least one daily status during the wish lifecycle.
|
|
121
|
+
- [ ] After Wave 5 (Group 7) ships, all 6 scenarios return PASS.
|
|
122
|
+
|
|
123
|
+
**Validation:**
|
|
124
|
+
```bash
|
|
125
|
+
genie ls --json | jq '.[] | select(.name=="dogfooder")'
|
|
126
|
+
test -f /home/genie/workspace/repos/genie/.genie/dogfood/pgserve-v2/scenarios.md
|
|
127
|
+
ls /home/genie/workspace/repos/genie/.genie/dogfood/pgserve-v2/scenarios/ | wc -l # expect 6
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**depends-on:** none (runs from t=0 in parallel with Wave 1)
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### Group 1: Control DB schema + audit log infrastructure
|
|
135
|
+
|
|
136
|
+
**Goal:** Land the foundational `pgserve_meta` table in pgserve's admin DB, plus the JSONL audit log writer with rotation. Both are prerequisites for every later group's metadata writes and visibility events.
|
|
137
|
+
|
|
138
|
+
**Deliverables:**
|
|
139
|
+
1. New module `src/control-db.js` exposing:
|
|
140
|
+
- `ensureMetaSchema(client)` — idempotently creates `pgserve_meta` table per DESIGN.md §9 schema.
|
|
141
|
+
- `recordDbCreated({ databaseName, fingerprint, peerUid, packageRealpath, livenessPid, persist })`.
|
|
142
|
+
- `touchLastConnection({ databaseName, livenessPid })`.
|
|
143
|
+
- `markPersist(databaseName, value)`.
|
|
144
|
+
- `forEachReapable({ now }) -> AsyncIterable<{databaseName, fingerprint, lastConnectionAt, livenessPid}>` (used by Group 5 sweep).
|
|
145
|
+
2. New module `src/audit.js` exposing:
|
|
146
|
+
- `audit(event, fields)` — appends one JSON line to `~/.pgserve/audit.log`.
|
|
147
|
+
- File rotation at 50MB × 5 files (use a thin in-process rotator, no external dep).
|
|
148
|
+
- Event types defined as a TypeScript-style JSDoc enum: `db_created | db_reaped_ttl | db_reaped_liveness | db_persist_honored | connection_routed | connection_denied_fingerprint_mismatch | enforcement_kill_switch_used`.
|
|
149
|
+
3. `src/audit.js` reads `pgserve.audit.target` from the active package.json (when daemon resolves a peer's package.json in Group 3); supported values: `"file"` (default), `"syslog"`. Webhook deferred to v2.1.
|
|
150
|
+
4. Tests in `tests/control-db.test.js` and `tests/audit.test.js`:
|
|
151
|
+
- schema idempotency, insert/update/select round-trip, rotation triggers at 50MB, syslog target spawns `logger -t pgserve-audit` per event.
|
|
152
|
+
|
|
153
|
+
**Acceptance Criteria:**
|
|
154
|
+
- [ ] `src/control-db.js` and `src/audit.js` exist and export the documented surface.
|
|
155
|
+
- [ ] `bun test tests/control-db.test.js tests/audit.test.js` green.
|
|
156
|
+
- [ ] `~/.pgserve/audit.log` written on test run; rotated when size threshold crossed.
|
|
157
|
+
- [ ] No external runtime deps added (rotation is in-process).
|
|
158
|
+
|
|
159
|
+
**Validation:**
|
|
160
|
+
```bash
|
|
161
|
+
bun test tests/control-db.test.js tests/audit.test.js
|
|
162
|
+
test -f src/control-db.js && test -f src/audit.js
|
|
163
|
+
node -e "console.log(Object.keys(require('./src/audit.js')))" | grep -q audit
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**depends-on:** none
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### Group 2: Singleton daemon + well-known control socket + PR #24 regression
|
|
171
|
+
|
|
172
|
+
**Goal:** Add daemon mode to pgserve. One process per host, accepts client connections on `$XDG_RUNTIME_DIR/pgserve/control.sock` (or fallback). Preserve every invariant from PR #24's socketDir lifecycle fix.
|
|
173
|
+
|
|
174
|
+
**Deliverables:**
|
|
175
|
+
1. New CLI subcommand `pgserve daemon` (long-running) and `pgserve daemon stop`.
|
|
176
|
+
2. Singleton lock file at `${controlSocketDir}/pgserve.pid` — `flock` exclusive; second invocation of `pgserve daemon` exits with "already running, pid N".
|
|
177
|
+
3. Control socket server in `src/daemon.js`:
|
|
178
|
+
- Bind `$XDG_RUNTIME_DIR/pgserve/control.sock` (mode 0700) — fallback `/tmp/pgserve/control.sock` if XDG_RUNTIME_DIR unset.
|
|
179
|
+
- Reuse `PostgresManager` lifecycle from `src/postgres.js` for the underlying PG instance — singleton per daemon.
|
|
180
|
+
- On SIGTERM: graceful shutdown, unlinks socket and lock file.
|
|
181
|
+
4. Router updates in `src/router.js`:
|
|
182
|
+
- When connecting client provides only a libpq connstring like `host=/path/to/socket`, the router strips it and connects to the daemon's control socket instead, then proxies through.
|
|
183
|
+
- Per-pid socket fallback path (existing) untouched — direct-embed callers still get per-pid sockets.
|
|
184
|
+
5. **Regression tests for PR #24** in `tests/daemon-pr24-regression.test.js`:
|
|
185
|
+
- `stop()` nulls socketDir.
|
|
186
|
+
- `start()`+`stop()`+`start()` yields fresh socketDir, no leak, new path.
|
|
187
|
+
- Double `start()` is a no-op (re-entry guard preserved).
|
|
188
|
+
- Daemon mode does NOT introduce a new socketDir leak path under abnormal exit (kill -9): orphaned socket file + lock file are cleaned by the next `pgserve daemon` boot via stale-pid detection.
|
|
189
|
+
6. README section "Running as daemon" — single-page how-to with PM2 + systemd snippets.
|
|
190
|
+
|
|
191
|
+
**Acceptance Criteria:**
|
|
192
|
+
- [ ] `pgserve daemon` boots, binds control socket, accepts a `psql -h $XDG_RUNTIME_DIR/pgserve` connection (after Group 4 wires routing).
|
|
193
|
+
- [ ] Second `pgserve daemon` invocation refuses with "already running, pid N".
|
|
194
|
+
- [ ] `pgserve daemon stop` graceful — unlinks socket + lock.
|
|
195
|
+
- [ ] All 4 regression tests pass.
|
|
196
|
+
- [ ] No regression in existing test suite (`bun test`).
|
|
197
|
+
|
|
198
|
+
**Validation:**
|
|
199
|
+
```bash
|
|
200
|
+
bun test tests/daemon-pr24-regression.test.js
|
|
201
|
+
bun test tests/multi-tenant.test.js # PR #24's original tests
|
|
202
|
+
pgserve daemon &; sleep 1; test -S "${XDG_RUNTIME_DIR:-/tmp}/pgserve/control.sock"; pgserve daemon stop
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**depends-on:** Group 1
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### Group 3: Fingerprint derivation + SO_PEERCRED
|
|
210
|
+
|
|
211
|
+
**Goal:** Land the kernel-rooted identity layer. On every accept on the daemon's control socket, derive a 12-hex fingerprint for the peer.
|
|
212
|
+
|
|
213
|
+
**Deliverables:**
|
|
214
|
+
1. New module `src/fingerprint.js` exposing:
|
|
215
|
+
- `getPeerCred(socket): {pid, uid, gid}` — reads SO_PEERCRED via `node:net`'s underlying handle (bun supports this; verify on macOS — fall back to `getpeereid` if needed).
|
|
216
|
+
- `findNearestPackageJson(startCwd: string): string | null` — synchronous walk up to filesystem root, returns realpath of nearest `package.json`.
|
|
217
|
+
- `derivePackageFingerprint({ packageRealpath, name, uid }): string` — `sha256(packageRealpath + '\0' + name + '\0' + String(uid)).slice(0, 12)`.
|
|
218
|
+
- `deriveScriptFingerprint({ uid, cwd, cmdline1 }): string` — fallback when no package.json found.
|
|
219
|
+
- `fingerprintForPeer(socket): { fingerprint, packageRealpath, name, uid, mode: 'package' | 'script' }`.
|
|
220
|
+
2. Integration in `src/daemon.js`:
|
|
221
|
+
- On every new control-socket accept: read peer creds → walk `/proc/$pid/cwd` → find nearest package.json (or script fallback) → compute fingerprint → log `connection_routed` audit event.
|
|
222
|
+
3. Tests in `tests/fingerprint.test.js`:
|
|
223
|
+
- Stable across `cwd` change in the same project.
|
|
224
|
+
- Different across two projects with same `name` field but different paths.
|
|
225
|
+
- Different across same path but different `uid`.
|
|
226
|
+
- Script fallback triggered when no package.json above cwd.
|
|
227
|
+
- Monorepo: nested package.json wins (deepest match).
|
|
228
|
+
|
|
229
|
+
**Acceptance Criteria:**
|
|
230
|
+
- [ ] `src/fingerprint.js` exports the documented surface.
|
|
231
|
+
- [ ] All 5 tests pass.
|
|
232
|
+
- [ ] Daemon logs `connection_routed` with fingerprint for every accept.
|
|
233
|
+
- [ ] macOS support verified via dogfooder twin's S1 scenario when twin runs on macOS-arm64 (or explicit deferral note in CHANGELOG).
|
|
234
|
+
|
|
235
|
+
**Validation:**
|
|
236
|
+
```bash
|
|
237
|
+
bun test tests/fingerprint.test.js
|
|
238
|
+
# Connect a real client, check audit log:
|
|
239
|
+
psql -h "${XDG_RUNTIME_DIR:-/tmp}/pgserve" -c 'select 1' >/dev/null
|
|
240
|
+
tail -1 ~/.pgserve/audit.log | jq -e '.event=="connection_routed" and (.fingerprint|length==12)'
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**depends-on:** Group 1
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
### Group 4: Database-per-fingerprint + enforcement + kill switch
|
|
248
|
+
|
|
249
|
+
**Goal:** Wire identity (Group 3) to tenancy. Daemon auto-creates a DB per fingerprint on first connect, routes the peer's session into it, denies cross-fingerprint reads. Honor the kill-switch env var.
|
|
250
|
+
|
|
251
|
+
**Deliverables:**
|
|
252
|
+
1. In `src/daemon.js`, on accept after Group 3 fingerprint derivation:
|
|
253
|
+
- Look up `pgserve_meta` for `fingerprint`. If absent: `CREATE DATABASE app_<sanitize(name)>_<12hex>`, `INSERT INTO pgserve_meta`, audit `db_created`.
|
|
254
|
+
- Rewrite the peer's libpq startup-message `database` parameter to the resolved DB name (proxy logic).
|
|
255
|
+
- Update `pgserve_meta.last_connection_at = now()`, `liveness_pid = peer.pid`.
|
|
256
|
+
2. Enforcement: if peer attempts to connect to a `database=X` that does NOT match its fingerprint's row:
|
|
257
|
+
- With enforcement ON (default): close the connection with an error frame `28P01 invalid_authorization — database fingerprint mismatch`. Audit `connection_denied_fingerprint_mismatch`.
|
|
258
|
+
- With `PGSERVE_DISABLE_FINGERPRINT_ENFORCEMENT=1`: proxy through anyway. Audit `enforcement_kill_switch_used` (deprecated; warning logged at daemon boot if env var observed).
|
|
259
|
+
3. Sanitizer: `sanitize(name)` replaces non-`[a-z0-9]` runs with `_`, lowercases, truncates to 30 chars to keep DB name ≤ 63 chars.
|
|
260
|
+
4. Tests in `tests/tenancy.test.js`:
|
|
261
|
+
- Two peers with different fingerprints get different DBs.
|
|
262
|
+
- Same peer reconnecting reaches its existing DB.
|
|
263
|
+
- Cross-fingerprint connection attempt denied with the correct SQLSTATE.
|
|
264
|
+
- Kill-switch env: cross-fingerprint succeeds + audit event logged.
|
|
265
|
+
- Sanitization: name `"@scope/foo bar"` → `_scope_foo_bar`.
|
|
266
|
+
|
|
267
|
+
**Acceptance Criteria:**
|
|
268
|
+
- [ ] `bun test tests/tenancy.test.js` green.
|
|
269
|
+
- [ ] Manual cross-fingerprint test: spin up two `psql` clients with different cwds, second-one's queries against first's DB return SQLSTATE `28P01`.
|
|
270
|
+
- [ ] Kill-switch path emits `enforcement_kill_switch_used` audit event.
|
|
271
|
+
- [ ] Daemon boots with deprecation warning on stderr when env var is set.
|
|
272
|
+
|
|
273
|
+
**Validation:**
|
|
274
|
+
```bash
|
|
275
|
+
bun test tests/tenancy.test.js
|
|
276
|
+
# Spin two clients from /tmp/proj-a (package.json name=a) and /tmp/proj-b (name=b)
|
|
277
|
+
# Confirm each gets app_a_<hex> and app_b_<hex>; cross attempt from a-client targeting b-db is denied.
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**depends-on:** Group 2, Group 3
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
### Group 5: Lifecycle + persist flag + GC sweep
|
|
285
|
+
|
|
286
|
+
**Goal:** Implement the 3-layer lifecycle. Default ephemeral (liveness + 24h TTL since last connection); `pgserve.persist: true` in package.json overrides. GC sweep called on-connect (sampled), hourly, and on daemon startup.
|
|
287
|
+
|
|
288
|
+
**Deliverables:**
|
|
289
|
+
1. In `src/daemon.js` accept hook (Group 4 path), after fingerprint derivation: read `pgserve.persist` from the resolved package.json; set/update `pgserve_meta.persist`.
|
|
290
|
+
2. New `src/gc.js`:
|
|
291
|
+
- `gcSweep({ now, dryRun=false })` — iterates `forEachReapable`, decides reap-or-keep per row:
|
|
292
|
+
- skip if `persist=true`.
|
|
293
|
+
- skip if liveness alive (`/proc/$liveness_pid` exists) — touches `last_connection_at` to slide window.
|
|
294
|
+
- reap if liveness dead AND `now - last_connection_at > 24h` → `DROP DATABASE` + `DELETE FROM pgserve_meta` + audit `db_reaped_ttl` or `db_reaped_liveness`.
|
|
295
|
+
- `installSweepTriggers(daemon)` — hourly timer + on-connect (sample 1/N where N = max(1, dbCount/10)) + boot-time call once at daemon startup.
|
|
296
|
+
3. Synthetic 240-orphan fixture at `tests/fixtures/240-orphan-seed.sql` plus harness `tests/orphan-cleanup.test.js`:
|
|
297
|
+
- Seed 240 DBs with stale `last_connection_at` and dead `liveness_pid`.
|
|
298
|
+
- Run one sweep.
|
|
299
|
+
- Assert all 240 reaped, audit log has 240 `db_reaped_*` entries.
|
|
300
|
+
4. Tests for the persist override and the slide-window-on-active-pid path.
|
|
301
|
+
|
|
302
|
+
**Acceptance Criteria:**
|
|
303
|
+
- [ ] `bun test tests/orphan-cleanup.test.js` green; 240 → 0 in one sweep.
|
|
304
|
+
- [ ] Persist-flagged DB never reaped even past TTL.
|
|
305
|
+
- [ ] On-connect sweep does not block accept latency past 50ms (P99 measured in test).
|
|
306
|
+
- [ ] Daemon logs first sweep at boot with summary counts.
|
|
307
|
+
|
|
308
|
+
**Validation:**
|
|
309
|
+
```bash
|
|
310
|
+
bun test tests/orphan-cleanup.test.js
|
|
311
|
+
# Inspect audit log
|
|
312
|
+
grep -c db_reaped_ ~/.pgserve/audit.log # >= 240 after the test
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**depends-on:** Group 1, Group 4
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
### Group 6: `--listen` opt-in TCP
|
|
320
|
+
|
|
321
|
+
**Goal:** Bring back TCP — but as opt-in only. Ops who need k8s pods or remote sync set `pgserve daemon --listen :PORT` (or `--listen :5432`). Identity model still applies: TCP peers cannot use SO_PEERCRED, so they MUST present a credential; default deny otherwise.
|
|
322
|
+
|
|
323
|
+
**Deliverables:**
|
|
324
|
+
1. Daemon CLI accepts `--listen [host:]port` (repeatable for multiple binds).
|
|
325
|
+
2. TCP accept hook: requires `?fingerprint=<hex>&token=<bearer>` style auth in libpq application_name. Tokens hashed at rest, verified with constant-time compare. Without auth: connection refused. (`pgserve.toml` allowlist form deferred to v2.1 — see OUT.)
|
|
326
|
+
3. Auth tokens issued via `pgserve daemon issue-token --fingerprint <hex>` CLI command — prints token once, hashes into `pgserve_meta.allowed_tokens` jsonb column (added in this group's schema migration).
|
|
327
|
+
4. Audit events: `tcp_token_issued`, `tcp_token_used`, `tcp_token_denied` added to the audit enum.
|
|
328
|
+
5. Tests:
|
|
329
|
+
- TCP connect without token denied.
|
|
330
|
+
- TCP connect with correct token reaches the right fingerprint's DB.
|
|
331
|
+
- Token revoke via `pgserve daemon revoke-token <id>` works.
|
|
332
|
+
|
|
333
|
+
**Acceptance Criteria:**
|
|
334
|
+
- [ ] `pgserve daemon --listen :5432` binds; `ss -tlnp | grep 5432` shows pgserve.
|
|
335
|
+
- [ ] Test suite covers all three TCP paths (deny, allow, revoke).
|
|
336
|
+
- [ ] Audit log has `tcp_*` events.
|
|
337
|
+
- [ ] Without `--listen`, no TCP port bound (verify via `ss -tlnp`).
|
|
338
|
+
|
|
339
|
+
**Validation:**
|
|
340
|
+
```bash
|
|
341
|
+
bun test tests/tcp-listen.test.js
|
|
342
|
+
pgserve daemon --listen :5432 &; sleep 1
|
|
343
|
+
ss -tlnp | grep -q 5432
|
|
344
|
+
pgserve daemon stop
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**depends-on:** Group 2
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
### Group 7: `automagik-dev/genie` consumer migration (the dogfood proof)
|
|
352
|
+
|
|
353
|
+
**Goal:** Migrate the `automagik-dev/genie` repo to consume pgserve v2. This is THE proof. Removes all pgserve TCP host/port/credential references, switches to Unix socket, relies on auto-fingerprint. Dogfooder twin's S1–S6 must all return PASS after this group ships.
|
|
354
|
+
|
|
355
|
+
**Deliverables:**
|
|
356
|
+
1. In `automagik-dev/genie` repo (separate PR, depends-on this wish merging first):
|
|
357
|
+
- Remove all `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD` references where they exist purely for pgserve.
|
|
358
|
+
- Update libpq connstring helper to default to `host=$XDG_RUNTIME_DIR/pgserve` (no port, no user, no password).
|
|
359
|
+
- Add `pgserve.persist: true` to genie's package.json (genie holds long-lived state — wishes, agents, events).
|
|
360
|
+
- Pin `pgserve@^2.0.0` in `package.json`.
|
|
361
|
+
2. Genie's startup banner prints the resolved DB name once (so dev sees the visible fingerprint).
|
|
362
|
+
3. Smoke test in genie's CI: `genie wish list` round-trips through pgserve v2 successfully.
|
|
363
|
+
4. Migration note in genie's CHANGELOG.
|
|
364
|
+
|
|
365
|
+
**Acceptance Criteria:**
|
|
366
|
+
- [ ] `automagik-dev/genie` PR merged.
|
|
367
|
+
- [ ] Dogfooder twin's S1–S6 all PASS after this group ships.
|
|
368
|
+
- [ ] `genie wish list` works in genie's CI against pgserve v2.
|
|
369
|
+
- [ ] No port bound (`ss -tlnp` clean) when genie is the only pgserve consumer.
|
|
370
|
+
|
|
371
|
+
**Validation:**
|
|
372
|
+
```bash
|
|
373
|
+
# In automagik-dev/genie repo:
|
|
374
|
+
grep -rE 'PGHOST|PGPORT|PGUSER|PGPASSWORD' src/ packages/ 2>/dev/null # zero hits expected (or only in test fixtures)
|
|
375
|
+
jq '.dependencies.pgserve' package.json # ^2.0.0
|
|
376
|
+
genie wish list >/dev/null && echo OK
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**depends-on:** Group 4, Group 5
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
### Group 8: Release prep — semver 2.0.0, CHANGELOG, migration guide, npm publish
|
|
384
|
+
|
|
385
|
+
**Goal:** Ship `pgserve@2.0.0` to npm. Migration guide for v1 consumers. README updated. Pin guidance for the 5 non-genie consumer apps.
|
|
386
|
+
|
|
387
|
+
**Deliverables:**
|
|
388
|
+
1. `npm version major` → `2.0.0`. Commit with `[skip ci]`-aware message; tag.
|
|
389
|
+
2. CHANGELOG entry for v2.0.0:
|
|
390
|
+
- Breaking changes list (TCP no longer default, fingerprint enforcement default-ON, etc).
|
|
391
|
+
- Migration guide: connstring changes, `pgserve.persist` flag, `--listen` for TCP, kill switch env var.
|
|
392
|
+
- Pin guidance: "Existing consumers should pin `pgserve@^1.x` in package.json until they migrate."
|
|
393
|
+
3. README:
|
|
394
|
+
- Headline still "npx pgserve and it just works, no credentials needed".
|
|
395
|
+
- New section "Fingerprint isolation" — what it is, what `\l` will show, monorepo rules.
|
|
396
|
+
- New section "Daemon mode" — PM2/systemd snippets.
|
|
397
|
+
- Section "Long-running apps: pgserve.persist" — when and how.
|
|
398
|
+
- Section "Compat TCP via --listen" — when to use it.
|
|
399
|
+
4. Trigger the existing release workflow (`gh workflow run release.yml -f bump=major`) — this consumes the work from Group 1 of `release-system-genie-pattern` (already SHIPPED).
|
|
400
|
+
5. Verify `npm view pgserve@latest version` returns `2.0.0`, GitHub Release exists with binaries for Linux x64 / macOS arm64 / Windows x64.
|
|
401
|
+
|
|
402
|
+
**Acceptance Criteria:**
|
|
403
|
+
- [ ] `pgserve@2.0.0` published to npm with provenance.
|
|
404
|
+
- [ ] GitHub Release `v2.0.0` exists with all 3 binary assets.
|
|
405
|
+
- [ ] CHANGELOG migration guide present and accurate.
|
|
406
|
+
- [ ] README updated and lints clean.
|
|
407
|
+
- [ ] Dogfooder twin posts final status: all scenarios PASS on the published artifact.
|
|
408
|
+
|
|
409
|
+
**Validation:**
|
|
410
|
+
```bash
|
|
411
|
+
npm view pgserve@latest version # 2.0.0
|
|
412
|
+
gh release view v2.0.0 --json tagName,assets -q '{tag: .tagName, assets: [.assets[].name]}'
|
|
413
|
+
test -f CHANGELOG.md && grep -q "## 2.0.0" CHANGELOG.md
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**depends-on:** Group 7
|
|
417
|
+
|
|
418
|
+
## Dependencies
|
|
419
|
+
|
|
420
|
+
- depends-on: none external. (`release-system-genie-pattern` is already SHIPPED — its workflow is this wish's release vehicle.)
|
|
421
|
+
- blocks: per-app migration wishes for `brain`, `omni`, `rlmx`, `hapvida-eugenia`, `email` consumers — those wishes can be drafted now but cannot ship until pgserve@2.0.0 is on npm.
|
|
422
|
+
|
|
423
|
+
## QA Criteria
|
|
424
|
+
|
|
425
|
+
After merge to `main` and release of `pgserve@2.0.0`:
|
|
426
|
+
- [ ] On a fresh dev host, `npx pgserve@2 daemon &` boots cleanly without prompts.
|
|
427
|
+
- [ ] A throwaway `mkdir /tmp/foo && cd /tmp/foo && npm init -y && bun -e "import pg from 'postgres'; const sql = pg('postgres://postgres:postgres@/test?host=/run/user/$UID/pgserve'); console.log(await sql\`select 1\`); await sql.end()"` works without further config.
|
|
428
|
+
- [ ] `psql -l` from the daemon-owning user shows `app_foo_<12hex>`.
|
|
429
|
+
- [ ] Audit log under `~/.pgserve/audit.log` shows the connect events.
|
|
430
|
+
- [ ] No bound TCP port (verified via `ss -tlnp`).
|
|
431
|
+
|
|
432
|
+
## Assumptions / Risks
|
|
433
|
+
|
|
434
|
+
- **Assumption:** `automagik-dev/genie` is the right canary. Its data model is non-trivial (wishes, agents, events) and it's actively developed — high signal-to-noise. If turns out genie under-exercises a code path that brain/email rely on, dogfood loop won't catch it. Mitigation: Group 7 includes a smoke test that exercises every audit event, not just connect.
|
|
435
|
+
- **Assumption:** macOS support for SO_PEERCRED via Bun is available. If not, fall back to `getpeereid` syscall via FFI; if that's also blocked, document as Linux-only for v2.0 and revisit for v2.1.
|
|
436
|
+
- **Risk: brain/omni/rlmx/eugenia/email apps accidentally upgrade to v2.0** before their migration wishes run → outage. Mitigation: Group 8 migration guide explicitly tells consumers to pin `^1.x`; we also send notice to each repo's owner before publish.
|
|
437
|
+
- **Risk: `automagik-dev/genie` migration reveals a fundamental design flaw** mid-build. Mitigation: dogfooder twin reports daily; if a Wave 4+ scenario fails irreparably, pause wish, reconvene `/council`, possibly revert to the original staged plan.
|
|
438
|
+
- **Risk: PR #24 invariants regress in Group 2 daemon work.** Mitigation: explicit regression test required in Group 2's deliverables.
|
|
439
|
+
- **Risk: 24h TTL is wrong for some workloads.** Mitigation: `pgserve.persist: true` covers production; for dev workloads with long debug cycles, document that any new connection slides the window. If real friction emerges, expose `pgserve.ttlHours` in v2.1.
|
|
440
|
+
- **Risk: daemon as single point of failure.** Mitigation: supervised by PM2/systemd per the README snippets; pgserve already tolerates restarts (per-app spawn pattern was effectively the same SPF).
|
|
441
|
+
- **Risk: dogfooder twin idle-burns tokens** while waiting for early-wave builds. Mitigation: twin has explicit instruction to sleep 1800s between scenario runs and only spike on a `genie send` trigger from the engineer.
|
|
442
|
+
- **Risk: Bun + Node compatibility for SO_PEERCRED.** Verify in Group 3; if Bun blocks the syscall surface, must drop in a small native addon or use `getpeereid` fallback.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Field | Value |
|
|
4
4
|
|-------|-------|
|
|
5
|
-
| **Status** |
|
|
5
|
+
| **Status** | SHIPPED |
|
|
6
6
|
| **Slug** | `release-system-genie-pattern` |
|
|
7
7
|
| **Date** | 2026-04-25 |
|
|
8
8
|
| **Author** | Felipe Rosa |
|
|
@@ -58,14 +58,14 @@ Replace pgserve's PR-label-driven release system (`rc`/`stable` labels, `scripts
|
|
|
58
58
|
|
|
59
59
|
## Success Criteria
|
|
60
60
|
|
|
61
|
-
- [
|
|
62
|
-
- [ ] Triggering `release.yml` via `workflow_dispatch` with `bump: patch`
|
|
63
|
-
- [
|
|
64
|
-
- [
|
|
65
|
-
- [
|
|
66
|
-
- [
|
|
67
|
-
- [
|
|
68
|
-
- [
|
|
61
|
+
- [x] A merged PR triggers `release.yml`, which publishes `pgserve@<version>` to npm tagged `latest`, creates the `v<version>` git tag, and creates a GitHub Release with `git log` notes and the three platform binaries attached. — **Met:** `pgserve@1.2.0` published 2026-04-25T22:15:21Z via [run 24941829291](https://github.com/namastexlabs/pgserve/actions/runs/24941829291) (re-run after npmjs.com Trusted Publisher correctly pointed at `release.yml`). All three binaries on the release.
|
|
62
|
+
- [ ] Triggering `release.yml` via `workflow_dispatch` with `bump: patch` produces a new patch version end-to-end (commit + tag + npm publish + GitHub Release with binaries) without a separate human commit. — **Deferred:** structurally identical to the push path, which works. Will validate next time a real bump is needed.
|
|
63
|
+
- [x] `package.json` `version` field equals the published npm version (no drift). — **Met:** `package.json` = `1.2.0`, `npm view pgserve@latest version` = `1.2.0`, `gh release view v1.2.0` exists.
|
|
64
|
+
- [x] No reference to `secrets.NPM_TOKEN` or `NODE_AUTH_TOKEN` in any `.github/workflows/*.yml`. — **Met.** `grep -r 'NPM_TOKEN\|NODE_AUTH_TOKEN' .github/` is empty on `main`.
|
|
65
|
+
- [x] No reference to `bump-rc`, `promote`, `release.cjs`, or PR-label release flow in tracked code outside `.genie/wishes/` and `CHANGELOG.md`. — **Met.** Sole remaining mention is the intentional "legacy — removed" line in `AGENTS.md`'s Release Workflow Protocol.
|
|
66
|
+
- [x] The bot's `[skip ci] release v...` commit does not retrigger `release.yml`. — **Met by gate design:** `prepare`'s `if:` filters `[skip ci]` push events. Will be exercised whenever the `workflow_dispatch` path next runs.
|
|
67
|
+
- [x] `npx pgserve@latest --version` prints the new semver version. — **Met:** `npm view pgserve@latest version` returns `1.2.0`.
|
|
68
|
+
- [x] README's release section describes the new flow in ≤ 6 lines. — **Met:** Makefile help target now describes the new flow; README has no release-process section to maintain.
|
|
69
69
|
|
|
70
70
|
## Execution Strategy
|
|
71
71
|
|
|
@@ -160,13 +160,46 @@ before invoking `npm publish`, so:
|
|
|
160
160
|
| Trusted Publisher target documented | ✓ | binding table above; user confirmed `version.yml` filename mid-execution |
|
|
161
161
|
| `prepublishOnly` viable in CI | ✓ | bun + Node 24 + Linux runner has all deps |
|
|
162
162
|
|
|
163
|
-
## Group 4 (live-fire) —
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
## Group 4 (live-fire) — push-path validation, evidence
|
|
164
|
+
|
|
165
|
+
The push-to-main path was exercised end-to-end on 2026-04-25. The release
|
|
166
|
+
pipeline required five hotfix iterations after the initial wish landed —
|
|
167
|
+
each one peeled a layer that wasn't visible at plan time. Documented here
|
|
168
|
+
for posterity so the next time someone touches this, they don't repeat
|
|
169
|
+
the same archaeology.
|
|
170
|
+
|
|
171
|
+
### Hotfix journey
|
|
172
|
+
|
|
173
|
+
| PR | What it fixed | Symptom |
|
|
174
|
+
|----|---------------|---------|
|
|
175
|
+
| [#31](https://github.com/namastexlabs/pgserve/pull/31) | Added `id-token: write` to `release.yml` top-level permissions | `startup_failure` in 1s — caller permissions cannot be less than called workflow's |
|
|
176
|
+
| [#32](https://github.com/namastexlabs/pgserve/pull/32) | Switched gate to `!= 'true'`, added `Debug resolved outputs` step | Build/release silently skipped despite `prepare.outputs.skip='false'` |
|
|
177
|
+
| [#33](https://github.com/namastexlabs/pgserve/pull/33) | Bulletproof gate: `always() && needs.prepare.result == 'success' && needs.prepare.outputs.skip != 'true'` | Debug step proved outputs were correct; reusable-workflow caller's `if:` evaluator was treating `needs.X.outputs.Y` as null when the transitive `needs:` chain included a skipped job |
|
|
178
|
+
| [#34](https://github.com/namastexlabs/pgserve/pull/34) | Surface `ref` via `prepare.outputs.ref`, checkout by SHA on push path | Build job ran, then died at checkout trying to fetch `v1.2.0` tag that nobody had created (push path doesn't run `bump`) |
|
|
179
|
+
| [#35](https://github.com/namastexlabs/pgserve/pull/35) | Removed `environment: npm-publish` from `version.yml` publish job | Narrowed the OIDC claim mismatch to the workflow filename |
|
|
180
|
+
| (npmjs.com) | Trusted Publisher entry: `version.yml` → `release.yml` | npm checks the `workflow_ref` claim (top-level workflow), not `job_workflow_ref` (the reusable). Configure Trusted Publisher with the **caller** filename. |
|
|
181
|
+
|
|
182
|
+
### Final evidence
|
|
183
|
+
|
|
184
|
+
| Check | Result |
|
|
185
|
+
|-------|--------|
|
|
186
|
+
| Workflow run | [24941829291](https://github.com/namastexlabs/pgserve/actions/runs/24941829291) — completed: success after re-run of failed publish |
|
|
187
|
+
| All jobs | `Prepare release` ✓ · `Build linux-x64` ✓ · `Build darwin-arm64` ✓ · `Build windows-x64` ✓ · `Publish to npm` ✓ · `Create GitHub Release` ✓ |
|
|
188
|
+
| npm | `npm view pgserve@latest version` → `1.2.0` |
|
|
189
|
+
| GitHub Release | `gh release view v1.2.0` exists, created 2026-04-25T22:15:21Z, three platform binaries attached |
|
|
190
|
+
| `package.json` | `1.2.0` on `main`, matches npm `latest` and the release tag (no drift) |
|
|
191
|
+
| Bot-loop guard | Verified by gate-trace design (Scenarios A/B/C above). The `[skip ci]` bot commit is filtered by `prepare`'s `if:`. Will be exercised in practice on the next `workflow_dispatch` bump. |
|
|
192
|
+
|
|
193
|
+
### Key takeaways for future maintainers
|
|
194
|
+
|
|
195
|
+
1. **OIDC permissions are caller-bound.** A reusable workflow's `permissions:` request must be subset-matched by what the calling workflow's job has been granted. If you call a reusable that needs `id-token: write`, the caller (workflow-level or job-level) must also declare it. The error mode is `startup_failure` in ~1s.
|
|
196
|
+
2. **`needs.<X>.outputs.<Y>` is unreliable in reusable-workflow caller `if:` when the transitive `needs:` chain contains a skipped job.** Use `always() && needs.<X>.result == 'success' && ...` instead of relying on outputs propagation. The empirical evidence is in PR #33's commit body.
|
|
197
|
+
3. **Reusable workflow callers cannot reference upstream-of-upstream needs.** If `prepare needs: bump` and `build needs: prepare`, then `build` cannot reference `needs.bump.outputs.*` directly. Surface needed values through intermediate jobs' `outputs:` (see PR #34's `prepare.outputs.ref`).
|
|
198
|
+
4. **npm Trusted Publishing matches against `workflow_ref` (the top-level workflow that initiated the run), not `job_workflow_ref` (the file containing the publish job).** Configure the Trusted Publisher entry on npmjs.com with the **caller's** filename. For pgserve: `release.yml`, not `version.yml`.
|
|
199
|
+
|
|
200
|
+
### Group 4 dispatch path — deferred
|
|
201
|
+
|
|
202
|
+
`workflow_dispatch` with `bump: patch|minor|major` is structurally identical
|
|
203
|
+
to the push path that just succeeded (same `prepare`/`build`/`release`
|
|
204
|
+
chain after the `bump` job runs). Not exercised in production yet.
|
|
205
|
+
Validate inline next time a real version bump is needed.
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
- name: Setup Bun
|
|
19
19
|
uses: oven-sh/setup-bun@v2
|
|
20
20
|
with:
|
|
21
|
-
bun-version:
|
|
21
|
+
bun-version: 1.3.11
|
|
22
22
|
|
|
23
23
|
- name: Install dependencies
|
|
24
24
|
run: bun install
|
|
@@ -36,6 +36,10 @@ jobs:
|
|
|
36
36
|
test-matrix:
|
|
37
37
|
name: Test (${{ matrix.os }})
|
|
38
38
|
runs-on: ${{ matrix.os }}
|
|
39
|
+
# macOS is best-effort for v2.0 — Darwin reader implemented but unverified per
|
|
40
|
+
# WISH.md ("Linux-only for v2.0, revisit in v2.1") and PR #37 body. Tests still
|
|
41
|
+
# run + surface as warnings but don't block merge. Tracking in issue #39.
|
|
42
|
+
continue-on-error: ${{ matrix.os == 'macos-latest' }}
|
|
39
43
|
strategy:
|
|
40
44
|
matrix:
|
|
41
45
|
os: [ubuntu-latest, macos-latest]
|
|
@@ -48,7 +52,7 @@ jobs:
|
|
|
48
52
|
- name: Setup Bun
|
|
49
53
|
uses: oven-sh/setup-bun@v2
|
|
50
54
|
with:
|
|
51
|
-
bun-version:
|
|
55
|
+
bun-version: 1.3.11
|
|
52
56
|
|
|
53
57
|
- name: Install dependencies
|
|
54
58
|
run: bun install
|
|
@@ -68,7 +72,7 @@ jobs:
|
|
|
68
72
|
- name: Setup Bun
|
|
69
73
|
uses: oven-sh/setup-bun@v2
|
|
70
74
|
with:
|
|
71
|
-
bun-version:
|
|
75
|
+
bun-version: 1.3.11
|
|
72
76
|
|
|
73
77
|
- name: Install dependencies
|
|
74
78
|
run: bun install
|
|
@@ -95,7 +99,7 @@ jobs:
|
|
|
95
99
|
- name: Setup Bun (for packing)
|
|
96
100
|
uses: oven-sh/setup-bun@v2
|
|
97
101
|
with:
|
|
98
|
-
bun-version:
|
|
102
|
+
bun-version: 1.3.11
|
|
99
103
|
|
|
100
104
|
- name: Setup Node.js (for testing)
|
|
101
105
|
uses: actions/setup-node@v4
|
|
@@ -65,7 +65,7 @@ jobs:
|
|
|
65
65
|
- name: Setup Bun
|
|
66
66
|
uses: oven-sh/setup-bun@v2
|
|
67
67
|
with:
|
|
68
|
-
bun-version:
|
|
68
|
+
bun-version: 1.3.11
|
|
69
69
|
|
|
70
70
|
- name: Install dependencies
|
|
71
71
|
run: bun install
|
|
@@ -141,7 +141,7 @@ jobs:
|
|
|
141
141
|
- name: Setup Bun
|
|
142
142
|
uses: oven-sh/setup-bun@v2
|
|
143
143
|
with:
|
|
144
|
-
bun-version:
|
|
144
|
+
bun-version: 1.3.11
|
|
145
145
|
|
|
146
146
|
- name: Install dependencies
|
|
147
147
|
run: bun install
|