Files
openhuman/.github/workflows/release.yml
T
Steven Enamakel cfd0f841fe fix(release): adjust argument parsing for OpenHuman binary in release workflow
- Updated the release workflow to ensure the `--bin OpenHuman` argument is correctly passed to Cargo by adding an additional separator. This change addresses an issue with Yarn v1 stripping the argument layer, improving the build process for standalone binaries.
2026-03-26 19:20:21 -07:00

429 lines
15 KiB
YAML

name: Release
on:
workflow_dispatch:
inputs:
release_type:
description: Version increment type
required: true
type: choice
options:
- patch
- minor
- major
permissions:
contents: write
concurrency:
group: release-main
cancel-in-progress: false
jobs:
prepare-release:
name: Prepare release commit and tag
runs-on: ubuntu-latest
environment: Production
outputs:
version: ${{ steps.bump.outputs.version }}
tag: ${{ steps.bump.outputs.tag }}
sha: ${{ steps.push.outputs.sha }}
steps:
- name: Enforce main branch
if: github.ref != 'refs/heads/main'
run: |
echo "This workflow can only run from main. Current ref: $GITHUB_REF"
exit 1
- name: Generate GitHub App token
id: app-token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.XGITHUB_APP_ID }}
private_key: ${{ secrets.XGITHUB_APP_PRIVATE_KEY }}
- name: Checkout main
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24.x
- name: Configure Git
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git remote set-url origin https://${APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
git fetch origin --tags
git checkout main
git pull origin main --ff-only
- 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 = 'package.json';
const tauriPath = 'src-tauri/tauri.conf.json';
const cargoPath = '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 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
- name: Ensure tag does not already exist
env:
TAG: ${{ steps.bump.outputs.tag }}
run: |
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag already exists locally: $TAG"
exit 1
fi
if git ls-remote --tags origin "refs/tags/$TAG" | grep -q .; then
echo "Tag already exists on origin: $TAG"
exit 1
fi
- name: Commit, push and tag
id: push
env:
VERSION: ${{ steps.bump.outputs.version }}
TAG: ${{ steps.bump.outputs.tag }}
run: |
git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml
git commit -m "chore(release): v${VERSION}"
git push origin main
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
create-release:
name: Create GitHub release
runs-on: ubuntu-latest
environment: Production
needs: prepare-release
outputs:
release_id: ${{ steps.create.outputs.release_id }}
upload_url: ${{ steps.create.outputs.upload_url }}
steps:
- name: Create published release with generated notes
id: create
uses: actions/github-script@v7
with:
script: |
const tag = '${{ needs.prepare-release.outputs.tag }}';
const version = '${{ needs.prepare-release.outputs.version }}';
const target = '${{ needs.prepare-release.outputs.sha }}';
const { owner, repo } = context.repo;
try {
await github.rest.repos.getReleaseByTag({ owner, repo, tag });
core.setFailed(`Release already exists for ${tag}`);
return;
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
const release = await github.rest.repos.createRelease({
owner,
repo,
tag_name: tag,
target_commitish: target,
name: `OpenHuman v${version}`,
draft: false,
prerelease: false,
generate_release_notes: true,
});
core.setOutput('release_id', String(release.data.id));
core.setOutput('upload_url', release.data.upload_url);
build-artifacts:
name: Build and upload artifacts
needs: [prepare-release, create-release]
runs-on: ${{ matrix.settings.platform }}
environment: Production
strategy:
fail-fast: false
matrix:
settings:
# macOS builds are disabled for now to ensure the flow works first
# - platform: macos-latest
# args: --target aarch64-apple-darwin
# target: aarch64-apple-darwin
# artifact_suffix: aarch64-apple-darwin
# - platform: macos-latest
# args: --target x86_64-apple-darwin
# target: x86_64-apple-darwin
# artifact_suffix: x86_64-apple-darwin
- platform: ubuntu-22.04
args: ""
target: ""
artifact_suffix: ubuntu
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout tag
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare-release.outputs.tag }}
fetch-depth: 1
submodules: true
- name: Set Xcode version
if: matrix.settings.platform == 'macos-latest'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Setup Node.js 24.x
uses: actions/setup-node@v4
with:
node-version: 24.x
cache: yarn
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Install Tauri dependencies (ubuntu only)
if: matrix.settings.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
# Skip first 7 lines of Cargo.lock (workspace package version bumps) so the key tracks dependency changes only
- name: Cargo.lock fingerprint (deps only)
id: cargo-lock-fingerprint
shell: bash
run: |
echo "hash=$(tail -n +8 src-tauri/Cargo.lock | openssl dgst -sha256 | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: Cache Cargo registry and git sources
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-registry-${{ steps.cargo-lock-fingerprint.outputs.hash }}
restore-keys: |
${{ runner.os }}-cargo-registry-
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Install skills dependencies
run: cd skills && yarn install --frozen-lockfile
- name: Build skills
run: cd skills && yarn build
- name: Define Tauri configuration overrides
id: config-overrides
uses: actions/github-script@v7
env:
BASE_URL: ${{ vars.BASE_URL }}
UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY }}
WITH_UPDATER: "true"
with:
script: |
const workspacePath = process.env.GITHUB_WORKSPACE.replace(/\\/g, '/');
const prefix = workspacePath.startsWith('/') ? 'file://' : 'file:///';
const moduleUrl = `${prefix}${workspacePath}/scripts/prepareTauriConfig.js`;
const { default: prepareTauriConfig } = await import(moduleUrl);
const config = prepareTauriConfig();
core.setOutput('json', JSON.stringify(config));
- name: Build frontend
run: yarn build
env:
NODE_ENV: production
VITE_BACKEND_URL: ${{ vars.VITE_BACKEND_URL }}
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
VITE_DEBUG: ${{ vars.VITE_DEBUG }}
- name: Build, package and upload to release
uses: tauri-apps/tauri-action@v0.6.2
id: tauri-build
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
BASE_URL: ${{ vars.BASE_URL }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD || secrets.UPDATER_PRIVATE_KEY_PASSWORD }}
WITH_UPDATER: "true"
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.settings.platform == 'macos-latest' && '10.15' || '' }}
with:
# Tools discovery now uses a JS mock registry (no Rust discovery
# binary target), so there is no extra helper executable for Tauri
# to copy/sign inside release app bundles.
# Build/package only the app binary to keep standalone CLI bins
# out of the signed macOS app bundle.
# Yarn v1 strips one "--" layer when invoking scripts.
# Keep a second separator so `--bin OpenHuman` reaches cargo.
args: -c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }} -- -- --bin OpenHuman
includeDebug: false
includeRelease: true
releaseId: ${{ needs.create-release.outputs.release_id }}
owner: tinyhumansai
repo: openhuman
- name: Verify macOS app bundle excludes standalone core binary
if: matrix.settings.platform == 'macos-latest'
shell: bash
run: |
APP_PATH="src-tauri/target/${{ matrix.settings.target }}/release/bundle/macos/OpenHuman.app"
echo "Inspecting bundle at: $APP_PATH"
ls -la "$APP_PATH/Contents/MacOS"
if [ -f "$APP_PATH/Contents/MacOS/openhuman-core" ]; then
echo "Unexpected standalone binary found inside app bundle: openhuman-core"
exit 1
fi
- name: Build standalone CLI binaries
shell: bash
run: |
CARGO_TARGET=""
if [ -n "$MATRIX_TARGET" ]; then
CARGO_TARGET="--target $MATRIX_TARGET"
fi
cargo build --manifest-path src-tauri/Cargo.toml --release --features standalone-bins $CARGO_TARGET --bin openhuman-core --bin openhuman-cli
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
- name: Resolve standalone CLI artifact paths
id: cli-paths
shell: bash
run: |
if [ -n "$MATRIX_TARGET" ]; then
BASE_DIR="src-tauri/target/$MATRIX_TARGET/release"
else
BASE_DIR="src-tauri/target/release"
fi
echo "base_dir=$BASE_DIR" >> "$GITHUB_OUTPUT"
echo "core_path=$BASE_DIR/openhuman-core" >> "$GITHUB_OUTPUT"
echo "cli_path=$BASE_DIR/openhuman-cli" >> "$GITHUB_OUTPUT"
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
- name: Upload standalone CLI artifacts
uses: actions/upload-artifact@v4
with:
name: standalone-bins-${{ matrix.settings.platform }}-${{ matrix.settings.artifact_suffix }}
path: |
${{ steps.cli-paths.outputs.core_path }}
${{ steps.cli-paths.outputs.cli_path }}
cleanup-failed-release:
name: Remove release and tag if build failed
runs-on: ubuntu-latest
environment: Production
needs: [prepare-release, create-release, build-artifacts]
if: always() && needs.create-release.result == 'success' && (needs.build-artifacts.result == 'failure' || needs.build-artifacts.result == 'cancelled')
steps:
- name: Delete GitHub release
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const releaseId = Number('${{ needs.create-release.outputs.release_id }}');
if (!Number.isFinite(releaseId) || releaseId <= 0) {
core.setFailed('Invalid or missing release_id; cannot delete release.');
return;
}
try {
await github.rest.repos.deleteRelease({ owner, repo, release_id: releaseId });
core.info(`Deleted release ${releaseId}`);
} catch (e) {
core.warning(`deleteRelease failed: ${e.message}`);
}
- name: Delete remote tag
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const tag = '${{ needs.prepare-release.outputs.tag }}';
try {
await github.rest.git.deleteRef({ owner, repo, ref: `tags/${tag}` });
core.info(`Deleted remote tag ${tag}`);
} catch (e) {
if (e.status === 404) {
core.info(`Tag ${tag} already absent on remote`);
} else {
throw e;
}
}