build: harden local release verification

This commit is contained in:
Peter Steinberger
2026-03-23 17:17:07 -07:00
parent ce75f60ae9
commit ffd722bc2c
5 changed files with 91 additions and 4 deletions

View File

@@ -164,12 +164,24 @@ OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke
`scripts/package-mac-dist.sh` to build, sign, notarize, and package the app;
manual GitHub release asset upload; then `scripts/make_appcast.sh` plus the
`appcast.xml` commit to `main`.
- `scripts/package-mac-dist.sh` now fails closed for release builds if the
bundled app comes out with a debug bundle id, an empty Sparkle feed URL, or a
`CFBundleVersion` below the canonical Sparkle build floor for that short
version. For correction tags, set a higher explicit `APP_BUILD`.
- `scripts/make_appcast.sh` first uses `generate_appcast` from `PATH`, then
falls back to the SwiftPM Sparkle tool output under `apps/macos/.build`.
- For stable tags, the local fallback may update the shared production
`appcast.xml`.
- For beta tags, the local fallback still publishes the mac assets but must not
update the shared production `appcast.xml` unless a separate beta feed exists.
- Treat the local workflow as fallback only. Prefer the CI/CD publish workflow
when it is working.
- After any stable mac publish, verify all of the following before you call the
release finished:
- the GitHub release has `.zip`, `.dmg`, and `.dSYM.zip` assets
- `appcast.xml` on `main` points at the new stable zip
- the packaged app reports the expected short version and a numeric
`CFBundleVersion` at or above the canonical Sparkle build floor
## Run the release sequence

View File

@@ -34,17 +34,27 @@ OpenClaw has three public release lanes:
## Release preflight
- Run `pnpm build` before `pnpm release:check` so the expected `dist/*` release
artifacts exist for the pack validation step
- Run `pnpm release:check` before every tagged release
- Run `RELEASE_TAG=vYYYY.M.D node --import tsx scripts/openclaw-npm-release-check.ts`
(or the matching beta/correction tag) before approval
- npm release preflight fails closed unless the tarball includes both
`dist/control-ui/index.html` and a non-empty `dist/control-ui/assets/` payload
so we do not ship an empty browser dashboard again
- Stable macOS release readiness also includes the updater surfaces:
- the GitHub release must end up with the packaged `.zip`, `.dmg`, and `.dSYM.zip`
- `appcast.xml` on `main` must point at the new stable zip after publish
- the packaged app must keep a non-debug bundle id, a non-empty Sparkle feed
URL, and a `CFBundleVersion` at or above the canonical Sparkle build floor
for that release version
## Public references
- [`.github/workflows/openclaw-npm-release.yml`](https://github.com/openclaw/openclaw/blob/main/.github/workflows/openclaw-npm-release.yml)
- [`scripts/openclaw-npm-release-check.ts`](https://github.com/openclaw/openclaw/blob/main/scripts/openclaw-npm-release-check.ts)
- [`scripts/package-mac-dist.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/package-mac-dist.sh)
- [`scripts/make_appcast.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/make_appcast.sh)
Maintainers use the private release docs in
[`openclaw/maintainers/release/README.md`](https://github.com/openclaw/maintainers/blob/main/release/README.md)

View File

@@ -5,6 +5,16 @@ ROOT=$(cd "$(dirname "$0")/.." && pwd)
ZIP=${1:?"Usage: $0 OpenClaw-<ver>.zip"}
FEED_URL=${2:-"https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml"}
PRIVATE_KEY_FILE=${SPARKLE_PRIVATE_KEY_FILE:-}
find_generate_appcast() {
if command -v generate_appcast >/dev/null 2>&1; then
command -v generate_appcast
return 0
fi
find "$ROOT/apps/macos/.build" -type f -path "*/artifacts/sparkle/Sparkle/bin/generate_appcast" -print -quit 2>/dev/null
}
if [[ -z "$PRIVATE_KEY_FILE" ]]; then
echo "Set SPARKLE_PRIVATE_KEY_FILE to your ed25519 private key (Sparkle)." >&2
exit 1
@@ -52,13 +62,13 @@ cp -f "$NOTES_HTML" "$TMP_DIR/${ZIP_BASE}.html"
DOWNLOAD_URL_PREFIX=${SPARKLE_DOWNLOAD_URL_PREFIX:-"https://github.com/openclaw/openclaw/releases/download/v${VERSION}/"}
export PATH="$ROOT/apps/macos/.build/artifacts/sparkle/Sparkle/bin:$PATH"
if ! command -v generate_appcast >/dev/null; then
echo "generate_appcast not found in PATH. Build Sparkle tools via SwiftPM." >&2
GENERATE_APPCAST="$(find_generate_appcast)"
if [[ -z "$GENERATE_APPCAST" ]]; then
echo "generate_appcast not found. Install Sparkle tooling or build the mac app first so SwiftPM emits the Sparkle binaries." >&2
exit 1
fi
generate_appcast \
"$GENERATE_APPCAST" \
--ed-key-file "$PRIVATE_KEY_FILE" \
--download-url-prefix "$DOWNLOAD_URL_PREFIX" \
--embed-release-notes \

View File

@@ -12,14 +12,29 @@ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
BUILD_ROOT="$ROOT_DIR/apps/macos/.build"
PRODUCT="OpenClaw"
BUILD_CONFIG="${BUILD_CONFIG:-release}"
APP_VERSION_INPUT="${APP_VERSION:-$(cd "$ROOT_DIR" && node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0")}"
# Default to universal binary for distribution builds (supports both Apple Silicon and Intel Macs)
export BUILD_ARCHS="${BUILD_ARCHS:-all}"
export BUILD_CONFIG
# Use release bundle ID (not .debug) so Sparkle auto-update works.
# The .debug suffix in package-mac-app.sh blanks SUFeedURL intentionally for dev builds.
export BUNDLE_ID="${BUNDLE_ID:-ai.openclaw.mac}"
canonical_sparkle_build() {
node --import tsx "$ROOT_DIR/scripts/sparkle-build.ts" canonical-build "$1"
}
# Local fallback releases must not silently fall back to a git-rev-count build number.
# For correction tags, pass a higher explicit APP_BUILD than the canonical floor.
if [[ -z "${APP_BUILD:-}" && "$BUILD_CONFIG" == "release" ]]; then
CANONICAL_APP_BUILD="$(canonical_sparkle_build "$APP_VERSION_INPUT" 2>/dev/null || true)"
if [[ "$CANONICAL_APP_BUILD" =~ ^[0-9]+$ ]]; then
export APP_BUILD="$CANONICAL_APP_BUILD"
fi
fi
"$ROOT_DIR/scripts/package-mac-app.sh"
APP="$ROOT_DIR/dist/OpenClaw.app"
@@ -29,6 +44,9 @@ if [[ ! -d "$APP" ]]; then
fi
VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$APP/Contents/Info.plist" 2>/dev/null || echo "0.0.0")
BUNDLE_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "$APP/Contents/Info.plist" 2>/dev/null || echo "")
ACTUAL_BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "$APP/Contents/Info.plist" 2>/dev/null || echo "")
ACTUAL_FEED_URL=$(/usr/libexec/PlistBuddy -c "Print SUFeedURL" "$APP/Contents/Info.plist" 2>/dev/null || echo "")
ZIP="$ROOT_DIR/dist/OpenClaw-$VERSION.zip"
DMG="$ROOT_DIR/dist/OpenClaw-$VERSION.dmg"
NOTARY_ZIP="$ROOT_DIR/dist/OpenClaw-$VERSION.notary.zip"
@@ -42,6 +60,31 @@ if [[ "$SKIP_NOTARIZE" == "1" ]]; then
NOTARIZE=0
fi
if [[ "$BUILD_CONFIG" == "release" ]]; then
if [[ "$ACTUAL_BUNDLE_ID" == *.debug ]]; then
echo "Error: release packaging produced debug bundle id '$ACTUAL_BUNDLE_ID'." >&2
exit 1
fi
if [[ -z "$ACTUAL_FEED_URL" ]]; then
echo "Error: release packaging produced an empty SUFeedURL." >&2
exit 1
fi
CANONICAL_APP_BUILD="$(canonical_sparkle_build "$VERSION" 2>/dev/null || true)"
if [[ "$CANONICAL_APP_BUILD" =~ ^[0-9]+$ ]]; then
if [[ ! "$BUNDLE_VERSION" =~ ^[0-9]+$ ]]; then
echo "Error: release packaging produced non-numeric CFBundleVersion '$BUNDLE_VERSION'." >&2
exit 1
fi
if (( BUNDLE_VERSION < CANONICAL_APP_BUILD )); then
echo "Error: CFBundleVersion '$BUNDLE_VERSION' is below the canonical Sparkle floor '$CANONICAL_APP_BUILD' for '$VERSION'." >&2
echo "Set APP_BUILD explicitly only when you need a higher correction build." >&2
exit 1
fi
fi
fi
if [[ "$NOTARIZE" == "1" ]]; then
echo "📦 Notary zip: $NOTARY_ZIP"
rm -f "$NOTARY_ZIP"

View File

@@ -329,6 +329,18 @@ async function main() {
for (const path of missing) {
console.error(` - ${path}`);
}
if (
missing.some(
(path) =>
path === "dist/build-info.json" ||
path === "dist/control-ui/index.html" ||
path.startsWith("dist/"),
)
) {
console.error(
"release-check: build artifacts are missing. Run `pnpm build` before `pnpm release:check`.",
);
}
}
if (forbidden.length > 0) {
console.error("release-check: forbidden files in npm pack:");