pgserve 2.4.0 → 2.6.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/README.md +5 -8
- package/bin/pgserve-wrapper.cjs +23 -0
- package/bin/postgres-server.js +28 -0
- package/package.json +2 -1
- package/scripts/aggregate-manifest.sh +184 -0
- package/scripts/assemble-tarball.sh +191 -0
- package/scripts/audit-redaction-lint.js +349 -0
- package/scripts/build-binary.sh +213 -0
- package/scripts/fetch-postgres-bins.sh +234 -0
- package/scripts/postinstall.cjs +102 -18
- package/scripts/verify-published-artifacts.sh +211 -0
- package/src/audit/audit.js +134 -0
- package/src/cli-install.cjs +258 -26
- package/src/commands/doctor.js +465 -0
- package/src/commands/gc.js +276 -0
- package/src/commands/provision.js +396 -0
- package/src/commands/trust.js +187 -0
- package/src/commands/verify.js +360 -0
- package/src/cosign/cache-token.js +328 -0
- package/src/cosign/schema.js +97 -0
- package/src/cosign/trust-list.js +81 -0
- package/src/cosign/trust-store.js +250 -0
- package/src/cosign/verify-binary.js +277 -0
- package/src/gc/audit-log.js +150 -0
- package/src/gc/orphan-detection.js +190 -0
- package/src/gc/queries.js +193 -0
- package/src/lib/pg-query.js +145 -0
- package/src/lib/runtime-json.js +181 -0
- package/src/provision/advisory-lock.js +91 -0
- package/src/provision/db-naming.js +130 -0
- package/src/provision/fingerprint.js +144 -0
- package/src/schema/pgserve-meta.js +120 -0
- package/src/security/blocked-versions.js +103 -0
- package/src/upgrade/index.js +5 -0
- package/src/upgrade/steps/binary-cache-flush.js +2 -2
- package/src/upgrade/steps/cosign-meta-migration.js +123 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Audit redaction lint — Group 6, autopg-distribution-cutover.
|
|
4
|
+
*
|
|
5
|
+
* Walks every .js / .cjs source under `src/` (excluding `src/audit/audit.js`
|
|
6
|
+
* itself) and locates every `auditEmit(...)` call. For each call site, the
|
|
7
|
+
* lint asserts:
|
|
8
|
+
*
|
|
9
|
+
* 1. No object-literal key matches /password|secret|token|connection_string|database_url/i.
|
|
10
|
+
* 2. No value is `process.env.*PASSWORD*|*SECRET*|*TOKEN*|*DATABASE_URL*|*CONNECTION_STRING*`.
|
|
11
|
+
* 3. No string-literal value looks like a `postgres://user:pass@host/...` URL.
|
|
12
|
+
*
|
|
13
|
+
* Failure = exit 1 with file:line per offending site. Clean tree = exit 0.
|
|
14
|
+
*
|
|
15
|
+
* The walker is a hand-rolled scanner rather than a full parser: it tracks
|
|
16
|
+
* string state (single, double, backtick), template-literal nesting, line
|
|
17
|
+
* comments, block comments, and balanced brace depth. That's enough to
|
|
18
|
+
* isolate the first object-literal argument of every `auditEmit(...)` call
|
|
19
|
+
* without pulling in a parser dependency. New AST-flavored rules can be
|
|
20
|
+
* added by extending `scanRecord()`.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* bun run scripts/audit-redaction-lint.js
|
|
24
|
+
* bun run scripts/audit-redaction-lint.js path/to/file.js
|
|
25
|
+
* bun run scripts/audit-redaction-lint.js --fixture tests/audit/fixtures
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import fs from 'fs';
|
|
29
|
+
import path from 'path';
|
|
30
|
+
|
|
31
|
+
const REPO_ROOT = path.resolve(import.meta.dir, '..');
|
|
32
|
+
const DEFAULT_ROOT = path.join(REPO_ROOT, 'src');
|
|
33
|
+
const SELF_EXCLUDE = path.join('src', 'audit', 'audit.js');
|
|
34
|
+
|
|
35
|
+
const FORBIDDEN_KEY_RE = /^(password|secret|token|connection_string|database_url)$/i;
|
|
36
|
+
const ENV_SECRET_RE =
|
|
37
|
+
/process\.env\.[A-Za-z_][A-Za-z0-9_]*(PASSWORD|SECRET|TOKEN|DATABASE_URL|CONNECTION_STRING)[A-Za-z0-9_]*\b/i;
|
|
38
|
+
const POSTGRES_URL_RE = /\bpostgres(?:ql)?:\/\/[^\s'"]*:[^\s'"@]+@/i;
|
|
39
|
+
|
|
40
|
+
function listSourceFiles(roots) {
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const root of roots) {
|
|
43
|
+
if (!fs.existsSync(root)) continue;
|
|
44
|
+
const stat = fs.statSync(root);
|
|
45
|
+
if (stat.isFile()) {
|
|
46
|
+
if (/\.(js|cjs|mjs)$/.test(root)) out.push(root);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
walk(root, out);
|
|
50
|
+
}
|
|
51
|
+
return out.filter((p) => !p.endsWith(SELF_EXCLUDE));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function walk(dir, out) {
|
|
55
|
+
for (const name of fs.readdirSync(dir)) {
|
|
56
|
+
if (name === 'node_modules' || name.startsWith('.')) continue;
|
|
57
|
+
const full = path.join(dir, name);
|
|
58
|
+
const st = fs.statSync(full);
|
|
59
|
+
if (st.isDirectory()) {
|
|
60
|
+
walk(full, out);
|
|
61
|
+
} else if (/\.(js|cjs|mjs)$/.test(name)) {
|
|
62
|
+
out.push(full);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Find every `auditEmit(...)` call in `src` and yield each one's first
|
|
69
|
+
* argument span (the object literal between `{` and matching `}`), plus
|
|
70
|
+
* the file-relative line number of the `auditEmit` identifier.
|
|
71
|
+
*/
|
|
72
|
+
function* findAuditEmitCalls(src) {
|
|
73
|
+
const len = src.length;
|
|
74
|
+
let i = 0;
|
|
75
|
+
let line = 1;
|
|
76
|
+
|
|
77
|
+
const isIdentChar = (ch) => /[A-Za-z0-9_$]/.test(ch);
|
|
78
|
+
|
|
79
|
+
while (i < len) {
|
|
80
|
+
const ch = src[i];
|
|
81
|
+
|
|
82
|
+
// Track newlines for accurate line reporting.
|
|
83
|
+
if (ch === '\n') { line++; i++; continue; }
|
|
84
|
+
|
|
85
|
+
// Skip line comments.
|
|
86
|
+
if (ch === '/' && src[i + 1] === '/') {
|
|
87
|
+
while (i < len && src[i] !== '\n') i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// Skip block comments.
|
|
91
|
+
if (ch === '/' && src[i + 1] === '*') {
|
|
92
|
+
i += 2;
|
|
93
|
+
while (i < len && !(src[i] === '*' && src[i + 1] === '/')) {
|
|
94
|
+
if (src[i] === '\n') line++;
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
i += 2;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Skip strings.
|
|
101
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
102
|
+
i = skipString(src, i, ch, (n) => { line += n; });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Match `auditEmit` as a whole identifier (must be word-boundary-prefixed).
|
|
107
|
+
if (ch === 'a' && src.startsWith('auditEmit', i)) {
|
|
108
|
+
const before = src[i - 1];
|
|
109
|
+
const after = src[i + 'auditEmit'.length];
|
|
110
|
+
if ((!before || !isIdentChar(before)) && !isIdentChar(after)) {
|
|
111
|
+
const idLine = line;
|
|
112
|
+
// Advance past identifier, skip whitespace, expect '('.
|
|
113
|
+
let j = i + 'auditEmit'.length;
|
|
114
|
+
while (j < len && /\s/.test(src[j])) {
|
|
115
|
+
if (src[j] === '\n') line++;
|
|
116
|
+
j++;
|
|
117
|
+
}
|
|
118
|
+
if (src[j] === '(') {
|
|
119
|
+
// Now find first `{` (the object-literal arg start) before the
|
|
120
|
+
// matching ')'. Object literals as function args appear directly
|
|
121
|
+
// after `(` (perhaps after whitespace) — but `auditEmit` callers
|
|
122
|
+
// pass an object literal by spec, so this is the path we care
|
|
123
|
+
// about. If we find a `)` first, it's auditEmit() with no
|
|
124
|
+
// object-literal arg → not redaction-relevant; skip.
|
|
125
|
+
let k = j + 1;
|
|
126
|
+
while (k < len) {
|
|
127
|
+
const c = src[k];
|
|
128
|
+
if (c === '\n') { line++; k++; continue; }
|
|
129
|
+
if (/\s/.test(c)) { k++; continue; }
|
|
130
|
+
if (c === '/' && src[k + 1] === '/') {
|
|
131
|
+
while (k < len && src[k] !== '\n') k++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (c === '/' && src[k + 1] === '*') {
|
|
135
|
+
k += 2;
|
|
136
|
+
while (k < len && !(src[k] === '*' && src[k + 1] === '/')) {
|
|
137
|
+
if (src[k] === '\n') line++;
|
|
138
|
+
k++;
|
|
139
|
+
}
|
|
140
|
+
k += 2;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
if (src[k] === '{') {
|
|
146
|
+
const literalLine = line;
|
|
147
|
+
const end = findBalanced(src, k, '{', '}', (n) => { line += n; });
|
|
148
|
+
if (end !== -1) {
|
|
149
|
+
const literal = src.slice(k, end + 1);
|
|
150
|
+
yield { call: 'auditEmit', idLine, literalLine, literal };
|
|
151
|
+
i = end + 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
i++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function skipString(src, i, quote, onNewline) {
|
|
164
|
+
// Returns the index just past the closing quote.
|
|
165
|
+
const len = src.length;
|
|
166
|
+
i++; // open quote
|
|
167
|
+
while (i < len) {
|
|
168
|
+
const ch = src[i];
|
|
169
|
+
if (ch === '\\') { i += 2; continue; }
|
|
170
|
+
if (ch === '\n') { onNewline(1); i++; continue; }
|
|
171
|
+
if (ch === quote) { return i + 1; }
|
|
172
|
+
if (quote === '`' && ch === '$' && src[i + 1] === '{') {
|
|
173
|
+
// Template literal interpolation — find matching `}`.
|
|
174
|
+
i = findBalanced(src, i + 1, '{', '}', onNewline);
|
|
175
|
+
if (i === -1) return len;
|
|
176
|
+
i++;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
i++;
|
|
180
|
+
}
|
|
181
|
+
return len;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function findBalanced(src, start, open, close, onNewline) {
|
|
185
|
+
const len = src.length;
|
|
186
|
+
let depth = 0;
|
|
187
|
+
let i = start;
|
|
188
|
+
while (i < len) {
|
|
189
|
+
const ch = src[i];
|
|
190
|
+
if (ch === '\n') { onNewline(1); i++; continue; }
|
|
191
|
+
if (ch === '/' && src[i + 1] === '/') {
|
|
192
|
+
while (i < len && src[i] !== '\n') i++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (ch === '/' && src[i + 1] === '*') {
|
|
196
|
+
i += 2;
|
|
197
|
+
while (i < len && !(src[i] === '*' && src[i + 1] === '/')) {
|
|
198
|
+
if (src[i] === '\n') onNewline(1);
|
|
199
|
+
i++;
|
|
200
|
+
}
|
|
201
|
+
i += 2;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
205
|
+
i = skipString(src, i, ch, onNewline);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (ch === open) depth++;
|
|
209
|
+
else if (ch === close) {
|
|
210
|
+
depth--;
|
|
211
|
+
if (depth === 0) return i;
|
|
212
|
+
}
|
|
213
|
+
i++;
|
|
214
|
+
}
|
|
215
|
+
return -1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Walk the captured object-literal source and pull out each top-level key.
|
|
220
|
+
* Skips nested objects/arrays so a nested `meta: { secret: 'x' }` is still
|
|
221
|
+
* caught (see scanForNestedSecrets) — but the simple flat-key shape is the
|
|
222
|
+
* primary check.
|
|
223
|
+
*/
|
|
224
|
+
function extractTopLevelKeys(literal) {
|
|
225
|
+
// Drop the outer braces.
|
|
226
|
+
if (literal[0] !== '{' || literal[literal.length - 1] !== '}') return [];
|
|
227
|
+
const inner = literal.slice(1, -1);
|
|
228
|
+
const keys = [];
|
|
229
|
+
let i = 0;
|
|
230
|
+
let line = 0;
|
|
231
|
+
const len = inner.length;
|
|
232
|
+
let depth = 0;
|
|
233
|
+
let atKeyPosition = true;
|
|
234
|
+
|
|
235
|
+
while (i < len) {
|
|
236
|
+
const ch = inner[i];
|
|
237
|
+
if (ch === '\n') { line++; i++; continue; }
|
|
238
|
+
if (ch === '/' && inner[i + 1] === '/') {
|
|
239
|
+
while (i < len && inner[i] !== '\n') i++;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (ch === '/' && inner[i + 1] === '*') {
|
|
243
|
+
i += 2;
|
|
244
|
+
while (i < len && !(inner[i] === '*' && inner[i + 1] === '/')) {
|
|
245
|
+
if (inner[i] === '\n') line++;
|
|
246
|
+
i++;
|
|
247
|
+
}
|
|
248
|
+
i += 2;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
252
|
+
const next = skipString(inner, i, ch, (n) => { line += n; });
|
|
253
|
+
if (depth === 0 && atKeyPosition) {
|
|
254
|
+
const raw = inner.slice(i + 1, next - 1);
|
|
255
|
+
keys.push({ name: raw, line });
|
|
256
|
+
atKeyPosition = false;
|
|
257
|
+
}
|
|
258
|
+
i = next;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (ch === '{' || ch === '[' || ch === '(') { depth++; atKeyPosition = false; i++; continue; }
|
|
262
|
+
if (ch === '}' || ch === ']' || ch === ')') { depth--; i++; continue; }
|
|
263
|
+
if (ch === ',' && depth === 0) { atKeyPosition = true; i++; continue; }
|
|
264
|
+
if (ch === ':' && depth === 0) { atKeyPosition = false; i++; continue; }
|
|
265
|
+
if (depth === 0 && atKeyPosition && /[A-Za-z_$]/.test(ch)) {
|
|
266
|
+
const start = i;
|
|
267
|
+
while (i < len && /[A-Za-z0-9_$]/.test(inner[i])) i++;
|
|
268
|
+
const name = inner.slice(start, i);
|
|
269
|
+
keys.push({ name, line });
|
|
270
|
+
atKeyPosition = false;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
i++;
|
|
274
|
+
}
|
|
275
|
+
return keys;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function lintFile(file) {
|
|
279
|
+
const src = fs.readFileSync(file, 'utf8');
|
|
280
|
+
const errors = [];
|
|
281
|
+
const rel = path.relative(REPO_ROOT, file);
|
|
282
|
+
|
|
283
|
+
for (const call of findAuditEmitCalls(src)) {
|
|
284
|
+
const keys = extractTopLevelKeys(call.literal);
|
|
285
|
+
for (const key of keys) {
|
|
286
|
+
if (FORBIDDEN_KEY_RE.test(key.name)) {
|
|
287
|
+
errors.push({
|
|
288
|
+
file: rel,
|
|
289
|
+
line: call.literalLine + key.line,
|
|
290
|
+
message: `auditEmit field "${key.name}" matches forbidden secret pattern (${FORBIDDEN_KEY_RE})`,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Value-side checks run on the whole literal: env-secret references and
|
|
295
|
+
// postgres:// URLs would be caught here even if buried inside nested
|
|
296
|
+
// objects.
|
|
297
|
+
const envMatch = call.literal.match(ENV_SECRET_RE);
|
|
298
|
+
if (envMatch) {
|
|
299
|
+
const offset = envMatch.index || 0;
|
|
300
|
+
const lineOffset = call.literal.slice(0, offset).split('\n').length - 1;
|
|
301
|
+
errors.push({
|
|
302
|
+
file: rel,
|
|
303
|
+
line: call.literalLine + lineOffset,
|
|
304
|
+
message: `auditEmit value sources from secret env var: ${envMatch[0]}`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const urlMatch = call.literal.match(POSTGRES_URL_RE);
|
|
308
|
+
if (urlMatch) {
|
|
309
|
+
const offset = urlMatch.index || 0;
|
|
310
|
+
const lineOffset = call.literal.slice(0, offset).split('\n').length - 1;
|
|
311
|
+
errors.push({
|
|
312
|
+
file: rel,
|
|
313
|
+
line: call.literalLine + lineOffset,
|
|
314
|
+
message: `auditEmit value contains postgres URL with embedded password: ${urlMatch[0]}…`,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return errors;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function main(argv) {
|
|
322
|
+
const args = argv.slice(2);
|
|
323
|
+
let roots;
|
|
324
|
+
if (args.length === 0) {
|
|
325
|
+
roots = [DEFAULT_ROOT];
|
|
326
|
+
} else {
|
|
327
|
+
roots = args.map((a) => path.resolve(a));
|
|
328
|
+
}
|
|
329
|
+
const files = listSourceFiles(roots);
|
|
330
|
+
const allErrors = [];
|
|
331
|
+
for (const f of files) {
|
|
332
|
+
allErrors.push(...lintFile(f));
|
|
333
|
+
}
|
|
334
|
+
if (allErrors.length === 0) {
|
|
335
|
+
console.log(`audit-redaction-lint: scanned ${files.length} file(s); 0 issues.`);
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
for (const err of allErrors) {
|
|
339
|
+
console.error(`${err.file}:${err.line}: ${err.message}`);
|
|
340
|
+
}
|
|
341
|
+
console.error(`audit-redaction-lint: ${allErrors.length} issue(s) across ${files.length} file(s).`);
|
|
342
|
+
return 1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (import.meta.main) {
|
|
346
|
+
process.exit(main(process.argv));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export { lintFile, findAuditEmitCalls, extractTopLevelKeys, listSourceFiles };
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# build-binary.sh — Group 7 of autopg-distribution-cutover.
|
|
4
|
+
#
|
|
5
|
+
# Compiles the autopg CLI to a static binary using `bun build --compile`
|
|
6
|
+
# for one or all of the 5 supported platforms:
|
|
7
|
+
#
|
|
8
|
+
# linux-x64-glibc, linux-x64-musl, linux-arm64,
|
|
9
|
+
# darwin-x64, darwin-arm64
|
|
10
|
+
#
|
|
11
|
+
# Outputs land at: dist/<platform>/autopg/autopg
|
|
12
|
+
#
|
|
13
|
+
# Per the wish G7 fallback contract (distribution-exodus G1): if
|
|
14
|
+
# `bun build --compile` fails for a target, retry with `pkg`/`nexe`
|
|
15
|
+
# when AUTOPG_BUILD_FALLBACK=1. The fallback is recorded in the build
|
|
16
|
+
# log so Group 9's CDN publish can surface it.
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# scripts/build-binary.sh --platform linux-x64-glibc
|
|
20
|
+
# scripts/build-binary.sh --all
|
|
21
|
+
# scripts/build-binary.sh --platform darwin-arm64 --version 2.260503.1
|
|
22
|
+
#
|
|
23
|
+
# Exit codes:
|
|
24
|
+
# 0 success
|
|
25
|
+
# 1 bun build failed AND fallback disabled or also failed
|
|
26
|
+
# 2 invalid arguments / unsupported platform
|
|
27
|
+
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
|
|
30
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
31
|
+
ENTRY_POINT="${AUTOPG_ENTRY_POINT:-bin/postgres-server.js}"
|
|
32
|
+
DIST_DIR="${AUTOPG_DIST_DIR:-${REPO_ROOT}/dist}"
|
|
33
|
+
FALLBACK_ENABLED="${AUTOPG_BUILD_FALLBACK:-0}"
|
|
34
|
+
|
|
35
|
+
PLATFORMS=(linux-x64-glibc linux-x64-musl linux-arm64 darwin-x64 darwin-arm64)
|
|
36
|
+
|
|
37
|
+
# Map autopg platform tag → bun --target value.
|
|
38
|
+
bun_target_for() {
|
|
39
|
+
case "$1" in
|
|
40
|
+
linux-x64-glibc) echo "bun-linux-x64" ;;
|
|
41
|
+
linux-x64-musl) echo "bun-linux-x64-musl" ;;
|
|
42
|
+
linux-arm64) echo "bun-linux-arm64" ;;
|
|
43
|
+
darwin-x64) echo "bun-darwin-x64" ;;
|
|
44
|
+
darwin-arm64) echo "bun-darwin-arm64" ;;
|
|
45
|
+
*) return 1 ;;
|
|
46
|
+
esac
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
usage() {
|
|
50
|
+
cat <<EOF
|
|
51
|
+
Usage: $0 (--platform <p> | --all) [--version <v>] [--entry <path>]
|
|
52
|
+
|
|
53
|
+
Platforms: ${PLATFORMS[*]}
|
|
54
|
+
|
|
55
|
+
Environment:
|
|
56
|
+
AUTOPG_ENTRY_POINT Override entry file (default: bin/postgres-server.js)
|
|
57
|
+
AUTOPG_DIST_DIR Override output root (default: \$REPO/dist)
|
|
58
|
+
AUTOPG_BUILD_FALLBACK Set to 1 to retry failed bun builds via pkg/nexe
|
|
59
|
+
EOF
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
parse_args() {
|
|
63
|
+
TARGET_PLATFORM=""
|
|
64
|
+
BUILD_ALL=0
|
|
65
|
+
VERSION="${AUTOPG_VERSION:-}"
|
|
66
|
+
|
|
67
|
+
while [[ $# -gt 0 ]]; do
|
|
68
|
+
case "$1" in
|
|
69
|
+
--platform) TARGET_PLATFORM="$2"; shift 2 ;;
|
|
70
|
+
--all) BUILD_ALL=1; shift ;;
|
|
71
|
+
--version) VERSION="$2"; shift 2 ;;
|
|
72
|
+
--entry) ENTRY_POINT="$2"; shift 2 ;;
|
|
73
|
+
-h|--help) usage; exit 0 ;;
|
|
74
|
+
*) echo "unknown arg: $1" >&2; usage; exit 2 ;;
|
|
75
|
+
esac
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
if [[ "$BUILD_ALL" -eq 0 && -z "$TARGET_PLATFORM" ]]; then
|
|
79
|
+
echo "error: pass --platform <p> or --all" >&2
|
|
80
|
+
usage; exit 2
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
if [[ -z "$VERSION" ]]; then
|
|
84
|
+
VERSION=$(node -p "require('${REPO_ROOT}/package.json').version" 2>/dev/null || echo "0.0.0")
|
|
85
|
+
fi
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
build_one() {
|
|
89
|
+
local platform="$1"
|
|
90
|
+
local target
|
|
91
|
+
target="$(bun_target_for "$platform")" || {
|
|
92
|
+
echo "error: unsupported platform: $platform" >&2
|
|
93
|
+
return 2
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
local out_dir="${DIST_DIR}/${platform}/autopg"
|
|
97
|
+
# `build_one` runs without `set -e` (called via `|| rc=$?` in main), so
|
|
98
|
+
# mkdir failures must propagate explicitly (gemini PR #84 HIGH review).
|
|
99
|
+
mkdir -p "$out_dir" || return 1
|
|
100
|
+
local outfile="${out_dir}/autopg"
|
|
101
|
+
|
|
102
|
+
echo "==> [${platform}] bun build --compile --target=${target}"
|
|
103
|
+
if bun build --compile \
|
|
104
|
+
--target="${target}" \
|
|
105
|
+
--define BUILD_VERSION="'${VERSION}'" \
|
|
106
|
+
"${REPO_ROOT}/${ENTRY_POINT}" \
|
|
107
|
+
--outfile "${outfile}" 2>&1 | tee -a "${DIST_DIR}/build.log"; then
|
|
108
|
+
echo " ✓ built: ${outfile}"
|
|
109
|
+
record_build "$platform" "bun" "ok"
|
|
110
|
+
return 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
echo " ✗ bun build failed for ${platform}" >&2
|
|
114
|
+
|
|
115
|
+
if [[ "$FALLBACK_ENABLED" -eq 1 ]]; then
|
|
116
|
+
echo "==> [${platform}] retry via pkg/nexe (AUTOPG_BUILD_FALLBACK=1)"
|
|
117
|
+
if try_fallback "$platform" "$outfile"; then
|
|
118
|
+
record_build "$platform" "fallback" "ok"
|
|
119
|
+
return 0
|
|
120
|
+
fi
|
|
121
|
+
record_build "$platform" "fallback" "fail"
|
|
122
|
+
else
|
|
123
|
+
record_build "$platform" "bun" "fail"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
return 1
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Fallback: try pkg first, then nexe. Both consume the same entrypoint and
|
|
130
|
+
# emit a single executable. We only attempt the fallback when bun fails;
|
|
131
|
+
# this is per the distribution-exodus G1 contract.
|
|
132
|
+
try_fallback() {
|
|
133
|
+
local platform="$1"
|
|
134
|
+
local outfile="$2"
|
|
135
|
+
|
|
136
|
+
if command -v pkg >/dev/null 2>&1; then
|
|
137
|
+
echo " -> trying pkg"
|
|
138
|
+
local pkg_target
|
|
139
|
+
pkg_target="$(pkg_target_for "$platform")" || return 1
|
|
140
|
+
if pkg --target "$pkg_target" \
|
|
141
|
+
--output "$outfile" \
|
|
142
|
+
"${REPO_ROOT}/${ENTRY_POINT}" 2>&1 | tee -a "${DIST_DIR}/build.log"; then
|
|
143
|
+
echo " ✓ pkg succeeded"
|
|
144
|
+
return 0
|
|
145
|
+
fi
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
if command -v nexe >/dev/null 2>&1; then
|
|
149
|
+
echo " -> trying nexe"
|
|
150
|
+
local nexe_target
|
|
151
|
+
nexe_target="$(nexe_target_for "$platform")" || return 1
|
|
152
|
+
if nexe --target "$nexe_target" \
|
|
153
|
+
--output "$outfile" \
|
|
154
|
+
"${REPO_ROOT}/${ENTRY_POINT}" 2>&1 | tee -a "${DIST_DIR}/build.log"; then
|
|
155
|
+
echo " ✓ nexe succeeded"
|
|
156
|
+
return 0
|
|
157
|
+
fi
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
echo " ✗ no fallback worked (install pkg or nexe to enable)" >&2
|
|
161
|
+
return 1
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
pkg_target_for() {
|
|
165
|
+
case "$1" in
|
|
166
|
+
linux-x64-glibc) echo "node20-linux-x64" ;;
|
|
167
|
+
linux-x64-musl) echo "node20-linuxstatic-x64" ;;
|
|
168
|
+
linux-arm64) echo "node20-linux-arm64" ;;
|
|
169
|
+
darwin-x64) echo "node20-macos-x64" ;;
|
|
170
|
+
darwin-arm64) echo "node20-macos-arm64" ;;
|
|
171
|
+
*) return 1 ;;
|
|
172
|
+
esac
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
nexe_target_for() {
|
|
176
|
+
case "$1" in
|
|
177
|
+
linux-x64-glibc) echo "linux-x64-20.0.0" ;;
|
|
178
|
+
linux-x64-musl) echo "alpine-x64-20.0.0" ;;
|
|
179
|
+
linux-arm64) echo "linux-arm64-20.0.0" ;;
|
|
180
|
+
darwin-x64) echo "mac-x64-20.0.0" ;;
|
|
181
|
+
darwin-arm64) echo "mac-arm64-20.0.0" ;;
|
|
182
|
+
*) return 1 ;;
|
|
183
|
+
esac
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
record_build() {
|
|
187
|
+
local platform="$1" tool="$2" status="$3"
|
|
188
|
+
local rec="${DIST_DIR}/build-record.tsv"
|
|
189
|
+
mkdir -p "$DIST_DIR"
|
|
190
|
+
printf '%s\t%s\t%s\t%s\t%s\n' "$(date -u +%FT%TZ)" "$platform" "$tool" "$status" "$VERSION" >> "$rec"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main() {
|
|
194
|
+
parse_args "$@"
|
|
195
|
+
mkdir -p "$DIST_DIR"
|
|
196
|
+
: > "${DIST_DIR}/build.log"
|
|
197
|
+
|
|
198
|
+
local rc=0
|
|
199
|
+
if [[ "$BUILD_ALL" -eq 1 ]]; then
|
|
200
|
+
for p in "${PLATFORMS[@]}"; do
|
|
201
|
+
build_one "$p" || rc=$?
|
|
202
|
+
done
|
|
203
|
+
else
|
|
204
|
+
build_one "$TARGET_PLATFORM" || rc=$?
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
if [[ $rc -ne 0 ]]; then
|
|
208
|
+
echo "error: at least one build target failed (see ${DIST_DIR}/build.log)" >&2
|
|
209
|
+
fi
|
|
210
|
+
exit $rc
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
main "$@"
|