pgserve 1.1.10 → 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 +268 -0
- package/.genie/wishes/release-system-genie-pattern/validation.md +205 -0
- package/.github/workflows/ci.yml +8 -4
- package/.github/workflows/release.yml +233 -111
- package/.github/workflows/{build-all-platforms.yml → version.yml} +32 -8
- package/AGENTS.md +10 -8
- package/CHANGELOG.md +150 -0
- package/Makefile +18 -41
- package/README.md +186 -1
- package/SECURITY.md +109 -0
- 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/postgres.js +54 -0
- package/src/protocol.js +131 -0
- package/src/router.js +78 -5
- 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/multi-tenant.test.js +164 -0
- package/tests/orphan-cleanup.test.js +390 -0
- package/tests/tcp-listen.test.js +368 -0
- package/tests/tenancy.test.js +403 -0
- package/.github/release.yml +0 -30
- package/scripts/release.cjs +0 -198
|
@@ -1,138 +1,253 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Single-branch release pipeline modeled on khal-os/desktop.
|
|
4
|
+
#
|
|
5
|
+
# Two trigger paths into the same workflow:
|
|
6
|
+
#
|
|
7
|
+
# 1. push to main with a hand-bumped package.json (no [skip ci] marker)
|
|
8
|
+
# -> auto-detect path: prepare reads package.json, checks if v${version}
|
|
9
|
+
# tag exists, builds + publishes + creates GitHub Release if not.
|
|
10
|
+
#
|
|
11
|
+
# 2. workflow_dispatch with bump=patch|minor|major
|
|
12
|
+
# -> bump job runs `npm version`, commits with [skip ci], tags, pushes.
|
|
13
|
+
# prepare/build/release continue inline in the same workflow run.
|
|
14
|
+
#
|
|
15
|
+
# Bot-loop guard: bump commits carry [skip ci]. Push of those commits is
|
|
16
|
+
# filtered out by the prepare gate, so the bot's own push does not retrigger.
|
|
17
|
+
#
|
|
18
|
+
# Auth: npm OIDC Trusted Publishing (configured via build-all-platforms.yml).
|
|
2
19
|
|
|
3
20
|
on:
|
|
4
|
-
|
|
5
|
-
types: [closed]
|
|
21
|
+
push:
|
|
6
22
|
branches: [main]
|
|
7
23
|
workflow_dispatch:
|
|
8
24
|
inputs:
|
|
9
|
-
|
|
10
|
-
description:
|
|
25
|
+
bump:
|
|
26
|
+
description: "Version bump type"
|
|
11
27
|
required: true
|
|
12
28
|
type: choice
|
|
13
29
|
options:
|
|
14
|
-
-
|
|
15
|
-
-
|
|
30
|
+
- patch
|
|
31
|
+
- minor
|
|
32
|
+
- major
|
|
16
33
|
|
|
17
34
|
concurrency:
|
|
18
|
-
group:
|
|
35
|
+
group: release-${{ github.ref }}
|
|
19
36
|
cancel-in-progress: false
|
|
20
37
|
|
|
21
38
|
permissions:
|
|
22
39
|
contents: write
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
id-token: write # required so the reusable `version.yml` workflow can mint
|
|
41
|
+
# the OIDC token for npm Trusted Publishing — without this,
|
|
42
|
+
# GH rejects the workflow at parse time (startup_failure)
|
|
43
|
+
# because the called job's `id-token: write` permission
|
|
44
|
+
# exceeds what the caller has granted.
|
|
25
45
|
|
|
26
46
|
jobs:
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
(github.event.pull_request.merged == true &&
|
|
34
|
-
(contains(github.event.pull_request.labels.*.name, 'rc') ||
|
|
35
|
-
contains(github.event.pull_request.labels.*.name, 'stable')))
|
|
36
|
-
outputs:
|
|
37
|
-
should_run: ${{ steps.check.outputs.should_run }}
|
|
38
|
-
action: ${{ steps.detect.outputs.action }}
|
|
39
|
-
|
|
40
|
-
steps:
|
|
41
|
-
- name: Check for bot commits
|
|
42
|
-
id: check
|
|
43
|
-
run: |
|
|
44
|
-
# Skip if this is a bot commit (prevents infinite loops)
|
|
45
|
-
if [[ "${{ github.actor }}" == "github-actions[bot]" ]]; then
|
|
46
|
-
echo "Skipping: bot commit detected"
|
|
47
|
-
echo "should_run=false" >> $GITHUB_OUTPUT
|
|
48
|
-
else
|
|
49
|
-
echo "should_run=true" >> $GITHUB_OUTPUT
|
|
50
|
-
fi
|
|
51
|
-
|
|
52
|
-
- name: Detect action
|
|
53
|
-
id: detect
|
|
54
|
-
if: steps.check.outputs.should_run == 'true'
|
|
55
|
-
run: |
|
|
56
|
-
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
|
57
|
-
echo "action=${{ inputs.action }}" >> $GITHUB_OUTPUT
|
|
58
|
-
echo "Action from dispatch: ${{ inputs.action }}"
|
|
59
|
-
elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'stable') }}" == "true" ]]; then
|
|
60
|
-
echo "action=promote" >> $GITHUB_OUTPUT
|
|
61
|
-
echo "Action from label: promote (stable)"
|
|
62
|
-
elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'rc') }}" == "true" ]]; then
|
|
63
|
-
echo "action=bump-rc" >> $GITHUB_OUTPUT
|
|
64
|
-
echo "Action from label: bump-rc"
|
|
65
|
-
else
|
|
66
|
-
echo "No release label found"
|
|
67
|
-
echo "action=" >> $GITHUB_OUTPUT
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
# Version: Bump version and create tag
|
|
71
|
-
version:
|
|
72
|
-
name: Bump Version
|
|
73
|
-
needs: gate
|
|
74
|
-
if: needs.gate.outputs.should_run == 'true' && needs.gate.outputs.action != ''
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Bump (workflow_dispatch only): npm version, commit [skip ci], tag, push.
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
bump:
|
|
51
|
+
name: Bump version
|
|
52
|
+
if: github.event_name == 'workflow_dispatch'
|
|
75
53
|
runs-on: ubuntu-latest
|
|
54
|
+
timeout-minutes: 5
|
|
76
55
|
outputs:
|
|
77
56
|
version: ${{ steps.bump.outputs.version }}
|
|
78
57
|
tag: ${{ steps.bump.outputs.tag }}
|
|
79
|
-
npm_tag: ${{ steps.bump.outputs.npm_tag }}
|
|
80
|
-
is_promote: ${{ steps.bump.outputs.is_promote }}
|
|
81
|
-
|
|
82
58
|
steps:
|
|
83
|
-
-
|
|
84
|
-
uses: actions/checkout@v4
|
|
59
|
+
- uses: actions/checkout@v4
|
|
85
60
|
with:
|
|
61
|
+
ref: main
|
|
86
62
|
fetch-depth: 0
|
|
87
63
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
88
64
|
|
|
89
|
-
-
|
|
90
|
-
uses: actions/setup-node@v4
|
|
65
|
+
- uses: actions/setup-node@v4
|
|
91
66
|
with:
|
|
92
|
-
node-version:
|
|
67
|
+
node-version: "22"
|
|
93
68
|
|
|
94
|
-
- name: Configure
|
|
69
|
+
- name: Configure git
|
|
95
70
|
run: |
|
|
96
71
|
git config user.name "github-actions[bot]"
|
|
97
72
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
98
73
|
|
|
99
|
-
- name:
|
|
74
|
+
- name: Bump version
|
|
100
75
|
id: bump
|
|
101
|
-
run:
|
|
76
|
+
run: |
|
|
77
|
+
npm version "${{ inputs.bump }}" --no-git-tag-version
|
|
78
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
79
|
+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
80
|
+
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
|
81
|
+
echo "Bumped to ${VERSION}"
|
|
82
|
+
|
|
83
|
+
- name: Commit, tag, push
|
|
84
|
+
run: |
|
|
85
|
+
VERSION="${{ steps.bump.outputs.version }}"
|
|
86
|
+
TAG="${{ steps.bump.outputs.tag }}"
|
|
87
|
+
git add package.json
|
|
88
|
+
git commit -m "[skip ci] release ${TAG}"
|
|
89
|
+
git tag "${TAG}"
|
|
90
|
+
git push origin HEAD --follow-tags
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Prepare: resolve version, skip if tag already exists, build changelog.
|
|
94
|
+
#
|
|
95
|
+
# Gate handles both event types:
|
|
96
|
+
# - push: bump is skipped; the !cancelled() && !failure() guard lets
|
|
97
|
+
# this job run regardless. The [skip ci] check filters the
|
|
98
|
+
# bot's own bump-commit push so it does not retrigger.
|
|
99
|
+
# - dispatch: bump succeeded; the workflow_dispatch branch of the OR
|
|
100
|
+
# short-circuits the [skip ci] check (the dispatch event
|
|
101
|
+
# itself does not carry the bump's commit message).
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
prepare:
|
|
104
|
+
name: Prepare release
|
|
105
|
+
needs: bump
|
|
106
|
+
if: |
|
|
107
|
+
!cancelled() && !failure() &&
|
|
108
|
+
(github.event_name == 'workflow_dispatch' ||
|
|
109
|
+
(github.event_name == 'push' &&
|
|
110
|
+
!startsWith(github.event.head_commit.message, '[skip ci]')))
|
|
111
|
+
runs-on: ubuntu-latest
|
|
112
|
+
timeout-minutes: 5
|
|
113
|
+
outputs:
|
|
114
|
+
version: ${{ steps.ver.outputs.version }}
|
|
115
|
+
tag: ${{ steps.ver.outputs.tag }}
|
|
116
|
+
skip: ${{ steps.ver.outputs.skip }}
|
|
117
|
+
changelog: ${{ steps.changelog.outputs.notes }}
|
|
118
|
+
# Surface the resolved checkout-ref so downstream jobs (build, release)
|
|
119
|
+
# can use it. They cannot reference `needs.bump.outputs.*` directly
|
|
120
|
+
# because they only have `needs: prepare` (or [prepare, build]) — not
|
|
121
|
+
# `bump` — in their needs context.
|
|
122
|
+
ref: ${{ needs.bump.outputs.tag || github.sha }}
|
|
123
|
+
steps:
|
|
124
|
+
- uses: actions/checkout@v4
|
|
125
|
+
with:
|
|
126
|
+
# On dispatch, check out the freshly-pushed tag; on push, the
|
|
127
|
+
# triggering SHA already has the bumped package.json.
|
|
128
|
+
# (Prepare cannot reference its own `outputs.ref` here — that's
|
|
129
|
+
# only available to downstream jobs.)
|
|
130
|
+
ref: ${{ needs.bump.outputs.tag || github.sha }}
|
|
131
|
+
fetch-depth: 0
|
|
132
|
+
|
|
133
|
+
- name: Resolve version
|
|
134
|
+
id: ver
|
|
135
|
+
env:
|
|
136
|
+
GH_TOKEN: ${{ github.token }}
|
|
137
|
+
run: |
|
|
138
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
139
|
+
TAG="v${VERSION}"
|
|
140
|
+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
141
|
+
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
|
142
|
+
|
|
143
|
+
if gh release view "${TAG}" > /dev/null 2>&1; then
|
|
144
|
+
echo "Release ${TAG} already exists — skipping"
|
|
145
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
146
|
+
else
|
|
147
|
+
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
- name: Find previous release tag
|
|
151
|
+
id: prev
|
|
152
|
+
if: steps.ver.outputs.skip == 'false'
|
|
153
|
+
env:
|
|
154
|
+
GH_TOKEN: ${{ github.token }}
|
|
155
|
+
run: |
|
|
156
|
+
TAG="${{ steps.ver.outputs.tag }}"
|
|
157
|
+
PREV=$(gh release list --limit 50 --json tagName -q '.[].tagName' | grep -v "^${TAG}$" | head -1)
|
|
158
|
+
if [ -n "$PREV" ] && git merge-base --is-ancestor "$PREV" HEAD 2>/dev/null; then
|
|
159
|
+
echo "tag=${PREV}" >> "$GITHUB_OUTPUT"
|
|
160
|
+
echo "Previous tag: ${PREV}"
|
|
161
|
+
else
|
|
162
|
+
echo "tag=" >> "$GITHUB_OUTPUT"
|
|
163
|
+
echo "No previous tag reachable from HEAD"
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
- name: Generate changelog
|
|
167
|
+
id: changelog
|
|
168
|
+
if: steps.ver.outputs.skip == 'false' && steps.prev.outputs.tag != ''
|
|
169
|
+
run: |
|
|
170
|
+
PREV="${{ steps.prev.outputs.tag }}"
|
|
171
|
+
NOTES=$(git log --oneline "${PREV}..HEAD" --pretty="- %s" | head -50)
|
|
172
|
+
{
|
|
173
|
+
echo "notes<<EOF"
|
|
174
|
+
echo "$NOTES"
|
|
175
|
+
echo "EOF"
|
|
176
|
+
} >> "$GITHUB_OUTPUT"
|
|
102
177
|
|
|
103
|
-
-
|
|
178
|
+
# Echo the resolved outputs so downstream skip/no-skip decisions are
|
|
179
|
+
# debuggable from the run log without re-running with step debug.
|
|
180
|
+
- name: Debug resolved outputs
|
|
104
181
|
run: |
|
|
105
|
-
|
|
106
|
-
|
|
182
|
+
echo "version=${{ steps.ver.outputs.version }}"
|
|
183
|
+
echo "tag=${{ steps.ver.outputs.tag }}"
|
|
184
|
+
echo "skip=${{ steps.ver.outputs.skip }}"
|
|
185
|
+
echo "prev=${{ steps.prev.outputs.tag }}"
|
|
107
186
|
|
|
108
|
-
#
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# Build & Publish: matrix build of platform binaries + npm publish via OIDC.
|
|
189
|
+
#
|
|
190
|
+
# The reusable workflow filename is `version.yml` because npm Trusted
|
|
191
|
+
# Publisher is configured against that exact path. Renaming requires
|
|
192
|
+
# updating the npmjs.com Trusted Publisher entry first.
|
|
193
|
+
#
|
|
194
|
+
# The `if:` uses `always() && needs.prepare.result == 'success' &&
|
|
195
|
+
# needs.prepare.outputs.skip != 'true'`. This is the bulletproof pattern
|
|
196
|
+
# for reusable-workflow callers when any upstream job in the `needs:`
|
|
197
|
+
# chain was SKIPPED. With the simpler `needs.prepare.outputs.skip != 'true'`
|
|
198
|
+
# alone, GH Actions silently evaluated the gate as false even though the
|
|
199
|
+
# debug step in `prepare` proved the output was actually `'false'` — a
|
|
200
|
+
# known GH Actions quirk: when a job's transitive `needs:` chain includes
|
|
201
|
+
# a skipped job (here, `bump` is skipped on push events), the reusable-
|
|
202
|
+
# workflow caller's expression evaluator treats `needs.<job>.outputs.<x>`
|
|
203
|
+
# as null/missing regardless of the actual value.
|
|
204
|
+
#
|
|
205
|
+
# `always()` opts out of the implicit success() filter; the explicit
|
|
206
|
+
# `result == 'success'` reinstates it correctly; the outputs check then
|
|
207
|
+
# works as intended.
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
109
209
|
build:
|
|
110
210
|
name: Build & Publish
|
|
111
|
-
needs:
|
|
112
|
-
|
|
211
|
+
needs: prepare
|
|
212
|
+
if: |
|
|
213
|
+
always() &&
|
|
214
|
+
needs.prepare.result == 'success' &&
|
|
215
|
+
needs.prepare.outputs.skip != 'true'
|
|
216
|
+
uses: ./.github/workflows/version.yml
|
|
113
217
|
with:
|
|
114
|
-
version: ${{ needs.
|
|
115
|
-
npm_tag:
|
|
116
|
-
|
|
218
|
+
version: ${{ needs.prepare.outputs.version }}
|
|
219
|
+
npm_tag: latest
|
|
220
|
+
# Use prepare.outputs.ref (which resolves to the bump-job tag on
|
|
221
|
+
# dispatch, or `github.sha` on push). Build cannot reference
|
|
222
|
+
# `needs.bump.*` directly — only `prepare` is in its `needs:` chain.
|
|
223
|
+
# On the push path nobody creates the tag before this runs; the
|
|
224
|
+
# SHA-based checkout works because the merge commit already has the
|
|
225
|
+
# bumped package.json. The release job creates the tag at the end
|
|
226
|
+
# via `gh release create`.
|
|
227
|
+
ref: ${{ needs.prepare.outputs.ref }}
|
|
117
228
|
secrets: inherit
|
|
118
229
|
|
|
119
|
-
#
|
|
120
|
-
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# Release: download artifacts, create GitHub Release with cliff-free notes.
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
release:
|
|
121
234
|
name: Create GitHub Release
|
|
122
|
-
needs: [
|
|
123
|
-
if:
|
|
235
|
+
needs: [prepare, build]
|
|
236
|
+
if: |
|
|
237
|
+
always() &&
|
|
238
|
+
needs.prepare.result == 'success' &&
|
|
239
|
+
needs.build.result == 'success' &&
|
|
240
|
+
needs.prepare.outputs.skip != 'true'
|
|
124
241
|
runs-on: ubuntu-latest
|
|
125
|
-
|
|
126
|
-
contents: write
|
|
127
|
-
|
|
242
|
+
timeout-minutes: 10
|
|
128
243
|
steps:
|
|
129
|
-
-
|
|
130
|
-
uses: actions/checkout@v4
|
|
244
|
+
- uses: actions/checkout@v4
|
|
131
245
|
with:
|
|
132
|
-
|
|
246
|
+
# Same as the build job: prefer the bump job's tag (dispatch
|
|
247
|
+
# path) but fall back to the SHA (push path, no tag exists yet).
|
|
248
|
+
ref: ${{ needs.prepare.outputs.ref }}
|
|
133
249
|
|
|
134
|
-
- name: Download
|
|
135
|
-
if: needs.build.result == 'success'
|
|
250
|
+
- name: Download binaries
|
|
136
251
|
uses: actions/download-artifact@v4
|
|
137
252
|
with:
|
|
138
253
|
path: dist/
|
|
@@ -140,28 +255,35 @@ jobs:
|
|
|
140
255
|
merge-multiple: true
|
|
141
256
|
|
|
142
257
|
- name: List artifacts
|
|
258
|
+
run: ls -la dist/
|
|
259
|
+
|
|
260
|
+
- name: Create release
|
|
261
|
+
env:
|
|
262
|
+
GH_TOKEN: ${{ github.token }}
|
|
263
|
+
RELEASE_NOTES: ${{ needs.prepare.outputs.changelog }}
|
|
143
264
|
run: |
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
265
|
+
TAG="${{ needs.prepare.outputs.tag }}"
|
|
266
|
+
VERSION="${{ needs.prepare.outputs.version }}"
|
|
267
|
+
|
|
268
|
+
if [ -z "$RELEASE_NOTES" ]; then
|
|
269
|
+
RELEASE_NOTES="Release ${TAG}"
|
|
149
270
|
fi
|
|
150
271
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
272
|
+
{
|
|
273
|
+
echo "$RELEASE_NOTES"
|
|
274
|
+
echo ""
|
|
275
|
+
echo "## Install"
|
|
276
|
+
echo ""
|
|
277
|
+
echo '```bash'
|
|
278
|
+
echo "npm install pgserve@${VERSION}"
|
|
279
|
+
echo "bunx pgserve@${VERSION}"
|
|
280
|
+
echo '```'
|
|
281
|
+
} > /tmp/release-notes.md
|
|
282
|
+
|
|
283
|
+
# The tag already exists (created by bump job on dispatch, or by the
|
|
284
|
+
# human commit on push). gh release create resolves --target via the
|
|
285
|
+
# tag automatically when omitted.
|
|
286
|
+
gh release create "${TAG}" \
|
|
287
|
+
--title "${TAG}" \
|
|
288
|
+
--notes-file /tmp/release-notes.md \
|
|
166
289
|
dist/*
|
|
167
|
-
fail_on_unmatched_files: false
|
|
@@ -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
|
|
@@ -112,7 +112,12 @@ jobs:
|
|
|
112
112
|
needs: build
|
|
113
113
|
runs-on: ubuntu-latest
|
|
114
114
|
if: inputs.version != ''
|
|
115
|
-
environment: npm-publish
|
|
115
|
+
# Note: `environment: npm-publish` was removed because the npmjs.com
|
|
116
|
+
# Trusted Publisher entry for `pgserve` does not declare an environment
|
|
117
|
+
# name. With the env gate present here, the OIDC token's environment
|
|
118
|
+
# claim did not match the registry's expectation and `npm publish`
|
|
119
|
+
# returned 404. Re-add this line if/when the Trusted Publisher entry
|
|
120
|
+
# has its Environment Name field set to `npm-publish`.
|
|
116
121
|
permissions:
|
|
117
122
|
id-token: write
|
|
118
123
|
contents: read
|
|
@@ -123,20 +128,34 @@ jobs:
|
|
|
123
128
|
with:
|
|
124
129
|
ref: ${{ inputs.ref || github.ref }}
|
|
125
130
|
|
|
131
|
+
# Node 24 ships npm 11.5+ which has built-in OIDC trusted-publisher
|
|
132
|
+
# support. Avoids the `npm install -g npm@latest` self-upgrade bug
|
|
133
|
+
# (Arborist clobbering its own promise-retry dep mid-install) that
|
|
134
|
+
# broke rlmx's OIDC publish on Node 22.
|
|
126
135
|
- name: Setup Node.js
|
|
127
136
|
uses: actions/setup-node@v4
|
|
128
137
|
with:
|
|
129
|
-
node-version: '
|
|
138
|
+
node-version: '24'
|
|
130
139
|
registry-url: 'https://registry.npmjs.org'
|
|
131
140
|
|
|
132
141
|
- name: Setup Bun
|
|
133
142
|
uses: oven-sh/setup-bun@v2
|
|
134
143
|
with:
|
|
135
|
-
bun-version:
|
|
144
|
+
bun-version: 1.3.11
|
|
136
145
|
|
|
137
146
|
- name: Install dependencies
|
|
138
147
|
run: bun install
|
|
139
148
|
|
|
149
|
+
- name: Verify npm version supports OIDC trusted publishing
|
|
150
|
+
run: |
|
|
151
|
+
NPM_VERSION=$(npm --version)
|
|
152
|
+
echo "npm version: ${NPM_VERSION}"
|
|
153
|
+
MAJOR=$(echo "${NPM_VERSION}" | cut -d. -f1)
|
|
154
|
+
if [ "${MAJOR}" -lt 11 ]; then
|
|
155
|
+
echo "::error::npm ${NPM_VERSION} too old — OIDC requires >= 11.5.1. Bump node-version above."
|
|
156
|
+
exit 1
|
|
157
|
+
fi
|
|
158
|
+
|
|
140
159
|
- name: Download all artifacts
|
|
141
160
|
uses: actions/download-artifact@v4
|
|
142
161
|
with:
|
|
@@ -179,13 +198,18 @@ jobs:
|
|
|
179
198
|
echo "published=false" >> $GITHUB_OUTPUT
|
|
180
199
|
fi
|
|
181
200
|
|
|
182
|
-
- name: Publish to npm
|
|
201
|
+
- name: Publish to npm via OIDC
|
|
183
202
|
if: steps.check.outputs.published == 'false'
|
|
203
|
+
env:
|
|
204
|
+
HUSKY: "0"
|
|
205
|
+
# npm auto-enables provenance in any CI env with `id-token: write`,
|
|
206
|
+
# regardless of the --provenance CLI flag. On some runners the
|
|
207
|
+
# server-side sigstore check fails with 422; disable explicitly.
|
|
208
|
+
# OIDC token exchange still happens.
|
|
209
|
+
NPM_CONFIG_PROVENANCE: "false"
|
|
184
210
|
run: |
|
|
185
|
-
echo "Publishing version ${{ inputs.version }} with tag ${{ inputs.npm_tag }}"
|
|
211
|
+
echo "Publishing version ${{ inputs.version }} with tag ${{ inputs.npm_tag }} via OIDC"
|
|
186
212
|
npm publish --access public --tag ${{ inputs.npm_tag }}
|
|
187
|
-
env:
|
|
188
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
189
213
|
|
|
190
214
|
- name: Verify publish
|
|
191
215
|
if: steps.check.outputs.published == 'false'
|
package/AGENTS.md
CHANGED
|
@@ -190,16 +190,18 @@ Before editing ANY implementation file, Base Genie must check:
|
|
|
190
190
|
|
|
191
191
|
**Protocol:** `@.genie/spells/orchestration-boundary-protocol.md`
|
|
192
192
|
|
|
193
|
-
**Release Workflow Protocol:**
|
|
194
|
-
-
|
|
195
|
-
-
|
|
196
|
-
- ✅
|
|
197
|
-
-
|
|
198
|
-
|
|
199
|
-
|
|
193
|
+
**Release Workflow Protocol (push-to-main, single tier):**
|
|
194
|
+
- ✅ Manual path: bump locally with `npm version patch|minor|major`, commit, PR to main. Merge → `release.yml` fires automatically.
|
|
195
|
+
- ✅ Bot path: `gh workflow run release.yml -f bump=patch` (or `minor`/`major`). Bot bumps, tags, builds binaries, publishes to npm via OIDC.
|
|
196
|
+
- ✅ Skip: any commit message starting with `[skip ci]` is filtered by the prepare gate.
|
|
197
|
+
- ❌ No `rc`/`stable` PR labels (legacy — removed). No `scripts/release.cjs` (deleted).
|
|
198
|
+
- ❌ Don't edit `package.json` `version` directly on `main` outside the `npm version` flow above.
|
|
199
|
+
- ⚙️ npm publish runs from `.github/workflows/version.yml` (the file npmjs.com Trusted Publisher is bound to).
|
|
200
|
+
|
|
201
|
+
**Documented Violations (history):**
|
|
200
202
|
- Bug #168, task b51db539, 2025-10-21 (duplicate implementation)
|
|
201
203
|
- 2025-10-26 (claimed release implementation steps without investigating automation)
|
|
202
|
-
- 2025-12-08 (manually set version to 1.1.0 + triggered workflow_dispatch → version jumped to 1.1.1-rc.1)
|
|
204
|
+
- 2025-12-08 (manually set version to 1.1.0 + triggered workflow_dispatch → version jumped to 1.1.1-rc.1; this class of error is no longer possible — the bump path goes through `gh workflow run`, not direct package.json edits)
|
|
203
205
|
|
|
204
206
|
### 4. Task State Optimization - Live State, Not Documentation 🔴 CRITICAL
|
|
205
207
|
**Rule:** Task state is ephemeral runtime data, not permanent documentation
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `pgserve` are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
|
|
5
|
+
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## 2.0.0 — Unreleased
|
|
8
|
+
|
|
9
|
+
> The release date will replace "Unreleased" when the v2.0.0 release workflow
|
|
10
|
+
> fires. The CHANGELOG is committed ahead of the release trigger so consumers
|
|
11
|
+
> can review the migration plan before the artifact lands on npm.
|
|
12
|
+
|
|
13
|
+
### Pin guidance (read this first)
|
|
14
|
+
|
|
15
|
+
Existing v1 consumers should pin `pgserve@^1.x` in their `package.json` until
|
|
16
|
+
they have completed the migration described below. v2 changes the default
|
|
17
|
+
transport (Unix socket, no TCP), the identity model (kernel-rooted
|
|
18
|
+
fingerprint), the database layout (one DB per fingerprint), and the daemon
|
|
19
|
+
process model (singleton). A blind upgrade will break v1 connection strings.
|
|
20
|
+
|
|
21
|
+
```jsonc
|
|
22
|
+
// package.json — keep v1 until you migrate
|
|
23
|
+
{
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"pgserve": "^1.2.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Breaking changes
|
|
31
|
+
|
|
32
|
+
- **TCP is no longer the default.** v1 bound `127.0.0.1:8432` for every
|
|
33
|
+
consumer. v2 binds a Unix control socket at
|
|
34
|
+
`${XDG_RUNTIME_DIR:-/tmp}/pgserve/control.sock` (mode `0600`, dir mode
|
|
35
|
+
`0700`) plus a `.s.PGSQL.5432` symlink so libpq clients connect with no
|
|
36
|
+
host/port/user/password. To keep a TCP listener, opt in explicitly with
|
|
37
|
+
`--listen <port>` (see "Compat TCP via --listen" in the README).
|
|
38
|
+
- **Fingerprint enforcement is default-ON.** Each connecting peer is
|
|
39
|
+
identified via `SO_PEERCRED` + the resolved `package.json` `name`,
|
|
40
|
+
collapsed to a 12-hex fingerprint. The daemon refuses to route a peer
|
|
41
|
+
into a database that does not match its fingerprint with SQLSTATE
|
|
42
|
+
`28P01 invalid_authorization — database fingerprint mismatch`. The
|
|
43
|
+
emergency kill switch is `PGSERVE_DISABLE_FINGERPRINT_ENFORCEMENT=1`
|
|
44
|
+
(deprecated; the daemon emits a stderr warning at boot when the env var
|
|
45
|
+
is observed).
|
|
46
|
+
- **Database-per-fingerprint isolation.** v1 served arbitrary database
|
|
47
|
+
names freely. v2 auto-creates `app_<sanitized-name>_<12hex>` for each
|
|
48
|
+
unique fingerprint on first connect; cross-fingerprint reads are denied.
|
|
49
|
+
`psql -l` will show one row per consumer rather than the shared pool
|
|
50
|
+
v1 produced. Monorepo rule: the root `package.json` `name` wins for all
|
|
51
|
+
packages under it.
|
|
52
|
+
- **Singleton daemon via control socket.** v1 spun up a server per
|
|
53
|
+
invocation, leaving consumers to coordinate ports themselves. v2
|
|
54
|
+
enforces one daemon per host: a second `pgserve daemon` exits with
|
|
55
|
+
`already running, pid N`. Run it under PM2 or systemd (snippets in the
|
|
56
|
+
README) — there is no PM-managed multi-process mode anymore.
|
|
57
|
+
- **GC sweep emits `db_reaped_ttl` and `db_reaped_liveness` audit events.**
|
|
58
|
+
Default lifecycle is now ephemeral: a database whose `liveness_pid` is
|
|
59
|
+
dead AND whose `last_connection_at` is older than 24h is dropped on the
|
|
60
|
+
next sweep (boot, hourly, sampled on-connect). To opt out, add
|
|
61
|
+
`pgserve.persist: true` to the consumer's `package.json` — flagged
|
|
62
|
+
databases are never reaped.
|
|
63
|
+
|
|
64
|
+
### Migration guide
|
|
65
|
+
|
|
66
|
+
1. **Connection strings** — drop credentials and the port; switch to the
|
|
67
|
+
socket form.
|
|
68
|
+
|
|
69
|
+
```diff
|
|
70
|
+
- postgres://user:pass@localhost:5432/db
|
|
71
|
+
+ postgres:///db?host=${XDG_RUNTIME_DIR:-/tmp}/pgserve
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Equivalently, for `psql`:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
psql -h "${XDG_RUNTIME_DIR:-/tmp}/pgserve" -d myapp
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
2. **Long-lived apps** — anything whose data needs to outlive a 24h idle
|
|
81
|
+
window (genie state stores, dashboards, anything with state worth
|
|
82
|
+
keeping) must declare persistence in its `package.json`:
|
|
83
|
+
|
|
84
|
+
```jsonc
|
|
85
|
+
{
|
|
86
|
+
"name": "my-long-lived-app",
|
|
87
|
+
"pgserve": { "persist": true }
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Without this flag, the GC sweep will reap the database after the TTL
|
|
92
|
+
plus liveness check passes.
|
|
93
|
+
|
|
94
|
+
3. **Need TCP?** Opt in with `--listen` and use issued tokens. TCP peers
|
|
95
|
+
cannot use `SO_PEERCRED`, so they must authenticate at connect time.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pgserve daemon --listen :5432
|
|
99
|
+
|
|
100
|
+
# Issue a bearer token for a known fingerprint (printed once):
|
|
101
|
+
pgserve daemon issue-token --fingerprint <12hex>
|
|
102
|
+
|
|
103
|
+
# TCP clients pass the token via libpq application_name as
|
|
104
|
+
# ?fingerprint=<hex>&token=<bearer>
|
|
105
|
+
# Revoke when done:
|
|
106
|
+
pgserve daemon revoke-token <token-id>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Without `--listen`, no TCP port is bound — verify with
|
|
110
|
+
`ss -tlnp | grep -v pgserve` returning no pgserve rows.
|
|
111
|
+
|
|
112
|
+
4. **Kill switch (emergency only).** If the fingerprint enforcement
|
|
113
|
+
denies a connection you cannot otherwise unblock, set
|
|
114
|
+
`PGSERVE_DISABLE_FINGERPRINT_ENFORCEMENT=1` for the daemon. The
|
|
115
|
+
bypassed connection emits an `enforcement_kill_switch_used` audit
|
|
116
|
+
event; the daemon logs a deprecation warning at boot whenever the
|
|
117
|
+
variable is observed. The kill switch will be removed in a future
|
|
118
|
+
major; treat it as a debugging tool, not a production setting.
|
|
119
|
+
|
|
120
|
+
### New features (group references map to wish execution groups)
|
|
121
|
+
|
|
122
|
+
- **Group 4 — Database-per-fingerprint + enforcement + kill switch.**
|
|
123
|
+
Auto-create `app_<name>_<12hex>` on first connect, deny
|
|
124
|
+
cross-fingerprint reads with SQLSTATE `28P01`, audit event
|
|
125
|
+
`connection_denied_fingerprint_mismatch`. Sanitizer collapses
|
|
126
|
+
non-`[a-z0-9]` runs to `_`, lowercases, truncates to 30 chars to keep
|
|
127
|
+
the resulting DB name ≤ 63 chars.
|
|
128
|
+
- **Group 5 — Lifecycle + persist flag + GC sweep.** Three-layer
|
|
129
|
+
lifecycle: liveness (peer pid alive), 24h TTL since last connection,
|
|
130
|
+
and `pgserve.persist: true` override. Sweep runs at daemon boot,
|
|
131
|
+
hourly, and sampled on-connect at 1/N where N = max(1, dbCount/10).
|
|
132
|
+
Reaped databases emit `db_reaped_ttl` or `db_reaped_liveness` audit
|
|
133
|
+
events; the on-connect sweep does not block accept latency past 50 ms
|
|
134
|
+
P99.
|
|
135
|
+
- **Group 6 — `--listen` opt-in TCP + token auth.** Daemon CLI accepts
|
|
136
|
+
`--listen [host:]port` (repeatable). Tokens issued via
|
|
137
|
+
`pgserve daemon issue-token --fingerprint <hex>`, hashed at rest into
|
|
138
|
+
`pgserve_meta.allowed_tokens`, verified with constant-time compare.
|
|
139
|
+
New audit events: `tcp_token_issued`, `tcp_token_used`,
|
|
140
|
+
`tcp_token_denied`. Without `--listen`, no TCP port is bound.
|
|
141
|
+
|
|
142
|
+
### Compatibility
|
|
143
|
+
|
|
144
|
+
- Node.js >= 18 (unchanged).
|
|
145
|
+
- Linux x64, macOS ARM64/x64, Windows x64. Windows uses named pipes for
|
|
146
|
+
the control socket; PM2/systemd snippets are Linux-first.
|
|
147
|
+
- `--ram` (Linux/WSL2 `/dev/shm`), `--pgvector`, `--sync-to`, and the
|
|
148
|
+
rest of the v1 runtime flags continue to work unchanged.
|
|
149
|
+
|
|
150
|
+
---
|