pgserve 2.5.0 → 2.6.1

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.
@@ -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 "$@"
@@ -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
- function main() {
25
- if (process.env.AUTOPG_SKIP_POSTINSTALL === '1') {
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(getAutopgRoot(), 'data');
29
- if (!fs.existsSync(dataDir)) {
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(__dirname, '..', 'bin', 'pgserve-wrapper.cjs');
35
- if (!fs.existsSync(cliEntry)) {
36
- process.stderr.write(`[autopg-postinstall] wrapper not found at ${cliEntry}, skipping\n`);
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
- const result = spawnSync(process.execPath, [cliEntry, 'upgrade', '--quiet'], {
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
- process.stderr.write(`[autopg-postinstall] WARNING: upgrade invocation failed: ${result.error.message}\n`);
45
- process.stderr.write('[autopg-postinstall] Run `autopg upgrade` manually to retry.\n');
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
- process.stderr.write(`[autopg-postinstall] WARNING: \`autopg upgrade\` exited ${result.status}\n`);
50
- process.stderr.write('[autopg-postinstall] Run `autopg upgrade` manually to investigate.\n');
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
- try {
55
- main();
56
- } catch (err) {
57
- process.stderr.write(`[autopg-postinstall] WARNING: unexpected error: ${err.message}\n`);
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
- process.exit(0);
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 "$@"