refactor: extract release workflow inline bash into modular scripts (#175)

Split monolithic inline bash from release.yml and release-packages.yml
into standalone scripts under scripts/release/ for easier debugging
and manual execution.

New scripts:
- bump-version.js: version bumping across package.json/tauri/Cargo
- stage-sidecar.sh: stage + verify sidecar binary for Tauri bundler
- sign-and-notarize-macos.sh: macOS code signing and notarization
- repackage-dmg.sh: re-create and notarize DMG post-signing
- upload-macos-artifacts.sh: re-upload notarized artifacts to release
- package-cli-tarball.sh: package CLI binary into release tarball
- build-linux-arm64.sh: build Linux arm64 CLI tarball
- update-homebrew.sh: render and commit Homebrew formula to tap
- build-apt-packages.sh: build .deb packages and apt repository
- publish-npm.sh: stamp version and publish npm package

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Steven Enamakel
2026-04-01 12:13:11 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 6f8b5d6c9f
commit 88a4647dae
12 changed files with 638 additions and 394 deletions
+20 -146
View File
@@ -60,33 +60,17 @@ jobs:
sudo apt-get install -y --no-install-recommends \
pkg-config libssl-dev build-essential
- name: Build CLI binary
run: cargo build --release --bin openhuman-core
env:
OPENHUMAN_SENTRY_DSN: ${{ vars.OPENHUMAN_SENTRY_DSN }}
- name: Package tarball and upload to release
- name: Build CLI binary and package tarball
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENHUMAN_SENTRY_DSN: ${{ vars.OPENHUMAN_SENTRY_DSN }}
run: |
set -euo pipefail
cargo build --release --bin openhuman-core
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}"
TARGET="aarch64-unknown-linux-gnu"
TARBALL="openhuman-core-${VERSION}-${TARGET}.tar.gz"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
cp target/release/openhuman-core "$WORK/"
chmod +x "$WORK/openhuman-core"
tar -czf "$TARBALL" -C "$WORK" openhuman-core
openssl dgst -sha256 -r "$TARBALL" | awk '{print $1}' > "${TARBALL}.sha256"
gh release upload "${{ github.event.release.tag_name }}" \
"$TARBALL" "${TARBALL}.sha256" \
--repo tinyhumansai/openhuman --clobber
echo "[pkg] Uploaded $TARBALL"
bash scripts/release/package-cli-tarball.sh \
target/release/openhuman-core \
"${VERSION#v}" \
aarch64-unknown-linux-gnu
# ────────────────────────────────────────────────────────────────────────────
# 2. Update Homebrew tap
@@ -111,55 +95,14 @@ jobs:
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: tap
- name: Download tarballs and compute checksums
- name: Update Homebrew formula
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${{ github.event.release.tag_name }}"
VERSION="${TAG#v}"
mkdir -p /tmp/tarballs
for row in \
"aarch64-apple-darwin:SHA256_MACOS_ARM64" \
"x86_64-apple-darwin:SHA256_MACOS_X64" \
"x86_64-unknown-linux-gnu:SHA256_LINUX_X64" \
"aarch64-unknown-linux-gnu:SHA256_LINUX_ARM64"
do
TARGET="${row%%:*}"
VAR="${row##*:}"
TARBALL="openhuman-core-${VERSION}-${TARGET}.tar.gz"
echo "[homebrew] Downloading $TARBALL ..."
gh release download "$TAG" \
--pattern "$TARBALL" \
--repo tinyhumansai/openhuman \
--dir /tmp/tarballs
SHA=$(openssl dgst -sha256 -r "/tmp/tarballs/$TARBALL" | awk '{print $1}')
echo "${VAR}=${SHA}" >> "$GITHUB_ENV"
echo "[homebrew] $TARGET → $SHA"
done
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
- name: Render and commit formula
run: |
set -euo pipefail
mkdir -p tap/Formula
sed \
-e "s/@VERSION@/${VERSION}/g" \
-e "s/@SHA256_MACOS_ARM64@/${SHA256_MACOS_ARM64}/g" \
-e "s/@SHA256_MACOS_X64@/${SHA256_MACOS_X64}/g" \
-e "s/@SHA256_LINUX_X64@/${SHA256_LINUX_X64}/g" \
-e "s/@SHA256_LINUX_ARM64@/${SHA256_LINUX_ARM64}/g" \
src/packages/homebrew/openhuman.rb > tap/Formula/openhuman.rb
cd tap
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/openhuman.rb
git diff --cached --quiet && echo "[homebrew] No changes to commit." && exit 0
git commit -m "chore: update formula to v${VERSION}"
git push
bash src/scripts/release/update-homebrew.sh \
"${{ github.event.release.tag_name }}" \
src/packages/homebrew/openhuman.rb \
tap
# ────────────────────────────────────────────────────────────────────────────
# 3. Build Debian apt repository and deploy to GitHub Pages
@@ -191,76 +134,21 @@ jobs:
echo "$APT_SIGNING_KEY" | gpg --batch --import
gpg --list-secret-keys
- name: Download Linux CLI tarballs from release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${{ github.event.release.tag_name }}"
VERSION="${TAG#v}"
mkdir -p /tmp/tarballs
for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
TARBALL="openhuman-core-${VERSION}-${target}.tar.gz"
gh release download "$TAG" \
--pattern "$TARBALL" \
--repo tinyhumansai/openhuman \
--dir /tmp/tarballs
echo "[apt] Downloaded $TARBALL"
done
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
- name: Extract binaries and build .deb packages
run: |
set -euo pipefail
mkdir -p /tmp/bins
tar -xzf "/tmp/tarballs/openhuman-core-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" \
-C /tmp/bins
mv /tmp/bins/openhuman-core /tmp/bins/openhuman-core-amd64
tar -xzf "/tmp/tarballs/openhuman-core-${VERSION}-aarch64-unknown-linux-gnu.tar.gz" \
-C /tmp/bins
mv /tmp/bins/openhuman-core /tmp/bins/openhuman-core-arm64
chmod +x /tmp/bins/openhuman-core-amd64 /tmp/bins/openhuman-core-arm64
bash packages/deb/build.sh /tmp/bins/openhuman-core-amd64 "${VERSION}" amd64
bash packages/deb/build.sh /tmp/bins/openhuman-core-arm64 "${VERSION}" arm64
ls -lh openhuman_*.deb
- name: Build apt repository
env:
APT_SIGNING_KEY_ID: ${{ secrets.APT_SIGNING_KEY_ID }}
run: |
bash scripts/build-apt-repo.sh /tmp/apt-repo \
"openhuman_${VERSION}_amd64.deb" \
"openhuman_${VERSION}_arm64.deb"
- name: Checkout gh-pages branch
uses: actions/checkout@v4
with:
ref: gh-pages
path: gh-pages
# Create the branch if it doesn't exist
fetch-depth: 0
- name: Update apt/ directory on gh-pages
- name: Build .deb packages, apt repo, and deploy to gh-pages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APT_SIGNING_KEY_ID: ${{ secrets.APT_SIGNING_KEY_ID }}
run: |
set -euo pipefail
mkdir -p gh-pages/apt
# Remove old apt content, replace with freshly built repo
rm -rf gh-pages/apt/*
cp -r /tmp/apt-repo/. gh-pages/apt/
cd gh-pages
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add apt/
git diff --cached --quiet && echo "[apt] No changes." && exit 0
git commit -m "chore(apt): publish v${VERSION}"
git push origin gh-pages
bash scripts/release/build-apt-packages.sh \
"${{ github.event.release.tag_name }}" \
--deploy-gh-pages gh-pages
# ────────────────────────────────────────────────────────────────────────────
# 4. Publish npm package
@@ -285,21 +173,7 @@ jobs:
- name: Set version and publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}"
cd packages/npm
# Stamp version without creating a git commit
npm version "$VERSION" --no-git-tag-version
# SKIP_OPENHUMAN_BINARY_DOWNLOAD prevents postinstall from running
# during publish (the binary doesn't exist yet on the publish runner)
SKIP_OPENHUMAN_BINARY_DOWNLOAD=1 npm publish --access public
echo "[npm] Published openhuman@${VERSION}"
run: bash scripts/release/publish-npm.sh "${{ github.event.release.tag_name }}"
# ────────────────────────────────────────────────────────────────────────────
# 5. Smoke test: Homebrew
+23 -248
View File
@@ -93,65 +93,7 @@ jobs:
- name: Compute next version and sync release files
id: bump
env:
RELEASE_TYPE: ${{ inputs.release_type }}
run: |
node <<'NODE'
const fs = require('fs');
const releaseType = process.env.RELEASE_TYPE;
const allowed = new Set(['patch', 'minor', 'major']);
if (!allowed.has(releaseType)) {
throw new Error(`Invalid release_type: ${releaseType}`);
}
const packagePath = 'app/package.json';
const tauriPath = 'app/src-tauri/tauri.conf.json';
const cargoPath = 'app/src-tauri/Cargo.toml';
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
const match = String(pkg.version || '').match(/^(\d+)\.(\d+)\.(\d+)$/);
if (!match) {
throw new Error(`package.json version must be SemVer X.Y.Z, found: ${pkg.version}`);
}
let major = Number(match[1]);
let minor = Number(match[2]);
let patch = Number(match[3]);
if (releaseType === 'major') {
major += 1;
minor = 0;
patch = 0;
} else if (releaseType === 'minor') {
minor += 1;
patch = 0;
} else {
patch += 1;
}
const nextVersion = `${major}.${minor}.${patch}`;
pkg.version = nextVersion;
fs.writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`);
const tauri = JSON.parse(fs.readFileSync(tauriPath, 'utf8'));
tauri.version = nextVersion;
fs.writeFileSync(tauriPath, `${JSON.stringify(tauri, null, 2)}\n`);
const cargo = fs.readFileSync(cargoPath, 'utf8');
const updatedCargo = cargo.replace(
/(\[package\][\s\S]*?^version\s*=\s*")([^"]+)(")/m,
`$1${nextVersion}$3`,
);
if (updatedCargo === cargo) {
throw new Error('Failed to update [package].version in app/src-tauri/Cargo.toml');
}
fs.writeFileSync(cargoPath, updatedCargo);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${nextVersion}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `tag=v${nextVersion}\n`);
NODE
run: node scripts/release/bump-version.js "${{ inputs.release_type }}"
- name: Ensure tag does not already exist
env:
@@ -412,44 +354,11 @@ jobs:
- name: Stage sidecar for Tauri bundler
shell: bash
run: |
mkdir -p app/src-tauri/binaries
EXE_SUFFIX=""
if [[ "$MATRIX_TARGET" == *"windows"* ]]; then
EXE_SUFFIX=".exe"
fi
SOURCE="$CORE_TARGET_DIR/${CORE_BIN_NAME}${EXE_SUFFIX}"
DEST="app/src-tauri/binaries/${SIDECAR_BASE}-${MATRIX_TARGET}${EXE_SUFFIX}"
cp "$SOURCE" "$DEST"
if [[ "$MATRIX_TARGET" != *"windows"* ]]; then
chmod +x "$DEST"
fi
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
CORE_TARGET_DIR: ${{ steps.core-paths.outputs.core_target_dir }}
CORE_BIN_NAME: ${{ steps.core-paths.outputs.core_bin_name }}
SIDECAR_BASE: ${{ steps.core-paths.outputs.sidecar_base }}
- name: Verify staged sidecar for bundler (all platforms)
shell: bash
run: |
EXE_SUFFIX=""
if [[ "$MATRIX_TARGET" == *"windows"* ]]; then
EXE_SUFFIX=".exe"
fi
SIDE_CAR_PATH="app/src-tauri/binaries/${SIDECAR_BASE}-${MATRIX_TARGET}${EXE_SUFFIX}"
echo "Checking staged sidecar: $SIDE_CAR_PATH"
if [ ! -f "$SIDE_CAR_PATH" ]; then
echo "Missing staged sidecar binary: $SIDE_CAR_PATH"
exit 1
fi
if [ "$MATRIX_PLATFORM" != "windows-latest" ] && [ ! -x "$SIDE_CAR_PATH" ]; then
echo "Staged sidecar is not executable: $SIDE_CAR_PATH"
exit 1
fi
env:
MATRIX_PLATFORM: ${{ matrix.settings.platform }}
MATRIX_TARGET: ${{ matrix.settings.target }}
SIDECAR_BASE: ${{ steps.core-paths.outputs.sidecar_base }}
bash scripts/release/stage-sidecar.sh \
"${{ matrix.settings.target }}" \
"${{ steps.core-paths.outputs.core_target_dir }}" \
"${{ steps.core-paths.outputs.core_bin_name }}" \
"${{ steps.core-paths.outputs.sidecar_base }}"
- name: Resolve standalone core CLI artifact path
id: cli-paths
@@ -536,84 +445,9 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -euo pipefail
APP_PATH="${{ steps.locate-app.outputs.app_path }}"
ENTITLEMENTS="app/src-tauri/entitlements.sidecar.plist"
# Ensure signing identity is available — re-import cert into a keychain
# in case tauri-action's keychain was cleaned up
KEYCHAIN="resign-$$.keychain-db"
KEYCHAIN_PW="$(openssl rand -base64 24)"
CERT_FILE="$(mktemp /tmp/cert-XXXXXX.p12)"
echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > "$CERT_FILE"
security create-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN"
security import "$CERT_FILE" -k "$KEYCHAIN" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PW" "$KEYCHAIN"
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
rm -f "$CERT_FILE"
echo "Signing identity imported into $KEYCHAIN"
echo "=== Signing .app contents and bundle ==="
echo "Bundle contents:"
ls -la "$APP_PATH/Contents/MacOS/"
MAIN_EXE="$(defaults read "$APP_PATH/Contents/Info.plist" CFBundleExecutable 2>/dev/null || echo "OpenHuman")"
echo "Main executable (from plist): $MAIN_EXE"
# Sign all non-main binaries (sidecars) first
for bin in "$APP_PATH/Contents/MacOS/"*; do
[ -f "$bin" ] && [ -x "$bin" ] || continue
BASENAME="$(basename "$bin")"
[ "$BASENAME" = "$MAIN_EXE" ] && continue
echo " Signing sidecar: $BASENAME"
codesign --force --options runtime \
--entitlements "$ENTITLEMENTS" \
--sign "$APPLE_SIGNING_IDENTITY" \
--timestamp \
"$bin"
done
# Sign sidecars in Resources/ if any
for bin in "$APP_PATH/Contents/Resources/"openhuman-core-*; do
[ -f "$bin" ] || continue
echo " Signing resource sidecar: $(basename "$bin")"
codesign --force --options runtime \
--entitlements "$ENTITLEMENTS" \
--sign "$APPLE_SIGNING_IDENTITY" \
--timestamp \
"$bin"
done
# Sign the .app bundle (signs main exe + updates seal)
echo " Signing .app bundle..."
codesign --force --options runtime \
--entitlements "$ENTITLEMENTS" \
--sign "$APPLE_SIGNING_IDENTITY" \
--timestamp \
"$APP_PATH"
echo "=== Verifying signatures ==="
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
echo "=== Notarizing ==="
NOTARIZE_ZIP="$(mktemp /tmp/OpenHuman-notarize-XXXXXX.zip)"
ditto -c -k --keepParent "$APP_PATH" "$NOTARIZE_ZIP"
xcrun notarytool submit "$NOTARIZE_ZIP" \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
rm -f "$NOTARIZE_ZIP"
echo "=== Stapling ==="
xcrun stapler staple "$APP_PATH"
echo "=== Notarization complete ==="
bash scripts/release/sign-and-notarize-macos.sh \
"${{ steps.locate-app.outputs.app_path }}" \
"app/src-tauri/entitlements.sidecar.plist"
- name: Re-package DMG after notarization
if: matrix.settings.platform == 'macos-latest'
@@ -623,30 +457,9 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -euo pipefail
APP_PATH="${{ steps.locate-app.outputs.app_path }}"
BUNDLE_DIR="${{ steps.locate-app.outputs.bundle_dir }}"
DMG_PATH="$(find "$BUNDLE_DIR/dmg" -name '*.dmg' -maxdepth 1 2>/dev/null | head -1)"
if [ -z "$DMG_PATH" ]; then
echo "No DMG found — skipping DMG re-package"
exit 0
fi
echo "Re-creating DMG with notarized .app..."
DMG_TEMP="$(mktemp /tmp/OpenHuman-XXXXXX.dmg)"
hdiutil create -volname "OpenHuman" -srcfolder "$APP_PATH" -ov -format UDZO "$DMG_TEMP"
mv "$DMG_TEMP" "$DMG_PATH"
echo "Notarizing DMG..."
xcrun notarytool submit "$DMG_PATH" \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
xcrun stapler staple "$DMG_PATH"
echo "DMG notarization complete: $DMG_PATH"
bash scripts/release/repackage-dmg.sh \
"${{ steps.locate-app.outputs.app_path }}" \
"${{ steps.locate-app.outputs.bundle_dir }}"
- name: Re-upload notarized macOS artifacts to release
if: matrix.settings.platform == 'macos-latest'
@@ -655,33 +468,11 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
run: |
set -euo pipefail
BUNDLE_DIR="${{ steps.locate-app.outputs.bundle_dir }}"
APP_PATH="${{ steps.locate-app.outputs.app_path }}"
VERSION="${{ needs.prepare-release.outputs.version }}"
ARCH="${{ matrix.settings.target }}"
# Re-upload the DMG (overwrite the one tauri-action uploaded)
DMG_PATH="$(find "$BUNDLE_DIR/dmg" -name '*.dmg' -maxdepth 1 2>/dev/null | head -1)"
if [ -n "$DMG_PATH" ]; then
DMG_NAME="$(basename "$DMG_PATH")"
echo "Deleting old DMG asset from release..."
ASSET_ID="$(gh api "repos/tinyhumansai/openhuman/releases/$RELEASE_ID/assets" \
--jq ".[] | select(.name == \"$DMG_NAME\") | .id" 2>/dev/null || true)"
if [ -n "$ASSET_ID" ]; then
gh api -X DELETE "repos/tinyhumansai/openhuman/releases/assets/$ASSET_ID" || true
fi
echo "Uploading notarized DMG..."
gh release upload "v${VERSION}" "$DMG_PATH" --repo tinyhumansai/openhuman --clobber
fi
# Re-upload the .app as a zip if tauri-action uploaded one
if [ -n "$APP_PATH" ]; then
APP_ZIP="/tmp/OpenHuman_${VERSION}_${ARCH}.app.tar.gz"
tar -czf "$APP_ZIP" -C "$(dirname "$APP_PATH")" "$(basename "$APP_PATH")"
gh release upload "v${VERSION}" "$APP_ZIP" --repo tinyhumansai/openhuman --clobber
rm -f "$APP_ZIP"
fi
bash scripts/release/upload-macos-artifacts.sh \
"${{ steps.locate-app.outputs.app_path }}" \
"${{ steps.locate-app.outputs.bundle_dir }}" \
"${{ needs.prepare-release.outputs.version }}" \
"${{ matrix.settings.target }}"
- name: Verify macOS app bundle sidecar layout
if: matrix.settings.platform == 'macos-latest'
@@ -715,18 +506,9 @@ jobs:
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.prepare-release.outputs.version }}
MATRIX_PLATFORM: ${{ matrix.settings.platform }}
MATRIX_TARGET: ${{ matrix.settings.target }}
run: |
set -euo pipefail
TARBALL="openhuman-core-${VERSION}-${MATRIX_TARGET}.tar.gz"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
if [[ "$MATRIX_PLATFORM" == "macos-latest" ]]; then
# Prefer the notarized+signed binary extracted from inside the .app bundle
# For macOS, prefer the notarized binary from inside the .app bundle
if [[ "${{ matrix.settings.platform }}" == "macos-latest" ]]; then
APP_PATH="${{ steps.locate-app.outputs.app_path }}"
BIN=$(find "$APP_PATH/Contents/MacOS" -maxdepth 1 -name "openhuman-core-*" \
! -name "*.sig" 2>/dev/null | head -1 || true)
@@ -740,17 +522,10 @@ jobs:
BIN="${{ steps.cli-paths.outputs.cli_path }}"
fi
cp "$BIN" "$WORK/openhuman-core"
chmod +x "$WORK/openhuman-core"
tar -czf "$TARBALL" -C "$WORK" openhuman-core
# openssl dgst works on both macOS and Linux runners
openssl dgst -sha256 -r "$TARBALL" | awk '{print $1}' > "${TARBALL}.sha256"
gh release upload "v${VERSION}" "$TARBALL" "${TARBALL}.sha256" \
--repo tinyhumansai/openhuman --clobber
echo "[pkg] Uploaded $TARBALL and ${TARBALL}.sha256"
bash scripts/release/package-cli-tarball.sh \
"$BIN" \
"${{ needs.prepare-release.outputs.version }}" \
"${{ matrix.settings.target }}"
- name: Upload standalone CLI artifacts
uses: actions/upload-artifact@v4
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Download Linux CLI tarballs from a GitHub release, build .deb packages,
# then build a signed apt repository and optionally deploy to gh-pages.
#
# Usage:
# build-apt-packages.sh <tag> [--deploy-gh-pages <gh_pages_dir>]
#
# Required environment:
# GITHUB_TOKEN — download release assets
# APT_SIGNING_KEY_ID — GPG key ID for signing (must be imported)
#
# Optional environment:
# UPLOAD_REPO — GitHub repo slug (default: tinyhumansai/openhuman)
# DRY_RUN — set to "true" to skip git push
set -euo pipefail
TAG="${1:?Usage: build-apt-packages.sh <tag> [--deploy-gh-pages <gh_pages_dir>]}"
shift
VERSION="${TAG#v}"
UPLOAD_REPO="${UPLOAD_REPO:-tinyhumansai/openhuman}"
DEPLOY_GH_PAGES=""
while [[ $# -gt 0 ]]; do
case "$1" in
--deploy-gh-pages) DEPLOY_GH_PAGES="${2:?--deploy-gh-pages requires a path}"; shift 2 ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
# ── Download tarballs ────────────────────────────────────────────────────────
echo "[apt] Downloading Linux CLI tarballs for $TAG ..."
mkdir -p "$TMPDIR/tarballs" "$TMPDIR/bins"
for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
TARBALL="openhuman-core-${VERSION}-${target}.tar.gz"
gh release download "$TAG" \
--pattern "$TARBALL" \
--repo "$UPLOAD_REPO" \
--dir "$TMPDIR/tarballs"
echo "[apt] Downloaded $TARBALL"
done
# ── Extract binaries ─────────────────────────────────────────────────────────
tar -xzf "$TMPDIR/tarballs/openhuman-core-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" \
-C "$TMPDIR/bins"
mv "$TMPDIR/bins/openhuman-core" "$TMPDIR/bins/openhuman-core-amd64"
tar -xzf "$TMPDIR/tarballs/openhuman-core-${VERSION}-aarch64-unknown-linux-gnu.tar.gz" \
-C "$TMPDIR/bins"
mv "$TMPDIR/bins/openhuman-core" "$TMPDIR/bins/openhuman-core-arm64"
chmod +x "$TMPDIR/bins/openhuman-core-amd64" "$TMPDIR/bins/openhuman-core-arm64"
# ── Build .deb packages ─────────────────────────────────────────────────────
echo "[apt] Building .deb packages ..."
bash "$REPO_ROOT/packages/deb/build.sh" "$TMPDIR/bins/openhuman-core-amd64" "${VERSION}" amd64
bash "$REPO_ROOT/packages/deb/build.sh" "$TMPDIR/bins/openhuman-core-arm64" "${VERSION}" arm64
ls -lh openhuman_*.deb
# ── Build apt repository ────────────────────────────────────────────────────
APT_REPO_DIR="$TMPDIR/apt-repo"
echo "[apt] Building apt repository ..."
bash "$REPO_ROOT/scripts/build-apt-repo.sh" "$APT_REPO_DIR" \
"openhuman_${VERSION}_amd64.deb" \
"openhuman_${VERSION}_arm64.deb"
# ── Deploy to gh-pages ───────────────────────────────────────────────────────
if [[ -n "$DEPLOY_GH_PAGES" ]]; then
echo "[apt] Deploying apt repo to gh-pages at $DEPLOY_GH_PAGES ..."
mkdir -p "$DEPLOY_GH_PAGES/apt"
rm -rf "$DEPLOY_GH_PAGES/apt/"*
cp -r "$APT_REPO_DIR/." "$DEPLOY_GH_PAGES/apt/"
cd "$DEPLOY_GH_PAGES"
git config user.name "${GIT_AUTHOR_NAME:-github-actions[bot]}"
git config user.email "${GIT_AUTHOR_EMAIL:-github-actions[bot]@users.noreply.github.com}"
git add apt/
if git diff --cached --quiet; then
echo "[apt] No changes."
exit 0
fi
git commit -m "chore(apt): publish v${VERSION}"
if [[ "${DRY_RUN:-}" == "true" ]]; then
echo "[apt] DRY_RUN: skipping push"
else
git push origin gh-pages
echo "[apt] Pushed to gh-pages"
fi
fi
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Build the Linux arm64 CLI tarball and optionally upload to a GitHub release.
#
# Usage:
# build-linux-arm64.sh <tag>
#
# Environment:
# GITHUB_TOKEN — upload to release when set
# OPENHUMAN_SENTRY_DSN — optional Sentry DSN baked into the binary
# UPLOAD_REPO — GitHub repo slug (default: tinyhumansai/openhuman)
set -euo pipefail
TAG="${1:?Usage: build-linux-arm64.sh <tag>}"
VERSION="${TAG#v}"
TARGET="aarch64-unknown-linux-gnu"
UPLOAD_REPO="${UPLOAD_REPO:-tinyhumansai/openhuman}"
echo "[linux-arm64] Building openhuman-core for $TARGET ..."
cargo build --release --bin openhuman-core
TARBALL="openhuman-core-${VERSION}-${TARGET}.tar.gz"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
cp target/release/openhuman-core "$WORK/"
chmod +x "$WORK/openhuman-core"
tar -czf "$TARBALL" -C "$WORK" openhuman-core
openssl dgst -sha256 -r "$TARBALL" | awk '{print $1}' > "${TARBALL}.sha256"
echo "[linux-arm64] Created $TARBALL (sha256: $(cat "${TARBALL}.sha256"))"
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
gh release upload "$TAG" \
"$TARBALL" "${TARBALL}.sha256" \
--repo "$UPLOAD_REPO" --clobber
echo "[linux-arm64] Uploaded $TARBALL"
fi
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env node
// Bump version in package.json, tauri.conf.json, and Cargo.toml.
//
// Usage:
// node scripts/release/bump-version.js <patch|minor|major>
//
// Outputs (to stdout, one per line):
// version=X.Y.Z
// tag=vX.Y.Z
//
// When GITHUB_OUTPUT is set (CI), the same key=value pairs are appended there.
'use strict';
const fs = require('fs');
const path = require('path');
const RELEASE_TYPE = process.argv[2] || process.env.RELEASE_TYPE;
const allowed = new Set(['patch', 'minor', 'major']);
if (!allowed.has(RELEASE_TYPE)) {
console.error(`Usage: bump-version.js <patch|minor|major> (got: "${RELEASE_TYPE}")`);
process.exit(1);
}
const root = path.resolve(__dirname, '..', '..');
const packagePath = path.join(root, 'app/package.json');
const tauriPath = path.join(root, 'app/src-tauri/tauri.conf.json');
const cargoPath = path.join(root, 'app/src-tauri/Cargo.toml');
// ── Read current version ────────────────────────────────────────────────────
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
const match = String(pkg.version || '').match(/^(\d+)\.(\d+)\.(\d+)$/);
if (!match) {
throw new Error(`package.json version must be SemVer X.Y.Z, found: ${pkg.version}`);
}
let major = Number(match[1]);
let minor = Number(match[2]);
let patch = Number(match[3]);
// ── Bump ────────────────────────────────────────────────────────────────────
if (RELEASE_TYPE === 'major') {
major += 1; minor = 0; patch = 0;
} else if (RELEASE_TYPE === 'minor') {
minor += 1; patch = 0;
} else {
patch += 1;
}
const nextVersion = `${major}.${minor}.${patch}`;
// ── Write package.json ──────────────────────────────────────────────────────
pkg.version = nextVersion;
fs.writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`);
// ── Write tauri.conf.json ───────────────────────────────────────────────────
const tauri = JSON.parse(fs.readFileSync(tauriPath, 'utf8'));
tauri.version = nextVersion;
fs.writeFileSync(tauriPath, `${JSON.stringify(tauri, null, 2)}\n`);
// ── Write Cargo.toml ────────────────────────────────────────────────────────
const cargo = fs.readFileSync(cargoPath, 'utf8');
const updatedCargo = cargo.replace(
/(\[package\][\s\S]*?^version\s*=\s*")([^"]+)(")/m,
`$1${nextVersion}$3`,
);
if (updatedCargo === cargo) {
throw new Error('Failed to update [package].version in app/src-tauri/Cargo.toml');
}
fs.writeFileSync(cargoPath, updatedCargo);
// ── Output ──────────────────────────────────────────────────────────────────
const lines = `version=${nextVersion}\ntag=v${nextVersion}\n`;
process.stdout.write(lines);
if (process.env.GITHUB_OUTPUT) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, lines);
}
console.error(`[bump-version] ${pkg.version}${nextVersion}`);
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Package the core CLI binary into a release tarball and optionally upload it.
#
# Usage:
# package-cli-tarball.sh <binary_path> <version> <target>
#
# Environment:
# GITHUB_TOKEN — if set, uploads tarball + sha256 to the GitHub release
# UPLOAD_REPO — GitHub repo slug (default: tinyhumansai/openhuman)
#
# Example:
# package-cli-tarball.sh target/release/openhuman-core 0.5.0 aarch64-apple-darwin
set -euo pipefail
BIN_PATH="${1:?Usage: package-cli-tarball.sh <binary_path> <version> <target>}"
VERSION="${2:?}"
TARGET="${3:?}"
UPLOAD_REPO="${UPLOAD_REPO:-tinyhumansai/openhuman}"
TARBALL="openhuman-core-${VERSION}-${TARGET}.tar.gz"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
cp "$BIN_PATH" "$WORK/openhuman-core"
chmod +x "$WORK/openhuman-core"
tar -czf "$TARBALL" -C "$WORK" openhuman-core
# openssl dgst works on both macOS and Linux
openssl dgst -sha256 -r "$TARBALL" | awk '{print $1}' > "${TARBALL}.sha256"
echo "[package-cli] Created $TARBALL (sha256: $(cat "${TARBALL}.sha256"))"
# ── Optional upload ──────────────────────────────────────────────────────────
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
gh release upload "v${VERSION}" \
"$TARBALL" "${TARBALL}.sha256" \
--repo "$UPLOAD_REPO" --clobber
echo "[package-cli] Uploaded $TARBALL to v${VERSION}"
fi
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Publish the openhuman npm package for a given version.
#
# Usage:
# publish-npm.sh <tag>
#
# Required environment:
# NODE_AUTH_TOKEN — npm automation token
#
# Optional environment:
# DRY_RUN — set to "true" to run npm publish --dry-run
set -euo pipefail
TAG="${1:?Usage: publish-npm.sh <tag>}"
VERSION="${TAG#v}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$REPO_ROOT/packages/npm"
# Stamp version without creating a git commit
npm version "$VERSION" --no-git-tag-version
PUBLISH_ARGS=(--access public)
if [[ "${DRY_RUN:-}" == "true" ]]; then
PUBLISH_ARGS+=(--dry-run)
fi
# SKIP_OPENHUMAN_BINARY_DOWNLOAD prevents postinstall from running
# during publish (the binary doesn't exist yet on the publish runner)
SKIP_OPENHUMAN_BINARY_DOWNLOAD=1 npm publish "${PUBLISH_ARGS[@]}"
echo "[npm] Published openhuman@${VERSION}"
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Re-create and notarize a DMG after the .app has been notarized.
#
# Usage:
# repackage-dmg.sh <app_path> <bundle_dir>
#
# Required environment variables:
# APPLE_ID
# APPLE_PASSWORD (app-specific password)
# APPLE_TEAM_ID
set -euo pipefail
APP_PATH="${1:?Usage: repackage-dmg.sh <app_path> <bundle_dir>}"
BUNDLE_DIR="${2:?}"
DMG_PATH="$(find "$BUNDLE_DIR/dmg" -name '*.dmg' -maxdepth 1 2>/dev/null | head -1)"
if [ -z "$DMG_PATH" ]; then
echo "[dmg] No DMG found — skipping DMG re-package"
exit 0
fi
echo "[dmg] Re-creating DMG with notarized .app..."
DMG_TEMP="$(mktemp /tmp/OpenHuman-XXXXXX.dmg)"
hdiutil create -volname "OpenHuman" -srcfolder "$APP_PATH" -ov -format UDZO "$DMG_TEMP"
mv "$DMG_TEMP" "$DMG_PATH"
echo "[dmg] Notarizing DMG..."
xcrun notarytool submit "$DMG_PATH" \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
xcrun stapler staple "$DMG_PATH"
echo "[dmg] DMG notarization complete: $DMG_PATH"
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# Re-sign all binaries inside a macOS .app bundle with hardened runtime
# and submit for Apple notarization.
#
# Usage:
# sign-and-notarize-macos.sh <app_path> [entitlements_plist]
#
# Required environment variables:
# APPLE_CERTIFICATE_BASE64
# APPLE_CERTIFICATE_PASSWORD
# APPLE_SIGNING_IDENTITY
# APPLE_ID
# APPLE_PASSWORD (app-specific password)
# APPLE_TEAM_ID
set -euo pipefail
APP_PATH="${1:?Usage: sign-and-notarize-macos.sh <app_path> [entitlements_plist]}"
ENTITLEMENTS="${2:-app/src-tauri/entitlements.sidecar.plist}"
for var in APPLE_CERTIFICATE_BASE64 APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do
if [ -z "${!var:-}" ]; then
echo "[sign] ERROR: Missing required env var: $var"
exit 1
fi
done
# ── Import signing certificate ───────────────────────────────────────────────
KEYCHAIN="resign-$$.keychain-db"
KEYCHAIN_PW="$(openssl rand -base64 24)"
CERT_FILE="$(mktemp /tmp/cert-XXXXXX.p12)"
echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > "$CERT_FILE"
security create-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN"
security import "$CERT_FILE" -k "$KEYCHAIN" \
-P "$APPLE_CERTIFICATE_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PW" "$KEYCHAIN"
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
rm -f "$CERT_FILE"
echo "[sign] Signing identity imported into $KEYCHAIN"
# ── Sign .app contents ──────────────────────────────────────────────────────
echo "[sign] Signing .app contents and bundle"
echo "[sign] Bundle contents:"
ls -la "$APP_PATH/Contents/MacOS/"
MAIN_EXE="$(defaults read "$APP_PATH/Contents/Info.plist" CFBundleExecutable 2>/dev/null || echo "OpenHuman")"
echo "[sign] Main executable (from plist): $MAIN_EXE"
# Sign all non-main binaries (sidecars) in MacOS/
for bin in "$APP_PATH/Contents/MacOS/"*; do
[ -f "$bin" ] && [ -x "$bin" ] || continue
BASENAME="$(basename "$bin")"
[ "$BASENAME" = "$MAIN_EXE" ] && continue
echo "[sign] Signing sidecar: $BASENAME"
codesign --force --options runtime \
--entitlements "$ENTITLEMENTS" \
--sign "$APPLE_SIGNING_IDENTITY" \
--timestamp \
"$bin"
done
# Sign sidecars in Resources/ if any
for bin in "$APP_PATH/Contents/Resources/"openhuman-core-*; do
[ -f "$bin" ] || continue
echo "[sign] Signing resource sidecar: $(basename "$bin")"
codesign --force --options runtime \
--entitlements "$ENTITLEMENTS" \
--sign "$APPLE_SIGNING_IDENTITY" \
--timestamp \
"$bin"
done
# Sign the .app bundle itself
echo "[sign] Signing .app bundle..."
codesign --force --options runtime \
--entitlements "$ENTITLEMENTS" \
--sign "$APPLE_SIGNING_IDENTITY" \
--timestamp \
"$APP_PATH"
# ── Verify ───────────────────────────────────────────────────────────────────
echo "[sign] Verifying signatures"
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
# ── Notarize ─────────────────────────────────────────────────────────────────
echo "[sign] Notarizing..."
NOTARIZE_ZIP="$(mktemp /tmp/OpenHuman-notarize-XXXXXX.zip)"
ditto -c -k --keepParent "$APP_PATH" "$NOTARIZE_ZIP"
xcrun notarytool submit "$NOTARIZE_ZIP" \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
rm -f "$NOTARIZE_ZIP"
# ── Staple ───────────────────────────────────────────────────────────────────
echo "[sign] Stapling..."
xcrun stapler staple "$APP_PATH"
echo "[sign] Notarization complete"
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Stage the core sidecar binary next to Tauri resources for bundling.
#
# Usage:
# stage-sidecar.sh <target> <core_target_dir> <core_bin_name> <sidecar_base>
#
# Example:
# stage-sidecar.sh aarch64-apple-darwin target/aarch64-apple-darwin/release openhuman-core openhuman-core
set -euo pipefail
TARGET="${1:?Usage: stage-sidecar.sh <target> <core_target_dir> <core_bin_name> <sidecar_base>}"
CORE_TARGET_DIR="${2:?}"
CORE_BIN_NAME="${3:?}"
SIDECAR_BASE="${4:?}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
EXE_SUFFIX=""
if [[ "$TARGET" == *"windows"* ]]; then
EXE_SUFFIX=".exe"
fi
SOURCE="${REPO_ROOT}/${CORE_TARGET_DIR}/${CORE_BIN_NAME}${EXE_SUFFIX}"
DEST_DIR="${REPO_ROOT}/app/src-tauri/binaries"
DEST="${DEST_DIR}/${SIDECAR_BASE}-${TARGET}${EXE_SUFFIX}"
mkdir -p "$DEST_DIR"
cp "$SOURCE" "$DEST"
if [[ "$TARGET" != *"windows"* ]]; then
chmod +x "$DEST"
fi
echo "[stage-sidecar] Staged: $DEST"
# ── Verify ───────────────────────────────────────────────────────────────────
if [ ! -f "$DEST" ]; then
echo "[stage-sidecar] ERROR: Missing staged sidecar binary: $DEST"
exit 1
fi
if [[ "$TARGET" != *"windows"* ]] && [ ! -x "$DEST" ]; then
echo "[stage-sidecar] ERROR: Staged sidecar is not executable: $DEST"
exit 1
fi
echo "[stage-sidecar] Verified OK"
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Download release tarballs, compute SHA-256 checksums, render the Homebrew
# formula from the template and commit it to the tap repository.
#
# Usage:
# update-homebrew.sh <tag> <formula_template> <tap_dir>
#
# Example:
# update-homebrew.sh v0.5.0 packages/homebrew/openhuman.rb /tmp/tap
#
# Required environment:
# GITHUB_TOKEN — to download release assets
#
# The tap directory must be a git checkout of tinyhumansai/homebrew-openhuman.
set -euo pipefail
TAG="${1:?Usage: update-homebrew.sh <tag> <formula_template> <tap_dir>}"
TEMPLATE="${2:?}"
TAP_DIR="${3:?}"
VERSION="${TAG#v}"
UPLOAD_REPO="${UPLOAD_REPO:-tinyhumansai/openhuman}"
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
echo "[homebrew] Downloading release tarballs for $TAG ..."
SHA256_MACOS_ARM64=""
SHA256_MACOS_X64=""
SHA256_LINUX_X64=""
SHA256_LINUX_ARM64=""
for row in \
"aarch64-apple-darwin:SHA256_MACOS_ARM64" \
"x86_64-apple-darwin:SHA256_MACOS_X64" \
"x86_64-unknown-linux-gnu:SHA256_LINUX_X64" \
"aarch64-unknown-linux-gnu:SHA256_LINUX_ARM64"
do
TARGET="${row%%:*}"
VAR="${row##*:}"
TARBALL="openhuman-core-${VERSION}-${TARGET}.tar.gz"
echo "[homebrew] Downloading $TARBALL ..."
gh release download "$TAG" \
--pattern "$TARBALL" \
--repo "$UPLOAD_REPO" \
--dir "$TMPDIR"
SHA=$(openssl dgst -sha256 -r "$TMPDIR/$TARBALL" | awk '{print $1}')
eval "${VAR}=${SHA}"
echo "[homebrew] $TARGET$SHA"
done
# ── Render formula ───────────────────────────────────────────────────────────
mkdir -p "$TAP_DIR/Formula"
sed \
-e "s/@VERSION@/${VERSION}/g" \
-e "s/@SHA256_MACOS_ARM64@/${SHA256_MACOS_ARM64}/g" \
-e "s/@SHA256_MACOS_X64@/${SHA256_MACOS_X64}/g" \
-e "s/@SHA256_LINUX_X64@/${SHA256_LINUX_X64}/g" \
-e "s/@SHA256_LINUX_ARM64@/${SHA256_LINUX_ARM64}/g" \
"$TEMPLATE" > "$TAP_DIR/Formula/openhuman.rb"
echo "[homebrew] Rendered formula → $TAP_DIR/Formula/openhuman.rb"
# ── Commit and push ──────────────────────────────────────────────────────────
cd "$TAP_DIR"
git config user.name "${GIT_AUTHOR_NAME:-github-actions[bot]}"
git config user.email "${GIT_AUTHOR_EMAIL:-github-actions[bot]@users.noreply.github.com}"
git add Formula/openhuman.rb
if git diff --cached --quiet; then
echo "[homebrew] No changes to commit."
exit 0
fi
git commit -m "chore: update formula to v${VERSION}"
if [[ "${DRY_RUN:-}" == "true" ]]; then
echo "[homebrew] DRY_RUN: skipping push"
else
git push
echo "[homebrew] Pushed to tap"
fi
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Re-upload notarized macOS artifacts (DMG + .app tarball) to GitHub release.
#
# Usage:
# upload-macos-artifacts.sh <app_path> <bundle_dir> <version> <arch>
#
# Required environment:
# GITHUB_TOKEN
# RELEASE_ID
set -euo pipefail
APP_PATH="${1:?Usage: upload-macos-artifacts.sh <app_path> <bundle_dir> <version> <arch>}"
BUNDLE_DIR="${2:?}"
VERSION="${3:?}"
ARCH="${4:?}"
UPLOAD_REPO="${UPLOAD_REPO:-tinyhumansai/openhuman}"
# ── Re-upload DMG ────────────────────────────────────────────────────────────
DMG_PATH="$(find "$BUNDLE_DIR/dmg" -name '*.dmg' -maxdepth 1 2>/dev/null | head -1)"
if [ -n "$DMG_PATH" ]; then
DMG_NAME="$(basename "$DMG_PATH")"
echo "[upload] Deleting old DMG asset from release..."
ASSET_ID="$(gh api "repos/${UPLOAD_REPO}/releases/${RELEASE_ID}/assets" \
--jq ".[] | select(.name == \"$DMG_NAME\") | .id" 2>/dev/null || true)"
if [ -n "$ASSET_ID" ]; then
gh api -X DELETE "repos/${UPLOAD_REPO}/releases/assets/$ASSET_ID" || true
fi
echo "[upload] Uploading notarized DMG..."
gh release upload "v${VERSION}" "$DMG_PATH" --repo "$UPLOAD_REPO" --clobber
fi
# ── Upload .app as tar.gz ────────────────────────────────────────────────────
if [ -n "$APP_PATH" ] && [ -d "$APP_PATH" ]; then
APP_ZIP="/tmp/OpenHuman_${VERSION}_${ARCH}.app.tar.gz"
tar -czf "$APP_ZIP" -C "$(dirname "$APP_PATH")" "$(basename "$APP_PATH")"
gh release upload "v${VERSION}" "$APP_ZIP" --repo "$UPLOAD_REPO" --clobber
rm -f "$APP_ZIP"
echo "[upload] Uploaded .app tarball"
fi