Skip to content

feat(release): cutover G7 + G8 + G9 (GitHub Releases pivot) — bun-build + cosign sign + gh release publish#84

Merged
namastex888 merged 5 commits into
mainfrom
feat/cutover-extract-unique-work
May 8, 2026
Merged

feat(release): cutover G7 + G8 + G9 (GitHub Releases pivot) — bun-build + cosign sign + gh release publish#84
namastex888 merged 5 commits into
mainfrom
feat/cutover-extract-unique-work

Conversation

@namastex888

@namastex888 namastex888 commented May 8, 2026

Copy link
Copy Markdown
Contributor

Cutover wish G7 + G8 surgical extractions, with G9 pivoted to GitHub Releases per Felipe's directive (2026-05-08): drop cdn.automagik.dev infrastructure, use the free GitHub Releases pipeline that already integrates with Sigstore Rekor.

Distribution model — GitHub Releases

Aspect Cutover G9 (dropped) This PR
Storage cdn.automagik.dev/autopg/<channel>/<v>/<plat>/ github.com/namastexlabs/pgserve/releases/download/v<v>/
Cost bucket + CloudFront + DNS $0
Maintenance bucket policy, cache headers, DNS none
Signature verification cosign verify-blob against repo pub key gh attestation verify (built-in, reads Sigstore Rekor)
Channel pointer latest.json w/ cache gymnastics .well-known/latest.json committed to repo
CDN DIY CloudFront GitHub's Fastly (free unlimited bandwidth on public repos)
Immutability bucket versioning + locked policies git tag immutability (native)

What ships

G7 — bun-build static binaries (5 platforms)

  • scripts/build-binary.shbun build --compile for 5 platforms
  • scripts/fetch-postgres-bins.sh — stages PG binaries
  • scripts/assemble-tarball.sh — packs per-platform release tarball
  • tests/integration/tarball-smoke.sh — fixture
  • .github/workflows/build-tarballs.yml — CI matrix

G8 — cosign keyless OIDC sign + SLSA L3 attestation

  • .github/workflows/sign-attest.yml — per-platform cosign sign-blob + actions/attest-build-provenance@v1
  • tests/integration/sign-attest-smoke.sh — hermetic smoke fixture (no real Sigstore)

G9 — Replaced with GitHub Releases pipeline (~30 LOC vs 191)

  • .github/workflows/release-publish.ymlgh release create + gh release upload, draft/prerelease channel support, commits .well-known/latest.json for install.sh consumers

CI red — fix in this PR

Initial commit referenced 3 CDN-specific scripts (aggregate-manifest.sh, verify-published-artifacts.sh, scripts/cdn-publish.sh) that were never copied. The pivot makes those references unnecessary; CI should now pass.

Validation

  • bash -n on all 4 scripts → syntax clean
  • YAML lint on all 3 workflows → clean
  • bun run lint → clean
  • bun run lint:audit → 32 files scanned, 0 issues
  • bun run deadcode → clean (added gh to ignoreBinaries)

Cohort

After this lands:

  • ✅ G6 (audit emitter, PR feat(audit): op-keyed audit emitter + redaction lint (cutover G6 extraction) #83)
  • ✅ G7+G8+G9 (this PR)
  • G5autopg create-app + manifest LOCK 1 (structural entanglement; needs proper engineering effort)
  • G10install.sh, when revived, fetches from github.com/.../releases/download/.... Collides with main's existing 123-line legacy installer; needs rename decision.

Provisioning prerequisites

None for GitHub Releases (already provisioned by GitHub). The cosign workflow uses keyless OIDC — no key management required.

Summary by CodeRabbit

  • New Features

    • Automated tarball building and packaging for 5 target platforms
    • Automated release publishing to GitHub Releases with configurable channels (stable, beta, canary)
    • Artifact signing and cryptographic verification with cosign
    • SLSA provenance attestation for release artifacts
  • Tests

    • Comprehensive integration tests for tarball assembly and integrity verification

…(cutover G7 extraction)

Surgical port of cutover wish G7 from origin/wish/autopg-cutover-transport-absorb
onto main. All target paths conflict-free (none existed on main); files copied
verbatim from cutover branch.

What ships:
- scripts/build-binary.sh: bun build --compile static binaries for 5 platforms
  (linux-x64-glibc, linux-x64-musl, linux-arm64, darwin-x64, darwin-arm64).
  Fallback to pkg/nexe via AUTOPG_BUILD_FALLBACK=1 per wish G7 contract.
- scripts/fetch-postgres-bins.sh: stages PostgreSQL server binaries per
  platform under dist/<platform>/autopg/postgres/{bin,share}/ for self-
  contained release tarballs.
- scripts/assemble-tarball.sh: packs binary + bundled postgres + LICENSE etc
  into a single per-platform tarball.
- tests/integration/tarball-smoke.sh: smoke test fixture validating tarball
  structure + autopg --version exit code.
- .github/workflows/build-tarballs.yml: CI matrix wiring.

Validation:
- bash -n on all 4 scripts: syntax clean.
- bun run lint: clean.
- bun run lint:audit: scanned 32 files, 0 issues.

Cohort: cutover wish unique extractions onto main (post-v2.5.0). Follows
G6 (PR #83 merged). Sets up release infrastructure for G8 (cosign sign +
SLSA L3), G9 (CDN publish), G10 (install.sh) follow-on PRs.

Note: G5 (autopg create-app + manifest LOCK 1) deferred — has structural
entanglement with admin-bootstrap.js + autopg_meta schema infrastructure
that doesn't exist on main; needs proper engineering effort, not file copy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Rate limit exceeded

@namastex888 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 25 minutes and 34 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a99dff9d-6c85-4694-aa33-ae40a8288a79

📥 Commits

Reviewing files that changed from the base of the PR and between 101f3bc and 3a0a818.

📒 Files selected for processing (3)
  • scripts/assemble-tarball.sh
  • scripts/build-binary.sh
  • scripts/fetch-postgres-bins.sh
📝 Walkthrough

Walkthrough

Adds a complete multi-platform build-and-release pipeline for autopg: compile binaries (with fallbacks), stage Postgres runtimes, create deterministic tarballs with per-file manifests and outer checksums, sign and attest artifacts, aggregate manifests, verify published artifacts, and CI workflows plus smoke tests.

Changes

Tarball Assembly & Distribution Pipeline

Layer / File(s) Summary
Tarball Assembly & Manifest Generation
scripts/assemble-tarball.sh
Assembles deterministic gzip tarballs from staged autopg/ trees, emits autopg/manifest.json with per-file SHA256 and sizes, enforces executables, and writes outer .sha256.
Binary Compilation with Fallback
scripts/build-binary.sh
Builds static autopg binaries for five platforms using bun build --compile; on failure (and when AUTOPG_BUILD_FALLBACK=1) retries with pkg then nexe; records results to dist/build-record.tsv and dist/build.log.
PostgreSQL Dependency Staging
scripts/fetch-postgres-bins.sh
Stages Postgres bin and share for each platform from (1) AUTOPG_POSTGRES_LOCAL_DIR, (2) @embedded-postgres npm package (version fallback to package.json optionalDependencies), or (3) a URL template; verifies bin/postgres.
Tarball Integration Testing
tests/integration/tarball-smoke.sh
Fixture and real-mode smoke tests that validate tarball existence, outer .sha256, extracted members, manifest.json fields and file SHA256 entries, and expected binary version outputs.
CI: Build Tarballs Workflow
.github/workflows/build-tarballs.yml
PR fixture jobs run lint + fixture smoke tests; non-PR matrix builds run binary build, postgres staging, tarball assembly, real smoke test, tarball size sanity check (30–120MB), and upload per-platform artifacts (tarball, .sha256, logs).
CI: Sign & Attest Workflow
.github/workflows/sign-attest.yml
Downloads per-platform artifacts, verifies SHA256, signs tarballs with cosign producing .sig and .intoto.jsonl provenance, self-verifies signatures, and aggregates signed bundles for manifest generation.
Aggregate Manifest
scripts/aggregate-manifest.sh
Generates dist/manifest.json listing per-platform tarball filename, resolved URL, SHA256 (from sibling .sha256), size, and optional signature/provenance URLs; resolves cosign_pub_url.
Verify Published Artifacts
scripts/verify-published-artifacts.sh
Verifies top-level autopg-*.tar.gz artifacts in dist/: checks sibling .sha256, verifies .sig via cosign, optionally verifies .intoto.jsonl via slsa-verifier, and ensures presence in manifest.json.
Sign-Attest Integration Smoke Tests
tests/integration/sign-attest-smoke.sh
Stages synthetic tarballs, signs them with fixture keys, self-verifies, generates aggregated manifest, and exercises verification in happy/tamper/missing-signature scenarios.
Release Publish Workflow
.github/workflows/release-publish.yml
Creates/updates GitHub Release v{VERSION}, uploads dist/* assets (tarballs, sigs, provenance, checksums), and optionally updates .well-known/latest.json for stable non-draft releases.
Cosign Test Fixtures
tests/fixtures/cosign/*
Adds fixture cosign.key, cosign.pub, and README for offline test signing/verifying (includes fixture password).
Misc config
knip.json
Adds gh to ignoreBinaries.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through code and shell, so spry,

Five platforms baked beneath the sky.

I stitched the manifest, sealed each tarball tight,

Signed with a key, then danced into the night.

CI hummed — the release takes flight! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main multi-phase cutover work: bun-based binary builds (G7), cosign signing and attestation (G8), and GitHub Releases publishing pivot (G9).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cutover-extract-unique-work

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a suite of Bash scripts and integration tests to automate the build, staging, and assembly of platform-specific autopg distribution tarballs. The scripts handle static binary compilation with fallbacks, PostgreSQL binary retrieval, and deterministic tarball creation. The review feedback highlights a recurring issue where error handling is bypassed because functions are called within OR lists in the main entry points, suppressing the set -e behavior. Actionable suggestions were provided to explicitly handle failures in these critical steps to prevent the production of broken artifacts. Additionally, it was recommended to escape file paths in the manifest generation logic to ensure valid JSON output.

Comment thread scripts/assemble-tarball.sh Outdated
fi

echo "==> [${platform}] assemble tarball"
verify_inputs "$stage" "$platform"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since assemble_one is invoked as part of an OR list (|| rc=$?) in the main function, the shell's set -e (exit on error) behavior is suppressed during its execution. Consequently, if verify_inputs fails and returns a non-zero status, the script will continue to execute the subsequent steps (like generating the manifest and rolling the tarball) instead of stopping. You should explicitly handle the failure to prevent producing broken artifacts.

Suggested change
verify_inputs "$stage" "$platform"
verify_inputs "$stage" "$platform" || return 1

Comment thread scripts/assemble-tarball.sh Outdated

# 1) emit per-file manifest BEFORE the tarball is rolled — manifest is
# bundled inside.
emit_manifest "$stage" "$platform" "${stage}/autopg/manifest.json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the verify_inputs call, any failure in emit_manifest will be ignored due to the suppression of set -e in this context. This could result in a tarball being created with an incomplete or missing manifest.json.

Suggested change
emit_manifest "$stage" "$platform" "${stage}/autopg/manifest.json"
emit_manifest "$stage" "$platform" "${stage}/autopg/manifest.json" || return 1

Comment thread scripts/assemble-tarball.sh Outdated
tar_flags+=(--owner=0 --group=0 --numeric-owner)
fi

tar -C "$stage" -czf "$tarball" "${tar_flags[@]}" autopg/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The tar command's failure will not be caught by set -e because assemble_one is called in a conditional context in main. If tar fails, the script will proceed to calculate the SHA256 of a potentially non-existent or corrupted file.

Suggested change
tar -C "$stage" -czf "$tarball" "${tar_flags[@]}" autopg/
tar -C "$stage" -czf "$tarball" "${tar_flags[@]}" autopg/ || return 1

Comment thread scripts/build-binary.sh Outdated
}

local out_dir="${DIST_DIR}/${platform}/autopg"
mkdir -p "$out_dir"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Because build_one is called within an OR list in main, set -e is disabled. If mkdir -p fails (e.g., due to permissions), the script will continue to attempt the bun build step instead of returning an error immediately.

Suggested change
mkdir -p "$out_dir"
mkdir -p "$out_dir" || return 1

Comment thread scripts/fetch-postgres-bins.sh Outdated
echo "==> [${platform}] fetch postgres bins"

if [[ -n "${AUTOPG_POSTGRES_LOCAL_DIR:-}" ]]; then
stage_from_local "$AUTOPG_POSTGRES_LOCAL_DIR" "$out_dir"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since fetch_one is called with || rc=$? in main, set -e is suppressed. If stage_from_local fails, the function will continue to the final validation checks at the end of fetch_one rather than exiting early. This could lead to inconsistent states if the validation logic is incomplete.

Suggested change
stage_from_local "$AUTOPG_POSTGRES_LOCAL_DIR" "$out_dir"
stage_from_local "$AUTOPG_POSTGRES_LOCAL_DIR" "$out_dir" || return 1

Comment thread scripts/fetch-postgres-bins.sh Outdated
local pkg
pkg="$(embedded_pkg_for "$platform")" || true
if [[ -n "$pkg" ]]; then
stage_from_pkg "$pkg" "$PG_VERSION" "$out_dir"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Explicit error handling is required here because set -e is disabled when fetch_one is called as part of an OR list.

Suggested change
stage_from_pkg "$pkg" "$PG_VERSION" "$out_dir"
stage_from_pkg "$pkg" "$PG_VERSION" "$out_dir" || return 1

Comment thread scripts/fetch-postgres-bins.sh Outdated
if [[ -n "$pkg" ]]; then
stage_from_pkg "$pkg" "$PG_VERSION" "$out_dir"
elif [[ -n "${AUTOPG_POSTGRES_URL_TEMPLATE:-}" ]]; then
stage_from_url "$AUTOPG_POSTGRES_URL_TEMPLATE" "$PG_VERSION" "$platform" "$out_dir"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Explicit error handling is required here because set -e is disabled when fetch_one is called as part of an OR list.

Suggested change
stage_from_url "$AUTOPG_POSTGRES_URL_TEMPLATE" "$PG_VERSION" "$platform" "$out_dir"
stage_from_url "$AUTOPG_POSTGRES_URL_TEMPLATE" "$PG_VERSION" "$platform" "$out_dir" || return 1

else
printf ',\n'
fi
printf ' { "path": "%s", "sha256": "%s", "size": %d }' "$f" "$h" "$sz"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The file path $f is inserted directly into the JSON manifest without escaping. If a filename contains a double quote or a backslash, the resulting manifest.json will be syntactically invalid. While the current set of binaries and PostgreSQL files is unlikely to have such characters, it is a best practice to ensure the generated JSON is valid.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f5039dc19c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/build-tarballs.yml Outdated
Comment on lines +90 to +92
scripts/aggregate-manifest.sh \
scripts/verify-published-artifacts.sh \
scripts/cdn-publish.sh \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove non-existent scripts from smoke lint step

This shellcheck invocation includes files that do not exist in this commit (scripts/aggregate-manifest.sh, scripts/verify-published-artifacts.sh, scripts/cdn-publish.sh, and additional integration scripts), so the smoke job exits non-zero before running the actual tarball fixture checks. As written, PR runs of this workflow will fail whenever this job is triggered unless those paths are added or removed from the command.

Useful? React with 👍 / 👎.

echo " -> source: npm @embedded-postgres/${pkg}@${version}"
local scratch
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' RETURN

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid RETURN trap on local scratch variable

This cleanup trap references a function-local variable under set -u, which produces scratch: unbound variable on failure paths and masks the original fetch error (reproducible by forcing an invalid AUTOPG_POSTGRES_PKG_VERSION). The same pattern appears again in stage_from_url, so cleanup should be scoped or unset explicitly to keep error handling deterministic.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
scripts/build-binary.sh (1)

113-113: 💤 Low value

-eq 1 on env-string can hard-fail under set -e.

AUTOPG_BUILD_FALLBACK is documented as "Set to 1 to retry…", but if a user sets it to true/yes, [[ "true" -eq 1 ]] triggers a bash error and set -euo pipefail aborts before the fallback even runs. Prefer string equality or accept any non-empty/non-zero value.

♻️ Suggested tweak
-  if [[ "$FALLBACK_ENABLED" -eq 1 ]]; then
+  if [[ "$FALLBACK_ENABLED" == "1" || "$FALLBACK_ENABLED" == "true" ]]; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/build-binary.sh` at line 113, Replace the numeric comparison against
"$FALLBACK_ENABLED" in the if-condition (the line with if [[ "$FALLBACK_ENABLED"
-eq 1 ]];) with a safe string check that accepts common truthy values; e.g. test
for non-empty and not "0" or match against "1|true|yes" (case-insensitive) so
AUTOPG_BUILD_FALLBACK can be "1", "true", or "yes" without triggering a bash
arithmetic error under set -euo pipefail; update the condition around
FALLBACK_ENABLED accordingly (keep the same block guarded by that if).
scripts/assemble-tarball.sh (1)

96-115: 💤 Low value

Manifest JSON is built by string concatenation; consider hardening with jq (or escape paths).

printf '... "path": "%s"' "$f" produces invalid JSON if $f ever contains ", \, or a control character. Today the inputs are tightly controlled (autopg + postgres bin/share), so the practical risk is low. If you'd like to make the manifest fully robust to whatever upstream postgres tarballs ship, prefer piping through jq -R -s or building entries with jq -n.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/assemble-tarball.sh` around lines 96 - 115, The manifest assembly
currently uses printf with unescaped "%s" for "path" (inside the loop over
variable f) which can produce invalid JSON for filenames containing
quotes/backslashes/control chars; replace the manual string-concatenation in the
loop (the block that uses h=$(sha256_of "$f"), sz=..., the first flag, and the
printf that emits '{ "path": "%s", "sha256": "%s", "size": %d }') with a safe
JSON builder: produce one JSON object per file using jq (e.g. jq -n --arg path
"$f" --arg sha256 "$h" --argjson size "$sz"
'{path:$path,sha256:$sha256,size:$size}') or accumulate the records into jq -s
to emit a compressed array, ensuring you still sort with find | sort and write
the final array to "$out"; keep the sha256_of, size detection, and the overall
surrounding redirection to > "$out" intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/build-tarballs.yml:
- Around line 213-222: The step name "Verify size band (50–80MB)" and the
enforced bounds (the SIZE_MB check using the if condition comparing to 30 and
120) are inconsistent; choose one corrective action: either update the step name
to reflect the actual sanity band "Verify size band (30–120MB)" or tighten the
numeric checks in the if condition to enforce 50–80 (change the lower/upper
bounds used with SIZE_MB). Locate the workflow step named "Verify size band
(50–80MB)" and modify either the human-readable name string or the numeric
comparisons that reference SIZE_MB so they match.
- Around line 152-155: Update the deprecated runner for the darwin-x64 entry:
replace the runner value "macos-13" used with the platform identifier
"darwin-x64" with a currently supported Intel macOS image such as
"macos-15-intel" or "macos-26-intel" (or "macos-latest" if you accept
macOS-26/ARM compatibility), ensuring the "platform: darwin-x64" line remains
unchanged and only the "runner: macos-13" token is updated.
- Around line 170-211: The Resolve version step currently injects
user-controlled values directly into run: shells (via ${{ inputs.version }} and
${{ steps.ver.outputs.version }}) which can lead to command injection; change
the Resolve version step to export the resolved VERSION into the job environment
(use env: to set VERSION from the step output) and update downstream steps
(Build autopg binary, Fetch postgres bins, Assemble tarball, Smoke real tarball)
to reference the environment variable (e.g. $VERSION) instead of embedding ${{ …
}} in the run scripts; ensure the step id "ver" still sets an output and that
all occurrences of --version ${{ steps.ver.outputs.version }} are replaced by
--version $VERSION so the value is passed via env and not interpolated into the
run block.
- Around line 84-95: The workflow fails because shellcheck lists scripts that
aren’t present and two jobs reference missing scripts, and the darwin runner
uses the retired macos-13; fix by removing or gating the missing-script work:
update the shellcheck invocation (the multi-line run under "Lint scripts") to
only include the five scripts added in this PR or wrap it with an if:
hashFiles(...) guard so it no-ops until other scripts exist, and remove or gate
the sign-attest-smoke and cdn-publish-smoke jobs (the jobs named
sign-attest-smoke and cdn-publish-smoke) so they don’t run until their scripts
land; finally change the runner label from macos-13 to macos-15 (replace
macos-13 with macos-15 in the job that builds darwin-x64).

In `@scripts/fetch-postgres-bins.sh`:
- Around line 195-196: The branch that calls stage_from_url with
AUTOPG_POSTGRES_URL_TEMPLATE currently hardcodes "16" for the version, which can
produce wrong/download-failing artifacts; change that call to derive the version
from the resolved PG_VERSION (use PG_VERSION if available) or from
AUTOPG_POSTGRES_URL_VERSION, and if necessary update the fallback to the
pipeline's current default (e.g., 18.3.0-beta.17) so
stage_from_url("$AUTOPG_POSTGRES_URL_TEMPLATE", "<resolved-version>",
"$platform", "$out_dir") always receives the correct/consistent version; refer
to AUTOPG_POSTGRES_URL_TEMPLATE, AUTOPG_POSTGRES_URL_VERSION, stage_from_url and
PG_VERSION when making the change.

---

Nitpick comments:
In `@scripts/assemble-tarball.sh`:
- Around line 96-115: The manifest assembly currently uses printf with unescaped
"%s" for "path" (inside the loop over variable f) which can produce invalid JSON
for filenames containing quotes/backslashes/control chars; replace the manual
string-concatenation in the loop (the block that uses h=$(sha256_of "$f"),
sz=..., the first flag, and the printf that emits '{ "path": "%s", "sha256":
"%s", "size": %d }') with a safe JSON builder: produce one JSON object per file
using jq (e.g. jq -n --arg path "$f" --arg sha256 "$h" --argjson size "$sz"
'{path:$path,sha256:$sha256,size:$size}') or accumulate the records into jq -s
to emit a compressed array, ensuring you still sort with find | sort and write
the final array to "$out"; keep the sha256_of, size detection, and the overall
surrounding redirection to > "$out" intact.

In `@scripts/build-binary.sh`:
- Line 113: Replace the numeric comparison against "$FALLBACK_ENABLED" in the
if-condition (the line with if [[ "$FALLBACK_ENABLED" -eq 1 ]];) with a safe
string check that accepts common truthy values; e.g. test for non-empty and not
"0" or match against "1|true|yes" (case-insensitive) so AUTOPG_BUILD_FALLBACK
can be "1", "true", or "yes" without triggering a bash arithmetic error under
set -euo pipefail; update the condition around FALLBACK_ENABLED accordingly
(keep the same block guarded by that if).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8d0d9237-f9bd-4236-9be1-d7f2dff34c12

📥 Commits

Reviewing files that changed from the base of the PR and between 4e9213d and f5039dc.

📒 Files selected for processing (5)
  • .github/workflows/build-tarballs.yml
  • scripts/assemble-tarball.sh
  • scripts/build-binary.sh
  • scripts/fetch-postgres-bins.sh
  • tests/integration/tarball-smoke.sh

Comment thread .github/workflows/build-tarballs.yml Outdated
Comment on lines +152 to +155
- platform: darwin-x64
runner: macos-13
- platform: darwin-arm64
runner: macos-latest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

GitHub Actions hosted runner macos-13 deprecation status 2026

💡 Result:

The GitHub Actions hosted runner image macos-13 (macOS Ventura, both x64 and arm64) is fully deprecated and unsupported as of December 2025. Deprecation began on September 22, 2025, with brownout periods in November 2025 to fail jobs using it, and full retirement by December 4-8, 2025 [1][2][3]. As of May 8, 2026, it is no longer available; the official GitHub Docs list current macOS runners as macos-15, macos-26 (arm64), and macos-15-intel/macOS-26-intel (x64), with no mention of macos-13 [4]. Projects continued migrating away in 2026 (e.g., April 2026) [5], and runner-images repo deprecated macOS 13 code by February 2026 [6]. Migrate workflows to macos-latest (macos-26 arm64), macos-15, macos-14 (until Nov 2026 [7]), or macos-15-intel for x64 needs [2].

Citations:


Update macos-13 runner to a currently supported macOS image.

macos-13 was fully deprecated and retired by GitHub on December 4-8, 2025, and as of May 2026 is no longer available. The darwin-x64 workflow entry using macos-13 will fail to schedule. Replace with macos-15-intel or macos-26-intel for x64 support, or macos-latest (macos-26) if arm64 compatibility is acceptable for the x64 entry.

🧰 Tools
🪛 actionlint (1.7.12)

[error] 153-153: label "macos-13" is unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025", "windows-2025-vs2026", "windows-2022", "windows-11-arm", "ubuntu-slim", "ubuntu-latest", "ubuntu-latest-4-cores", "ubuntu-latest-8-cores", "ubuntu-latest-16-cores", "ubuntu-24.04", "ubuntu-24.04-arm", "ubuntu-22.04", "ubuntu-22.04-arm", "macos-latest", "macos-latest-xlarge", "macos-latest-large", "macos-26-intel", "macos-26-xlarge", "macos-26-large", "macos-26", "macos-15-intel", "macos-15-xlarge", "macos-15-large", "macos-15", "macos-14-xlarge", "macos-14-large", "macos-14", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file

(runner-label)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-tarballs.yml around lines 152 - 155, Update the
deprecated runner for the darwin-x64 entry: replace the runner value "macos-13"
used with the platform identifier "darwin-x64" with a currently supported Intel
macOS image such as "macos-15-intel" or "macos-26-intel" (or "macos-latest" if
you accept macOS-26/ARM compatibility), ensuring the "platform: darwin-x64" line
remains unchanged and only the "runner: macos-13" token is updated.

Comment on lines +170 to +211
- name: Resolve version
id: ver
shell: bash
run: |
if [[ -n "${{ inputs.version }}" ]]; then
VERSION="${{ inputs.version }}"
else
VERSION=$(node -p "require('./package.json').version")
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Resolved version: ${VERSION}"

# Stage 1 — compile autopg.
# Fallback (pkg/nexe) is enabled per the wish G7 contract; bun is
# the primary, the fallback is documented in build.log.
- name: Build autopg binary
shell: bash
env:
AUTOPG_BUILD_FALLBACK: '1'
run: bash scripts/build-binary.sh --platform ${{ matrix.platform }} --version ${{ steps.ver.outputs.version }}

# Stage 2 — fetch postgres bins.
# @embedded-postgres covers linux-x64-glibc + darwin-{x64,arm64}.
# linux-x64-musl + linux-arm64 require AUTOPG_POSTGRES_URL_TEMPLATE
# (passed via repo secret in production) or AUTOPG_POSTGRES_LOCAL_DIR
# in a self-hosted runner cache. Failure here is loud, not silent.
- name: Fetch postgres bins
shell: bash
env:
AUTOPG_POSTGRES_PKG_VERSION: ${{ inputs.pg_pkg_version || '18.3.0-beta.17' }}
AUTOPG_POSTGRES_URL_TEMPLATE: ${{ vars.AUTOPG_POSTGRES_URL_TEMPLATE }}
run: bash scripts/fetch-postgres-bins.sh --platform ${{ matrix.platform }}

# Stage 3 — assemble the tarball + emit outer .sha256.
- name: Assemble tarball
shell: bash
run: bash scripts/assemble-tarball.sh --platform ${{ matrix.platform }} --version ${{ steps.ver.outputs.version }}

# Stage 4 — gate via the real-mode smoke test.
- name: Smoke real tarball
shell: bash
run: bash tests/integration/tarball-smoke.sh --real --platform ${{ matrix.platform }} --version ${{ steps.ver.outputs.version }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Major: GitHub Actions script injection — pass user-controlled inputs via env:, not ${{ }} interpolation.

inputs.version (workflow_dispatch) and the resolved steps.ver.outputs.version are templated directly into the shell of run: blocks at Lines 174–175, 189, 206, 211 (and 216, 229–230). A maliciously crafted dispatch input (e.g. 1.0.0"; curl …| sh; echo ") is then literally substituted into the script body. Workflow_dispatch is gated by repo permissions, but the pattern is a well-known GHA injection sink that linters/audit tooling will flag.

The standard mitigation is to bind ${{ … }} once into an env: and reference the env var in the shell:

🛡️ Suggested pattern for the version-resolve step
       - name: Resolve version
         id: ver
         shell: bash
+        env:
+          INPUT_VERSION: ${{ inputs.version }}
         run: |
-          if [[ -n "${{ inputs.version }}" ]]; then
-            VERSION="${{ inputs.version }}"
+          if [[ -n "${INPUT_VERSION:-}" ]]; then
+            VERSION="$INPUT_VERSION"
           else
             VERSION=$(node -p "require('./package.json').version")
           fi
           echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
           echo "Resolved version: ${VERSION}"

The downstream --version ${{ steps.ver.outputs.version }} usages are lower risk because the value has been captured into the output, but the same env-binding pattern keeps things consistent and lint-clean.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-tarballs.yml around lines 170 - 211, The Resolve
version step currently injects user-controlled values directly into run: shells
(via ${{ inputs.version }} and ${{ steps.ver.outputs.version }}) which can lead
to command injection; change the Resolve version step to export the resolved
VERSION into the job environment (use env: to set VERSION from the step output)
and update downstream steps (Build autopg binary, Fetch postgres bins, Assemble
tarball, Smoke real tarball) to reference the environment variable (e.g.
$VERSION) instead of embedding ${{ … }} in the run scripts; ensure the step id
"ver" still sets an output and that all occurrences of --version ${{
steps.ver.outputs.version }} are replaced by --version $VERSION so the value is
passed via env and not interpolated into the run block.

Comment on lines +213 to +222
- name: Verify size band (50–80MB)
shell: bash
run: |
TARBALL="dist/autopg-${{ steps.ver.outputs.version }}-${{ matrix.platform }}.tar.gz"
SIZE_MB=$(du -m "$TARBALL" | cut -f1)
echo "Tarball size: ${SIZE_MB}MB"
if [[ "$SIZE_MB" -lt 30 || "$SIZE_MB" -gt 120 ]]; then
echo "::error::tarball size out of expected band (got ${SIZE_MB}MB; expected 30-120MB sanity-band)"
exit 1
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Comment vs check drift on the size-band assertion.

Step name says Verify size band (50–80MB) but the check enforces 30–120MB. Pick one — either tighten the check to 50–80 or rename the step to match the actual sanity band so future readers (and dashboards) aren't misled.

📝 Suggested rename
-      - name: Verify size band (50–80MB)
+      - name: Verify size band (30–120MB sanity)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Verify size band (50–80MB)
shell: bash
run: |
TARBALL="dist/autopg-${{ steps.ver.outputs.version }}-${{ matrix.platform }}.tar.gz"
SIZE_MB=$(du -m "$TARBALL" | cut -f1)
echo "Tarball size: ${SIZE_MB}MB"
if [[ "$SIZE_MB" -lt 30 || "$SIZE_MB" -gt 120 ]]; then
echo "::error::tarball size out of expected band (got ${SIZE_MB}MB; expected 30-120MB sanity-band)"
exit 1
fi
- name: Verify size band (30–120MB sanity)
shell: bash
run: |
TARBALL="dist/autopg-${{ steps.ver.outputs.version }}-${{ matrix.platform }}.tar.gz"
SIZE_MB=$(du -m "$TARBALL" | cut -f1)
echo "Tarball size: ${SIZE_MB}MB"
if [[ "$SIZE_MB" -lt 30 || "$SIZE_MB" -gt 120 ]]; then
echo "::error::tarball size out of expected band (got ${SIZE_MB}MB; expected 30-120MB sanity-band)"
exit 1
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-tarballs.yml around lines 213 - 222, The step name
"Verify size band (50–80MB)" and the enforced bounds (the SIZE_MB check using
the if condition comparing to 30 and 120) are inconsistent; choose one
corrective action: either update the step name to reflect the actual sanity band
"Verify size band (30–120MB)" or tighten the numeric checks in the if condition
to enforce 50–80 (change the lower/upper bounds used with SIZE_MB). Locate the
workflow step named "Verify size band (50–80MB)" and modify either the
human-readable name string or the numeric comparisons that reference SIZE_MB so
they match.

Comment on lines +195 to +196
elif [[ -n "${AUTOPG_POSTGRES_URL_TEMPLATE:-}" ]]; then
stage_from_url "$AUTOPG_POSTGRES_URL_TEMPLATE" "${AUTOPG_POSTGRES_URL_VERSION:-16}" "$platform" "$out_dir"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale fallback Postgres version (16) is inconsistent with the rest of the pipeline.

When PG_VERSION is unresolved, this branch substitutes 16 into AUTOPG_POSTGRES_URL_TEMPLATE's {ver} placeholder. Everywhere else (workflow default, package.json optionalDependencies) targets 18.3.0-beta.17, so this fallback is likely to produce a 404 or a wrong-version download for the musl/arm64 paths that depend on the URL template.

Either drop the hardcoded "16", reuse the resolved PG_VERSION (re-running the package.json read here), or align the default with the current pipeline version.

♻️ Suggested fix
-  elif [[ -n "${AUTOPG_POSTGRES_URL_TEMPLATE:-}" ]]; then
-    stage_from_url "$AUTOPG_POSTGRES_URL_TEMPLATE" "${AUTOPG_POSTGRES_URL_VERSION:-16}" "$platform" "$out_dir"
+  elif [[ -n "${AUTOPG_POSTGRES_URL_TEMPLATE:-}" ]]; then
+    if [[ -z "${AUTOPG_POSTGRES_URL_VERSION:-}" && -z "$PG_VERSION" ]]; then
+      echo "error: cannot resolve postgres version for URL template (set AUTOPG_POSTGRES_URL_VERSION or --postgres-version)" >&2
+      return 1
+    fi
+    stage_from_url "$AUTOPG_POSTGRES_URL_TEMPLATE" "${AUTOPG_POSTGRES_URL_VERSION:-$PG_VERSION}" "$platform" "$out_dir"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
elif [[ -n "${AUTOPG_POSTGRES_URL_TEMPLATE:-}" ]]; then
stage_from_url "$AUTOPG_POSTGRES_URL_TEMPLATE" "${AUTOPG_POSTGRES_URL_VERSION:-16}" "$platform" "$out_dir"
elif [[ -n "${AUTOPG_POSTGRES_URL_TEMPLATE:-}" ]]; then
if [[ -z "${AUTOPG_POSTGRES_URL_VERSION:-}" && -z "$PG_VERSION" ]]; then
echo "error: cannot resolve postgres version for URL template (set AUTOPG_POSTGRES_URL_VERSION or --postgres-version)" >&2
return 1
fi
stage_from_url "$AUTOPG_POSTGRES_URL_TEMPLATE" "${AUTOPG_POSTGRES_URL_VERSION:-$PG_VERSION}" "$platform" "$out_dir"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/fetch-postgres-bins.sh` around lines 195 - 196, The branch that calls
stage_from_url with AUTOPG_POSTGRES_URL_TEMPLATE currently hardcodes "16" for
the version, which can produce wrong/download-failing artifacts; change that
call to derive the version from the resolved PG_VERSION (use PG_VERSION if
available) or from AUTOPG_POSTGRES_URL_VERSION, and if necessary update the
fallback to the pipeline's current default (e.g., 18.3.0-beta.17) so
stage_from_url("$AUTOPG_POSTGRES_URL_TEMPLATE", "<resolved-version>",
"$platform", "$out_dir") always receives the correct/consistent version; refer
to AUTOPG_POSTGRES_URL_TEMPLATE, AUTOPG_POSTGRES_URL_VERSION, stage_from_url and
PG_VERSION when making the change.

…er G8 + G9 extraction)

Surgical port of cutover wish G8 (cosign keyless OIDC sign + SLSA L3 attest)
and G9 (CDN publish to cdn.automagik.dev/autopg/*) from cutover branch onto
main. All target paths conflict-free.

What ships:
- .github/workflows/sign-attest.yml: per-platform cosign sign-blob +
  attest-build-provenance@v1; aggregates a top-level manifest.json. Runs
  after build-tarballs (G7) on tag.
- .github/workflows/cdn-publish.yml: uploads signed tarballs + manifest +
  channel pointer to cdn.automagik.dev/autopg/<channel>/<version>/<platform>/.
  Cache-control headers per wish D7 (immutable for versioned paths, short
  TTL for latest.json).
- tests/integration/sign-attest-smoke.sh: hermetic smoke fixture exercising
  the cosign + manifest + per-platform attestation pipeline without hitting
  real Sigstore. Referenced by G7's build-tarballs.yml — without this PR,
  G7's CI matrix references non-existent files.
- tests/integration/cdn-publish.sh: smoke fixture validating CDN publish
  layout end-to-end against a synthetic local-FS CDN.

Validation:
- bash -n on both scripts: syntax clean.
- bun run lint: clean.
- bun run lint:audit: scanned 32 files, 0 issues.

Cohort: cutover wish unique extractions onto main. Follows G6 (PR #83 merged)
and G7 (PR #84). Closes G8 + G9 gaps from PR #82's audit report.

G10 (install.sh ≤80 lines) DEFERRED — main has an existing 123-line
install.sh (legacy npm-based pgserve installer). Cutover's 79-line CDN
bootstrap installer is a DIFFERENT distribution path (CDN, not npm).
Putting both at install.sh collides; needs a separate rename decision
(install.sh → install-pgserve.sh, or cutover's at install-autopg.sh).
Plus the CDN doesn't actually exist yet (G9 workflow publishes to it but
Felipe needs to provision the bucket + DNS first).

G5 (autopg create-app + manifest LOCK 1) DEFERRED — structural entanglement
with admin-bootstrap.js + autopg_meta schema infrastructure that doesn't
exist on main. Needs proper engineering effort, not file copy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@namastex888 namastex888 changed the title feat(build): bun-build static binaries (5 platforms) + smoke fixture (cutover G7) feat(release): cutover G7 + G8 + G9 — bun-build + cosign sign + CDN publish May 8, 2026
Felipe directive 2026-05-08: pick free alternative with native signature
verification. GitHub Releases + 'gh attestation verify' (Sigstore Rekor)
satisfies both — zero infra cost, zero maintenance, semver tag immutability
built-in.

What changes vs the prior G9 cdn.automagik.dev approach:
- DROP .github/workflows/cdn-publish.yml
- DROP tests/integration/cdn-publish.sh
- ADD .github/workflows/release-publish.yml (gh release create + upload,
  draft/prerelease channel support, latest.json committed to .well-known/
  for install.sh consumers)
- PATCH build-tarballs.yml: remove refs to non-existent CDN-specific scripts
  (aggregate-manifest, verify-published-artifacts, scripts/cdn-publish)
- PATCH knip.json: ignore 'gh' binary (used by release-publish.yml)

What stays unchanged:
- G7 build-tarballs.yml smoke (matrix shellcheck + tarball assembly)
- G8 sign-attest.yml + sign-attest-smoke.sh (cosign keyless OIDC + SLSA L3
  work the same regardless of where binaries are stored)

CI red root cause: build-tarballs.yml's shellcheck step referenced 3 scripts
that were never copied (CDN-specific helpers from cutover branch). Pivot
makes those refs unnecessary.

Cohort impact:
- G7+G8 ship as-is (storage-agnostic)
- G9 pivots to GitHub Releases (~30 LOC vs 191 LOC)
- G10 install.sh, when revived, fetches from
  github.com/namastexlabs/pgserve/releases/download/v<version>/

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@namastex888 namastex888 changed the title feat(release): cutover G7 + G8 + G9 — bun-build + cosign sign + CDN publish feat(release): cutover G7 + G8 + G9 (GitHub Releases pivot) — bun-build + cosign sign + gh release publish May 8, 2026
…pts + cosign fixture keypair

Sign + Attest (fixture) job on PR #84 was red because:
1. tests/fixtures/cosign/{cosign.key,cosign.pub,README.md} were missing —
   sign-attest-smoke.sh aborts at the fixture-keypair-existence check.
2. scripts/aggregate-manifest.sh was missing — sign-attest-smoke.sh calls it
   to roll up per-platform signatures into top-level manifest.json.
3. scripts/verify-published-artifacts.sh was missing — sign-attest-smoke.sh
   calls it to assert verify rejects tampered tarballs / missing-sig
   siblings.

These three scripts/files are STORAGE-AGNOSTIC — they operate on cosign
signatures + attestations, which live in Sigstore Rekor regardless of where
the tarballs are stored (GitHub Releases, S3, anywhere). The earlier
elimination of CDN-specific helpers (cdn-publish.sh) was correct; these
three are not CDN-specific despite being named alongside it on the cutover
branch.

Local validation:
- bash tests/integration/sign-attest-smoke.sh → result: pass=15 fail=0
- bash -n + shellcheck on both new scripts: clean
- bun run lint: clean
- bun run lint:audit: 32 files scanned, 0 issues
- bun run deadcode: clean

build-tarballs.yml shellcheck list updated to include the two new scripts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (3)
.github/workflows/build-tarballs.yml (3)

140-143: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

macos-13 runner is retired — workflow will fail to schedule the darwin-x64 build.

This is unchanged from the prior review and still trips actionlint locally. Replace with a current Intel-capable image (macos-15-intel or macos-13macos-15-intel/macos-26-intel).

🛠️ Proposed fix
           - platform: darwin-x64
-            runner:   macos-13
+            runner:   macos-15-intel
GitHub Actions hosted runner labels available 2026 macos-15-intel
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-tarballs.yml around lines 140 - 143, The workflow
uses an outdated runner label: update the runner value for the darwin-x64 job
(identify the block with platform: darwin-x64 and runner: macos-13) to a current
Intel-capable macOS hosted runner such as macos-15-intel (or macos-26-intel) so
the darwin-x64 build can be scheduled and actionlint will pass; simply replace
runner: macos-13 with runner: macos-15-intel.

201-210: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Step title says 50–80MB but the check enforces 30–120MB.

Same drift flagged previously and still present. Either rename the step to match the actual sanity band or tighten the bounds.

📝 Proposed rename
-      - name: Verify size band (50–80MB)
+      - name: Verify size band (30–120MB sanity)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-tarballs.yml around lines 201 - 210, The CI step
titled "Verify size band (50–80MB)" is inconsistent with the enforced bounds
(30–120MB); update either the step title or the bounds in the check so they
match. Locate the job step using the title string "Verify size band (50–80MB)"
and the variables TARBALL and SIZE_MB, then either change the title to reflect
the actual sanity band (e.g., "Verify size band (30–120MB)") or tighten the
conditional range in the shell run block to match 50–80 (adjust the numeric
literals used in the if [[ "$SIZE_MB" -lt ... || "$SIZE_MB" -gt ... ]]; then
check). Ensure the echoed error message and any documentation strings are
updated to the same bounds.

158-199: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Script-injection sink — bind ${{ inputs.version }} via env: instead of interpolating into run:.

${{ inputs.version }} is templated directly into the shell at lines 162-163, 177, 194, 199, 204, 217-218. A crafted dispatch input (e.g. 1.0.0"; curl evil | sh; echo ") is substituted into the script body. Capture the input into an env-bound variable on the resolve step and reference $VERSION (and $INPUT_VERSION on the resolve step itself) downstream.

🛡️ Suggested pattern
       - name: Resolve version
         id: ver
         shell: bash
+        env:
+          INPUT_VERSION: ${{ inputs.version }}
         run: |
-          if [[ -n "${{ inputs.version }}" ]]; then
-            VERSION="${{ inputs.version }}"
+          if [[ -n "${INPUT_VERSION:-}" ]]; then
+            VERSION="${INPUT_VERSION}"
           else
             VERSION=$(node -p "require('./package.json').version")
           fi
           echo "version=${VERSION}" >> "$GITHUB_OUTPUT"

Then add env: { VERSION: ${{ steps.ver.outputs.version }}, PLATFORM: ${{ matrix.platform }} } to each subsequent step and switch the run: lines to bash scripts/build-binary.sh --platform "$PLATFORM" --version "$VERSION" (and the same for the assemble/smoke/size/upload steps).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-tarballs.yml around lines 158 - 199, The workflow
interpolates untrusted inputs into run: scripts (Resolve version and subsequent
steps like "Build autopg binary", "Assemble tarball", and "Smoke real tarball"),
enabling script-injection via ${{ inputs.version }} and ${{ matrix.platform }};
capture the resolved value in the Resolve version step outputs
(steps.ver.outputs.version) and export it via env on each later step (e.g., add
env: { VERSION: ${{ steps.ver.outputs.version }}, PLATFORM: ${{ matrix.platform
}} }) then change the run invocations to use the env vars (call
scripts/build-binary.sh, scripts/assemble-tarball.sh,
tests/integration/tarball-smoke.sh with --platform "$PLATFORM" --version
"$VERSION") so no user input is directly templated into run: bodies.
🧹 Nitpick comments (5)
tests/fixtures/cosign/cosign.key (1)

1-11: ⚡ Quick win

Document this fixture key explicitly to avoid scanner alarms and accidental reuse.

This is an encrypted Sigstore key whose password (autopg-fixture) is hardcoded in tests/integration/sign-attest-smoke.sh, so the material is effectively non-secret and only suitable for offline test signing. Secret scanners (e.g., betterleaks already flagged it here) will continue to trip on the BEGIN ENCRYPTED SIGSTORE PRIVATE KEY block, and there's nothing in-tree telling future maintainers it must never be used for real signing. Add a sibling README.md (and/or a scanner allowlist entry) in tests/fixtures/cosign/ calling out: (a) test-only purpose, (b) the fixed password, (c) the matching cosign.pub, (d) explicit "do not reuse for production" warning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/fixtures/cosign/cosign.key` around lines 1 - 11, Create a sibling
README.md next to the cosign key fixture (tests/fixtures/cosign/) that documents
the encrypted Sigstore key in cosign.key: state it is test-only/offline, include
the fixed password "autopg-fixture" (used by
tests/integration/sign-attest-smoke.sh), reference the matching public key file
name (cosign.pub), and add a clear "DO NOT USE IN PRODUCTION / DO NOT REUSE"
warning; also mention the purpose of the key (offline test signing) and suggest
adding a scanner allowlist entry for this fixture if your repo uses secret
scanners.
.github/workflows/release-publish.yml (2)

139-140: 💤 Low value

Behavior on tag-push trigger: !inputs.draft evaluates to true (because inputs.draft is undefined on push).

In GitHub expressions !null === true, so on a push: tags: v* trigger this guard reduces to channel == 'stable', meaning every stable tag push will commit-and-push .well-known/latest.json. That appears intentional (the channel pointer should advance on stable tag), but worth confirming — if you want tag pushes to also require a non-draft release, add an explicit inputs.draft != true && github.event_name == 'workflow_dispatch' clause, or compute the effective draft flag upstream.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-publish.yml around lines 139 - 140, The current
`if` condition on the "Update latest.json channel pointer" step uses `if: ${{
steps.ch.outputs.channel == 'stable' && !inputs.draft }}` which treats undefined
`inputs.draft` as true on tag-push events; update the condition to explicitly
require a non-draft release or that the workflow was manually dispatched.
Replace the guard with an explicit check such as ensuring `inputs.draft != true`
and/or `github.event_name == 'workflow_dispatch'` (e.g., require both
`steps.ch.outputs.channel == 'stable' && inputs.draft != true &&
github.event_name == 'workflow_dispatch'`) so tag-push triggers don't
unintentionally pass when `inputs.draft` is undefined.

130-137: ⚡ Quick win

gh release upload … dist/* will also try to upload build.log and build-record.tsv.

build-tarballs.yml (line 219-220) bundles dist/build.log and dist/build-record.tsv into each per-platform artifact. After the merged download those land in dist/ alongside the tarballs and get attached to the public release. That's likely not what you want for end-users (and --clobber across 5 platforms means each platform's build.log overwrites the last). Filter the upload glob to release-bound assets only.

🛠️ Proposed fix
-          if [ -d dist ] && [ "$(ls -A dist 2>/dev/null)" ]; then
-            gh release upload "v${VERSION}" \
-              --repo "${{ github.repository }}" \
-              --clobber \
-              dist/*
-          else
+          shopt -s nullglob
+          ASSETS=( dist/autopg-*.tar.gz dist/autopg-*.tar.gz.sha256 \
+                   dist/autopg-*.tar.gz.sig dist/autopg-*.tar.gz.intoto.jsonl \
+                   dist/manifest.json )
+          if [ "${`#ASSETS`[@]}" -gt 0 ]; then
+            gh release upload "v${VERSION}" \
+              --repo "${{ github.repository }}" \
+              --clobber \
+              "${ASSETS[@]}"
+          else
             echo "::warning::dist/ is empty — no assets to upload"
           fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-publish.yml around lines 130 - 137, The release
upload currently uses the glob "dist/*" with gh release upload (and --clobber),
which causes non-release files like build.log and build-record.tsv (produced by
build-tarballs.yml) to be attached and overwritten across platforms; change the
gh release upload invocation to only upload explicit release asset patterns
(e.g. tarball/zip/deb package globs or a whitelist) or explicitly exclude
build.log and build-record.tsv so only the intended per-platform artifacts are
uploaded, and keep --clobber semantics aligned with the narrowed glob.
tests/integration/sign-attest-smoke.sh (1)

116-127: 💤 Low value

Manifest assertions are coupled to whitespace in the JSON layout.

grep -q '"version": "${VERSION}"' and grep -q '"platform": "${p}"' will silently fail if scripts/aggregate-manifest.sh ever emits the JSON with different spacing (e.g., compact "version":"…" or extra whitespace). Prefer a structural check via jq (already a near-universal CI dep) so the smoke test stays accurate when the formatter changes.

♻️ Suggested rewrite
-  if grep -q "\"version\": \"${VERSION}\"" "${WORK_DIR}/manifest.json"; then
+  if [[ "$(jq -r '.version' "${WORK_DIR}/manifest.json")" == "${VERSION}" ]]; then
     ok "manifest.json carries version"
   else
     bad "manifest.json version field missing"
   fi
   for p in "${PLATFORMS[@]}"; do
-    if grep -q "\"platform\": \"${p}\"" "${WORK_DIR}/manifest.json"; then
+    if jq -e --arg p "${p}" '[.. | objects | .platform? // empty] | index($p)' \
+         "${WORK_DIR}/manifest.json" >/dev/null; then
       ok "manifest.json lists ${p}"
     else
       bad "manifest.json missing ${p}"
     fi
   done
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/sign-attest-smoke.sh` around lines 116 - 127, Replace the
fragile grep-based JSON checks in the sign-attest-smoke.sh test with structural
jq queries: read "${WORK_DIR}/manifest.json" and use jq to compare .version to
the shell $VERSION variable and to test that each element of the shell PLATFORMS
array exists in .platforms (or the appropriate array key) before calling ok/bad;
update the blocks that currently use grep -q "\"version\": \"${VERSION}\"" and
grep -q "\"platform\": \"${p}\"" to run jq tests, keep using the ok and bad
helpers for results, and fail fast if jq returns an error so spacing/formatting
changes won't break the assertions.
.github/workflows/sign-attest.yml (1)

56-58: 💤 Low value

cancel-in-progress: false plus a 5-platform matrix can deadlock if two tag pushes race.

The concurrency group is keyed on github.ref + inputs.version, with cancel disabled. If a second tag push (or re-dispatch) lands while the first run is mid-flight on the same ref/version, the second will queue indefinitely (until the first finishes or hits its 15-min timeout). That's the safer trade-off for signing — just confirm the operational expectation and consider documenting the queue behavior so reruns don't appear "stuck."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/sign-attest.yml around lines 56 - 58, The concurrency
block (concurrency, group, cancel-in-progress) can cause queued runs to deadlock
across the 5-platform matrix because group is keyed only on github.ref +
inputs.version; either enable cancel-in-progress: true to allow a new run to
cancel in-flight runs for the same group, or widen the group key (e.g., include
github.run_id or matrix.platform) so concurrent tag pushes don't serialize all
matrix jobs behind the first run; update the concurrency stanza accordingly and
add a short comment documenting the chosen behavior so reruns aren't mistaken
for stuck jobs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release-publish.yml:
- Around line 68-86: The Resolve version (id: ver) and Resolve channel (id: ch)
steps currently interpolate ${{ inputs.version }} and ${{ inputs.channel }}
directly into the shell which allows script injection; change both steps to pass
the values via env (e.g., set env: INPUT_VERSION: ${{ inputs.version }} and env:
INPUT_CHANNEL: ${{ inputs.channel }}) and update the shell bodies to read from
the environment variables (e.g., use $INPUT_VERSION / $INPUT_CHANNEL) when
emitting to $GITHUB_OUTPUT, keeping the same fallback logic for empty values;
reference the steps by name "Resolve version" and "Resolve channel" and their
ids (ver, ch).
- Around line 139-162: The "Update latest.json channel pointer" step currently
uses git diff --quiet .well-known/latest.json which won't detect newly
created/untracked .well-known/latest.json; change the check to detect untracked
files by either staging first and using git diff --cached --quiet or comparing
against HEAD (git diff --quiet HEAD -- .well-known/latest.json), ensure you run
git add .well-known/latest.json before the diff if you choose --cached, and then
commit; additionally avoid pushing a detached tag HEAD by syncing to main first
(e.g., git fetch origin main && git checkout main && git pull) before committing
and use git push origin main (or use an action like
peter-evans/create-pull-request or stefanzweifel/git-auto-commit-action) instead
of git push origin HEAD:main to ensure the update lands on the main branch.
- Around line 88-101: Replace the two pattern-based download steps that use
patterns autopg-*-tarball and autopg-*-signed with a single
actions/download-artifact@v4 step that requests the aggregated artifact by name:
autopg-signed-bundle-${{ steps.ver.outputs.version }} and writes to path: dist/
(remove or stop using the pattern steps that expect autopg-${{
steps.ver.outputs.version }}-${{ matrix.platform }} or autopg-signed-${{
steps.ver.outputs.version }}-${{ matrix.platform }} artifacts); if this download
needs to pull artifacts from a different workflow run, also add the run-id and
github-token inputs to the download step so cross-workflow artifact access
succeeds.

In @.github/workflows/sign-attest.yml:
- Around line 87-97: The "Resolve version" step (id: ver) currently interpolates
`${{ inputs.version }}` directly into the shell which is a script-injection
sink; bind the input via an environment variable instead (add an env:
INPUT_VERSION: ${{ inputs.version }} to the step) and use the env var inside the
shell (e.g. read $INPUT_VERSION or fall back to package.json when empty) so the
shell body no longer contains direct GitHub Actions interpolation; apply the
same change to the aggregate job's resolve step (the resolve block at lines
~212-221) so both steps use env: INPUT_VERSION and reference the env variable in
their run bodies.
- Around line 153-164: The workflow step that runs cosign verify-blob references
keys/cosign.pub but that file is not in the repo, causing the verify and later
upload steps to fail; update the workflow so the cosign verification uses a
valid public key path or supplies one at runtime — either (A) add a step before
the cosign verify-blob step to populate keys/cosign.pub (e.g., copy
tests/fixtures/cosign/cosign.pub or fetch the production key into
keys/cosign.pub) or (B) change the key argument in the cosign verify-blob
invocation to point to the existing fixture file
(tests/fixtures/cosign/cosign.pub) when a production key isn’t available; ensure
the change affects the cosign verify-blob command that uses TARBALL (built from
steps.ver.outputs.version and matrix.platform) and likewise update the artifact
upload call that later references keys/cosign.pub.

In `@tests/integration/sign-attest-smoke.sh`:
- Around line 56-62: The cleanup function exits early using an arithmetic test
[[ "${AUTOPG_KEEP_DIST:-0}" -eq 1 ]] which fails for non-numeric values (e.g.
"yes") under set -euo pipefail; change the check in cleanup to a string test
against the truthy value(s) (for example use [[ "${AUTOPG_KEEP_DIST:-}" = "1" ]]
or [[ -n "${AUTOPG_KEEP_DIST:-}" && "${AUTOPG_KEEP_DIST:-}" != "0" ]] ) so any
non-empty/truthy AUTOPG_KEEP_DIST prevents removal of WORK_DIR without causing
an arithmetic error when trap cleanup EXIT runs.

---

Duplicate comments:
In @.github/workflows/build-tarballs.yml:
- Around line 140-143: The workflow uses an outdated runner label: update the
runner value for the darwin-x64 job (identify the block with platform:
darwin-x64 and runner: macos-13) to a current Intel-capable macOS hosted runner
such as macos-15-intel (or macos-26-intel) so the darwin-x64 build can be
scheduled and actionlint will pass; simply replace runner: macos-13 with runner:
macos-15-intel.
- Around line 201-210: The CI step titled "Verify size band (50–80MB)" is
inconsistent with the enforced bounds (30–120MB); update either the step title
or the bounds in the check so they match. Locate the job step using the title
string "Verify size band (50–80MB)" and the variables TARBALL and SIZE_MB, then
either change the title to reflect the actual sanity band (e.g., "Verify size
band (30–120MB)") or tighten the conditional range in the shell run block to
match 50–80 (adjust the numeric literals used in the if [[ "$SIZE_MB" -lt ... ||
"$SIZE_MB" -gt ... ]]; then check). Ensure the echoed error message and any
documentation strings are updated to the same bounds.
- Around line 158-199: The workflow interpolates untrusted inputs into run:
scripts (Resolve version and subsequent steps like "Build autopg binary",
"Assemble tarball", and "Smoke real tarball"), enabling script-injection via ${{
inputs.version }} and ${{ matrix.platform }}; capture the resolved value in the
Resolve version step outputs (steps.ver.outputs.version) and export it via env
on each later step (e.g., add env: { VERSION: ${{ steps.ver.outputs.version }},
PLATFORM: ${{ matrix.platform }} }) then change the run invocations to use the
env vars (call scripts/build-binary.sh, scripts/assemble-tarball.sh,
tests/integration/tarball-smoke.sh with --platform "$PLATFORM" --version
"$VERSION") so no user input is directly templated into run: bodies.

---

Nitpick comments:
In @.github/workflows/release-publish.yml:
- Around line 139-140: The current `if` condition on the "Update latest.json
channel pointer" step uses `if: ${{ steps.ch.outputs.channel == 'stable' &&
!inputs.draft }}` which treats undefined `inputs.draft` as true on tag-push
events; update the condition to explicitly require a non-draft release or that
the workflow was manually dispatched. Replace the guard with an explicit check
such as ensuring `inputs.draft != true` and/or `github.event_name ==
'workflow_dispatch'` (e.g., require both `steps.ch.outputs.channel == 'stable'
&& inputs.draft != true && github.event_name == 'workflow_dispatch'`) so
tag-push triggers don't unintentionally pass when `inputs.draft` is undefined.
- Around line 130-137: The release upload currently uses the glob "dist/*" with
gh release upload (and --clobber), which causes non-release files like build.log
and build-record.tsv (produced by build-tarballs.yml) to be attached and
overwritten across platforms; change the gh release upload invocation to only
upload explicit release asset patterns (e.g. tarball/zip/deb package globs or a
whitelist) or explicitly exclude build.log and build-record.tsv so only the
intended per-platform artifacts are uploaded, and keep --clobber semantics
aligned with the narrowed glob.

In @.github/workflows/sign-attest.yml:
- Around line 56-58: The concurrency block (concurrency, group,
cancel-in-progress) can cause queued runs to deadlock across the 5-platform
matrix because group is keyed only on github.ref + inputs.version; either enable
cancel-in-progress: true to allow a new run to cancel in-flight runs for the
same group, or widen the group key (e.g., include github.run_id or
matrix.platform) so concurrent tag pushes don't serialize all matrix jobs behind
the first run; update the concurrency stanza accordingly and add a short comment
documenting the chosen behavior so reruns aren't mistaken for stuck jobs.

In `@tests/fixtures/cosign/cosign.key`:
- Around line 1-11: Create a sibling README.md next to the cosign key fixture
(tests/fixtures/cosign/) that documents the encrypted Sigstore key in
cosign.key: state it is test-only/offline, include the fixed password
"autopg-fixture" (used by tests/integration/sign-attest-smoke.sh), reference the
matching public key file name (cosign.pub), and add a clear "DO NOT USE IN
PRODUCTION / DO NOT REUSE" warning; also mention the purpose of the key (offline
test signing) and suggest adding a scanner allowlist entry for this fixture if
your repo uses secret scanners.

In `@tests/integration/sign-attest-smoke.sh`:
- Around line 116-127: Replace the fragile grep-based JSON checks in the
sign-attest-smoke.sh test with structural jq queries: read
"${WORK_DIR}/manifest.json" and use jq to compare .version to the shell $VERSION
variable and to test that each element of the shell PLATFORMS array exists in
.platforms (or the appropriate array key) before calling ok/bad; update the
blocks that currently use grep -q "\"version\": \"${VERSION}\"" and grep -q
"\"platform\": \"${p}\"" to run jq tests, keep using the ok and bad helpers for
results, and fail fast if jq returns an error so spacing/formatting changes
won't break the assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b1ce7764-1d0d-4e31-95fa-4e5337ef80b3

📥 Commits

Reviewing files that changed from the base of the PR and between f5039dc and 101f3bc.

⛔ Files ignored due to path filters (1)
  • tests/fixtures/cosign/cosign.pub is excluded by !**/*.pub
📒 Files selected for processing (9)
  • .github/workflows/build-tarballs.yml
  • .github/workflows/release-publish.yml
  • .github/workflows/sign-attest.yml
  • knip.json
  • scripts/aggregate-manifest.sh
  • scripts/verify-published-artifacts.sh
  • tests/fixtures/cosign/README.md
  • tests/fixtures/cosign/cosign.key
  • tests/integration/sign-attest-smoke.sh
✅ Files skipped from review due to trivial changes (4)
  • tests/fixtures/cosign/README.md
  • scripts/aggregate-manifest.sh
  • scripts/verify-published-artifacts.sh
  • knip.json

Comment on lines +68 to +86
- name: Resolve version
id: ver
run: |
if [ -n "${{ inputs.version }}" ]; then
echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
else
# Strip leading `v` from tag ref
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
fi

- name: Resolve channel
id: ch
run: |
if [ -n "${{ inputs.channel }}" ]; then
echo "channel=${{ inputs.channel }}" >> "$GITHUB_OUTPUT"
else
# Default tag pushes to stable; manual dispatches override above.
echo "channel=stable" >> "$GITHUB_OUTPUT"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Same script-injection sink — bind inputs.version and inputs.channel via env:.

Both resolve steps interpolate ${{ inputs.* }} directly into shell. A malicious dispatch input would be substituted into the echo body. Use the same env: INPUT_VERSION / INPUT_CHANNEL pattern requested in build-tarballs.yml and sign-attest.yml.

🛡️ Proposed fix
       - name: Resolve version
         id: ver
+        env:
+          INPUT_VERSION: ${{ inputs.version }}
         run: |
-          if [ -n "${{ inputs.version }}" ]; then
-            echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
+          if [ -n "${INPUT_VERSION:-}" ]; then
+            echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT"
           else
             echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
           fi

       - name: Resolve channel
         id: ch
+        env:
+          INPUT_CHANNEL: ${{ inputs.channel }}
         run: |
-          if [ -n "${{ inputs.channel }}" ]; then
-            echo "channel=${{ inputs.channel }}" >> "$GITHUB_OUTPUT"
+          if [ -n "${INPUT_CHANNEL:-}" ]; then
+            echo "channel=${INPUT_CHANNEL}" >> "$GITHUB_OUTPUT"
           else
             echo "channel=stable" >> "$GITHUB_OUTPUT"
           fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Resolve version
id: ver
run: |
if [ -n "${{ inputs.version }}" ]; then
echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
else
# Strip leading `v` from tag ref
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
fi
- name: Resolve channel
id: ch
run: |
if [ -n "${{ inputs.channel }}" ]; then
echo "channel=${{ inputs.channel }}" >> "$GITHUB_OUTPUT"
else
# Default tag pushes to stable; manual dispatches override above.
echo "channel=stable" >> "$GITHUB_OUTPUT"
fi
- name: Resolve version
id: ver
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
if [ -n "${INPUT_VERSION:-}" ]; then
echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT"
else
# Strip leading `v` from tag ref
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
fi
- name: Resolve channel
id: ch
env:
INPUT_CHANNEL: ${{ inputs.channel }}
run: |
if [ -n "${INPUT_CHANNEL:-}" ]; then
echo "channel=${INPUT_CHANNEL}" >> "$GITHUB_OUTPUT"
else
# Default tag pushes to stable; manual dispatches override above.
echo "channel=stable" >> "$GITHUB_OUTPUT"
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-publish.yml around lines 68 - 86, The Resolve
version (id: ver) and Resolve channel (id: ch) steps currently interpolate ${{
inputs.version }} and ${{ inputs.channel }} directly into the shell which allows
script injection; change both steps to pass the values via env (e.g., set env:
INPUT_VERSION: ${{ inputs.version }} and env: INPUT_CHANNEL: ${{ inputs.channel
}}) and update the shell bodies to read from the environment variables (e.g.,
use $INPUT_VERSION / $INPUT_CHANNEL) when emitting to $GITHUB_OUTPUT, keeping
the same fallback logic for empty values; reference the steps by name "Resolve
version" and "Resolve channel" and their ids (ver, ch).

Comment on lines +88 to +101
- name: Download tarball artifacts (build-tarballs.yml)
uses: actions/download-artifact@v4
with:
pattern: autopg-*-tarball
merge-multiple: true
path: dist/

- name: Download signature artifacts (sign-attest.yml)
uses: actions/download-artifact@v4
with:
pattern: autopg-*-signed
merge-multiple: true
path: dist/
continue-on-error: true # sign-attest.yml may not have run on dispatch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm upload names so the download pattern can be aligned exactly.
rg -nP --type=yaml -C2 '^\s*name:\s*autopg-' .github/workflows/build-tarballs.yml .github/workflows/sign-attest.yml
echo "---"
rg -nP --type=yaml -C2 'pattern:\s*autopg' .github/workflows/release-publish.yml

Repository: namastexlabs/pgserve

Length of output: 2097


Critical: artifact download patterns don't match the names produced upstream — dist/ will be empty.

  • build-tarballs.yml line 215 uploads with name: autopg-${{ steps.ver.outputs.version }}-${{ matrix.platform }} (e.g. autopg-2.5.0-linux-x64-glibc), so pattern autopg-*-tarball matches nothing.
  • sign-attest.yml line 192 uploads with name: autopg-signed-${{ steps.ver.outputs.version }}-${{ matrix.platform }} (e.g. autopg-signed-2.5.0-linux-x64-glibc), so pattern autopg-*-signed matches nothing.

Together with continue-on-error: true on the second download, the release will silently publish with zero assets. Either align the patterns to the actual artifact names, or download the aggregated autopg-signed-bundle-${{ steps.ver.outputs.version }} artifact emitted by sign-attest.yml line 276 (preferred — single artifact, already verified).

🛠️ Proposed fix (option A: align patterns)
       - name: Download tarball artifacts (build-tarballs.yml)
         uses: actions/download-artifact@v4
         with:
-          pattern: autopg-*-tarball
+          pattern: autopg-${{ steps.ver.outputs.version }}-*
           merge-multiple: true
           path: dist/

       - name: Download signature artifacts (sign-attest.yml)
         uses: actions/download-artifact@v4
         with:
-          pattern: autopg-*-signed
+          pattern: autopg-signed-${{ steps.ver.outputs.version }}-*
           merge-multiple: true
           path: dist/
         continue-on-error: true

Option B (cleaner): download the aggregated bundle from sign-attest.yml in one shot:

      - name: Download aggregated signed bundle
        uses: actions/download-artifact@v4
        with:
          name: autopg-signed-bundle-${{ steps.ver.outputs.version }}
          path: dist/

Note: cross-workflow downloads still need run-id + github-token. Without run-id, this action only sees artifacts from the current workflow run, so neither option will work as-is on a push: tags trigger. Consider triggering release-publish from workflow_run of sign-attest, or thread a run_id input through.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-publish.yml around lines 88 - 101, Replace the two
pattern-based download steps that use patterns autopg-*-tarball and
autopg-*-signed with a single actions/download-artifact@v4 step that requests
the aggregated artifact by name: autopg-signed-bundle-${{
steps.ver.outputs.version }} and writes to path: dist/ (remove or stop using the
pattern steps that expect autopg-${{ steps.ver.outputs.version }}-${{
matrix.platform }} or autopg-signed-${{ steps.ver.outputs.version }}-${{
matrix.platform }} artifacts); if this download needs to pull artifacts from a
different workflow run, also add the run-id and github-token inputs to the
download step so cross-workflow artifact access succeeds.

Comment on lines +139 to +162
- name: Update latest.json channel pointer
if: ${{ steps.ch.outputs.channel == 'stable' && !inputs.draft }}
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.ver.outputs.version }}
run: |
mkdir -p .well-known
cat > .well-known/latest.json <<EOF
{
"channel": "stable",
"version": "${VERSION}",
"released_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"tarball_base": "https://github.com/${{ github.repository }}/releases/download/v${VERSION}",
"platforms": ["linux-x64-glibc","linux-x64-musl","linux-arm64","darwin-x64","darwin-arm64"]
}
EOF
# Commit only if changed; CI bot configured globally
if ! git diff --quiet .well-known/latest.json; then
git config user.name "release-bot"
git config user.email "release-bot@namastex.com"
git add .well-known/latest.json
git commit -m "chore(release): update latest.json → v${VERSION}"
git push origin HEAD:main || echo "::warning::push failed (likely permission); update latest.json manually"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: git diff --quiet won't detect a newly created latest.json — first publish silently skips the commit.

git diff --quiet PATH only inspects modifications to files already tracked by git. On the first run (or any run where .well-known/latest.json has just been created/recreated and isn't in HEAD yet), the diff returns 0 and the entire git add/commit/push block is skipped — the channel pointer never lands. Compare against HEAD (or stage first and use --cached) so untracked/new files are detected.

Also, git push origin HEAD:main from a tag-push run is pushing the tag's detached HEAD onto main — that will replay any commits that exist on the tag but not on main, which is rarely what you want. Consider git fetch origin main && git checkout main && git pull (or use peter-evans/create-pull-request / a dedicated commit action) before staging the file.

🛠️ Proposed fix (minimum viable)
-          # Commit only if changed; CI bot configured globally
-          if ! git diff --quiet .well-known/latest.json; then
-            git config user.name "release-bot"
-            git config user.email "release-bot@namastex.com"
-            git add .well-known/latest.json
-            git commit -m "chore(release): update latest.json → v${VERSION}"
-            git push origin HEAD:main || echo "::warning::push failed (likely permission); update latest.json manually"
-          fi
+          git config user.name "release-bot"
+          git config user.email "release-bot@namastex.com"
+          git add .well-known/latest.json
+          # `--cached` works for both new and modified files
+          if ! git diff --cached --quiet -- .well-known/latest.json; then
+            git commit -m "chore(release): update latest.json → v${VERSION}"
+            # Don't push tag HEAD onto main — fetch/check out main first.
+            git fetch origin main
+            git checkout -B main origin/main -- || true
+            git cherry-pick --allow-empty HEAD@{1} 2>/dev/null || true
+            git push origin HEAD:main \
+              || echo "::warning::push failed; update latest.json manually"
+          fi

(Or replace the manual git block with stefanzweifel/git-auto-commit-action.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-publish.yml around lines 139 - 162, The "Update
latest.json channel pointer" step currently uses git diff --quiet
.well-known/latest.json which won't detect newly created/untracked
.well-known/latest.json; change the check to detect untracked files by either
staging first and using git diff --cached --quiet or comparing against HEAD (git
diff --quiet HEAD -- .well-known/latest.json), ensure you run git add
.well-known/latest.json before the diff if you choose --cached, and then commit;
additionally avoid pushing a detached tag HEAD by syncing to main first (e.g.,
git fetch origin main && git checkout main && git pull) before committing and
use git push origin main (or use an action like peter-evans/create-pull-request
or stefanzweifel/git-auto-commit-action) instead of git push origin HEAD:main to
ensure the update lands on the main branch.

Comment on lines +87 to +97
- name: Resolve version
id: ver
shell: bash
run: |
if [[ -n "${{ inputs.version }}" ]]; then
VERSION="${{ inputs.version }}"
else
VERSION=$(node -p "require('./package.json').version")
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Resolved version: ${VERSION}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Same script-injection sink — bind ${{ inputs.version }} via env: here and in the aggregate job's resolve step (lines 212-221).

inputs.version is interpolated directly into both resolve steps' shell bodies. Apply the same env: INPUT_VERSION binding pattern requested for build-tarballs.yml to keep the surface lint-clean and consistent.

🛡️ Proposed fix (apply to both resolve steps)
       - name: Resolve version
         id: ver
         shell: bash
+        env:
+          INPUT_VERSION: ${{ inputs.version }}
         run: |
-          if [[ -n "${{ inputs.version }}" ]]; then
-            VERSION="${{ inputs.version }}"
+          if [[ -n "${INPUT_VERSION:-}" ]]; then
+            VERSION="${INPUT_VERSION}"
           else
             VERSION=$(node -p "require('./package.json').version")
           fi
           echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/sign-attest.yml around lines 87 - 97, The "Resolve
version" step (id: ver) currently interpolates `${{ inputs.version }}` directly
into the shell which is a script-injection sink; bind the input via an
environment variable instead (add an env: INPUT_VERSION: ${{ inputs.version }}
to the step) and use the env var inside the shell (e.g. read $INPUT_VERSION or
fall back to package.json when empty) so the shell body no longer contains
direct GitHub Actions interpolation; apply the same change to the aggregate
job's resolve step (the resolve block at lines ~212-221) so both steps use env:
INPUT_VERSION and reference the env variable in their run bodies.

Comment on lines +153 to +164
- name: cosign verify-blob (self-check)
shell: bash
env:
VERSION: ${{ steps.ver.outputs.version }}
PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
TARBALL="dist/autopg-${VERSION}-${PLATFORM}.tar.gz"
cosign verify-blob \
--key keys/cosign.pub \
--signature "${TARBALL}.sig" \
"${TARBALL}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm presence (or absence) of the production public key referenced by sign-attest.yml.
fd -uH cosign.pub
echo "---"
ls -la keys/ 2>/dev/null || echo "no keys/ dir"
echo "---"
# Cross-check workflow references
rg -nP --type=yaml -C2 'keys/cosign\.pub'

Repository: namastexlabs/pgserve

Length of output: 118


🏁 Script executed:

# Get the full sign-attest.yml file to check line 283
wc -l .github/workflows/sign-attest.yml
echo "---"
# Show lines around 283
sed -n '280,286p' .github/workflows/sign-attest.yml

Repository: namastexlabs/pgserve

Length of output: 378


Missing production public key will cause workflow failures — keys/cosign.pub is referenced but not present in the repository.

Lines 162 and 283 reference keys/cosign.pub for signature verification (self-check) and artifact upload, respectively. The repository contains only the fixture keypair at tests/fixtures/cosign/cosign.pub. Without the production public key at keys/cosign.pub, the verify-blob step will fail and the artifact upload will error on if-no-files-found: error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/sign-attest.yml around lines 153 - 164, The workflow step
that runs cosign verify-blob references keys/cosign.pub but that file is not in
the repo, causing the verify and later upload steps to fail; update the workflow
so the cosign verification uses a valid public key path or supplies one at
runtime — either (A) add a step before the cosign verify-blob step to populate
keys/cosign.pub (e.g., copy tests/fixtures/cosign/cosign.pub or fetch the
production key into keys/cosign.pub) or (B) change the key argument in the
cosign verify-blob invocation to point to the existing fixture file
(tests/fixtures/cosign/cosign.pub) when a production key isn’t available; ensure
the change affects the cosign verify-blob command that uses TARBALL (built from
steps.ver.outputs.version and matrix.platform) and likewise update the artifact
upload call that later references keys/cosign.pub.

Comment on lines +56 to +62
cleanup() {
if [[ "${AUTOPG_KEEP_DIST:-0}" -eq 1 ]]; then
note "AUTOPG_KEEP_DIST=1 — keeping ${WORK_DIR}"
return
fi
rm -rf "${WORK_DIR}"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

-eq will abort cleanup if AUTOPG_KEEP_DIST is set to a non-numeric value.

[[ "${AUTOPG_KEEP_DIST:-0}" -eq 1 ]] performs arithmetic evaluation; a value like yes/true raises value too great for base / not a valid number and, because this runs from a trap cleanup EXIT under set -euo pipefail, the script exits with a confusing error instead of cleaning up. Use a string comparison so any "truthy-ish" value works.

🛡️ Proposed fix
 cleanup() {
-  if [[ "${AUTOPG_KEEP_DIST:-0}" -eq 1 ]]; then
+  if [[ "${AUTOPG_KEEP_DIST:-0}" == "1" ]]; then
     note "AUTOPG_KEEP_DIST=1 — keeping ${WORK_DIR}"
     return
   fi
   rm -rf "${WORK_DIR}"
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cleanup() {
if [[ "${AUTOPG_KEEP_DIST:-0}" -eq 1 ]]; then
note "AUTOPG_KEEP_DIST=1 — keeping ${WORK_DIR}"
return
fi
rm -rf "${WORK_DIR}"
}
cleanup() {
if [[ "${AUTOPG_KEEP_DIST:-0}" == "1" ]]; then
note "AUTOPG_KEEP_DIST=1 — keeping ${WORK_DIR}"
return
fi
rm -rf "${WORK_DIR}"
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/sign-attest-smoke.sh` around lines 56 - 62, The cleanup
function exits early using an arithmetic test [[ "${AUTOPG_KEEP_DIST:-0}" -eq 1
]] which fails for non-numeric values (e.g. "yes") under set -euo pipefail;
change the check in cleanup to a string test against the truthy value(s) (for
example use [[ "${AUTOPG_KEEP_DIST:-}" = "1" ]] or [[ -n "${AUTOPG_KEEP_DIST:-}"
&& "${AUTOPG_KEEP_DIST:-}" != "0" ]] ) so any non-empty/truthy AUTOPG_KEEP_DIST
prevents removal of WORK_DIR without causing an arithmetic error when trap
cleanup EXIT runs.

…N trap unbound var

Closes the worth-fixing bot-review findings on PR #84:

[HIGH × 5] set -e suppressed inside assemble_one / build_one / fetch_one
because main calls them via 'fn ... || rc=$?'. Failures of mkdir / tar /
verify_inputs / emit_manifest / stage_from_local|pkg|url silently
propagate as success, producing corrupt or partial tarballs.
Fix: explicit '|| return 1' after each potentially-failing command in:
  - scripts/assemble-tarball.sh: verify_inputs, emit_manifest, tar, sha256_of
  - scripts/build-binary.sh:     mkdir -p
  - scripts/fetch-postgres-bins.sh: rm -rf, mkdir -p, stage_from_local,
    stage_from_pkg, stage_from_url

[MEDIUM] RETURN trap on local 'scratch' under set -u prints
'scratch: unbound variable' on early-return paths (e.g. stage_from_pkg
when AUTOPG_POSTGRES_PKG_VERSION is invalid), masking the original error.
Fix: initialize 'local scratch=""' before installing the trap.

Filtered noise (intentionally NOT addressed):
- shellcheck-references-missing-scripts: already fixed in earlier commit;
  bots haven't re-scanned the latest PR head.
- JSON escaping in $f: no realistic special chars in binary filenames.
- macos-13 deprecation: cosmetic until GH actually removes the runner.

Local validation:
- shellcheck -S warning on all 3 scripts: clean
- bash tests/integration/sign-attest-smoke.sh: 15 pass / 0 fail

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@namastex888 namastex888 merged commit 071045c into main May 8, 2026
16 checks passed
@namastex888 namastex888 deleted the feat/cutover-extract-unique-work branch May 10, 2026 02:44
namastex888 added a commit that referenced this pull request May 12, 2026
v2.6.4 published to npm but the GH Releases pipeline failed at
build-tarballs because scripts/fetch-postgres-bins.sh's stage_from_url
declared 'local scratch' without initialization. Under 'set -u' the
RETURN trap fired with an unbound $scratch, masking the real fetch
state and exiting 1 across all four platform builds.

Codex P2 review on PR #84 caught the same bug in stage_from_pkg
(line 119) but stage_from_url was missed at that time. This commit
applies the same fix to stage_from_url.

v2.6.5 is purely the GH Releases completion — npm consumers on
pgserve: ^2.x will see v2.6.5 instead of v2.6.4 on next install
but the runtime surface is identical.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant