Files
openhuman/.github/workflows/release.yml
T
Steven Enamakel fee01cd6c7 Comment out Docker image promotion and pull instructions in release workflow
- Temporarily disabled the steps for promoting Docker images from staging to release tags and appending Docker pull instructions to release notes in the GitHub Actions workflow. This change prevents potential errors during the release process while maintaining the structure for future implementation.lease
2026-04-01 23:32:46 -07:00

735 lines
29 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
packages: write
concurrency:
group: release-main
cancel-in-progress: false
# ---------------------------------------------------------------------------
# Job dependency graph
#
# prepare-release
# │
# ├─── create-release
# │ │
# │ ┌────┴────────────┐
# │ │ │
# │ build-desktop build-docker
# │ (matrix: macOS, (GHCR image)
# │ linux, windows)
# │ │ │
# │ └────┬────────────┘
# │ │
# │ publish-release
# │ │
# │ notify-discord
# │
# └─── cleanup-failed-release (on failure)
# ---------------------------------------------------------------------------
jobs:
# =========================================================================
# Phase 1: Version bump, commit, tag
# =========================================================================
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
run: node scripts/release/bump-version.js "${{ inputs.release_type }}"
- 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 app/package.json app/src-tauri/tauri.conf.json app/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"
# =========================================================================
# Phase 2: Create draft GitHub release
# =========================================================================
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 draft 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: true,
prerelease: false,
generate_release_notes: true,
});
core.setOutput('release_id', String(release.data.id));
core.setOutput('upload_url', release.data.upload_url);
# =========================================================================
# Phase 3a: Build desktop artifacts (parallel matrix)
# =========================================================================
build-desktop:
name: "Desktop: ${{ matrix.settings.artifact_suffix }}"
needs: [prepare-release, create-release]
runs-on: ${{ matrix.settings.platform }}
environment: Production
strategy:
fail-fast: false
matrix:
settings:
- 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
# - platform: windows-latest
# args: --target x86_64-pc-windows-msvc
# target: x86_64-pc-windows-msvc
# artifact_suffix: windows
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 (rust-toolchain.toml)
uses: dtolnay/rust-toolchain@1.93.0
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 cmake
# 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 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: Validate signing prerequisites
shell: bash
env:
MATRIX_PLATFORM: ${{ matrix.settings.platform }}
UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY || vars.UPDATER_PUBLIC_KEY }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }}
APPLE_CERTIFICATE_BASE64: ${{ 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 }}
run: |
if [ -z "$UPDATER_PUBLIC_KEY" ]; then
echo "Missing UPDATER_PUBLIC_KEY (set as secret or repo/environment variable)."
exit 1
fi
if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then
echo "Missing TAURI_SIGNING_PRIVATE_KEY (or fallback UPDATER_PRIVATE_KEY)."
exit 1
fi
if [ "$MATRIX_PLATFORM" = "macos-latest" ]; then
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 "Missing required macOS signing secret: $var"
exit 1
fi
done
fi
- 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 || vars.UPDATER_PUBLIC_KEY }}
UPDATER_ENDPOINT: ${{ vars.UPDATER_ENDPOINT }}
UPDATER_REPO: tinyhumansai/openhuman
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.BASE_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"
elif [ -f "Cargo.toml" ] && grep -q '^name = "openhuman"' Cargo.toml; then
CORE_DIR="."
else
echo "No core Cargo manifest found (expected root Cargo.toml with openhuman, 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('app/src-tauri/tauri.conf.json','utf8'));const b=(c.bundle&&Array.isArray(c.bundle.externalBin)&&c.bundle.externalBin[0])||'binaries/openhuman-core';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 }}
OPENHUMAN_SENTRY_DSN: ${{ vars.OPENHUMAN_SENTRY_DSN }}
- name: Stage sidecar for Tauri bundler
shell: bash
run: |
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
shell: bash
run: |
BASE_DIR="$CORE_TARGET_DIR"
EXE_SUFFIX=""
if [[ "$MATRIX_TARGET" == *"windows"* ]]; then
EXE_SUFFIX=".exe"
fi
echo "base_dir=$BASE_DIR" >> "$GITHUB_OUTPUT"
echo "cli_path=$BASE_DIR/${CORE_BIN_NAME}${EXE_SUFFIX}" >> "$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: Build, package and upload to release
uses: tauri-apps/tauri-action@v0.6.2
id: tauri-build
env:
# NOTE: APPLE_CERTIFICATE, APPLE_SIGNING_IDENTITY, APPLE_ID, etc. are
# intentionally omitted so tauri-action builds WITHOUT signing.
# Signing + notarization are handled in a dedicated step afterwards
# where we sign everything with hardened runtime + entitlements
# (required by Apple notarization).
BASE_URL: ${{ vars.BASE_URL }}
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.settings.platform == 'macos-latest' && '10.15' || '' }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD || secrets.UPDATER_PRIVATE_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }}
WITH_UPDATER: "true"
with:
projectPath: app
args: -c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }}
includeDebug: false
includeRelease: true
releaseId: ${{ needs.create-release.outputs.release_id }}
owner: tinyhumansai
repo: openhuman
- name: Locate macOS .app bundle
if: matrix.settings.platform == 'macos-latest'
id: locate-app
shell: bash
run: |
set -euo pipefail
# tauri-action with projectPath: app may output to either location
# depending on workspace config — search both
APP_PATH=""
for candidate in \
"app/src-tauri/target/${{ matrix.settings.target }}/release/bundle/macos/OpenHuman.app" \
"target/${{ matrix.settings.target }}/release/bundle/macos/OpenHuman.app"; do
if [ -d "$candidate" ]; then
APP_PATH="$candidate"
break
fi
done
# Fallback: search for it
if [ -z "$APP_PATH" ]; then
APP_PATH="$(find . -path '*/release/bundle/macos/OpenHuman.app' -type d 2>/dev/null | head -1)"
fi
if [ -z "$APP_PATH" ]; then
echo "ERROR: Could not find OpenHuman.app bundle anywhere"
find . -name 'OpenHuman.app' -type d 2>/dev/null || true
exit 1
fi
BUNDLE_DIR="$(dirname "$(dirname "$APP_PATH")")"
echo "app_path=$APP_PATH" >> "$GITHUB_OUTPUT"
echo "bundle_dir=$BUNDLE_DIR" >> "$GITHUB_OUTPUT"
echo "Found .app at: $APP_PATH"
echo "Bundle dir: $BUNDLE_DIR"
- name: Re-sign sidecar with hardened runtime and notarize
if: matrix.settings.platform == 'macos-latest'
shell: bash
env:
APPLE_CERTIFICATE_BASE64: ${{ 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_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
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'
shell: bash
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
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'
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
run: |
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'
shell: bash
run: |
APP_PATH="${{ steps.locate-app.outputs.app_path }}"
echo "Inspecting bundle at: $APP_PATH"
ls -la "$APP_PATH/Contents/MacOS"
ls -la "$APP_PATH/Contents/Resources" | grep openhuman || true
# Sidecar may be in MacOS/ or Resources/ depending on Tauri version
FOUND=false
if ls "$APP_PATH/Contents/MacOS"/openhuman* 2>/dev/null | grep -qv OpenHuman; then
echo "Sidecar found in Contents/MacOS/"
FOUND=true
fi
if ls "$APP_PATH/Contents/Resources"/openhuman-* 2>/dev/null; then
echo "Sidecar found in Contents/Resources/"
FOUND=true
fi
if [ "$FOUND" = "false" ]; then
echo "WARNING: Sidecar binary not found in expected locations"
fi
# Verify notarization staple
echo "Checking staple..."
xcrun stapler validate "$APP_PATH" || echo "WARNING: Staple validation failed"
- name: Package CLI tarball and upload to release
if: matrix.settings.platform != 'windows-latest'
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# 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)
[[ -z "$BIN" ]] && BIN=$(find "$APP_PATH/Contents/Resources" -maxdepth 1 \
-name "openhuman-core-*" ! -name "*.sig" 2>/dev/null | head -1 || true)
if [[ -z "$BIN" ]]; then
echo "[pkg] Falling back to target dir binary (no notarized sidecar found)"
BIN="${{ steps.cli-paths.outputs.cli_path }}"
fi
else
BIN="${{ steps.cli-paths.outputs.cli_path }}"
fi
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
with:
name: standalone-bins-${{ matrix.settings.platform }}-${{ matrix.settings.artifact_suffix }}
path: |
${{ steps.cli-paths.outputs.cli_path }}
# =========================================================================
# Phase 3b: Build & push Docker image (runs parallel with build-desktop)
# =========================================================================
build-docker:
name: "Docker: build and push"
needs: [prepare-release, create-release]
runs-on: ubuntu-latest
environment: Production
env:
REGISTRY: ghcr.io
IMAGE_NAME: tinyhumansai/openhuman-core
steps:
- name: Checkout tag
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare-release.outputs.tag }}
fetch-depth: 1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# TODO: Uncomment this when we have a staging tag
# - name: Build and push staging tag
# uses: docker/build-push-action@v6
# with:
# context: .
# file: Dockerfile
# push: true
# tags: |
# ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:staging-${{ needs.prepare-release.outputs.tag }}
# cache-from: type=gha
# cache-to: type=gha,mode=max
# labels: |
# org.opencontainers.image.title=openhuman-core
# org.opencontainers.image.description=OpenHuman core JSON-RPC server
# org.opencontainers.image.version=${{ needs.prepare-release.outputs.version }}
# org.opencontainers.image.source=https://github.com/tinyhumansai/openhuman
# org.opencontainers.image.revision=${{ needs.prepare-release.outputs.sha }}
# =========================================================================
# Phase 4: Publish the draft release (waits for ALL build phases)
# =========================================================================
publish-release:
name: Publish draft release
runs-on: ubuntu-latest
environment: Production
needs: [prepare-release, create-release, build-desktop, build-docker]
if: needs.build-desktop.result == 'success' && needs.build-docker.result == 'success'
steps:
- name: Validate required installer assets exist
uses: actions/github-script@v7
with:
script: |
const releaseId = Number('${{ needs.create-release.outputs.release_id }}');
const { owner, repo } = context.repo;
const { data: assets } = await github.rest.repos.listReleaseAssets({
owner,
repo,
release_id: releaseId,
per_page: 100,
});
const names = assets.map((a) => a.name);
const requiredPatterns = [
/OpenHuman_.*_aarch64\.dmg$/,
/OpenHuman_.*_x64\.dmg$/,
/OpenHuman_.*_amd64\.AppImage$/,
/OpenHuman_.*_amd64\.deb$/,
// Windows build is currently disabled in the matrix
// /(OpenHuman_.*_x64-setup\.exe$|OpenHuman_.*_x64.*\.msi$)/,
];
const missing = requiredPatterns.filter((pattern) => !names.some((name) => pattern.test(name)));
if (missing.length > 0) {
core.setFailed(`Missing required installer assets. Got: ${names.join(', ')}`);
return;
}
core.info('All required installer assets are present.');
# - name: Promote Docker image from staging to release tags
# env:
# REGISTRY: ghcr.io
# IMAGE_NAME: tinyhumansai/openhuman-core
# TAG: ${{ needs.prepare-release.outputs.tag }}
# run: |
# # Log in to GHCR
# echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
# STAGING="${REGISTRY}/${IMAGE_NAME}:staging-${TAG}"
# VERSIONED="${REGISTRY}/${IMAGE_NAME}:${TAG}"
# LATEST="${REGISTRY}/${IMAGE_NAME}:latest"
# echo "Promoting ${STAGING} → ${VERSIONED}, ${LATEST}"
# docker buildx imagetools create --tag "${VERSIONED}" --tag "${LATEST}" "${STAGING}"
# - name: Append Docker pull instructions to release notes
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# TAG: ${{ needs.prepare-release.outputs.tag }}
# IMAGE_NAME: tinyhumansai/openhuman-core
# run: |
# gh release view "${TAG}" --repo tinyhumansai/openhuman --json body -q .body > /tmp/body.md
# printf '\n---\n\n### Docker\n\n```\ndocker pull ghcr.io/%s:%s\ndocker run -p 7788:7788 --env-file .env ghcr.io/%s:%s\n```\n' \
# "${IMAGE_NAME}" "${TAG}" "${IMAGE_NAME}" "${TAG}" >> /tmp/body.md
# gh release edit "${TAG}" --repo tinyhumansai/openhuman --notes-file /tmp/body.md
- name: Publish release
uses: actions/github-script@v7
with:
script: |
const releaseId = Number('${{ needs.create-release.outputs.release_id }}');
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId,
draft: false,
});
core.info(`Published release ${releaseId}`);
# =========================================================================
# Cleanup: remove draft release + tag if ANY build phase failed
# =========================================================================
cleanup-failed-release:
name: Remove release and tag if build failed
runs-on: ubuntu-latest
environment: Production
needs: [prepare-release, create-release, build-desktop, build-docker]
if: >-
always()
&& needs.create-release.result == 'success'
&& (needs.build-desktop.result == 'failure' || needs.build-desktop.result == 'cancelled'
|| needs.build-docker.result == 'failure' || needs.build-docker.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;
}
}
- name: Delete staging Docker image
continue-on-error: true
env:
TAG: ${{ needs.prepare-release.outputs.tag }}
run: |
PACKAGE="openhuman-core"
STAGING_TAG="staging-${TAG}"
echo "Attempting to delete staging Docker tag: ${STAGING_TAG}"
# List package versions and find the staging tag
VERSION_ID="$(gh api \
-H "Accept: application/vnd.github+json" \
"/orgs/tinyhumansai/packages/container/${PACKAGE}/versions" \
--paginate --jq ".[] | select(.metadata.container.tags[] == \"${STAGING_TAG}\") | .id" 2>/dev/null | head -1)"
if [ -n "$VERSION_ID" ]; then
gh api -X DELETE "/orgs/tinyhumansai/packages/container/${PACKAGE}/versions/${VERSION_ID}" || true
echo "Deleted staging image version ${VERSION_ID}"
else
echo "Staging tag ${STAGING_TAG} not found or already deleted"
fi