diff --git a/.github/workflows/release-production.yml b/.github/workflows/release-production.yml index 8bcabc3b3..49697850f 100644 --- a/.github/workflows/release-production.yml +++ b/.github/workflows/release-production.yml @@ -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 — matches the GitHub Release tag (e.g. v1.2.4) + # - : — 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<> "$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@+`) + # 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 / : 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` and `:` 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 diff --git a/.github/workflows/release-staging.yml b/.github/workflows/release-staging.yml index 6b83f47d3..f9198e4d8 100644 --- a/.github/workflows/release-staging.yml +++ b/.github/workflows/release-staging.yml @@ -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 diff --git a/gitbooks/features/cloud-deploy.md b/gitbooks/features/cloud-deploy.md index 20a248297..e8cb9f72b 100644 --- a/gitbooks/features/cloud-deploy.md +++ b/gitbooks/features/cloud-deploy.md @@ -109,6 +109,33 @@ doctl apps update --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--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.