Always pass --skip-ui and --accept-risk to openclaw onboard so the wizard never prompts for TUI/Web UI selection — bootstrap manages the web UI lifecycle itself. Add a post-onboard spinner to eliminate the silent gap while config-set calls, gateway probing, and web runtime startup run. Fix remediation messages to use `npx denchclaw`.
313 lines
10 KiB
Bash
Executable File
313 lines
10 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"
|
|
ALIAS_PACKAGE_NAME="dench"
|
|
ALIAS_PACKAGE_DIR="packages/dench"
|
|
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
|
|
|
|
# ── telemetry ────────────────────────────────────────────────────────────────
|
|
|
|
if [[ -z "${POSTHOG_KEY:-}" ]]; then
|
|
echo "warning: POSTHOG_KEY not set — telemetry will be disabled in this build"
|
|
fi
|
|
export POSTHOG_KEY="${POSTHOG_KEY:-}"
|
|
export NEXT_PUBLIC_POSTHOG_KEY="${POSTHOG_KEY:-}"
|
|
|
|
# ── 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[@]}"
|
|
|
|
# ── publish alias package (dench → denchclaw) ────────────────────────────────
|
|
|
|
ALIAS_DIR="${ROOT_DIR}/${ALIAS_PACKAGE_DIR}"
|
|
if [[ -d "$ALIAS_DIR" ]]; then
|
|
# Pin the alias package version and its denchclaw dependency to this release.
|
|
node -e "
|
|
const fs = require('fs');
|
|
const pkg = JSON.parse(fs.readFileSync('${ALIAS_DIR}/package.json', 'utf-8'));
|
|
pkg.version = '${VERSION}';
|
|
pkg.dependencies.denchclaw = '^${VERSION}';
|
|
fs.writeFileSync('${ALIAS_DIR}/package.json', JSON.stringify(pkg, null, 2) + '\n');
|
|
"
|
|
echo "publishing ${ALIAS_PACKAGE_NAME}@${VERSION}..."
|
|
if (cd "$ALIAS_DIR" && npm publish --access public --tag latest "${NPM_FLAGS[@]}"); then
|
|
echo "published ${ALIAS_PACKAGE_NAME}@${VERSION}"
|
|
else
|
|
echo "warning: failed to publish ${ALIAS_PACKAGE_NAME}@${VERSION} (non-fatal)"
|
|
echo " npx ${PACKAGE_NAME} still works; ${ALIAS_PACKAGE_NAME} alias is optional"
|
|
fi
|
|
fi
|
|
|
|
# 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 (via dench package)" \
|
|
npx --yes "${ALIAS_PACKAGE_NAME}@${VERSION}" --version
|
|
verify_npx_invocation "npx denchclaw update --help" \
|
|
npx --yes "${ALIAS_PACKAGE_NAME}@${VERSION}" update --help
|
|
verify_npx_invocation "npx denchclaw start --help" \
|
|
npx --yes "${ALIAS_PACKAGE_NAME}@${VERSION}" start --help
|
|
verify_npx_invocation "npx denchclaw stop --help" \
|
|
npx --yes "${ALIAS_PACKAGE_NAME}@${VERSION}" 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} + ${ALIAS_PACKAGE_NAME}@${VERSION}"
|
|
echo "install: npm i -g ${PACKAGE_NAME} (or: npm i -g ${ALIAS_PACKAGE_NAME})"
|