Skip to content

Public release: 2026-05-23 #10

Public release: 2026-05-23

Public release: 2026-05-23 #10

Workflow file for this run

# Release the observer Go binary as 6 npm packages whenever a v* tag
# is pushed.
#
# Flow:
# 0. frontend job — npm ci + npm run build inside web/.
# Uploads web/dist as an artifact so the 5
# platform binaries embed the SAME bundle
# (build-once, ship-everywhere). Prior to
# this job the workflow trusted whatever
# `internal/intelligence/dashboard/webapp/
# dist/` had been committed — a dev who
# tagged without running `make web-build`
# shipped a stale React bundle.
# 1. build job (matrix x5) — cross-compile each binary against the
# fresh dist; linux variants additionally
# cross-compile `antigravity-bridge.exe`
# (Windows amd64) into the same bin/ dir
# so npm-installed observer-on-WSL2 finds
# the bridge next to the observer binary
# (locateBridgeBinary's exe-dir lookup).
# Uploads the whole bin/ folder per
# platform.
# 2. publish job — pulls every platform's bin/ folder,
# drops it into npm/observer-<plat>/bin/,
# stamps the version into every
# package.json, npm-publishes the 5
# platform packages, then publishes the
# main shim last so its
# optionalDependencies resolve.
# 3. release job — creates a GitHub Release on the private
# repo using the matching CHANGELOG.md
# section as the body. Useful as a
# maintainer-side changelog browser.
# 4. public_release job — creates a GitHub Release on the PUBLIC
# repo (marmutapp/superbased-observer)
# with per-platform binary archives +
# SHA256SUMS attached. Runs in parallel
# with publish (only needs build), so
# direct downloads land independent of
# npm publish success. Waits for the v*
# tag to exist on the public repo —
# scripts/release.sh public <version>
# pushes it as part of the maintainer's
# `release.sh full <version>` invocation.
#
# Auth:
# - NPM_TOKEN_OBSERVER npm token scoped to @superbased/observer*.
# See https://docs.npmjs.com/about-access-tokens.
# - PUBLIC_REPO_TOKEN fine-grained PAT with `contents: write` on
# marmutapp/superbased-observer (the public repo). Used by the
# public_release job to create the GH Release with binary assets.
# Must be set BEFORE the first tag that uses this flow lands —
# missing-token failures surface as a 404 from `gh release create`.
name: npm-release
on:
push:
tags:
- 'v*'
permissions:
contents: write # GH Release step needs write to create the release.
jobs:
# ----------------------------------------------------------------
# Build the React frontend once, upload as artifact. All 5 binary
# jobs download the same artifact so they embed identical bundles.
# ----------------------------------------------------------------
frontend:
name: build frontend
# Gate the entire pipeline to the private repo. The orphan tree
# pushed to the public repo includes .github/workflows/ (because
# PRIVATE_ONLY_PATHS in scripts/release.sh doesn't list it — and
# there's value in publishing the workflow source for transparency),
# so the v* tag push to public:main also fires this workflow on
# the public repo. The public repo has no NPM_TOKEN_OBSERVER (and
# shouldn't — npm publish belongs to the maintainer), so the
# publish job would error with ENEEDAUTH. Beyond auth, the whole
# cross-compile + publish + release pipeline is wasted work on the
# public side (the private run already produces every artifact).
# Gating frontend skips it on the public repo; every other job
# chains via `needs: frontend` transitively, so they all skip too.
if: github.repository == 'marmutapp/superbased-observer-private'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- name: Install + build webapp
run: |
cd web
npm ci
npm run build
ls -l dist/
# Sanity: index.html must reference the freshly-hashed
# bundle. If Vite produced an empty dist (silent error)
# we want to fail loudly rather than embed an empty FS.
test -f dist/index.html
grep -qE 'assets/index-[A-Za-z0-9_-]+\.js' dist/index.html
- name: Upload web/dist
uses: actions/upload-artifact@v5
with:
name: web-dist
path: web/dist/
retention-days: 7
if-no-files-found: error
# ----------------------------------------------------------------
# Build the 5 platform binaries in parallel. Each binary embeds the
# web-dist artifact built in the frontend job above.
# ----------------------------------------------------------------
build:
name: build ${{ matrix.target }}
needs: frontend
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
include:
- target: linux-x64
goos: linux
goarch: amd64
ext: ''
include_bridge: true
- target: linux-arm64
goos: linux
goarch: arm64
ext: ''
include_bridge: true
- target: darwin-x64
goos: darwin
goarch: amd64
ext: ''
include_bridge: false
- target: darwin-arm64
goos: darwin
goarch: arm64
ext: ''
include_bridge: false
- target: win32-x64
goos: windows
goarch: amd64
ext: '.exe'
include_bridge: false
steps:
- uses: actions/checkout@v5
- name: Download web/dist
uses: actions/download-artifact@v6
with:
name: web-dist
path: web/dist/
- name: Sync embed dir from web/dist
# The Go binary embeds internal/intelligence/dashboard/webapp/
# dist/ via //go:embed; web/dist is the build output. Mirror
# the layout `make web-build` produces locally.
run: |
set -euo pipefail
rm -rf internal/intelligence/dashboard/webapp/dist
mkdir -p internal/intelligence/dashboard/webapp/dist
cp -R web/dist/. internal/intelligence/dashboard/webapp/dist/
ls -l internal/intelligence/dashboard/webapp/dist/
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
cache: true
- name: Cross-compile observer
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: '0' # pure-Go thanks to modernc.org/sqlite
run: |
# Strip the leading 'v' from the tag (v1.2.3 -> 1.2.3) so the
# binary's `observer --version` matches the npm package version.
VERSION="${GITHUB_REF_NAME#v}"
mkdir -p npm/observer-${{ matrix.target }}/bin
go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" \
-o npm/observer-${{ matrix.target }}/bin/observer${{ matrix.ext }} \
./cmd/observer
# Make the unix binary executable. Windows .exe is fine as-is.
if [ -z "${{ matrix.ext }}" ]; then
chmod +x npm/observer-${{ matrix.target }}/bin/observer
fi
ls -l npm/observer-${{ matrix.target }}/bin/
# Sanity-check the embedded version inline on the linux-x64
# build (the runner OS) — fail the job if it doesn't match.
if [ "${{ matrix.target }}" = "linux-x64" ]; then
EMBEDDED=$(./npm/observer-linux-x64/bin/observer --version 2>&1 | head -1 | awk '{print $NF}')
if [ "$EMBEDDED" != "$VERSION" ]; then
echo "::error::version mismatch: --version reports '$EMBEDDED' want '$VERSION'"
exit 1
fi
echo "embedded version: $EMBEDDED ✓"
fi
- name: Cross-compile antigravity-bridge.exe
# WSL2-on-Windows linux installs need the Windows-side helper
# binary that runs under powershell.exe to bridge the WSL→
# Windows-localhost network gap when calling Antigravity's
# local language_server gRPC API. Pre-CI this binary was a
# `make build` afterthought on the maintainer's box; shipping
# it inside the linux-* npm packages means WSL2 users no
# longer have to clone + build to use the Antigravity adapter.
# Always Windows amd64 — observer's locateBridgeBinary looks
# for it next to the observer executable.
if: matrix.include_bridge
env:
GOOS: windows
GOARCH: amd64
CGO_ENABLED: '0'
run: |
go build -trimpath -ldflags="-s -w" \
-o npm/observer-${{ matrix.target }}/bin/antigravity-bridge.exe \
./cmd/antigravity-bridge
ls -l npm/observer-${{ matrix.target }}/bin/
- name: Upload bin/ folder
# Upload the whole bin/ dir (not just observer) so the linux
# variants' antigravity-bridge.exe rides along. The publish
# job re-hydrates by copying the entire artifact tree back
# into npm/observer-<plat>/bin/.
uses: actions/upload-artifact@v5
with:
name: observer-${{ matrix.target }}
path: npm/observer-${{ matrix.target }}/bin/
retention-days: 7
if-no-files-found: error
# ----------------------------------------------------------------
# Publish all 6 packages from a single job after every binary is
# built. Sequential publish so a failure mid-flight is recoverable
# (idempotent — npm rejects re-publishing the same version, so just
# re-run from where it stopped).
# ----------------------------------------------------------------
publish:
name: publish to npm
needs: build
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
# Node 22 is the active LTS; v20 emits a deprecation warning
# on every workflow run. The publish job only invokes `npm
# publish` so it's Node-version-insensitive — bumping clears
# the warning at zero risk.
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Resolve version
id: version
run: |
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Download all platform bin/ folders
uses: actions/download-artifact@v6
with:
path: artifacts
- name: Place binaries in their package directories
run: |
set -euo pipefail
for target in linux-x64 linux-arm64 darwin-x64 darwin-arm64 win32-x64; do
mkdir -p "npm/observer-${target}/bin"
# The artifact contains the whole bin/ tree — observer +
# (for linux variants) antigravity-bridge.exe. cp -R
# preserves both.
cp -R "artifacts/observer-${target}/." "npm/observer-${target}/bin/"
# Make unix binaries executable.
if [ "${target}" != "win32-x64" ]; then
chmod +x "npm/observer-${target}/bin/observer"
fi
done
# Sanity print before publish so the workflow log shows
# exactly which binaries were packaged.
for target in linux-x64 linux-arm64 darwin-x64 darwin-arm64 win32-x64; do
echo "=== npm/observer-${target}/bin/ ==="
ls -l "npm/observer-${target}/bin/"
done
- name: Stamp release version into every package.json
run: ./scripts/sync-npm-version.sh "${GITHUB_REF_NAME}"
- name: Publish platform packages
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_OBSERVER }}
run: |
set -euo pipefail
for target in linux-x64 linux-arm64 darwin-x64 darwin-arm64 win32-x64; do
(cd "npm/observer-${target}" && npm publish --access public)
done
- name: Publish main package
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_OBSERVER }}
run: |
# Main goes last so its optionalDependencies resolve cleanly
# for any user installing the moment after this step finishes.
cd npm/observer && npm publish --access public
- name: Summary
run: |
{
echo "## npm-release ${GITHUB_REF_NAME} published"
echo ""
echo "Six packages updated:"
echo "- \`@superbased/observer@${GITHUB_REF_NAME#v}\`"
for t in linux-x64 linux-arm64 darwin-x64 darwin-arm64 win32-x64; do
echo "- \`@superbased/observer-${t}@${GITHUB_REF_NAME#v}\`"
done
echo ""
echo "linux-x64 + linux-arm64 packages include \`antigravity-bridge.exe\` for WSL2 users."
echo ""
echo "Smoke test:"
echo '```bash'
echo "npm install -g @superbased/observer@${GITHUB_REF_NAME#v}"
echo "observer --version"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# ----------------------------------------------------------------
# GitHub Release on the private repo. The body is the CHANGELOG
# section matching the tag — pulls everything between
# "## [<version>] —" and the next "## [" so the release page
# mirrors what landed in CHANGELOG.md. Public repo is orphan +
# force-pushed so its Releases tab isn't a fit; this is for the
# maintainer-side changelog browser.
# ----------------------------------------------------------------
release:
name: GitHub Release
needs: publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Extract CHANGELOG section
id: changelog
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
# awk pulls everything between the matching version header
# and the next "## [" header (exclusive). Header form is
# "## [1.2.3] — 2026-05-17".
BODY=$(awk -v v="$VERSION" '
/^## \[/ {
if (in_section) exit
if ($0 ~ "^## \\[" v "\\]") { in_section = 1; next }
}
in_section { print }
' CHANGELOG.md)
if [ -z "$BODY" ]; then
echo "::warning::CHANGELOG.md has no section for v${VERSION}; release body will be empty"
BODY="(no changelog entry for v${VERSION} — check CHANGELOG.md)"
fi
# GH Actions multiline output via heredoc:
{
echo "body<<CHANGELOG_EOF"
echo "$BODY"
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
body: ${{ steps.changelog.outputs.body }}
draft: false
prerelease: false
# ----------------------------------------------------------------
# Public repo GitHub Release with per-platform binary archives.
# Runs in parallel with publish (only needs build) so direct
# downloaders aren't blocked on npm-publish quirks. Waits for the
# public tag to exist — scripts/release.sh public <version> pushes
# it ~30s after this workflow's trigger.
# ----------------------------------------------------------------
public_release:
name: Public Release (binaries)
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Download all platform bin/ folders
uses: actions/download-artifact@v6
with:
path: artifacts
- name: Wait for v* tag on public repo
run: |
# scripts/release.sh public <version> force-pushes the same
# tag to the public remote right after the private push that
# triggered this workflow. The build job typically takes
# 2–3 min; the public tag push is sub-30s, so under normal
# `release.sh full` flows the tag is already there. The
# poll is for the edge case where the public push lags
# (e.g. operator ran `release.sh tag` then `release.sh
# public` separately with a delay between).
#
# Uses unauthenticated curl rather than `gh api` because the
# public repo's ref endpoint is readable anonymously — and
# because the workflow's gh CLI inherits empty GH_TOKEN when
# PUBLIC_REPO_TOKEN isn't set yet (first-time setup), which
# makes gh fail with 401 instead of falling back to public
# access. The original gh-based wait silenced 401s through
# `--silent 2>/dev/null` and looked indistinguishable from
# "tag not yet pushed" — the v1.6.19 first-time release hit
# exactly this misdiagnosis.
set -euo pipefail
URL="https://api.github.com/repos/marmutapp/superbased-observer/git/ref/tags/${GITHUB_REF_NAME}"
for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' "$URL" || echo "000")
case "$STATUS" in
200)
echo "Public tag ${GITHUB_REF_NAME} exists ✓"
exit 0
;;
404)
echo "[$i/12] Waiting 10s for public tag ${GITHUB_REF_NAME}... (404)"
;;
403)
# Rate-limited (anonymous quota: 60 req/hr/IP). Sleep
# the recommended backoff and keep trying — at 12
# attempts × 10s this is highly unlikely.
echo "[$i/12] Rate-limited (HTTP 403); backing off 10s"
;;
*)
# Network blip, DNS hiccup, or unexpected upstream
# response. Surface but keep polling.
echo "[$i/12] Unexpected HTTP $STATUS from $URL; retrying"
;;
esac
sleep 10
done
echo "::error::Public tag ${GITHUB_REF_NAME} not present after 120s. Run \`scripts/release.sh public ${GITHUB_REF_NAME}\` locally, then re-run this job."
exit 1
- name: Sanity-check PUBLIC_REPO_TOKEN is set
env:
PUBLIC_REPO_TOKEN: ${{ secrets.PUBLIC_REPO_TOKEN }}
run: |
# Fail fast with a clear message when the PAT isn't set.
# Without this, the `Create / update public GitHub Release`
# step below would error with `gh: HTTP 404` (cross-repo
# write rejected as a 404 to avoid leaking repo existence),
# which is harder to diagnose than "secret not set".
set -euo pipefail
if [ -z "${PUBLIC_REPO_TOKEN:-}" ]; then
echo "::error::PUBLIC_REPO_TOKEN secret is not set on this repo. See docs/release-runbook.md '#### Required CI secret: PUBLIC_REPO_TOKEN' for setup. Until set, this job fails fast; the rest of the npm-release pipeline (npm publish, private GH Release) is unaffected."
exit 1
fi
echo "PUBLIC_REPO_TOKEN is set ✓"
- name: Build per-platform archives + SHA256SUMS
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME}"
mkdir -p release-assets
# Linux + Darwin: tar.gz preserves the executable bit
# (chmod +x set in the build job rides along). Windows:
# zip is the conventional Windows artifact format.
for target in linux-x64 linux-arm64 darwin-x64 darwin-arm64; do
tar -czf "release-assets/observer-${VERSION}-${target}.tar.gz" \
-C "artifacts/observer-${target}" .
done
(cd "artifacts/observer-win32-x64" && \
zip -r "$GITHUB_WORKSPACE/release-assets/observer-${VERSION}-win32-x64.zip" .)
# SHA256SUMS — verifiable via `shasum -a 256 -c SHA256SUMS`
# or `sha256sum -c SHA256SUMS` from inside the downloads dir.
(cd release-assets && sha256sum *.tar.gz *.zip > SHA256SUMS)
echo "=== release-assets/ ==="
ls -lh release-assets/
echo "=== SHA256SUMS ==="
cat release-assets/SHA256SUMS
- name: Compose release body
id: changelog
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
# Identical extraction logic to the private 'release' job.
BODY=$(awk -v v="$VERSION" '
/^## \[/ {
if (in_section) exit
if ($0 ~ "^## \\[" v "\\]") { in_section = 1; next }
}
in_section { print }
' CHANGELOG.md)
if [ -z "$BODY" ]; then
BODY="(no changelog entry for v${VERSION})"
fi
# Append a Downloads section so visitors know what's
# attached + how to verify + that npm is the alternative.
{
echo "$BODY"
echo ""
echo "---"
echo ""
echo "### Downloads"
echo ""
echo "Pre-built binaries for each supported platform are attached below. Linux variants bundle \`antigravity-bridge.exe\` next to the observer binary for WSL2 users of the Antigravity adapter."
echo ""
echo "| Platform | Asset |"
echo "|---|---|"
echo "| Linux x86_64 | \`observer-${GITHUB_REF_NAME}-linux-x64.tar.gz\` |"
echo "| Linux arm64 | \`observer-${GITHUB_REF_NAME}-linux-arm64.tar.gz\` |"
echo "| macOS x86_64 (Intel) | \`observer-${GITHUB_REF_NAME}-darwin-x64.tar.gz\` |"
echo "| macOS arm64 (Apple Silicon) | \`observer-${GITHUB_REF_NAME}-darwin-arm64.tar.gz\` |"
echo "| Windows x86_64 | \`observer-${GITHUB_REF_NAME}-win32-x64.zip\` |"
echo ""
echo "Verify with \`sha256sum -c SHA256SUMS\` (or \`shasum -a 256 -c SHA256SUMS\` on macOS) from the directory containing the downloads."
echo ""
echo "Also available via npm: \`npm install -g @superbased/observer@${VERSION}\`"
} > release-body.md
echo "Wrote release-body.md ($(wc -l < release-body.md) lines)"
- name: Create / update public GitHub Release
env:
GH_TOKEN: ${{ secrets.PUBLIC_REPO_TOKEN }}
run: |
set -euo pipefail
# Use --clobber-via-recreate semantics: if a release for this
# tag already exists (e.g. a prior CI run was retried), edit
# the existing release rather than failing with "already
# exists". `gh release create --notes-file ...` errors on
# existing tag; switch to view-or-create.
if gh release view "${GITHUB_REF_NAME}" --repo marmutapp/superbased-observer >/dev/null 2>&1; then
echo "Release ${GITHUB_REF_NAME} already exists on public repo — updating body + re-uploading assets"
gh release edit "${GITHUB_REF_NAME}" \
--repo marmutapp/superbased-observer \
--notes-file release-body.md
gh release upload "${GITHUB_REF_NAME}" \
--repo marmutapp/superbased-observer \
--clobber \
release-assets/*.tar.gz \
release-assets/*.zip \
release-assets/SHA256SUMS
else
gh release create "${GITHUB_REF_NAME}" \
--repo marmutapp/superbased-observer \
--title "${GITHUB_REF_NAME}" \
--notes-file release-body.md \
release-assets/*.tar.gz \
release-assets/*.zip \
release-assets/SHA256SUMS
fi
- name: Summary
run: |
{
echo "## Public release ${GITHUB_REF_NAME} published"
echo ""
echo "[Release page](https://github.com/marmutapp/superbased-observer/releases/tag/${GITHUB_REF_NAME})"
echo ""
echo "Assets attached:"
echo '```'
ls -lh release-assets/
echo '```'
} >> "$GITHUB_STEP_SUMMARY"