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