Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Verify release

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: Release tag to verify
required: true
type: string

permissions:
contents: read

jobs:
verify:
runs-on: macos-15
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.repository.default_branch }}
fetch-depth: 0
persist-credentials: false
- name: Download and verify release assets
env:
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "invalid release tag: $RELEASE_TAG" >&2
exit 1
fi
git fetch --force --no-tags origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
if [ "$(git cat-file -t "refs/tags/${RELEASE_TAG}")" != "tag" ]; then
echo "release tag must be annotated" >&2
exit 1
fi
tag_object="$(git rev-parse "refs/tags/${RELEASE_TAG}")"
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}"
ref_json="$(curl --fail --silent --show-error --location --retry 3 \
--header "Authorization: Bearer ${GITHUB_TOKEN}" \
--header 'Accept: application/vnd.github+json' \
"${api}/git/ref/tags/${RELEASE_TAG}")"
if [ "$(jq -r '.object.type' <<<"$ref_json")" != "tag" ] || \
[ "$(jq -r '.object.sha' <<<"$ref_json")" != "$tag_object" ]; then
echo "release tag does not match the annotated GitHub tag object" >&2
exit 1
fi
tag_json="$(curl --fail --silent --show-error --location --retry 3 \
--header "Authorization: Bearer ${GITHUB_TOKEN}" \
--header 'Accept: application/vnd.github+json' \
"${api}/git/tags/${tag_object}")"
if [ "$(jq -r '.verification.verified' <<<"$tag_json")" != "true" ]; then
echo "release tag signature is not verified by GitHub" >&2
exit 1
fi
tag_commit="$(git rev-list -n 1 "refs/tags/${RELEASE_TAG}")"
if ! git merge-base --is-ancestor "$tag_commit" HEAD; then
echo "release tag is not reachable from protected default-branch history" >&2
exit 1
fi
version="${RELEASE_TAG#v}"
base="https://github.com/${GITHUB_REPOSITORY}/releases/download/${RELEASE_TAG}"
archive="CrawlBar-v${version}-macos.zip"
checksum="${archive}.sha256"
curl --fail --location --retry 3 --output "$archive" "${base}/${archive}"
curl --fail --location --retry 3 --output "$checksum" "${base}/${checksum}"
expected="$(awk -v name="$archive" '$2 == name { print $1 }' "$checksum")"
if [[ ! "$expected" =~ ^[0-9a-f]{64}$ ]]; then
echo "checksum asset does not bind exactly to $archive" >&2
exit 1
fi
actual="$(shasum -a 256 "$archive" | awk '{ print $1 }')"
test "$actual" = "$expected"
CRAWLBAR_EXPECTED_VERSION="$version" Scripts/verify_release.sh --require-notarized "$archive"
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
DerivedData/
CrawlBar.app/
dist/
.mac-release.env
.mac-release.local.env
*.xcuserdata/
*.xcodeproj/
*.xcworkspace/
Expand Down
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Changelog

## v0.4.1 - Unreleased
## v0.4.1 - 2026-07-09

### Changes

- Distribute notarized universal macOS builds signed by the OpenClaw Foundation, including Homebrew installs, so the app identity persists across upgrades.

### Fixes

- Accept current `crawlkit/control` manifests when optional descriptions or branding metadata are omitted.

## v0.4.0 - 2026-06-19

Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,13 @@ Scripts/package_app.sh

The packaged `.app` is written to `dist/CrawlBar.app`.

Local and CI packages use ad-hoc signing and do not need release credentials.
Official release packages fail closed unless they use the OpenClaw Foundation
Developer ID identity. Runtime keychain and notarization profile locators belong
in the ignored `.mac-release.local.env`, never in committed configuration.

## Releases

Release notes are kept in [CHANGELOG.md](CHANGELOG.md). The packaged app bundle
version is generated by `Scripts/package_app.sh`.
version is read from `version.env`. Official artifacts are universal, hardened,
notarized, stapled, and verified by `Scripts/package_release.sh`.
79 changes: 65 additions & 14 deletions Scripts/package_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,82 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DIST_DIR="$ROOT_DIR/dist"
APP_DIR="$DIST_DIR/CrawlBar.app"
STAGING_APP_DIR="$DIST_DIR/.CrawlBar.app.tmp.$$"
BUILD_DIR="${TMPDIR:-/tmp}/crawlbar-package.$$"
CONTENTS_DIR="$STAGING_APP_DIR/Contents"
MACOS_DIR="$CONTENTS_DIR/MacOS"
HELPERS_DIR="$CONTENTS_DIR/Helpers"
RESOURCES_DIR="$CONTENTS_DIR/Resources"
EXPECTED_IDENTITY="Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)"

# shellcheck source=version.env
source "$ROOT_DIR/version.env"

official_release="${CRAWLBAR_OFFICIAL_RELEASE:-0}"
universal_build="${CRAWLBAR_UNIVERSAL:-$official_release}"
signing_identity="${CRAWLBAR_CODESIGN_IDENTITY:-${MAC_RELEASE_CODESIGN_IDENTITY:-}}"

if [ "$official_release" = "1" ] && [ "$signing_identity" != "$EXPECTED_IDENTITY" ]; then
echo "official release requires signing identity: $EXPECTED_IDENTITY" >&2
exit 1
fi

cd "$ROOT_DIR"
swift build -c release --product CrawlBar >&2
swift build -c release --product crawlbarctl >&2
mkdir -p "$DIST_DIR"

if [ "$universal_build" = "1" ]; then
for arch in arm64 x86_64; do
swift build \
-c release \
--triple "${arch}-apple-macosx14.0" \
--scratch-path "$BUILD_DIR/$arch" \
--product CrawlBar >&2
swift build \
-c release \
--triple "${arch}-apple-macosx14.0" \
--scratch-path "$BUILD_DIR/$arch" \
--product crawlbarctl >&2
done
arm_release="$BUILD_DIR/arm64/arm64-apple-macosx/release"
intel_release="$BUILD_DIR/x86_64/x86_64-apple-macosx/release"
resource_bundle="$arm_release/CrawlBar_CrawlBar.bundle"
else
swift build -c release --product CrawlBar >&2
swift build -c release --product crawlbarctl >&2
native_release="$ROOT_DIR/.build/release"
resource_bundle="$native_release/CrawlBar_CrawlBar.bundle"
fi

rm -rf "$STAGING_APP_DIR"
trap 'rm -rf "$STAGING_APP_DIR"' EXIT
trap 'rm -rf "$STAGING_APP_DIR" "$BUILD_DIR"' EXIT
mkdir -p "$MACOS_DIR" "$HELPERS_DIR" "$RESOURCES_DIR"

cp ".build/release/CrawlBar" "$MACOS_DIR/CrawlBar"
cp ".build/release/crawlbarctl" "$HELPERS_DIR/crawlbar"
RESOURCE_BUNDLE=".build/release/CrawlBar_CrawlBar.bundle"
if [ -d "$RESOURCE_BUNDLE" ]; then
cp -R "$RESOURCE_BUNDLE" "$RESOURCES_DIR/CrawlBar_CrawlBar.bundle"
if [ "$universal_build" = "1" ]; then
lipo -create \
"$arm_release/CrawlBar" \
"$intel_release/CrawlBar" \
-output "$MACOS_DIR/CrawlBar"
lipo -create \
"$arm_release/crawlbarctl" \
"$intel_release/crawlbarctl" \
-output "$HELPERS_DIR/crawlbar"
else
cp "$native_release/CrawlBar" "$MACOS_DIR/CrawlBar"
cp "$native_release/crawlbarctl" "$HELPERS_DIR/crawlbar"
fi

if [ -d "$resource_bundle" ]; then
cp -R "$resource_bundle" "$RESOURCES_DIR/CrawlBar_CrawlBar.bundle"
if ! find "$RESOURCES_DIR/CrawlBar_CrawlBar.bundle" -type f -print -quit | grep -q .; then
echo "SwiftPM resource bundle is empty: $RESOURCE_BUNDLE" >&2
echo "SwiftPM resource bundle is empty: $resource_bundle" >&2
exit 1
fi
else
echo "missing SwiftPM resource bundle: $RESOURCE_BUNDLE" >&2
echo "missing SwiftPM resource bundle: $resource_bundle" >&2
exit 1
fi
Scripts/generate_app_icon.swift "$RESOURCES_DIR/CrawlBar.icns"

cat > "$CONTENTS_DIR/Info.plist" <<'PLIST'
cat > "$CONTENTS_DIR/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
Expand All @@ -49,9 +96,9 @@ cat > "$CONTENTS_DIR/Info.plist" <<'PLIST'
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.4.0</string>
<string>$CRAWLBAR_VERSION</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>$CRAWLBAR_BUILD_NUMBER</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>LSUIElement</key>
Expand All @@ -62,7 +109,11 @@ cat > "$CONTENTS_DIR/Info.plist" <<'PLIST'
</plist>
PLIST

if command -v codesign >/dev/null 2>&1; then
if [ "$official_release" = "1" ]; then
codesign --force --options runtime --timestamp --sign "$signing_identity" "$HELPERS_DIR/crawlbar"
codesign --force --options runtime --timestamp --sign "$signing_identity" "$STAGING_APP_DIR"
"$ROOT_DIR/Scripts/verify_release.sh" "$STAGING_APP_DIR"
elif command -v codesign >/dev/null 2>&1; then
codesign --force --deep --sign - "$STAGING_APP_DIR" >/dev/null
fi

Expand Down
51 changes: 51 additions & 0 deletions Scripts/package_release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DIST_DIR="$ROOT_DIR/dist"

# shellcheck source=version.env
source "$ROOT_DIR/version.env"

tag="v$CRAWLBAR_VERSION"
if [ "$(git -C "$ROOT_DIR" describe --tags --exact-match HEAD 2>/dev/null || true)" != "$tag" ]; then
echo "release must run from exact tag $tag" >&2
exit 1
fi
if [ -n "$(git -C "$ROOT_DIR" status --porcelain)" ]; then
echo "release requires a clean checkout" >&2
exit 1
fi

if [ -z "${NOTARYTOOL_KEYCHAIN_PROFILE:-}" ]; then
echo "NOTARYTOOL_KEYCHAIN_PROFILE is required at runtime" >&2
exit 1
fi

export CRAWLBAR_OFFICIAL_RELEASE=1
"$ROOT_DIR/Scripts/package_app.sh" >/dev/null

app="$DIST_DIR/CrawlBar.app"
archive="$DIST_DIR/CrawlBar-v$CRAWLBAR_VERSION-macos.zip"
checksum="$archive.sha256"

create_archive() {
rm -f "$archive"
(
cd "$DIST_DIR"
COPYFILE_DISABLE=1 /usr/bin/zip -q -r -X "$(basename "$archive")" "$(basename "$app")"
)
}

rm -f "$checksum"
create_archive
xcrun notarytool submit "$archive" --keychain-profile "$NOTARYTOOL_KEYCHAIN_PROFILE" --wait
xcrun stapler staple "$app"
xcrun stapler validate "$app"

create_archive
"$ROOT_DIR/Scripts/verify_release.sh" --require-notarized "$archive"

(cd "$DIST_DIR" && shasum -a 256 "$(basename "$archive")" > "$(basename "$checksum")")
echo "$archive"
echo "$checksum"
Loading