pgserve 2.5.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 +19 -0
- package/package.json +1 -1
- package/scripts/aggregate-manifest.sh +184 -0
- package/scripts/assemble-tarball.sh +191 -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/cli-install.cjs +65 -3
- 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/cosign/trust-list.js +3 -3
- package/src/cosign/trust-store.js +250 -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/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/steps/binary-cache-flush.js +2 -2
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# fetch-postgres-bins.sh — Group 7 of autopg-distribution-cutover.
|
|
4
|
+
#
|
|
5
|
+
# Stages PostgreSQL server binaries for one platform under
|
|
6
|
+
# dist/<platform>/autopg/postgres/{bin,share}/
|
|
7
|
+
# so the assemble-tarball step can pack a self-contained release.
|
|
8
|
+
#
|
|
9
|
+
# Source resolution (in priority order):
|
|
10
|
+
# 1. Explicit local override — AUTOPG_POSTGRES_LOCAL_DIR=<dir>
|
|
11
|
+
# Useful in CI when binaries are pre-fetched into a runner cache.
|
|
12
|
+
# Expects <dir>/{bin,share}/.
|
|
13
|
+
# 2. npm/bun package — AUTOPG_POSTGRES_PKG_VERSION (default reads
|
|
14
|
+
# package.json optionalDependencies). The function pulls
|
|
15
|
+
# @embedded-postgres/<platform-pkg> into a scratch dir and copies
|
|
16
|
+
# its native/{bin,share}/ payload.
|
|
17
|
+
# 3. URL template — AUTOPG_POSTGRES_URL_TEMPLATE='https://.../pg-{ver}-{pf}.tar.gz'
|
|
18
|
+
# with placeholders {ver} and {pf}.
|
|
19
|
+
#
|
|
20
|
+
# Platforms: linux-x64-glibc linux-x64-musl linux-arm64 darwin-x64 darwin-arm64
|
|
21
|
+
#
|
|
22
|
+
# Usage:
|
|
23
|
+
# scripts/fetch-postgres-bins.sh --platform linux-x64-glibc
|
|
24
|
+
# scripts/fetch-postgres-bins.sh --all
|
|
25
|
+
# AUTOPG_POSTGRES_LOCAL_DIR=/cache/pg16-linux-x64 scripts/fetch-postgres-bins.sh --platform linux-x64-glibc
|
|
26
|
+
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
|
|
29
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
30
|
+
DIST_DIR="${AUTOPG_DIST_DIR:-${REPO_ROOT}/dist}"
|
|
31
|
+
|
|
32
|
+
PLATFORMS=(linux-x64-glibc linux-x64-musl linux-arm64 darwin-x64 darwin-arm64)
|
|
33
|
+
|
|
34
|
+
# Map autopg platform tag → npm package suffix used by @embedded-postgres.
|
|
35
|
+
# linux-x64-musl + linux-arm64 currently lack a published @embedded-postgres
|
|
36
|
+
# package; for those, set AUTOPG_POSTGRES_LOCAL_DIR or AUTOPG_POSTGRES_URL_TEMPLATE.
|
|
37
|
+
embedded_pkg_for() {
|
|
38
|
+
case "$1" in
|
|
39
|
+
linux-x64-glibc) echo "linux-x64" ;;
|
|
40
|
+
linux-x64-musl) echo "" ;;
|
|
41
|
+
linux-arm64) echo "" ;;
|
|
42
|
+
darwin-x64) echo "darwin-x64" ;;
|
|
43
|
+
darwin-arm64) echo "darwin-arm64" ;;
|
|
44
|
+
*) return 1 ;;
|
|
45
|
+
esac
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# URL-template placeholder mapping for the 3rd-source path.
|
|
49
|
+
url_pf_for() {
|
|
50
|
+
case "$1" in
|
|
51
|
+
linux-x64-glibc) echo "linux-x86_64-glibc" ;;
|
|
52
|
+
linux-x64-musl) echo "linux-x86_64-musl" ;;
|
|
53
|
+
linux-arm64) echo "linux-aarch64" ;;
|
|
54
|
+
darwin-x64) echo "darwin-x86_64" ;;
|
|
55
|
+
darwin-arm64) echo "darwin-aarch64" ;;
|
|
56
|
+
*) return 1 ;;
|
|
57
|
+
esac
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
usage() {
|
|
61
|
+
cat <<EOF
|
|
62
|
+
Usage: $0 (--platform <p> | --all) [--postgres-version <v>]
|
|
63
|
+
|
|
64
|
+
Platforms: ${PLATFORMS[*]}
|
|
65
|
+
|
|
66
|
+
Source priority (first match wins):
|
|
67
|
+
AUTOPG_POSTGRES_LOCAL_DIR pre-fetched <dir>/{bin,share}/
|
|
68
|
+
AUTOPG_POSTGRES_PKG_VERSION npm @embedded-postgres/<platform-pkg>@<ver>
|
|
69
|
+
AUTOPG_POSTGRES_URL_TEMPLATE 'https://.../pg-{ver}-{pf}.tar.gz'
|
|
70
|
+
|
|
71
|
+
Defaults:
|
|
72
|
+
--postgres-version reads optionalDependencies @embedded-postgres/* version
|
|
73
|
+
EOF
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
parse_args() {
|
|
77
|
+
TARGET_PLATFORM=""
|
|
78
|
+
FETCH_ALL=0
|
|
79
|
+
PG_VERSION="${AUTOPG_POSTGRES_PKG_VERSION:-}"
|
|
80
|
+
|
|
81
|
+
while [[ $# -gt 0 ]]; do
|
|
82
|
+
case "$1" in
|
|
83
|
+
--platform) TARGET_PLATFORM="$2"; shift 2 ;;
|
|
84
|
+
--all) FETCH_ALL=1; shift ;;
|
|
85
|
+
--postgres-version) PG_VERSION="$2"; shift 2 ;;
|
|
86
|
+
-h|--help) usage; exit 0 ;;
|
|
87
|
+
*) echo "unknown arg: $1" >&2; usage; exit 2 ;;
|
|
88
|
+
esac
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
if [[ "$FETCH_ALL" -eq 0 && -z "$TARGET_PLATFORM" ]]; then
|
|
92
|
+
echo "error: pass --platform <p> or --all" >&2
|
|
93
|
+
usage; exit 2
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
if [[ -z "$PG_VERSION" ]]; then
|
|
97
|
+
PG_VERSION=$(node -p "require('${REPO_ROOT}/package.json').optionalDependencies['@embedded-postgres/linux-x64'] || ''" 2>/dev/null || true)
|
|
98
|
+
fi
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
stage_from_local() {
|
|
102
|
+
local local_dir="$1" out_dir="$2"
|
|
103
|
+
echo " -> source: AUTOPG_POSTGRES_LOCAL_DIR=$local_dir"
|
|
104
|
+
if [[ ! -d "$local_dir/bin" ]]; then
|
|
105
|
+
echo "error: $local_dir/bin missing" >&2; return 1
|
|
106
|
+
fi
|
|
107
|
+
cp -R "$local_dir/bin" "$out_dir/bin"
|
|
108
|
+
cp -R "$local_dir/share" "$out_dir/share" 2>/dev/null || mkdir -p "$out_dir/share"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
stage_from_pkg() {
|
|
112
|
+
local pkg="$1" version="$2" out_dir="$3"
|
|
113
|
+
echo " -> source: npm @embedded-postgres/${pkg}@${version}"
|
|
114
|
+
# Initialize before installing trap — under `set -u` the RETURN trap fires
|
|
115
|
+
# on any early-return path, including ones where `mktemp` hasn't run yet.
|
|
116
|
+
# Referencing an unset `$scratch` from the trap would print
|
|
117
|
+
# `scratch: unbound variable` and mask the real fetch error
|
|
118
|
+
# (chatgpt-codex P2 review on PR #84).
|
|
119
|
+
local scratch=""
|
|
120
|
+
trap 'rm -rf "$scratch"' RETURN
|
|
121
|
+
scratch=$(mktemp -d) || return 1
|
|
122
|
+
|
|
123
|
+
pushd "$scratch" >/dev/null
|
|
124
|
+
cat > package.json <<EOF
|
|
125
|
+
{ "name": "autopg-pg-fetch", "version": "0.0.0", "private": true,
|
|
126
|
+
"dependencies": { "@embedded-postgres/${pkg}": "${version}" } }
|
|
127
|
+
EOF
|
|
128
|
+
|
|
129
|
+
# Use npm; bun pulls into a different layout.
|
|
130
|
+
if ! npm install --no-audit --no-fund --silent --ignore-scripts; then
|
|
131
|
+
popd >/dev/null
|
|
132
|
+
echo "error: npm install failed for @embedded-postgres/${pkg}@${version}" >&2
|
|
133
|
+
return 1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
local native="node_modules/@embedded-postgres/${pkg}/native"
|
|
137
|
+
if [[ ! -d "$native" ]]; then
|
|
138
|
+
popd >/dev/null
|
|
139
|
+
echo "error: native/ missing in @embedded-postgres/${pkg} payload" >&2
|
|
140
|
+
return 1
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
cp -R "${native}/bin" "${out_dir}/bin"
|
|
144
|
+
cp -R "${native}/share" "${out_dir}/share" 2>/dev/null || mkdir -p "${out_dir}/share"
|
|
145
|
+
popd >/dev/null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
stage_from_url() {
|
|
149
|
+
local template="$1" version="$2" platform="$3" out_dir="$4"
|
|
150
|
+
local pf
|
|
151
|
+
pf="$(url_pf_for "$platform")" || { echo "error: no url mapping for $platform" >&2; return 1; }
|
|
152
|
+
local url="${template//\{ver\}/$version}"
|
|
153
|
+
url="${url//\{pf\}/$pf}"
|
|
154
|
+
echo " -> source: $url"
|
|
155
|
+
|
|
156
|
+
local scratch
|
|
157
|
+
scratch=$(mktemp -d)
|
|
158
|
+
trap 'rm -rf "$scratch"' RETURN
|
|
159
|
+
|
|
160
|
+
curl -fsSL "$url" -o "${scratch}/pg.tar.gz"
|
|
161
|
+
tar -xzf "${scratch}/pg.tar.gz" -C "$scratch"
|
|
162
|
+
|
|
163
|
+
# Find the first directory containing bin/postgres.
|
|
164
|
+
local root postgres_path
|
|
165
|
+
postgres_path=$(find "$scratch" -mindepth 1 -maxdepth 4 -type f -name postgres -path '*/bin/*' -print -quit)
|
|
166
|
+
if [[ -n "$postgres_path" ]]; then
|
|
167
|
+
root=$(dirname "$(dirname "$postgres_path")")
|
|
168
|
+
else
|
|
169
|
+
root=""
|
|
170
|
+
fi
|
|
171
|
+
if [[ -z "$root" ]]; then
|
|
172
|
+
echo "error: bin/postgres not found in extracted tarball" >&2
|
|
173
|
+
return 1
|
|
174
|
+
fi
|
|
175
|
+
cp -R "${root}/bin" "${out_dir}/bin"
|
|
176
|
+
cp -R "${root}/share" "${out_dir}/share" 2>/dev/null || mkdir -p "${out_dir}/share"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fetch_one() {
|
|
180
|
+
local platform="$1"
|
|
181
|
+
local out_dir="${DIST_DIR}/${platform}/autopg/postgres"
|
|
182
|
+
rm -rf "$out_dir" || return 1
|
|
183
|
+
mkdir -p "$out_dir" || return 1
|
|
184
|
+
|
|
185
|
+
echo "==> [${platform}] fetch postgres bins"
|
|
186
|
+
|
|
187
|
+
# `fetch_one` runs without `set -e` (called via `|| rc=$?` in main), so each
|
|
188
|
+
# stage_* helper must propagate failures explicitly (gemini PR #84 HIGH).
|
|
189
|
+
if [[ -n "${AUTOPG_POSTGRES_LOCAL_DIR:-}" ]]; then
|
|
190
|
+
stage_from_local "$AUTOPG_POSTGRES_LOCAL_DIR" "$out_dir" || return 1
|
|
191
|
+
elif [[ -n "$PG_VERSION" ]]; then
|
|
192
|
+
local pkg
|
|
193
|
+
pkg="$(embedded_pkg_for "$platform")" || true
|
|
194
|
+
if [[ -n "$pkg" ]]; then
|
|
195
|
+
stage_from_pkg "$pkg" "$PG_VERSION" "$out_dir" || return 1
|
|
196
|
+
elif [[ -n "${AUTOPG_POSTGRES_URL_TEMPLATE:-}" ]]; then
|
|
197
|
+
stage_from_url "$AUTOPG_POSTGRES_URL_TEMPLATE" "$PG_VERSION" "$platform" "$out_dir" || return 1
|
|
198
|
+
else
|
|
199
|
+
echo "error: no @embedded-postgres pkg for ${platform}; set AUTOPG_POSTGRES_URL_TEMPLATE or AUTOPG_POSTGRES_LOCAL_DIR" >&2
|
|
200
|
+
return 1
|
|
201
|
+
fi
|
|
202
|
+
elif [[ -n "${AUTOPG_POSTGRES_URL_TEMPLATE:-}" ]]; then
|
|
203
|
+
stage_from_url "$AUTOPG_POSTGRES_URL_TEMPLATE" "${AUTOPG_POSTGRES_URL_VERSION:-16}" "$platform" "$out_dir"
|
|
204
|
+
else
|
|
205
|
+
echo "error: no postgres source resolved for ${platform}" >&2
|
|
206
|
+
return 1
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
if [[ ! -x "${out_dir}/bin/postgres" ]]; then
|
|
210
|
+
chmod +x "${out_dir}/bin/postgres" 2>/dev/null || true
|
|
211
|
+
fi
|
|
212
|
+
if [[ ! -f "${out_dir}/bin/postgres" ]]; then
|
|
213
|
+
echo "error: ${out_dir}/bin/postgres missing after stage" >&2
|
|
214
|
+
return 1
|
|
215
|
+
fi
|
|
216
|
+
echo " ✓ staged ${out_dir} ($(du -sh "$out_dir" | cut -f1))"
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
main() {
|
|
220
|
+
parse_args "$@"
|
|
221
|
+
mkdir -p "$DIST_DIR"
|
|
222
|
+
|
|
223
|
+
local rc=0
|
|
224
|
+
if [[ "$FETCH_ALL" -eq 1 ]]; then
|
|
225
|
+
for p in "${PLATFORMS[@]}"; do
|
|
226
|
+
fetch_one "$p" || rc=$?
|
|
227
|
+
done
|
|
228
|
+
else
|
|
229
|
+
fetch_one "$TARGET_PLATFORM" || rc=$?
|
|
230
|
+
fi
|
|
231
|
+
exit $rc
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
main "$@"
|
package/scripts/postinstall.cjs
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
* - Upgrade install (data dir exists) → invoke `autopg upgrade --quiet`
|
|
8
8
|
* - Soft-fail: any error logs warning, exits 0 (never breaks `bun install`)
|
|
9
9
|
* - Skip override: AUTOPG_SKIP_POSTINSTALL=1 → exit 0 immediately
|
|
10
|
+
* - Dev-worktree auto-skip: if the package root sits inside a genie or
|
|
11
|
+
* git worktree, skip with a stderr note. Stops contributors running
|
|
12
|
+
* `bun install` in a worktree from accidentally migrating their real
|
|
13
|
+
* `~/.autopg/data` against half-built code.
|
|
14
|
+
* - Non-CI pre-warning: emit a stderr line BEFORE invoking upgrade so
|
|
15
|
+
* the operator can see what's about to happen and Ctrl+C if needed.
|
|
16
|
+
* CI runs (`CI=true`) stay quiet.
|
|
10
17
|
*
|
|
11
18
|
* The escape hatch for forced re-runs is `autopg upgrade` (manual).
|
|
12
19
|
*
|
|
@@ -21,40 +28,117 @@ function getAutopgRoot() {
|
|
|
21
28
|
return process.env.AUTOPG_CONFIG_DIR || process.env.PGSERVE_CONFIG_DIR || `${process.env.HOME}/.autopg`;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Return true if the package root sits inside a development worktree.
|
|
33
|
+
*
|
|
34
|
+
* Two markers:
|
|
35
|
+
* 1. Genie convention: path contains `/.genie/worktrees/`
|
|
36
|
+
* 2. Git worktree: `<pkgRoot>/.git` is a FILE (not a dir) whose
|
|
37
|
+
* `gitdir:` pointer references another `.git/worktrees/...`
|
|
38
|
+
*
|
|
39
|
+
* Either marker is enough; both have high precision for "this is a
|
|
40
|
+
* dev checkout, not a real consumer install".
|
|
41
|
+
*/
|
|
42
|
+
function isDevWorktree(pkgRoot) {
|
|
43
|
+
const sep = path.sep;
|
|
44
|
+
if (pkgRoot.includes(`${sep}.genie${sep}worktrees${sep}`)) return true;
|
|
45
|
+
try {
|
|
46
|
+
const gitMarker = path.join(pkgRoot, '.git');
|
|
47
|
+
const stat = fs.statSync(gitMarker);
|
|
48
|
+
if (stat.isFile()) {
|
|
49
|
+
const content = fs.readFileSync(gitMarker, 'utf8');
|
|
50
|
+
if (content.includes(`${sep}.git${sep}worktrees${sep}`)) return true;
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// No .git, unreadable, or path resolution error — not a worktree
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isCI() {
|
|
59
|
+
const v = process.env.CI;
|
|
60
|
+
return v === 'true' || v === '1';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Decision-and-side-effect entry. Accepts an optional `deps` object so
|
|
65
|
+
* tests can drive each branch (worktree-skip, fresh-install, CI-quiet,
|
|
66
|
+
* upgrade-invoke, soft-fail-on-error) without depending on the host
|
|
67
|
+
* environment. Production callers invoke `main()` with no args; the
|
|
68
|
+
* defaults reproduce the original behavior verbatim.
|
|
69
|
+
*
|
|
70
|
+
* Why dependency injection: a previous spawn-and-grep test attempted
|
|
71
|
+
* to verify the worktree-skip stderr by symlinking the script into a
|
|
72
|
+
* synthetic worktree dir. Node's symlink resolution defeated that —
|
|
73
|
+
* `__dirname` inside the script points at the real-on-disk source, not
|
|
74
|
+
* the symlink target — so `isDevWorktree(pkgRoot)` saw the real pkgRoot
|
|
75
|
+
* and the stderr note never fired. Injecting `pkgRoot` + the heuristic
|
|
76
|
+
* function decouples behavior from filesystem layout for tests.
|
|
77
|
+
*/
|
|
78
|
+
function main(deps = {}) {
|
|
79
|
+
const env = deps.env ?? process.env;
|
|
80
|
+
const pkgRoot = deps.pkgRoot ?? path.resolve(__dirname, '..');
|
|
81
|
+
const isDevWorktreeFn = deps.isDevWorktree ?? isDevWorktree;
|
|
82
|
+
const isCIFn = deps.isCI ?? isCI;
|
|
83
|
+
const getAutopgRootFn = deps.getAutopgRoot
|
|
84
|
+
?? (() => env.AUTOPG_CONFIG_DIR || env.PGSERVE_CONFIG_DIR || `${env.HOME}/.autopg`);
|
|
85
|
+
const fsApi = deps.fs ?? fs;
|
|
86
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
87
|
+
const spawnSyncFn = deps.spawnSync ?? spawnSync;
|
|
88
|
+
|
|
89
|
+
if (env.AUTOPG_SKIP_POSTINSTALL === '1') {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (isDevWorktreeFn(pkgRoot)) {
|
|
93
|
+
stderr.write(
|
|
94
|
+
`[autopg-postinstall] dev worktree detected at ${pkgRoot} — skipping upgrade.\n` +
|
|
95
|
+
'[autopg-postinstall] Set AUTOPG_SKIP_POSTINSTALL=1 to silence this notice.\n',
|
|
96
|
+
);
|
|
26
97
|
return;
|
|
27
98
|
}
|
|
28
|
-
const dataDir = path.join(
|
|
29
|
-
if (!
|
|
99
|
+
const dataDir = path.join(getAutopgRootFn(), 'data');
|
|
100
|
+
if (!fsApi.existsSync(dataDir)) {
|
|
30
101
|
// Fresh install — nothing to upgrade
|
|
31
102
|
return;
|
|
32
103
|
}
|
|
33
104
|
// Locate own CLI entry — script is run from the package dir at install time
|
|
34
|
-
const cliEntry = path.join(
|
|
35
|
-
if (!
|
|
36
|
-
|
|
105
|
+
const cliEntry = path.join(pkgRoot, 'bin', 'pgserve-wrapper.cjs');
|
|
106
|
+
if (!fsApi.existsSync(cliEntry)) {
|
|
107
|
+
stderr.write(`[autopg-postinstall] wrapper not found at ${cliEntry}, skipping\n`);
|
|
37
108
|
return;
|
|
38
109
|
}
|
|
39
|
-
|
|
110
|
+
if (!isCIFn()) {
|
|
111
|
+
stderr.write(
|
|
112
|
+
`[autopg-postinstall] About to run \`autopg upgrade --quiet\` against ${dataDir}.\n` +
|
|
113
|
+
'[autopg-postinstall] Set AUTOPG_SKIP_POSTINSTALL=1 in the environment to skip (recommended for dev worktrees).\n',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const result = spawnSyncFn(process.execPath, [cliEntry, 'upgrade', '--quiet'], {
|
|
40
117
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
41
118
|
timeout: 60_000,
|
|
42
119
|
});
|
|
43
120
|
if (result.error) {
|
|
44
|
-
|
|
45
|
-
|
|
121
|
+
stderr.write(`[autopg-postinstall] WARNING: upgrade invocation failed: ${result.error.message}\n`);
|
|
122
|
+
stderr.write('[autopg-postinstall] Run `autopg upgrade` manually to retry.\n');
|
|
46
123
|
return;
|
|
47
124
|
}
|
|
48
125
|
if (result.status !== 0) {
|
|
49
|
-
|
|
50
|
-
|
|
126
|
+
stderr.write(`[autopg-postinstall] WARNING: \`autopg upgrade\` exited ${result.status}\n`);
|
|
127
|
+
stderr.write('[autopg-postinstall] Run `autopg upgrade` manually to investigate.\n');
|
|
51
128
|
}
|
|
52
129
|
}
|
|
53
130
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
131
|
+
// Test surface: postinstall.test.js exercises isDevWorktree + isCI in
|
|
132
|
+
// isolation (pure path + env reads, no shellouts). require() this file
|
|
133
|
+
// from a test to inspect the helpers without the side-effect of running
|
|
134
|
+
// main(); main() is only invoked when this is the entry point.
|
|
135
|
+
module.exports = { isDevWorktree, isCI, getAutopgRoot, main };
|
|
59
136
|
|
|
60
|
-
|
|
137
|
+
if (require.main === module) {
|
|
138
|
+
try {
|
|
139
|
+
main();
|
|
140
|
+
} catch (err) {
|
|
141
|
+
process.stderr.write(`[autopg-postinstall] WARNING: unexpected error: ${err.message}\n`);
|
|
142
|
+
}
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# verify-published-artifacts.sh — Group 8 of autopg-distribution-cutover.
|
|
4
|
+
#
|
|
5
|
+
# Validates that every autopg tarball in the given directory is:
|
|
6
|
+
# 1. accompanied by its outer .sha256 and that the hash matches.
|
|
7
|
+
# 2. cosign-signed (verifies against keys/cosign.pub or AUTOPG_COSIGN_PUB).
|
|
8
|
+
# 3. accompanied by a SLSA L3 in-toto provenance attestation that
|
|
9
|
+
# slsa-verifier can verify against the source repo URI.
|
|
10
|
+
# 4. listed in the aggregated manifest.json with matching metadata.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# bash scripts/verify-published-artifacts.sh dist/
|
|
14
|
+
# bash scripts/verify-published-artifacts.sh dist/ --skip-slsa
|
|
15
|
+
# AUTOPG_COSIGN_PUB=tests/fixtures/cosign/cosign.pub \
|
|
16
|
+
# bash scripts/verify-published-artifacts.sh dist/
|
|
17
|
+
#
|
|
18
|
+
# Exit codes:
|
|
19
|
+
# 0 all artifacts verified
|
|
20
|
+
# 1 verification failure (any tarball fails any check)
|
|
21
|
+
# 2 invalid args / missing inputs
|
|
22
|
+
#
|
|
23
|
+
# Group 8 acceptance criteria:
|
|
24
|
+
# - cosign verify-blob succeeds for every platform tarball
|
|
25
|
+
# - slsa-verifier verify-artifact succeeds for every platform tarball
|
|
26
|
+
# - tampered tarball fails both verifications
|
|
27
|
+
# - script exits non-zero if any tarball ships without sig + provenance
|
|
28
|
+
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
|
|
31
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
32
|
+
COSIGN_PUB_DEFAULT="${REPO_ROOT}/keys/cosign.pub"
|
|
33
|
+
COSIGN_PUB="${AUTOPG_COSIGN_PUB:-${COSIGN_PUB_DEFAULT}}"
|
|
34
|
+
SOURCE_URI_DEFAULT="github.com/automagik-dev/autopg"
|
|
35
|
+
SOURCE_URI="${AUTOPG_SOURCE_URI:-${SOURCE_URI_DEFAULT}}"
|
|
36
|
+
|
|
37
|
+
PASS=0
|
|
38
|
+
FAIL=0
|
|
39
|
+
SKIP_SLSA=0
|
|
40
|
+
SKIP_MANIFEST=0
|
|
41
|
+
|
|
42
|
+
ok() { printf ' \xe2\x9c\x93 %s\n' "$*"; PASS=$((PASS + 1)); }
|
|
43
|
+
bad() { printf ' \xe2\x9c\x97 %s\n' "$*" >&2; FAIL=$((FAIL + 1)); }
|
|
44
|
+
note() { printf ' \xe2\x80\xa2 %s\n' "$*" >&2; }
|
|
45
|
+
|
|
46
|
+
usage() {
|
|
47
|
+
cat <<EOF
|
|
48
|
+
Usage: $0 <dist-dir> [--skip-slsa] [--skip-manifest]
|
|
49
|
+
|
|
50
|
+
Verifies every autopg-*.tar.gz in <dist-dir> using:
|
|
51
|
+
- keys/cosign.pub (override with AUTOPG_COSIGN_PUB=<path>)
|
|
52
|
+
- source URI ${SOURCE_URI_DEFAULT}
|
|
53
|
+
(override with AUTOPG_SOURCE_URI=<uri>)
|
|
54
|
+
|
|
55
|
+
Required siblings per tarball:
|
|
56
|
+
<tarball>.sha256
|
|
57
|
+
<tarball>.sig
|
|
58
|
+
<tarball>.intoto.jsonl (skip with --skip-slsa)
|
|
59
|
+
|
|
60
|
+
Optional:
|
|
61
|
+
manifest.json (skip with --skip-manifest)
|
|
62
|
+
EOF
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
parse_args() {
|
|
66
|
+
if [[ $# -lt 1 ]]; then usage; exit 2; fi
|
|
67
|
+
DIST_DIR="$1"; shift
|
|
68
|
+
while [[ $# -gt 0 ]]; do
|
|
69
|
+
case "$1" in
|
|
70
|
+
--skip-slsa) SKIP_SLSA=1; shift ;;
|
|
71
|
+
--skip-manifest) SKIP_MANIFEST=1; shift ;;
|
|
72
|
+
-h|--help) usage; exit 0 ;;
|
|
73
|
+
*) echo "unknown arg: $1" >&2; usage; exit 2 ;;
|
|
74
|
+
esac
|
|
75
|
+
done
|
|
76
|
+
if [[ ! -d "$DIST_DIR" ]]; then
|
|
77
|
+
echo "error: dist dir not found: $DIST_DIR" >&2; exit 2
|
|
78
|
+
fi
|
|
79
|
+
if [[ ! -f "$COSIGN_PUB" ]]; then
|
|
80
|
+
echo "error: cosign public key not found: $COSIGN_PUB" >&2; exit 2
|
|
81
|
+
fi
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
require_tools() {
|
|
85
|
+
command -v cosign >/dev/null 2>&1 || {
|
|
86
|
+
echo "error: cosign not on PATH (install: https://docs.sigstore.dev/cosign/installation/)" >&2
|
|
87
|
+
exit 2
|
|
88
|
+
}
|
|
89
|
+
if [[ "$SKIP_SLSA" -eq 0 ]]; then
|
|
90
|
+
if ! command -v slsa-verifier >/dev/null 2>&1; then
|
|
91
|
+
note "slsa-verifier not on PATH — provenance check will be skipped (pass --skip-slsa to silence)"
|
|
92
|
+
SKIP_SLSA=1
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Portable SHA256 — sha256sum on linux, shasum -a 256 on macOS.
|
|
98
|
+
sha256_of() {
|
|
99
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
100
|
+
sha256sum "$1" | awk '{print $1}'
|
|
101
|
+
else
|
|
102
|
+
shasum -a 256 "$1" | awk '{print $1}'
|
|
103
|
+
fi
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
verify_outer_sha() {
|
|
107
|
+
local tarball="$1"
|
|
108
|
+
local sha_file="${tarball}.sha256"
|
|
109
|
+
if [[ ! -f "$sha_file" ]]; then
|
|
110
|
+
bad "$(basename "$tarball"): missing .sha256 sibling"
|
|
111
|
+
return 1
|
|
112
|
+
fi
|
|
113
|
+
local recorded actual
|
|
114
|
+
recorded=$(awk '{print $1}' "$sha_file")
|
|
115
|
+
actual=$(sha256_of "$tarball")
|
|
116
|
+
if [[ "$recorded" != "$actual" ]]; then
|
|
117
|
+
bad "$(basename "$tarball"): sha256 mismatch (recorded=$recorded actual=$actual)"
|
|
118
|
+
return 1
|
|
119
|
+
fi
|
|
120
|
+
ok "$(basename "$tarball"): sha256 matches recorded"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
verify_cosign() {
|
|
124
|
+
local tarball="$1"
|
|
125
|
+
local sig="${tarball}.sig"
|
|
126
|
+
if [[ ! -f "$sig" ]]; then
|
|
127
|
+
bad "$(basename "$tarball"): missing .sig sibling"
|
|
128
|
+
return 1
|
|
129
|
+
fi
|
|
130
|
+
if cosign verify-blob \
|
|
131
|
+
--key "$COSIGN_PUB" \
|
|
132
|
+
--signature "$sig" \
|
|
133
|
+
"$tarball" >/dev/null 2>&1; then
|
|
134
|
+
ok "$(basename "$tarball"): cosign signature verifies"
|
|
135
|
+
else
|
|
136
|
+
bad "$(basename "$tarball"): cosign verify-blob FAILED"
|
|
137
|
+
return 1
|
|
138
|
+
fi
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
verify_slsa() {
|
|
142
|
+
local tarball="$1"
|
|
143
|
+
local prov="${tarball}.intoto.jsonl"
|
|
144
|
+
if [[ ! -f "$prov" ]]; then
|
|
145
|
+
bad "$(basename "$tarball"): missing .intoto.jsonl provenance"
|
|
146
|
+
return 1
|
|
147
|
+
fi
|
|
148
|
+
if slsa-verifier verify-artifact \
|
|
149
|
+
"$tarball" \
|
|
150
|
+
--provenance-path "$prov" \
|
|
151
|
+
--source-uri "$SOURCE_URI" >/dev/null 2>&1; then
|
|
152
|
+
ok "$(basename "$tarball"): SLSA provenance verifies"
|
|
153
|
+
else
|
|
154
|
+
bad "$(basename "$tarball"): slsa-verifier FAILED"
|
|
155
|
+
return 1
|
|
156
|
+
fi
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
verify_manifest_entry() {
|
|
160
|
+
local tarball="$1" manifest="$2"
|
|
161
|
+
local base
|
|
162
|
+
base=$(basename "$tarball")
|
|
163
|
+
if ! grep -q "\"file\": \"${base}\"" "$manifest"; then
|
|
164
|
+
bad "$(basename "$tarball"): not listed in manifest.json"
|
|
165
|
+
return 1
|
|
166
|
+
fi
|
|
167
|
+
ok "$(basename "$tarball"): manifest.json entry present"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
main() {
|
|
171
|
+
parse_args "$@"
|
|
172
|
+
require_tools
|
|
173
|
+
|
|
174
|
+
echo "==> verify-published-artifacts: $DIST_DIR"
|
|
175
|
+
echo " cosign pub: $COSIGN_PUB"
|
|
176
|
+
echo " source uri: $SOURCE_URI"
|
|
177
|
+
|
|
178
|
+
local tarballs=()
|
|
179
|
+
while IFS= read -r line; do
|
|
180
|
+
tarballs+=("$line")
|
|
181
|
+
done < <(find "$DIST_DIR" -maxdepth 1 -name 'autopg-*.tar.gz' -type f | LC_ALL=C sort)
|
|
182
|
+
|
|
183
|
+
if [[ ${#tarballs[@]} -eq 0 ]]; then
|
|
184
|
+
bad "no autopg-*.tar.gz files in $DIST_DIR"
|
|
185
|
+
echo "==> result: FAIL (no inputs)"
|
|
186
|
+
exit 1
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
local manifest="${DIST_DIR}/manifest.json"
|
|
190
|
+
if [[ "$SKIP_MANIFEST" -eq 0 && ! -f "$manifest" ]]; then
|
|
191
|
+
bad "manifest.json missing in $DIST_DIR (pass --skip-manifest to ignore)"
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
for tarball in "${tarballs[@]}"; do
|
|
195
|
+
echo " -- $(basename "$tarball")"
|
|
196
|
+
verify_outer_sha "$tarball" || true
|
|
197
|
+
verify_cosign "$tarball" || true
|
|
198
|
+
if [[ "$SKIP_SLSA" -eq 0 ]]; then
|
|
199
|
+
verify_slsa "$tarball" || true
|
|
200
|
+
fi
|
|
201
|
+
if [[ "$SKIP_MANIFEST" -eq 0 && -f "$manifest" ]]; then
|
|
202
|
+
verify_manifest_entry "$tarball" "$manifest" || true
|
|
203
|
+
fi
|
|
204
|
+
done
|
|
205
|
+
|
|
206
|
+
echo
|
|
207
|
+
echo "==> result: pass=${PASS} fail=${FAIL}"
|
|
208
|
+
if [[ "$FAIL" -gt 0 ]]; then exit 1; fi
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
main "$@"
|