ci(release): ship Linux openhuman-core tarballs and push GHCR image (#1442)

This commit is contained in:
Steven Enamakel
2026-05-09 16:09:04 -07:00
committed by GitHub
parent 97089a68a6
commit 1d28f2c8e3
3 changed files with 322 additions and 37 deletions
+220 -28
View File
@@ -43,18 +43,18 @@ concurrency:
# │
# ├─── create-release
# │ │
# │ ┌────┴────────────┐
# │ │ │
# │ build-desktop build-docker
# │ (reusable wf) (GHCR image)
# │ │ │
# │ └────────────────┘
# │ │
# │ publish-updater-manifest
# │ │
# │ publish-release
# │ │
# │ record-sentry-deploy
# │ ┌────┴───────────────┬────────────────
# │ │
# │ build-desktop build-cli-linux build-docker
# │ (reusable wf) (Linux tarballs) (GHCR image)
# │ │
# │ └────────┬───────────┴────────────────┘
# │
# │ publish-updater-manifest
# │
# │ publish-release
# │
# │ record-sentry-deploy
# │
# └─── cleanup-failed-release (on failure)
#
@@ -351,7 +351,27 @@ jobs:
build_sidecar: false
# =========================================================================
# Phase 3b: Build & push Docker image (runs parallel with build-desktop)
# Phase 3b: Build & push Docker image (runs parallel with build-desktop).
#
# Publishes `ghcr.io/tinyhumansai/openhuman-core` with two immutable tags
# per release:
# - :v<version> — matches the GitHub Release tag (e.g. v1.2.4)
# - :<version> — bare SemVer for tooling that strips the v
#
# `:latest` is intentionally NOT pushed here. If a downstream phase
# (build-cli-linux, publish-updater-manifest, the asset-validation gate
# in publish-release) fails, the immutable tags are deleted by
# cleanup-failed-release while the release is rolled back. Pushing
# :latest in this job would move the moving tag onto an image whose
# release got cleaned up, leaving downstream `docker pull …:latest`
# consumers on a build that has no GitHub Release behind it. The
# `tag-docker-latest` job below promotes :latest only after
# `publish-release` succeeds.
#
# linux/amd64 only for now. arm64 users pull the standalone CLI tarball
# (`build-cli-linux` matrix) or build the image from source. Adding arm64
# via QEMU here triples build time on Rust-heavy stages; revisit when an
# `ubuntu-24.04-arm` runner is wired into a per-arch matrix + manifest job.
# =========================================================================
build-docker:
name: "Docker: build and push"
@@ -376,9 +396,119 @@ jobs:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute image tags
id: image-tags
env:
REGISTRY: ${{ env.REGISTRY }}
IMAGE_NAME: ${{ env.IMAGE_NAME }}
TAG: ${{ needs.prepare-build.outputs.tag }}
VERSION: ${{ needs.prepare-build.outputs.version }}
run: |
set -euo pipefail
base="${REGISTRY}/${IMAGE_NAME}"
{
echo "tags<<EOF"
echo "${base}:${TAG}"
echo "${base}:${VERSION}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
push: true
platforms: linux/amd64
tags: ${{ steps.image-tags.outputs.tags }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ needs.prepare-build.outputs.sha }}
org.opencontainers.image.version=${{ needs.prepare-build.outputs.version }}
org.opencontainers.image.title=openhuman-core
cache-from: type=gha,scope=release-production
cache-to: type=gha,scope=release-production,mode=max
- name: Verify pushed image is pullable
run: |
set -euo pipefail
image="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare-build.outputs.tag }}"
docker pull "$image"
docker image inspect "$image" >/dev/null
# =========================================================================
# Phase 3c: Generate and upload latest.json for the Tauri auto-updater.
# Phase 3c: Build standalone Linux openhuman-core tarballs and attach them
# to the GitHub Release. Operators on Linux servers without Docker pull a
# plain tarball + sha256 from the release page; cloud-deploy.md links here.
#
# arm64 uses GitHub-hosted ubuntu-24.04-arm to avoid QEMU emulation
# (matches release-packages.yml). If that runner is unavailable for the
# repo's plan, fall back to ubuntu-22.04 + cross-rs (see comment in
# release-packages.yml `build-cli-linux-arm64`).
# =========================================================================
build-cli-linux:
name: "CLI: ${{ matrix.target }}"
needs: [prepare-build, create-release]
if: needs.create-release.result == 'success'
environment: Production
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
steps:
- name: Checkout build ref
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare-build.outputs.build_ref }}
fetch-depth: 1
submodules: recursive
- name: Install Rust (rust-toolchain.toml)
uses: dtolnay/rust-toolchain@1.93.0
- name: Cache Cargo
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}-release
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
pkg-config libssl-dev build-essential cmake \
libasound2-dev libxdo-dev libxtst-dev libx11-dev libevdev-dev \
clang
- name: Verify Sentry DSN is present
env:
OPENHUMAN_CORE_SENTRY_DSN: ${{ vars.OPENHUMAN_CORE_SENTRY_DSN || vars.OPENHUMAN_SENTRY_DSN }}
run: |
if [ -z "${OPENHUMAN_CORE_SENTRY_DSN}" ]; then
echo "::error::vars.OPENHUMAN_CORE_SENTRY_DSN (or legacy vars.OPENHUMAN_SENTRY_DSN) is empty — the Linux CLI tarball would ship without crash reporting."
exit 1
fi
echo "OPENHUMAN_CORE_SENTRY_DSN is set (length=${#OPENHUMAN_CORE_SENTRY_DSN})"
- name: Build openhuman-core binary
env:
OPENHUMAN_CORE_SENTRY_DSN: ${{ vars.OPENHUMAN_CORE_SENTRY_DSN || vars.OPENHUMAN_SENTRY_DSN }}
# Match the runtime release tag (`openhuman@<version>+<short_sha>`)
# baked elsewhere — see prepare-build.outputs.short_sha comment.
OPENHUMAN_BUILD_SHA: ${{ needs.prepare-build.outputs.short_sha }}
OPENHUMAN_APP_ENV: production
run: cargo build --release --bin openhuman-core
- name: Package and upload tarball to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
UPLOAD_REPO: ${{ github.repository }}
VERSION: ${{ needs.prepare-build.outputs.version }}
TARGET: ${{ matrix.target }}
run: |
bash scripts/release/package-cli-tarball.sh \
target/release/openhuman-core \
"$VERSION" \
"$TARGET"
# =========================================================================
# Phase 3d: Generate and upload latest.json for the Tauri auto-updater.
# Runs after every platform has uploaded its updater artifact (.sig files
# from createUpdaterArtifacts) so the manifest can reference all four
# platform entries.
@@ -415,10 +545,12 @@ jobs:
- prepare-build
- create-release
- build-desktop
- build-cli-linux
- build-docker
- publish-updater-manifest
if: >-
needs.build-desktop.result == 'success'
&& needs.build-cli-linux.result == 'success'
&& needs.build-docker.result == 'success'
&& needs.publish-updater-manifest.result == 'success'
steps:
@@ -442,6 +574,13 @@ jobs:
// Auto-updater manifest — without this, installed clients can't
// discover new releases via plugins.updater.endpoints.
/^latest\.json$/,
// Linux standalone openhuman-core CLI tarballs (build-cli-linux).
// Operators on headless Linux servers pull these instead of the
// Tauri bundle; cloud-deploy.md documents both arches.
/^openhuman-core-.*-x86_64-unknown-linux-gnu\.tar\.gz$/,
/^openhuman-core-.*-x86_64-unknown-linux-gnu\.tar\.gz\.sha256$/,
/^openhuman-core-.*-aarch64-unknown-linux-gnu\.tar\.gz$/,
/^openhuman-core-.*-aarch64-unknown-linux-gnu\.tar\.gz\.sha256$/,
];
const missing = requiredPatterns.filter((pattern) => !names.some((name) => pattern.test(name)));
if (missing.length > 0) {
@@ -463,6 +602,47 @@ jobs:
});
core.info(`Published release ${releaseId}`);
# =========================================================================
# Phase 4b: Promote :latest to the just-published image.
#
# `build-docker` only pushed the immutable :v<version> / :<version> tags.
# We delay :latest until publish-release has actually flipped the GitHub
# Release out of draft, so a downstream failure (build-cli-linux,
# publish-updater-manifest, the asset-validation gate) cleans up the
# tagged image without leaving :latest pointing at a build that has no
# release behind it. Uses `docker buildx imagetools create` to add the
# extra tag without re-pulling the build context — it operates against
# the registry manifest, not local layers.
# =========================================================================
tag-docker-latest:
name: "Docker: tag :latest"
runs-on: ubuntu-latest
environment: Production
needs: [prepare-build, publish-release]
if: needs.publish-release.result == 'success'
env:
REGISTRY: ghcr.io
IMAGE_NAME: tinyhumansai/openhuman-core
steps:
- 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 }}
- name: Promote :latest
env:
REGISTRY: ${{ env.REGISTRY }}
IMAGE_NAME: ${{ env.IMAGE_NAME }}
TAG: ${{ needs.prepare-build.outputs.tag }}
run: |
set -euo pipefail
src="${REGISTRY}/${IMAGE_NAME}:${TAG}"
dst="${REGISTRY}/${IMAGE_NAME}:latest"
docker buildx imagetools create --tag "$dst" "$src"
# =========================================================================
# Phase 5: Record a single Sentry deploy marker once the release has
# actually been published. Hangs off `publish-release` so a failed build
@@ -513,12 +693,15 @@ jobs:
- prepare-build
- create-release
- build-desktop
- build-cli-linux
- build-docker
- publish-updater-manifest
if: >-
always()
&& needs.create-release.result == 'success'
&& (needs.build-desktop.result == 'failure' || needs.build-desktop.result ==
'cancelled'
|| needs.build-cli-linux.result == 'failure' || needs.build-cli-linux.result ==
'cancelled'
|| needs.build-docker.result == 'failure' || needs.build-docker.result ==
'cancelled'
@@ -559,22 +742,31 @@ jobs:
throw e;
}
}
- name: Delete staging Docker image
- name: Delete published Docker image versions
# If `build-docker` already pushed but a downstream phase failed, the
# GHCR image would otherwise outlive the GitHub Release. Walk the
# `:v<version>` and `:<version>` tags we just pushed and remove the
# underlying package version. `:latest` is left alone — the previous
# release is still pointed at it, and clobbering the moving tag here
# would orphan downstream pulls.
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.prepare-build.outputs.tag }}
VERSION: ${{ needs.prepare-build.outputs.version }}
run: |-
set -uo pipefail
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
for IMAGE_TAG in "${TAG}" "${VERSION}"; do
echo "Attempting to delete Docker tag: ${IMAGE_TAG}"
VERSION_ID="$(gh api \
-H "Accept: application/vnd.github+json" \
"/orgs/tinyhumansai/packages/container/${PACKAGE}/versions" \
--paginate --jq ".[] | select(.metadata.container.tags[]? == \"${IMAGE_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 image version ${VERSION_ID} (tag ${IMAGE_TAG})"
else
echo "Tag ${IMAGE_TAG} not found or already deleted"
fi
done
+52 -9
View File
@@ -15,7 +15,8 @@ concurrency:
#
# prepare-build
# │
# build-desktop (delegated to .github/workflows/build-desktop.yml)
# ├── build-desktop (delegated to .github/workflows/build-desktop.yml)
# ├── build-docker (build only — no GHCR push on staging)
# │
# record-sentry-deploy
# │
@@ -182,13 +183,54 @@ jobs:
# No publish-updater-manifest job in staging — producing .sig artifacts
# would just leave them stranded in the Actions artifact tree.
with_updater: false
# Standalone openhuman-core CLI is shipped via the production GHCR
# docker image (build-docker in release-production.yml) and via
# brew / homebrew packages updated manually — the staging Actions
# artifact had no real consumer. Set `build_sidecar: true` here to
# re-enable the matrix-built CLI artifact + its Sentry DIF upload.
# Standalone openhuman-core CLI ships from the production cut only:
# `build-docker` pushes `ghcr.io/tinyhumansai/openhuman-core` and
# `build-cli-linux` attaches Linux x86_64 / aarch64 tarballs to the
# GitHub Release (see release-production.yml). Staging does not
# publish either surface — the matrix-built sidecar artifact had no
# real consumer. Set `build_sidecar: true` to re-enable a per-platform
# CLI Actions artifact + its Sentry DIF upload for QA spot-checks.
build_sidecar: false
# =========================================================================
# Phase 2b: Build the openhuman-core Docker image without pushing.
# Mirrors the production `build-docker` job so a Dockerfile regression
# surfaces on the staging cut — no GHCR push, no `:staging-*` tag
# pollution. `deploy-smoke.yml` already covers the build path on PRs
# that touch Dockerfile / src; this is the equivalent gate at the
# staging-tag boundary so a green staging cut means the next prod
# promotion's GHCR push will succeed too.
# =========================================================================
build-docker:
name: "Docker: build (no push)"
needs: [prepare-build]
runs-on: ubuntu-latest
environment: Production
steps:
- name: Checkout build ref
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare-build.outputs.build_ref }}
fetch-depth: 1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image (no push)
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
push: false
load: true
platforms: linux/amd64
tags: openhuman-core:staging-${{ needs.prepare-build.outputs.tag }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ needs.prepare-build.outputs.sha }}
org.opencontainers.image.version=${{ needs.prepare-build.outputs.version }}
org.opencontainers.image.title=openhuman-core
cache-from: type=gha,scope=release-staging
cache-to: type=gha,scope=release-staging,mode=max
# =========================================================================
# Phase 3: Record a single Sentry deploy marker once the matrix is
# complete. Lives in its own job (not inside the reusable workflow)
@@ -202,7 +244,7 @@ jobs:
name: Record Sentry deploy marker
runs-on: ubuntu-latest
environment: Production
needs: [prepare-build, build-desktop]
needs: [prepare-build, build-desktop, build-docker]
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
steps:
@@ -240,11 +282,12 @@ jobs:
name: Remove staging tag if build failed
runs-on: ubuntu-latest
environment: Production
needs: [prepare-build, build-desktop]
needs: [prepare-build, build-desktop, build-docker]
if: >-
always()
&& needs.prepare-build.result == 'success'
&& (needs.build-desktop.result == 'failure' || needs.build-desktop.result == 'cancelled')
&& (needs.build-desktop.result == 'failure' || needs.build-desktop.result == 'cancelled'
|| needs.build-docker.result == 'failure' || needs.build-docker.result == 'cancelled')
steps:
- name: Delete remote staging tag
uses: actions/github-script@v7
+50
View File
@@ -109,6 +109,33 @@ doctl apps update <app-id> --spec .do/app.yaml
Works on any host with Docker Engine ≥ 24 and the Compose plugin.
DigitalOcean Droplet, Hetzner, Linode, EC2, a home server.
Each production release publishes a multi-tagged image to GHCR:
```bash
docker pull ghcr.io/tinyhumansai/openhuman-core:latest # tracks the latest prod cut
docker pull ghcr.io/tinyhumansai/openhuman-core:v1.2.4 # pinned by GitHub Release tag
docker pull ghcr.io/tinyhumansai/openhuman-core:1.2.4 # pinned by SemVer
```
The image is `linux/amd64`. arm64 hosts pull the standalone tarball
attached to the same GitHub Release (`openhuman-core-<version>-aarch64-unknown-linux-gnu.tar.gz`)
or build the image from source on an arm64 builder.
Quick run with a published image:
```bash
docker run -d --name openhuman-core -p 7788:7788 \
-e OPENHUMAN_CORE_TOKEN="$(openssl rand -hex 32)" \
-e BACKEND_URL=https://api.tinyhumans.ai \
-e OPENHUMAN_APP_ENV=production \
-v openhuman-workspace:/home/openhuman/.openhuman \
ghcr.io/tinyhumansai/openhuman-core:latest
```
Or use the in-repo Compose file (still builds the image locally from
`Dockerfile`; switch the `image:` field to `ghcr.io/tinyhumansai/openhuman-core:latest`
in `docker-compose.yml` to consume the published image instead):
```bash
# On the server:
git clone https://github.com/tinyhumansai/openhuman.git
@@ -129,6 +156,29 @@ docker compose ps
curl -fsS http://localhost:7788/health
```
### Headless install without Docker
If you can't run Docker on the host, grab the standalone CLI tarball
attached to the latest [GitHub Release](https://github.com/tinyhumansai/openhuman/releases/latest):
```bash
# Pick the tarball that matches your host arch.
ARCH="$(uname -m)"
case "$ARCH" in
x86_64) TARGET=x86_64-unknown-linux-gnu ;;
aarch64) TARGET=aarch64-unknown-linux-gnu ;;
*) echo "Unsupported arch: $ARCH"; exit 1 ;;
esac
VERSION=1.2.4 # set to the release you want
curl -fsSL "https://github.com/tinyhumansai/openhuman/releases/download/v${VERSION}/openhuman-core-${VERSION}-${TARGET}.tar.gz" \
| tar -xz -C /usr/local/bin
openhuman-core --version
```
Then run `openhuman-core serve` under your service manager of choice
(systemd, supervisord, …) with the same environment variables documented
above.
The Compose file ([`docker-compose.yml`](../../docker-compose.yml)) maps the core
on `:7788`, mounts a named volume `openhuman-workspace` for persistence, and
sets `restart: unless-stopped` so the core comes back after host reboots.