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
package/README.md
CHANGED
|
@@ -101,17 +101,14 @@ psql postgresql://localhost:8432/myapp
|
|
|
101
101
|
## Installation
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# Global install
|
|
108
|
-
npm install -g pgserve
|
|
104
|
+
# Canonical install — signed binary from GitHub Releases
|
|
105
|
+
curl -fsSL https://raw.githubusercontent.com/namastexlabs/pgserve/main/install.sh | bash
|
|
109
106
|
|
|
110
|
-
#
|
|
111
|
-
|
|
107
|
+
# Pinned version
|
|
108
|
+
PGSERVE_VERSION=v2.6.0 curl -fsSL .../install.sh | bash
|
|
112
109
|
```
|
|
113
110
|
|
|
114
|
-
>
|
|
111
|
+
> `install.sh` fetches the signed tarball from GitHub Releases and verifies it via `gh attestation verify` (Sigstore Rekor public-good). Requires the [`gh` CLI](https://cli.github.com/). pgserve no longer depends on npm — the install + upgrade path is binary tarballs all the way down.
|
|
115
112
|
|
|
116
113
|
### Windows
|
|
117
114
|
|
package/bin/pgserve-wrapper.cjs
CHANGED
|
@@ -43,6 +43,29 @@ const __installSubcommands = new Set([
|
|
|
43
43
|
'upgrade',
|
|
44
44
|
'restart',
|
|
45
45
|
'ui',
|
|
46
|
+
// pgserve singleton (v2.4) — `pgserve-singleton-no-proxy` wish, Group 4.
|
|
47
|
+
// `verify` shells out to cosign + writes an HMAC cache token. Pure node
|
|
48
|
+
// (no bun) so it must skip the bun probe like the install surface above.
|
|
49
|
+
'verify',
|
|
50
|
+
// pgserve singleton (v2.4) — wish Group 3, read-only V1. `doctor` runs
|
|
51
|
+
// entirely in node (admin.json + supervisor probes + socket reachability)
|
|
52
|
+
// and must skip the bun probe so it works on any installed binary.
|
|
53
|
+
'doctor',
|
|
54
|
+
// pgserve singleton (v2.4) — wish Group 3. `trust` manages the
|
|
55
|
+
// user-extensible cosign trust store at ~/.pgserve/trust/identities.json.
|
|
56
|
+
// Pure node, must skip bun probe.
|
|
57
|
+
'trust',
|
|
58
|
+
// pgserve singleton (v2.4) — wish Group 3, verb 3. `gc` shells out to
|
|
59
|
+
// psql to scan pgserve_meta + pg_database, classify orphans, and
|
|
60
|
+
// (under --apply) DROP DATABASE. Pure node + child_process, must skip
|
|
61
|
+
// the bun probe.
|
|
62
|
+
'gc',
|
|
63
|
+
// pgserve singleton (v2.4) — wish Group 3, verb 4. `provision` shells
|
|
64
|
+
// out to psql to CREATE ROLE / CREATE DATABASE / GRANT / INSERT INTO
|
|
65
|
+
// pgserve_meta. Idempotency-driven (no advisory lock — see
|
|
66
|
+
// src/commands/provision.js header for why). Pure node + child_process,
|
|
67
|
+
// must skip bun probe.
|
|
68
|
+
'provision',
|
|
46
69
|
]);
|
|
47
70
|
if (__subcommand && __installSubcommands.has(__subcommand)) {
|
|
48
71
|
const cli = require(path.join(__dirname, '..', 'src', 'cli-install.cjs'));
|
package/bin/postgres-server.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { PostgresManager } from '../src/postgres.js';
|
|
19
19
|
import { resolveSocketDir, ensureSocketDir } from '../src/lib/socket-dir.js';
|
|
20
|
+
import { writeRuntimeJson, clearRuntimeJson } from '../src/lib/runtime-json.js';
|
|
20
21
|
import { createLogger } from '../src/logger.js';
|
|
21
22
|
|
|
22
23
|
// Global error handlers — surface unhandled rejections + uncaught errors
|
|
@@ -95,6 +96,27 @@ async function runPostmasterSubcommand(postmasterArgs) {
|
|
|
95
96
|
process.exit(1);
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
// cutover G19: drop a runtime discovery file at <socketDir>/runtime.json
|
|
100
|
+
// so consumers' UDS-first probes find the live socket without globbing
|
|
101
|
+
// ephemeral pid-stamped dirs. The file is intentionally separate from
|
|
102
|
+
// ~/.autopg/admin.json (which records supervisor metadata, not live
|
|
103
|
+
// socket info) — that split lets the postmaster restart under a new
|
|
104
|
+
// pid without rewriting the supervisor record. NO `supervisor` key
|
|
105
|
+
// here; the writer rejects it.
|
|
106
|
+
try {
|
|
107
|
+
writeRuntimeJson({
|
|
108
|
+
socketDir,
|
|
109
|
+
port: opts.port,
|
|
110
|
+
pid: manager.process?.pid ?? process.pid,
|
|
111
|
+
autopgPid: process.pid,
|
|
112
|
+
});
|
|
113
|
+
} catch (err) {
|
|
114
|
+
logger.warn(
|
|
115
|
+
{ err: err.message },
|
|
116
|
+
'pgserve postmaster: runtime.json write failed; consumers will fall back to admin.json',
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
98
120
|
logger.info(
|
|
99
121
|
{ port: opts.port, socketDir, dataDir: opts.dataDir },
|
|
100
122
|
'pgserve postmaster: ready (Unix socket + TCP)',
|
|
@@ -102,6 +124,12 @@ async function runPostmasterSubcommand(postmasterArgs) {
|
|
|
102
124
|
|
|
103
125
|
const shutdown = async (signal) => {
|
|
104
126
|
logger.info({ signal }, 'pgserve postmaster: stopping');
|
|
127
|
+
// Clear runtime.json BEFORE stopping the postmaster so the moment
|
|
128
|
+
// a graceful-shutdown signal lands, fresh consumers see "no live
|
|
129
|
+
// socket" instead of racing against a stale-pid record. On crash
|
|
130
|
+
// (uncaughtException, backend died) the file is left behind; the
|
|
131
|
+
// operator-facing detector is `process.kill(record.autopgPid, 0)`.
|
|
132
|
+
clearRuntimeJson(socketDir);
|
|
105
133
|
try {
|
|
106
134
|
await manager.stop();
|
|
107
135
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgserve",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Embedded PostgreSQL server with true concurrent connections - zero config, auto-provision databases",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"console:dev": "bun build console/src/main.jsx --target browser --define 'process.env.NODE_ENV=\"development\"' --watch --outfile console/dist/app.js",
|
|
31
31
|
"lint": "eslint src/ bin/",
|
|
32
32
|
"lint:fix": "eslint src/ bin/ --fix",
|
|
33
|
+
"lint:audit": "bun scripts/audit-redaction-lint.js",
|
|
33
34
|
"deadcode": "knip",
|
|
34
35
|
"test:npx": "scripts/test-npx.sh",
|
|
35
36
|
"test:bun-self-heal": "scripts/test-bun-self-heal.sh",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# aggregate-manifest.sh — Group 8 of autopg-distribution-cutover.
|
|
4
|
+
#
|
|
5
|
+
# Walks dist/autopg-<version>-<platform>.tar.gz, gathers each tarball's
|
|
6
|
+
# SHA256 + signature URL + provenance URL + platform tuple, and emits
|
|
7
|
+
# dist/manifest.json that consumers (install.sh, autopg update) read to
|
|
8
|
+
# resolve a download for a given (channel, version, platform).
|
|
9
|
+
#
|
|
10
|
+
# This runs after Group 8 signing + provenance generation and before
|
|
11
|
+
# Group 9 (CDN publish).
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# bash scripts/aggregate-manifest.sh --version 2.260503.1
|
|
15
|
+
# bash scripts/aggregate-manifest.sh --version 2.260503.1 --base-url https://cdn.automagik.dev/autopg/stable/2.260503.1
|
|
16
|
+
#
|
|
17
|
+
# Output:
|
|
18
|
+
# dist/manifest.json
|
|
19
|
+
#
|
|
20
|
+
# Exit codes:
|
|
21
|
+
# 0 ok
|
|
22
|
+
# 1 IO failure
|
|
23
|
+
# 2 invalid args / missing inputs
|
|
24
|
+
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
28
|
+
DIST_DIR="${AUTOPG_DIST_DIR:-${REPO_ROOT}/dist}"
|
|
29
|
+
|
|
30
|
+
usage() {
|
|
31
|
+
cat <<EOF
|
|
32
|
+
Usage: $0 --version <v> [--base-url <url>] [--channel <c>] [--cosign-pub-url <url>]
|
|
33
|
+
|
|
34
|
+
--version autopg version, e.g. 2.260503.1 (or read from package.json)
|
|
35
|
+
--base-url absolute base URL prefix for tarball URLs
|
|
36
|
+
(default: relative — consumers resolve against the
|
|
37
|
+
directory the manifest sits in).
|
|
38
|
+
--channel channel hint embedded in the manifest (stable|beta|canary).
|
|
39
|
+
default: stable
|
|
40
|
+
--cosign-pub-url absolute URL to the published cosign public key.
|
|
41
|
+
default: <base-url>/../../keys/cosign.pub for production
|
|
42
|
+
(cdn.automagik.dev layout) or "keys/cosign.pub" relative.
|
|
43
|
+
|
|
44
|
+
Reads:
|
|
45
|
+
dist/autopg-<version>-<platform>.tar.gz
|
|
46
|
+
dist/autopg-<version>-<platform>.tar.gz.sha256
|
|
47
|
+
dist/autopg-<version>-<platform>.tar.gz.sig (optional)
|
|
48
|
+
dist/autopg-<version>-<platform>.tar.gz.intoto.jsonl (optional)
|
|
49
|
+
|
|
50
|
+
Writes:
|
|
51
|
+
dist/manifest.json
|
|
52
|
+
EOF
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
parse_args() {
|
|
56
|
+
VERSION="${AUTOPG_VERSION:-}"
|
|
57
|
+
BASE_URL=""
|
|
58
|
+
CHANNEL="stable"
|
|
59
|
+
COSIGN_PUB_URL=""
|
|
60
|
+
while [[ $# -gt 0 ]]; do
|
|
61
|
+
case "$1" in
|
|
62
|
+
--version) VERSION="$2"; shift 2 ;;
|
|
63
|
+
--base-url) BASE_URL="$2"; shift 2 ;;
|
|
64
|
+
--channel) CHANNEL="$2"; shift 2 ;;
|
|
65
|
+
--cosign-pub-url) COSIGN_PUB_URL="$2"; shift 2 ;;
|
|
66
|
+
-h|--help) usage; exit 0 ;;
|
|
67
|
+
*) echo "unknown arg: $1" >&2; usage; exit 2 ;;
|
|
68
|
+
esac
|
|
69
|
+
done
|
|
70
|
+
if [[ -z "$VERSION" ]]; then
|
|
71
|
+
VERSION=$(node -p "require('${REPO_ROOT}/package.json').version" 2>/dev/null || echo "")
|
|
72
|
+
fi
|
|
73
|
+
if [[ -z "$VERSION" ]]; then
|
|
74
|
+
echo "error: --version required (or set in package.json)" >&2; exit 2
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Portable SHA256 — sha256sum on linux, shasum -a 256 on macOS.
|
|
79
|
+
sha256_of() {
|
|
80
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
81
|
+
sha256sum "$1" | awk '{print $1}'
|
|
82
|
+
else
|
|
83
|
+
shasum -a 256 "$1" | awk '{print $1}'
|
|
84
|
+
fi
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
prefix_url() {
|
|
88
|
+
local rel="$1"
|
|
89
|
+
if [[ -n "$BASE_URL" ]]; then
|
|
90
|
+
printf '%s/%s' "${BASE_URL%/}" "$rel"
|
|
91
|
+
else
|
|
92
|
+
printf '%s' "$rel"
|
|
93
|
+
fi
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
emit_entry() {
|
|
97
|
+
local tarball="$1"
|
|
98
|
+
local first="$2"
|
|
99
|
+
local base platform sha sz
|
|
100
|
+
base=$(basename "$tarball")
|
|
101
|
+
# Strip "autopg-<version>-" prefix and ".tar.gz" suffix to get platform.
|
|
102
|
+
platform="${base#autopg-${VERSION}-}"
|
|
103
|
+
platform="${platform%.tar.gz}"
|
|
104
|
+
|
|
105
|
+
if [[ ! -f "${tarball}.sha256" ]]; then
|
|
106
|
+
echo "error: ${tarball}.sha256 missing — run assemble-tarball.sh first" >&2
|
|
107
|
+
return 1
|
|
108
|
+
fi
|
|
109
|
+
sha=$(awk '{print $1}' "${tarball}.sha256")
|
|
110
|
+
sz=$(stat -c %s "$tarball" 2>/dev/null || stat -f %z "$tarball")
|
|
111
|
+
|
|
112
|
+
local sig_url="" prov_url=""
|
|
113
|
+
if [[ -f "${tarball}.sig" ]]; then
|
|
114
|
+
sig_url=$(prefix_url "${base}.sig")
|
|
115
|
+
fi
|
|
116
|
+
if [[ -f "${tarball}.intoto.jsonl" ]]; then
|
|
117
|
+
prov_url=$(prefix_url "${base}.intoto.jsonl")
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
if [[ "$first" -eq 0 ]]; then printf ',\n'; fi
|
|
121
|
+
printf ' {\n'
|
|
122
|
+
printf ' "platform": "%s",\n' "$platform"
|
|
123
|
+
printf ' "file": "%s",\n' "$base"
|
|
124
|
+
printf ' "url": "%s",\n' "$(prefix_url "$base")"
|
|
125
|
+
printf ' "sha256": "%s",\n' "$sha"
|
|
126
|
+
printf ' "size": %d,\n' "$sz"
|
|
127
|
+
printf ' "signature_url": "%s",\n' "$sig_url"
|
|
128
|
+
printf ' "provenance_url": "%s"\n' "$prov_url"
|
|
129
|
+
printf ' }'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main() {
|
|
133
|
+
parse_args "$@"
|
|
134
|
+
[[ -d "$DIST_DIR" ]] || { echo "error: $DIST_DIR not a directory" >&2; exit 2; }
|
|
135
|
+
|
|
136
|
+
local tarballs=()
|
|
137
|
+
while IFS= read -r line; do
|
|
138
|
+
tarballs+=("$line")
|
|
139
|
+
done < <(find "$DIST_DIR" -maxdepth 1 -name "autopg-${VERSION}-*.tar.gz" -type f | LC_ALL=C sort)
|
|
140
|
+
|
|
141
|
+
if [[ ${#tarballs[@]} -eq 0 ]]; then
|
|
142
|
+
echo "error: no autopg-${VERSION}-*.tar.gz files in ${DIST_DIR}" >&2
|
|
143
|
+
exit 2
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
local generated_at
|
|
147
|
+
generated_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
148
|
+
|
|
149
|
+
local out="${DIST_DIR}/manifest.json"
|
|
150
|
+
{
|
|
151
|
+
printf '{\n'
|
|
152
|
+
printf ' "name": "autopg",\n'
|
|
153
|
+
printf ' "version": "%s",\n' "$VERSION"
|
|
154
|
+
printf ' "channel": "%s",\n' "$CHANNEL"
|
|
155
|
+
printf ' "schemaVersion": 1,\n'
|
|
156
|
+
printf ' "generated_at": "%s",\n' "$generated_at"
|
|
157
|
+
local cpub
|
|
158
|
+
if [[ -n "$COSIGN_PUB_URL" ]]; then
|
|
159
|
+
cpub="$COSIGN_PUB_URL"
|
|
160
|
+
elif [[ -n "$BASE_URL" ]]; then
|
|
161
|
+
# CDN layout: <base>/autopg/<channel>/<version>/manifest.json
|
|
162
|
+
# Public key lives at: <base>/autopg/keys/cosign.pub
|
|
163
|
+
# If caller passes the full <base>/autopg/<channel>/<version> as
|
|
164
|
+
# base-url, walk two levels up to reach the keys/ sibling.
|
|
165
|
+
cpub="${BASE_URL%/*}"
|
|
166
|
+
cpub="${cpub%/*}/keys/cosign.pub"
|
|
167
|
+
else
|
|
168
|
+
cpub="keys/cosign.pub"
|
|
169
|
+
fi
|
|
170
|
+
printf ' "cosign_pub_url": "%s",\n' "$cpub"
|
|
171
|
+
printf ' "platforms": [\n'
|
|
172
|
+
local first=1
|
|
173
|
+
for t in "${tarballs[@]}"; do
|
|
174
|
+
emit_entry "$t" "$first"
|
|
175
|
+
first=0
|
|
176
|
+
done
|
|
177
|
+
printf '\n ]\n'
|
|
178
|
+
printf '}\n'
|
|
179
|
+
} > "$out"
|
|
180
|
+
|
|
181
|
+
echo "==> manifest: $out ($(wc -l < "$out") lines, ${#tarballs[@]} platforms)"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
main "$@"
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# assemble-tarball.sh — Group 7 of autopg-distribution-cutover.
|
|
4
|
+
#
|
|
5
|
+
# Assembles a single platform tarball with the locked shape:
|
|
6
|
+
#
|
|
7
|
+
# autopg/
|
|
8
|
+
# autopg # static binary (from build-binary.sh)
|
|
9
|
+
# postgres/
|
|
10
|
+
# bin/* # postgres + initdb + libpq + ...
|
|
11
|
+
# share/* # timezone data, locale, etc.
|
|
12
|
+
# manifest.json # per-file SHA256 + size
|
|
13
|
+
#
|
|
14
|
+
# The tarball lives at:
|
|
15
|
+
# dist/autopg-<version>-<platform>.tar.gz
|
|
16
|
+
# and a sibling .sha256 file holds the outer hash for Group 8 (cosign sign)
|
|
17
|
+
# and Group 9 (CDN publish) to consume.
|
|
18
|
+
#
|
|
19
|
+
# Inputs come from dist/<platform>/autopg/{autopg, postgres/}, populated by
|
|
20
|
+
# build-binary.sh + fetch-postgres-bins.sh.
|
|
21
|
+
#
|
|
22
|
+
# Usage:
|
|
23
|
+
# scripts/assemble-tarball.sh --platform linux-x64-glibc
|
|
24
|
+
# scripts/assemble-tarball.sh --all --version 2.260503.1
|
|
25
|
+
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
29
|
+
DIST_DIR="${AUTOPG_DIST_DIR:-${REPO_ROOT}/dist}"
|
|
30
|
+
|
|
31
|
+
PLATFORMS=(linux-x64-glibc linux-x64-musl linux-arm64 darwin-x64 darwin-arm64)
|
|
32
|
+
|
|
33
|
+
usage() {
|
|
34
|
+
cat <<EOF
|
|
35
|
+
Usage: $0 (--platform <p> | --all) [--version <v>]
|
|
36
|
+
|
|
37
|
+
Platforms: ${PLATFORMS[*]}
|
|
38
|
+
|
|
39
|
+
Inputs (must already exist):
|
|
40
|
+
dist/<platform>/autopg/autopg (build-binary.sh)
|
|
41
|
+
dist/<platform>/autopg/postgres/bin/* (fetch-postgres-bins.sh)
|
|
42
|
+
dist/<platform>/autopg/postgres/share/* (fetch-postgres-bins.sh)
|
|
43
|
+
|
|
44
|
+
Outputs:
|
|
45
|
+
dist/autopg-<version>-<platform>.tar.gz
|
|
46
|
+
dist/autopg-<version>-<platform>.tar.gz.sha256
|
|
47
|
+
EOF
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
parse_args() {
|
|
51
|
+
TARGET_PLATFORM=""
|
|
52
|
+
ASSEMBLE_ALL=0
|
|
53
|
+
VERSION="${AUTOPG_VERSION:-}"
|
|
54
|
+
|
|
55
|
+
while [[ $# -gt 0 ]]; do
|
|
56
|
+
case "$1" in
|
|
57
|
+
--platform) TARGET_PLATFORM="$2"; shift 2 ;;
|
|
58
|
+
--all) ASSEMBLE_ALL=1; shift ;;
|
|
59
|
+
--version) VERSION="$2"; shift 2 ;;
|
|
60
|
+
-h|--help) usage; exit 0 ;;
|
|
61
|
+
*) echo "unknown arg: $1" >&2; usage; exit 2 ;;
|
|
62
|
+
esac
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
if [[ "$ASSEMBLE_ALL" -eq 0 && -z "$TARGET_PLATFORM" ]]; then
|
|
66
|
+
echo "error: pass --platform <p> or --all" >&2; usage; exit 2
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
if [[ -z "$VERSION" ]]; then
|
|
70
|
+
VERSION=$(node -p "require('${REPO_ROOT}/package.json').version" 2>/dev/null || echo "0.0.0")
|
|
71
|
+
fi
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Portable SHA256 — use sha256sum on linux, shasum -a 256 on macOS.
|
|
75
|
+
sha256_of() {
|
|
76
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
77
|
+
sha256sum "$1" | awk '{print $1}'
|
|
78
|
+
else
|
|
79
|
+
shasum -a 256 "$1" | awk '{print $1}'
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Emit manifest.json for the given platform's staged tree.
|
|
84
|
+
# Walks autopg/ relative to <root>/, skipping manifest.json itself.
|
|
85
|
+
emit_manifest() {
|
|
86
|
+
local root="$1" platform="$2" out="$3"
|
|
87
|
+
|
|
88
|
+
pushd "$root" >/dev/null
|
|
89
|
+
{
|
|
90
|
+
printf '{\n'
|
|
91
|
+
printf ' "name": "autopg",\n'
|
|
92
|
+
printf ' "version": "%s",\n' "$VERSION"
|
|
93
|
+
printf ' "platform": "%s",\n' "$platform"
|
|
94
|
+
printf ' "schemaVersion": 1,\n'
|
|
95
|
+
printf ' "files": [\n'
|
|
96
|
+
|
|
97
|
+
local first=1
|
|
98
|
+
while IFS= read -r f; do
|
|
99
|
+
[[ "$f" == "autopg/manifest.json" ]] && continue
|
|
100
|
+
local h sz
|
|
101
|
+
h=$(sha256_of "$f")
|
|
102
|
+
sz=$(stat -c %s "$f" 2>/dev/null || stat -f %z "$f")
|
|
103
|
+
if [[ $first -eq 1 ]]; then
|
|
104
|
+
first=0
|
|
105
|
+
else
|
|
106
|
+
printf ',\n'
|
|
107
|
+
fi
|
|
108
|
+
printf ' { "path": "%s", "sha256": "%s", "size": %d }' "$f" "$h" "$sz"
|
|
109
|
+
done < <(find autopg -type f | LC_ALL=C sort)
|
|
110
|
+
|
|
111
|
+
printf '\n ]\n'
|
|
112
|
+
printf '}\n'
|
|
113
|
+
} > "$out"
|
|
114
|
+
popd >/dev/null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Verify staged inputs are present + executable.
|
|
118
|
+
verify_inputs() {
|
|
119
|
+
local stage="$1" platform="$2"
|
|
120
|
+
local missing=0
|
|
121
|
+
for required in autopg/autopg autopg/postgres/bin/postgres; do
|
|
122
|
+
if [[ ! -f "${stage}/${required}" ]]; then
|
|
123
|
+
echo "error: ${platform}: missing ${required}" >&2
|
|
124
|
+
missing=1
|
|
125
|
+
fi
|
|
126
|
+
done
|
|
127
|
+
return $missing
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
assemble_one() {
|
|
131
|
+
local platform="$1"
|
|
132
|
+
local stage="${DIST_DIR}/${platform}"
|
|
133
|
+
local tarball="${DIST_DIR}/autopg-${VERSION}-${platform}.tar.gz"
|
|
134
|
+
local outer_sha="${tarball}.sha256"
|
|
135
|
+
|
|
136
|
+
if [[ ! -d "${stage}/autopg" ]]; then
|
|
137
|
+
echo "error: ${stage}/autopg/ does not exist (run build-binary.sh + fetch-postgres-bins.sh first)" >&2
|
|
138
|
+
return 1
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
echo "==> [${platform}] assemble tarball"
|
|
142
|
+
# `assemble_one` is invoked from main as `assemble_one ... || rc=$?` which
|
|
143
|
+
# disables `set -e` for the duration of this function. Each potentially-
|
|
144
|
+
# failing command needs explicit `|| return 1` to halt early instead of
|
|
145
|
+
# silently producing a corrupt tarball (gemini bot review HIGH on PR #84).
|
|
146
|
+
verify_inputs "$stage" "$platform" || return 1
|
|
147
|
+
|
|
148
|
+
# 1) emit per-file manifest BEFORE the tarball is rolled — manifest is
|
|
149
|
+
# bundled inside.
|
|
150
|
+
emit_manifest "$stage" "$platform" "${stage}/autopg/manifest.json" || return 1
|
|
151
|
+
|
|
152
|
+
# 2) ensure binaries are executable inside the tar.
|
|
153
|
+
chmod +x "${stage}/autopg/autopg" || true
|
|
154
|
+
find "${stage}/autopg/postgres/bin" -type f -exec chmod +x {} +
|
|
155
|
+
|
|
156
|
+
# 3) build deterministic tarball: sorted entries, locked mtime.
|
|
157
|
+
local tar_flags=()
|
|
158
|
+
if tar --help 2>&1 | grep -q -- '--sort=name'; then
|
|
159
|
+
tar_flags+=(--sort=name)
|
|
160
|
+
fi
|
|
161
|
+
if tar --help 2>&1 | grep -q -- '--mtime='; then
|
|
162
|
+
tar_flags+=(--mtime=2026-01-01)
|
|
163
|
+
fi
|
|
164
|
+
if tar --help 2>&1 | grep -q -- '--owner='; then
|
|
165
|
+
tar_flags+=(--owner=0 --group=0 --numeric-owner)
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
tar -C "$stage" -czf "$tarball" "${tar_flags[@]}" autopg/ || return 1
|
|
169
|
+
echo " ✓ tarball: $tarball ($(du -h "$tarball" | cut -f1))"
|
|
170
|
+
|
|
171
|
+
# 4) outer SHA256 — Group 8 cosign-signs this; Group 9 publishes both.
|
|
172
|
+
sha256_of "$tarball" > "$outer_sha" || return 1
|
|
173
|
+
echo " ✓ sha256: $(cat "$outer_sha") $(basename "$tarball")"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
main() {
|
|
177
|
+
parse_args "$@"
|
|
178
|
+
mkdir -p "$DIST_DIR"
|
|
179
|
+
|
|
180
|
+
local rc=0
|
|
181
|
+
if [[ "$ASSEMBLE_ALL" -eq 1 ]]; then
|
|
182
|
+
for p in "${PLATFORMS[@]}"; do
|
|
183
|
+
assemble_one "$p" || rc=$?
|
|
184
|
+
done
|
|
185
|
+
else
|
|
186
|
+
assemble_one "$TARGET_PLATFORM" || rc=$?
|
|
187
|
+
fi
|
|
188
|
+
exit $rc
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
main "$@"
|