Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
062fc0dc3This file is pinned to the selected release. Download the original to open it in your design tool.
#!/usr/bin/env bash
# Release the `harness` adapter CLI: bump version -> bundle -> upload to a PUBLIC GCS bucket.
# Running daemons poll the manifest (ADAPTER_UPDATE_URL) and self-update within ~1 min. See RELEASE.md.
#
# Usage:
# bash scripts/upload-cli.sh # auto-bump (0.0.1 -> 0.0.2; 0.0.99 -> 0.1.1)
# bash scripts/upload-cli.sh 0.1.0 # release an explicit version
# bash scripts/upload-cli.sh --no-bump # keep the current version, bundle + upload
# bash scripts/upload-cli.sh --no-build # upload the existing dist/ artifact as-is
# GCS_BUCKET=other bash scripts/upload-cli.sh # env overrides (see below)
#
# The CURRENT version is read from the remote metadata.json on GCS (single source of truth) and the
# patch is bumped from there — nothing is git-committed (the version is injected into the bundle via
# ADAPTER_VERSION, so the running binary's version equals the published manifest version).
# Prereqs: `gcloud storage` authenticated with WRITE access on the bucket; the
# bucket/objects must be public-read; `node`+`npm`.
set -euo pipefail
ADAPTER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # this package
CLI="$ADAPTER_DIR/dist/cli.js"
NOTIFY="$ADAPTER_DIR/dist/notify.mjs"
# --- GCS config (all overridable via env) ---
GCS_BUCKET="${GCS_BUCKET:-s3-autonomous-upgrade-3}"
GCS_PUBLIC_BASE_URL="${GCS_PUBLIC_BASE_URL:-https://storage.googleapis.com/${GCS_BUCKET}}"
# The manifest itself stays on the GCS origin (read below, and in the merge step) — daemons poll it
# every ~60s and this zone's CDN caps ANY cacheable response at ~31 days regardless of origin headers
# (verified live), which would silently delay self-update fleet-wide. The bundle files it points at are
# a different story: immutable once published (never re-upload over an existing version — see
# RELEASE.md), so CDN caching them is pure upside. Only CLI_URL/NOTIFY_URL below use this.
CDN_ASSET_BASE_URL="${CDN_ASSET_BASE_URL:-https://cdn.autonomous.ai}"
METADATA_PATH="${METADATA_PATH:-harness/cli/metadata.json}"
OTA_KEY="${OTA_KEY:-cli}" # must match ADAPTER_UPDATE_KEY in src/config/env.ts
next_adapter_version() {
local current="$1" major minor patch
if [[ ! "$current" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "error: version '$current' must look like X.Y.Z" >&2
return 1
fi
major=$((10#${BASH_REMATCH[1]}))
minor=$((10#${BASH_REMATCH[2]}))
patch=$((10#${BASH_REMATCH[3]}))
if (( patch >= 99 )); then
printf '%d.%d.1\n' "$major" "$((minor + 1))"
else
printf '%d.%d.%d\n' "$major" "$minor" "$((patch + 1))"
fi
}
# --- Parse args (optional explicit version + flags) ---
NEW_VER=""
DO_BUMP=1
DO_BUILD=1
for arg in "$@"; do
case "$arg" in
--no-bump) DO_BUMP=0 ;;
--no-build) DO_BUILD=0 ;;
-*) echo "error: unknown flag '$arg'" >&2; exit 1 ;;
*) NEW_VER="$arg" ;;
esac
done
command -v node >/dev/null 2>&1 || { echo "error: node not found" >&2; exit 1; }
# --- GCS client: `gcloud storage`, and only `gcloud storage` ---
# gsutil was retired from this repo on 2026-09-17. It is a standalone Python tool that only
# understands gcloud's *user* and *service-account-key* credentials: it cannot use the
# external-account (federated) credential Workload Identity Federation issues, so every call fails
# under WIF while the identical `gcloud storage` call works — it is the same gcloud binary that
# performed the token exchange. Every release path here runs on WIF now. Do not reintroduce it.
command -v gcloud >/dev/null 2>&1 || {
echo "error: gcloud not found — install/authenticate the gcloud SDK" >&2
exit 1
}
gcloud storage --help >/dev/null 2>&1 || {
echo "error: this gcloud is too old for 'gcloud storage' — update the gcloud SDK" >&2
exit 1
}
# gcs_cp <src> <dst> [cache-control] [content-type] — either side may be gs:// or a local path or `-`.
gcs_cp() {
local src="$1" dst="$2" cc="${3:-}" ct="${4:-}" args=(storage cp)
if [ -n "$cc" ]; then args+=("--cache-control=$cc"); fi
if [ -n "$ct" ]; then args+=("--content-type=$ct"); fi
gcloud "${args[@]}" "$src" "$dst"
}
# gcs_refuse_republish <gs://…> — a versioned artifact is IMMUTABLE once published: the CDN in front of
# it caches for a year and serves the FIRST bytes it saw for that path, so a re-upload of the same
# version leaves the manifest naming bytes no daemon will ever receive (the self-updater verifies the
# sha and refuses). Same guard as desktop/scripts/upload-desktop.sh; always a new version, never an
# overwrite.
gcs_refuse_republish() {
local dst="$1"
if gcloud storage ls "$dst" >/dev/null 2>&1; then
echo "error: $dst already exists — versioned artifacts are immutable (the CDN keeps the first bytes)." >&2
echo " Publish a NEW version instead of re-uploading this one (see cli/RELEASE.md)." >&2
exit 1
fi
}
cleanup() { rm -f "${SRC:-}" "${DST:-}"; }
trap cleanup EXIT
# --- Step 1: resolve the version (source of truth = remote metadata.json; fallback = package.json) ---
META_URL="${GCS_PUBLIC_BASE_URL%/}/${METADATA_PATH#/}"
CUR="$(curl -fsSL "$META_URL" 2>/dev/null | node -e '
let s=""; process.stdin.on("data",d=>s+=d).on("end",()=>{
try { process.stdout.write(String((JSON.parse(s)[process.argv[1]]||{}).version||"")) } catch { process.stdout.write("") }
})' "$OTA_KEY" 2>/dev/null || true)"
if [ -n "$CUR" ]; then
echo ">> current published version (from metadata.json): $CUR"
else
CUR="$(node -p "require('$ADAPTER_DIR/package.json').version" 2>/dev/null || echo '0.0.0')"
echo ">> could not read remote metadata — falling back to package.json: $CUR" >&2
fi
if [ -n "$NEW_VER" ]; then
VER="$NEW_VER" # explicit version wins
elif [ "$DO_BUMP" -eq 1 ]; then
VER="$(next_adapter_version "$CUR")"
else
VER="$CUR" # --no-bump: keep current
fi
[[ "$VER" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "error: version '$VER' must look like X.Y.Z" >&2; exit 1; }
echo ">> releasing adapter version: $VER"
# --- Step 2: bundle (version baked into the artifact) ---
if [ "$DO_BUILD" -eq 1 ]; then
echo ">> bundling…"
( cd "$ADAPTER_DIR" && ADAPTER_VERSION="$VER" npm run bundle )
fi
[ -f "$CLI" ] && [ -f "$NOTIFY" ] || { echo "error: dist/cli.js or dist/notify.mjs missing — bundle first (drop --no-build)" >&2; exit 1; }
head -1 "$CLI" | grep -q '^#!' || { echo "error: dist/cli.js lost its shebang on line 1 (esbuild change?)" >&2; exit 1; }
[ "$(node "$CLI" version)" = "$VER" ] || { echo "error: bundled version != $VER (ADAPTER_VERSION not injected)" >&2; exit 1; }
# --- Step 3: upload both artifacts + merge the manifest ---
CLI_GCS="harness/cli/${VER}/cli.js"
NOTIFY_GCS="harness/cli/${VER}/notify.mjs"
CLI_URL="${CDN_ASSET_BASE_URL%/}/${CLI_GCS}"
NOTIFY_URL="${CDN_ASSET_BASE_URL%/}/${NOTIFY_GCS}"
# `shasum` is a Perl script and is absent from minimal Linux images (only perl-base is installed);
# `sha256sum` is coreutils and is always there. Prefer it so a release can also be cut from Ubuntu.
sha256_of() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}'
else shasum -a 256 "$1" | awk '{print $1}'
fi
}
CLI_SHA="$(sha256_of "$CLI")"; CLI_SIZE="$(wc -c < "$CLI" | tr -d ' ')"
NOTIFY_SHA="$(sha256_of "$NOTIFY")"; NOTIFY_SIZE="$(wc -c < "$NOTIFY" | tr -d ' ')"
echo ">> uploading cli.js ($CLI_SIZE bytes) + notify.mjs ($NOTIFY_SIZE bytes)"
# Immutable per-version path — see the CDN_ASSET_BASE_URL note above. Long max-age here is what
# actually lets the CDN cache these instead of hitting GCS on every daemon's install/update.
gcs_refuse_republish "gs://${GCS_BUCKET}/${CLI_GCS}"
gcs_refuse_republish "gs://${GCS_BUCKET}/${NOTIFY_GCS}"
gcs_cp "$CLI" "gs://${GCS_BUCKET}/${CLI_GCS}" "public, max-age=31536000, immutable"
gcs_cp "$NOTIFY" "gs://${GCS_BUCKET}/${NOTIFY_GCS}" "public, max-age=31536000, immutable"
echo ">> merging manifest: gs://${GCS_BUCKET}/${METADATA_PATH} (${OTA_KEY})"
SRC="$(mktemp)"; DST="$(mktemp)" # removed by cleanup() on EXIT
if ! gcs_cp "gs://${GCS_BUCKET}/${METADATA_PATH}" "$SRC" 2>/dev/null; then
echo " (no existing metadata.json — creating a new one)"
printf '{}' > "$SRC"
fi
# Pass paths/values via argv (NOT a heredoc through stdin — that would claim the pipe and blank the file).
node "$ADAPTER_DIR/scripts/merge-manifest.mjs" \
"$SRC" "$DST" "$OTA_KEY" "$VER" \
"$CLI_URL" "$CLI_SHA" "$CLI_SIZE" \
"$NOTIFY_URL" "$NOTIFY_SHA" "$NOTIFY_SIZE"
gcs_cp "$DST" "gs://${GCS_BUCKET}/${METADATA_PATH}" \
"no-cache, no-store, must-revalidate" "application/json"
echo ""
echo ">> published adapter $VER"
echo " cli: $CLI_URL (sha256 $CLI_SHA)"
echo " notify: $NOTIFY_URL"
echo " manifest: $META_URL"
echo " running daemons self-update to $VER within ~1 min."