Files
openhuman/.github/workflows/release.yml
T
Steven EnamakelandGitHub 5ecc7cbc75 refactor: consolidate CLI binaries and streamline CI workflows (#44)
* chore: update dependencies and remove unused packages

- Removed several unused dependencies from package.json, including `@tauri-apps/plugin-shell`, `@types/react-router-dom`, `immer`, `qrcode.react`, and `@testing-library/user-event`.
- Updated the Vite configuration to exclude `telegram` from the optimizeDeps include list.
- Cleaned up yarn.lock by removing references to deleted packages.
- Deleted outdated GitHub Actions workflows for macOS ARM64 build, package and publish, and update changelog.

* refactor: consolidate CLI binaries and update configurations

- Renamed the main binary from `openhuman-core` to `openhuman` for consistency across the project.
- Removed the `openhuman-cli` and `openhuman-core` binaries, consolidating functionality into the new `openhuman` binary.
- Updated build configurations in GitHub Actions and Tauri to reflect the new binary name and paths.
- Adjusted the Tailwind and Vite configurations to point to the correct HTML and source directories.
- Introduced a new method in the Rust core server for handling JSON-RPC calls, enhancing the API structure.

* chore: update subproject commit reference in skills

* chore: update Rust test workflow and improve CLI command formatting

- Modified the GitHub Actions workflow to include a new job for running Rust tests in the workspace.
- Updated the `test:rust` command in `package.json` to run tests for the entire workspace.
- Refactored CLI command definitions in `openhuman.rs` for improved readability by consolidating struct fields into single lines.

* chore: enhance E2E testing scripts and workflows

- Updated `.prettierignore` to exclude the `rust-core` directory.
- Added new E2E testing commands in `package.json` for running all flows and improved formatting commands to include `cargo fmt`.
- Enhanced GitHub Actions workflows to streamline E2E testing, including new scripts for running individual specs and all flows sequentially.
- Refactored existing E2E scripts to utilize a common function for running tests, improving maintainability and readability.
- Introduced a new script for resolving Node.js and Appium dependencies, ensuring compatibility for E2E tests.
- Cleaned up and simplified existing E2E scripts by removing redundant code and consolidating functionality.

* chore: update GitHub Actions workflows for Tauri build process

- Modified build and release workflows to reference the correct target directory for Rust binaries, ensuring compatibility with the Cargo workspace structure.
- Adjusted caching configuration in the test workflow for consistency and improved readability.
- Standardized environment variable formatting in the test workflow to enhance clarity.

* fix: standardize string formatting in GitHub Actions workflows

- Updated the test workflow to use single quotes for cache configuration and environment variables, enhancing consistency across the file.
2026-03-27 13:58:29 -07:00

480 lines
18 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 x86_64-unknown-linux-gnu
target: x86_64-unknown-linux-gnu
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: Resolve core manifest and binary names
id: core-paths
shell: bash
run: |
if [ -f "openhuman_core/Cargo.toml" ]; then
CORE_DIR="openhuman_core"
elif [ -f "rust-core/Cargo.toml" ]; then
CORE_DIR="rust-core"
else
echo "No core Cargo manifest found (expected openhuman_core/Cargo.toml or rust-core/Cargo.toml)"
exit 1
fi
SIDE_CAR_BASE="$(node -e "const fs=require('fs');const c=JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json','utf8'));const b=(c.bundle&&Array.isArray(c.bundle.externalBin)&&c.bundle.externalBin[0])||'binaries/openhuman';process.stdout.write(String(b).split('/').pop());")"
CORE_BIN_NAME="${SIDE_CAR_BASE}"
echo "core_dir=$CORE_DIR" >> "$GITHUB_OUTPUT"
echo "core_manifest=$CORE_DIR/Cargo.toml" >> "$GITHUB_OUTPUT"
# Cargo workspace: release artifacts are under repo root target/, not <member>/target/
echo "core_target_dir=target/$MATRIX_TARGET/release" >> "$GITHUB_OUTPUT"
echo "core_bin_name=$CORE_BIN_NAME" >> "$GITHUB_OUTPUT"
echo "sidecar_base=$SIDE_CAR_BASE" >> "$GITHUB_OUTPUT"
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
- name: Build sidecar core binary
shell: bash
run: |
cargo build \
--manifest-path "$CORE_MANIFEST" \
--release \
--target "$MATRIX_TARGET" \
--bin "$CORE_BIN_NAME"
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
CORE_MANIFEST: ${{ steps.core-paths.outputs.core_manifest }}
CORE_BIN_NAME: ${{ steps.core-paths.outputs.core_bin_name }}
- name: Stage sidecar for Tauri bundler
shell: bash
run: |
mkdir -p src-tauri/binaries
SOURCE="$CORE_TARGET_DIR/$CORE_BIN_NAME"
DEST="src-tauri/binaries/$SIDECAR_BASE-$MATRIX_TARGET"
cp "$SOURCE" "$DEST"
chmod +x "$DEST"
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: 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.
# Restrict Cargo bin selection for the Tauri bundle build so only
# the desktop UI app binary is packaged.
# Yarn v1 strips one "--" layer when invoking scripts, hence the
# double-separator when forwarding flags to 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 sidecar layout
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"
ls -la "$APP_PATH/Contents/Resources" | grep openhuman || true
if [ -f "$APP_PATH/Contents/MacOS/openhuman" ]; then
echo "Unexpected standalone core binary found in MacOS dir"
exit 1
fi
if ! ls "$APP_PATH/Contents/Resources"/openhuman-* >/dev/null 2>&1; then
echo "Sidecar core binary missing from app resources"
exit 1
fi
- name: Build standalone core CLI binary
shell: bash
run: |
cargo build --manifest-path "$CORE_MANIFEST" --release --target "$MATRIX_TARGET" --bin "$CORE_BIN_NAME"
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
CORE_MANIFEST: ${{ steps.core-paths.outputs.core_manifest }}
CORE_BIN_NAME: ${{ steps.core-paths.outputs.core_bin_name }}
- name: Resolve standalone core CLI artifact path
id: cli-paths
shell: bash
run: |
BASE_DIR="$CORE_TARGET_DIR"
echo "base_dir=$BASE_DIR" >> "$GITHUB_OUTPUT"
echo "cli_path=$BASE_DIR/$CORE_BIN_NAME" >> "$GITHUB_OUTPUT"
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 }}
- 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.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;
}
}