openclaw/scripts/deploy.sh
kumarabhirup af45d4d17b
CLI: add managed web runtime with start/stop/update commands and major-version gating
Refactor bootstrap to use a managed web runtime lifecycle instead of
ad-hoc standalone server spawning. The managed runtime copies packaged
Next.js assets into ~/.openclaw-dench/web-runtime/, tracks deployment
state via manifest/process metadata, and cleanly separates Dench-owned
processes from foreign listeners on the target port.

- Fix false-negative web readiness when /api/profiles returns null
  activeProfile (first-run regression).
- Add `dench start` (start without updating assets), `dench stop`
  (terminate only Dench-managed web server), and `dench update`
  (refresh web runtime with major-version OpenClaw update gate).
- Major-version transitions (e.g. v2->v3) require mandatory OpenClaw
  update; non-interactive mode fails closed without --yes.
- All lifecycle commands show the ASCII banner/logo animation.
- Deploy smoke checks now verify update/stop/start --help paths.
2026-03-04 16:32:58 -08:00

282 lines
8.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# deploy.sh — build and publish denchclaw to npm
#
# Versioning convention (standard semver):
# --bump <kind> Increment current package version.
# kind: major | minor | patch
# 2.0.0 --bump patch => 2.0.1
# --version <ver> Publish an explicit semver version (x.y.z).
# (no flag) Publish whatever version is already in package.json.
#
# Flags:
# --skip-tests Skip running tests before build/publish.
# --skip-npx-smoke Skip post-publish npx binary verification.
#
# Environment:
# NPM_TOKEN Required. npm auth token for publishing.
set -euo pipefail
PACKAGE_NAME="denchclaw"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT_DIR"
# ── helpers ──────────────────────────────────────────────────────────────────
die() { echo "error: $*" >&2; exit 1; }
current_version() {
node -p "require('./package.json').version"
}
# Check whether a specific version is already on the npm registry.
npm_version_exists() {
local v="$1"
npm view "${PACKAGE_NAME}@${v}" version 2>/dev/null | grep -q "${v}" 2>/dev/null
}
is_plain_semver() {
local v="$1"
[[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
}
bump_semver() {
local current="$1"
local kind="$2"
if ! is_plain_semver "$current"; then
die "current version must be plain semver (x.y.z) for --bump, got: $current"
fi
local major minor patch
IFS='.' read -r major minor patch <<<"$current"
case "$kind" in
major)
echo "$((major + 1)).0.0"
;;
minor)
echo "${major}.$((minor + 1)).0"
;;
patch)
echo "${major}.${minor}.$((patch + 1))"
;;
*)
die "--bump requires one of: major, minor, patch"
;;
esac
}
verify_npx_command() {
local version="$1"
local label="$2"
shift 2
local attempts=15
local delay_seconds=2
local output=""
local temp_dir
temp_dir="$(mktemp -d)"
for ((i = 1; i <= attempts; i++)); do
if output="$(cd "$temp_dir" && "$@" 2>/dev/null)"; then
if [[ "$output" == *"$version"* ]]; then
echo "verified ${label}: ${output}"
rm -rf "$temp_dir"
return 0
fi
fi
sleep "$delay_seconds"
done
rm -rf "$temp_dir"
echo "error: failed to verify ${label} for ${PACKAGE_NAME}@${version}" >&2
return 1
}
verify_npx_invocation() {
local label="$1"
shift
local attempts=15
local delay_seconds=2
local temp_dir
temp_dir="$(mktemp -d)"
for ((i = 1; i <= attempts; i++)); do
if (cd "$temp_dir" && "$@" >/dev/null 2>&1); then
echo "verified ${label}"
rm -rf "$temp_dir"
return 0
fi
sleep "$delay_seconds"
done
rm -rf "$temp_dir"
echo "error: failed to verify ${label}" >&2
return 1
}
# ── parse args ───────────────────────────────────────────────────────────────
MODE=""
BUMP_KIND=""
EXPLICIT_VERSION=""
DRY_RUN=false
SKIP_BUILD=false
SKIP_TESTS=false
SKIP_NPX_SMOKE=false
set_mode() {
local next="$1"
if [[ -n "$MODE" && "$MODE" != "$next" ]]; then
die "choose only one version mode: --version <x.y.z> or --bump <major|minor|patch>"
fi
MODE="$next"
}
while [[ $# -gt 0 ]]; do
case $1 in
--version)
set_mode "version"
EXPLICIT_VERSION="${2:?--version requires a semver argument (x.y.z)}"
shift 2
;;
--bump)
set_mode "bump"
BUMP_KIND="${2:?--bump requires one of: major, minor, patch}"
shift 2
;;
--upstream)
die "--upstream has been removed. Use --version <x.y.z> or --bump <major|minor|patch>."
;;
--dry-run)
DRY_RUN=true
shift
;;
--skip-build)
SKIP_BUILD=true
shift
;;
--skip-tests)
SKIP_TESTS=true
shift
;;
--skip-npx-smoke)
SKIP_NPX_SMOKE=true
shift
;;
--help|-h)
sed -n '2,/^[^#]/{ /^#/s/^# \{0,1\}//p; }' "$0"
exit 0
;;
*)
die "unknown argument: $1 (see --help)"
;;
esac
done
# ── auth ─────────────────────────────────────────────────────────────────────
if [[ -z "${NPM_TOKEN:-}" ]]; then
die "NPM_TOKEN environment variable is required"
fi
# Write a temporary .npmrc for auth (npm_config_ env vars can't encode
# registry-scoped keys because they contain slashes and colons).
NPMRC_TEMP="${ROOT_DIR}/.npmrc.deploy"
trap 'rm -f "$NPMRC_TEMP"' EXIT
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > "$NPMRC_TEMP"
NPM_FLAGS=(--userconfig "$NPMRC_TEMP")
# ── compute version ─────────────────────────────────────────────────────────
CURRENT="$(current_version)"
case "$MODE" in
version)
if ! is_plain_semver "$EXPLICIT_VERSION"; then
die "--version must be plain semver (x.y.z), got: $EXPLICIT_VERSION"
fi
VERSION="$EXPLICIT_VERSION"
echo "explicit version: $CURRENT$VERSION"
;;
bump)
VERSION="$(bump_semver "$CURRENT" "$BUMP_KIND")"
echo "semver bump (${BUMP_KIND}): $CURRENT$VERSION"
;;
*)
VERSION="$CURRENT"
echo "publishing current version: $VERSION"
;;
esac
if npm_version_exists "$VERSION"; then
die "version $VERSION already exists on npm. Use --bump <major|minor|patch> or --version <x.y.z>."
fi
if [[ "$DRY_RUN" == true ]]; then
echo "[dry-run] would publish ${PACKAGE_NAME}@${VERSION}"
exit 0
fi
# ── set version ──────────────────────────────────────────────────────────────
npm version "$VERSION" --no-git-tag-version --allow-same-version "${NPM_FLAGS[@]}"
# ── pre-flight: tests ────────────────────────────────────────────────────────
if [[ "$SKIP_TESTS" != true ]] && [[ "$SKIP_BUILD" != true ]]; then
echo "running tests..."
pnpm test
fi
# ── build ────────────────────────────────────────────────────────────────────
# The `prepack` script (triggered by `npm publish`) runs the DenchClaw build chain:
# pnpm build && pnpm web:build && pnpm web:prepack
# Running `pnpm build` here is a redundant fail-fast: catch CLI build errors
# before committing to a publish attempt.
if [[ "$SKIP_BUILD" != true ]]; then
echo "building..."
pnpm build
echo "building web app (standalone verification)..."
pnpm web:build
fi
# ── publish ──────────────────────────────────────────────────────────────────
# Always tag as "latest" — npm skips the latest tag for prerelease versions
# by default, but we want `npm i -g denchclaw` to always resolve to
# the most recently published version.
echo "publishing ${PACKAGE_NAME}@${VERSION}..."
npm publish --access public --tag latest "${NPM_FLAGS[@]}"
# Verify published npx flows for both CLI aliases.
if [[ "$SKIP_NPX_SMOKE" != true ]]; then
echo "verifying npx binaries..."
verify_npx_command "$VERSION" "npx denchclaw" \
npx --yes "${PACKAGE_NAME}@${VERSION}" --version
verify_npx_command "$VERSION" "npx dench" \
npx --yes --package "${PACKAGE_NAME}@${VERSION}" dench --version
verify_npx_invocation "npx dench update --help" \
npx --yes --package "${PACKAGE_NAME}@${VERSION}" dench update --help
verify_npx_invocation "npx dench start --help" \
npx --yes --package "${PACKAGE_NAME}@${VERSION}" dench start --help
verify_npx_invocation "npx dench stop --help" \
npx --yes --package "${PACKAGE_NAME}@${VERSION}" dench stop --help
fi
# Verify the standalone web app was included in the published package.
# `prepack` should have built it; if this file is missing, the web UI
# won't work for users who install globally.
STANDALONE_SERVER="apps/web/.next/standalone/apps/web/server.js"
if [[ ! -f "$STANDALONE_SERVER" ]]; then
echo "warning: standalone web app build not found after publish ($STANDALONE_SERVER)"
echo " users may not get a working Web UI — check the prepack step"
fi
echo ""
echo "published ${PACKAGE_NAME}@${VERSION}"
echo "install: npm i -g ${PACKAGE_NAME}"