diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml new file mode 100644 index 000000000..cc761c9fc --- /dev/null +++ b/.github/workflows/release-packages.yml @@ -0,0 +1,454 @@ +name: Release Packages + +# Triggered after the main release.yml publishes a release. +# By the time this fires, macOS (aarch64 + x86_64) and Linux x86_64 +# CLI tarballs are already attached to the release by release.yml. +# +# This workflow adds: +# 1. Linux arm64 CLI tarball +# 2. Homebrew tap formula update +# 3. Debian apt repository (gh-pages) +# 4. npm package publish +# 5. Smoke tests for each channel +# 6. One-time backlog issue for future package managers + +on: + release: + types: [published] + +permissions: + contents: write + pages: write + id-token: write + issues: write + +concurrency: + group: release-packages-${{ github.event.release.tag_name }} + cancel-in-progress: false + +jobs: + + # ──────────────────────────────────────────────────────────────────────────── + # 1. Build Linux arm64 CLI tarball (native runner) + # Requires: ubuntu-24.04-arm GitHub-hosted runner (free for public repos). + # If this runner type is unavailable on your plan, replace runs-on with + # ubuntu-22.04 and add: uses: taiki-e/install-action@cross + use + # `cross build --target aarch64-unknown-linux-gnu` instead of plain cargo. + # ──────────────────────────────────────────────────────────────────────────── + build-cli-linux-arm64: + name: Build Linux arm64 CLI tarball + runs-on: ubuntu-24.04-arm + steps: + - name: Checkout tag + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + fetch-depth: 1 + submodules: true + + - name: Install Rust + uses: dtolnay/rust-toolchain@1.93.0 + + - name: Cache Cargo + uses: Swatinem/rust-cache@v2 + with: + key: linux-arm64-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 + + - name: Build CLI binary + run: cargo build --release --bin openhuman-core + env: + OPENHUMAN_SENTRY_DSN: ${{ vars.OPENHUMAN_SENTRY_DSN }} + + - name: Package tarball and upload to release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" + TARGET="aarch64-unknown-linux-gnu" + TARBALL="openhuman-core-${VERSION}-${TARGET}.tar.gz" + + WORK=$(mktemp -d) + trap 'rm -rf "$WORK"' EXIT + cp target/release/openhuman-core "$WORK/" + chmod +x "$WORK/openhuman-core" + tar -czf "$TARBALL" -C "$WORK" openhuman-core + openssl dgst -sha256 -r "$TARBALL" | awk '{print $1}' > "${TARBALL}.sha256" + + gh release upload "${{ github.event.release.tag_name }}" \ + "$TARBALL" "${TARBALL}.sha256" \ + --repo tinyhumansai/openhuman --clobber + + echo "[pkg] Uploaded $TARBALL" + + # ──────────────────────────────────────────────────────────────────────────── + # 2. Update Homebrew tap + # Requires secret: HOMEBREW_TAP_TOKEN (PAT or App token with contents:write + # on tinyhumansai/homebrew-openhuman) + # ──────────────────────────────────────────────────────────────────────────── + update-homebrew: + name: Update Homebrew tap formula + runs-on: ubuntu-latest + needs: [build-cli-linux-arm64] + steps: + - name: Checkout main repo (for formula template) + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + path: src + + - name: Checkout Homebrew tap + uses: actions/checkout@v4 + with: + repository: tinyhumansai/homebrew-openhuman + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + path: tap + + - name: Download tarballs and compute checksums + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + TAG="${{ github.event.release.tag_name }}" + VERSION="${TAG#v}" + mkdir -p /tmp/tarballs + + for row in \ + "aarch64-apple-darwin:SHA256_MACOS_ARM64" \ + "x86_64-apple-darwin:SHA256_MACOS_X64" \ + "x86_64-unknown-linux-gnu:SHA256_LINUX_X64" \ + "aarch64-unknown-linux-gnu:SHA256_LINUX_ARM64" + do + TARGET="${row%%:*}" + VAR="${row##*:}" + TARBALL="openhuman-core-${VERSION}-${TARGET}.tar.gz" + echo "[homebrew] Downloading $TARBALL ..." + gh release download "$TAG" \ + --pattern "$TARBALL" \ + --repo tinyhumansai/openhuman \ + --dir /tmp/tarballs + SHA=$(openssl dgst -sha256 -r "/tmp/tarballs/$TARBALL" | awk '{print $1}') + echo "${VAR}=${SHA}" >> "$GITHUB_ENV" + echo "[homebrew] $TARGET → $SHA" + done + echo "VERSION=${VERSION}" >> "$GITHUB_ENV" + + - name: Render and commit formula + run: | + set -euo pipefail + mkdir -p tap/Formula + + sed \ + -e "s/@VERSION@/${VERSION}/g" \ + -e "s/@SHA256_MACOS_ARM64@/${SHA256_MACOS_ARM64}/g" \ + -e "s/@SHA256_MACOS_X64@/${SHA256_MACOS_X64}/g" \ + -e "s/@SHA256_LINUX_X64@/${SHA256_LINUX_X64}/g" \ + -e "s/@SHA256_LINUX_ARM64@/${SHA256_LINUX_ARM64}/g" \ + src/packages/homebrew/openhuman.rb > tap/Formula/openhuman.rb + + cd tap + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add Formula/openhuman.rb + git diff --cached --quiet && echo "[homebrew] No changes to commit." && exit 0 + git commit -m "chore: update formula to v${VERSION}" + git push + + # ──────────────────────────────────────────────────────────────────────────── + # 3. Build Debian apt repository and deploy to GitHub Pages + # Requires: APT_SIGNING_KEY (ASCII-armor GPG private key secret) + # APT_SIGNING_KEY_ID (key fingerprint / ID) + # GitHub Pages must be enabled (Settings → Pages → Source: gh-pages branch) + # ──────────────────────────────────────────────────────────────────────────── + build-apt-repo: + name: Build apt repository + runs-on: ubuntu-22.04 + needs: [build-cli-linux-arm64] + steps: + - name: Checkout tag + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + fetch-depth: 1 + + - name: Install apt-repo build tools + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends \ + dpkg-dev apt-utils gnupg2 + + - name: Import GPG signing key + env: + APT_SIGNING_KEY: ${{ secrets.APT_SIGNING_KEY }} + run: | + echo "$APT_SIGNING_KEY" | gpg --batch --import + gpg --list-secret-keys + + - name: Download Linux CLI tarballs from release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + TAG="${{ github.event.release.tag_name }}" + VERSION="${TAG#v}" + mkdir -p /tmp/tarballs + + for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do + TARBALL="openhuman-core-${VERSION}-${target}.tar.gz" + gh release download "$TAG" \ + --pattern "$TARBALL" \ + --repo tinyhumansai/openhuman \ + --dir /tmp/tarballs + echo "[apt] Downloaded $TARBALL" + done + echo "VERSION=${VERSION}" >> "$GITHUB_ENV" + + - name: Extract binaries and build .deb packages + run: | + set -euo pipefail + mkdir -p /tmp/bins + + tar -xzf "/tmp/tarballs/openhuman-core-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" \ + -C /tmp/bins + mv /tmp/bins/openhuman-core /tmp/bins/openhuman-core-amd64 + + tar -xzf "/tmp/tarballs/openhuman-core-${VERSION}-aarch64-unknown-linux-gnu.tar.gz" \ + -C /tmp/bins + mv /tmp/bins/openhuman-core /tmp/bins/openhuman-core-arm64 + + chmod +x /tmp/bins/openhuman-core-amd64 /tmp/bins/openhuman-core-arm64 + + bash packages/deb/build.sh /tmp/bins/openhuman-core-amd64 "${VERSION}" amd64 + bash packages/deb/build.sh /tmp/bins/openhuman-core-arm64 "${VERSION}" arm64 + + ls -lh openhuman_*.deb + + - name: Build apt repository + env: + APT_SIGNING_KEY_ID: ${{ secrets.APT_SIGNING_KEY_ID }} + run: | + bash scripts/build-apt-repo.sh /tmp/apt-repo \ + "openhuman_${VERSION}_amd64.deb" \ + "openhuman_${VERSION}_arm64.deb" + + - name: Checkout gh-pages branch + uses: actions/checkout@v4 + with: + ref: gh-pages + path: gh-pages + # Create the branch if it doesn't exist + fetch-depth: 0 + + - name: Update apt/ directory on gh-pages + run: | + set -euo pipefail + mkdir -p gh-pages/apt + # Remove old apt content, replace with freshly built repo + rm -rf gh-pages/apt/* + cp -r /tmp/apt-repo/. gh-pages/apt/ + + cd gh-pages + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add apt/ + git diff --cached --quiet && echo "[apt] No changes." && exit 0 + git commit -m "chore(apt): publish v${VERSION}" + git push origin gh-pages + + # ──────────────────────────────────────────────────────────────────────────── + # 4. Publish npm package + # Requires secret: NPM_TOKEN (automation token from npmjs.com) + # ──────────────────────────────────────────────────────────────────────────── + publish-npm: + name: Publish npm package + runs-on: ubuntu-latest + steps: + - name: Checkout tag + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24.x + registry-url: 'https://registry.npmjs.org' + + - name: Set version and publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + set -euo pipefail + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" + + cd packages/npm + + # Stamp version without creating a git commit + npm version "$VERSION" --no-git-tag-version + + # SKIP_OPENHUMAN_BINARY_DOWNLOAD prevents postinstall from running + # during publish (the binary doesn't exist yet on the publish runner) + SKIP_OPENHUMAN_BINARY_DOWNLOAD=1 npm publish --access public + + echo "[npm] Published openhuman@${VERSION}" + + # ──────────────────────────────────────────────────────────────────────────── + # 5. Smoke test: Homebrew + # ──────────────────────────────────────────────────────────────────────────── + smoke-homebrew: + name: Smoke — Homebrew (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + needs: [update-homebrew] + continue-on-error: true + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-22.04] + steps: + - name: Install Homebrew (Linux) + if: runner.os == 'Linux' + run: | + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + echo "/home/linuxbrew/.linuxbrew/bin" >> "$GITHUB_PATH" + + - name: Tap and install + run: | + brew tap tinyhumansai/openhuman + brew install openhuman + + - name: Smoke test + run: openhuman --version + + # ──────────────────────────────────────────────────────────────────────────── + # 6. Smoke test: apt + # ──────────────────────────────────────────────────────────────────────────── + smoke-apt: + name: Smoke — apt (ubuntu-22.04) + runs-on: ubuntu-22.04 + needs: [build-apt-repo] + continue-on-error: true + steps: + - name: Add apt repository + run: | + sudo apt-get install -y --no-install-recommends gnupg2 curl ca-certificates + curl -fsSL https://tinyhumansai.github.io/openhuman/apt/KEY.gpg \ + | sudo gpg --dearmor -o /etc/apt/keyrings/openhuman.gpg + echo "deb [signed-by=/etc/apt/keyrings/openhuman.gpg arch=amd64] \ + https://tinyhumansai.github.io/openhuman/apt stable main" \ + | sudo tee /etc/apt/sources.list.d/openhuman.list + + - name: Install and smoke test + run: | + sudo apt-get update + sudo apt-get install -y openhuman + openhuman --version + + # ──────────────────────────────────────────────────────────────────────────── + # 7. Smoke test: npm + # ──────────────────────────────────────────────────────────────────────────── + smoke-npm: + name: Smoke — npm (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + needs: [publish-npm] + continue-on-error: true + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24.x + + - name: Wait for npm propagation, then install + run: | + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" + # npm can take up to ~2 min to propagate a new publish + for i in 1 2 3 4 5; do + npm install -g "openhuman@${VERSION}" && break || sleep 30 + done + + - name: Smoke test + run: openhuman --version + + # ──────────────────────────────────────────────────────────────────────────── + # 8. File the "future package managers" backlog issue (once ever) + # ──────────────────────────────────────────────────────────────────────────── + create-backlog-issue: + name: Create backlog issue (once) + runs-on: ubuntu-latest + steps: + - name: Create issue if it doesn't exist + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const label = 'distribution-backlog'; + const title = '[Backlog] Package manager distribution — next tiers'; + + // Check for existing open or closed issue with this exact title + const { data: existing } = await github.rest.issues.listForRepo({ + owner, repo, + state: 'all', + labels: label, + per_page: 10, + }); + if (existing.some(i => i.title === title)) { + core.info('Backlog issue already exists — skipping.'); + return; + } + + // Ensure the label exists + try { + await github.rest.issues.createLabel({ + owner, repo, + name: label, + color: '0075ca', + description: 'Package distribution backlog', + }); + } catch (_) { /* label may already exist */ } + + const body = `## Summary + +Track remaining package manager channels. Each tier reflects expected maintenance commitment from the core team. + +## Tier 1 — Official (core team maintains) + +- [ ] **npx / pnpm dlx** — zero-install via the npm package already published; document the one-liner: \`npx openhuman@latest\` +- [ ] **Scoop (Windows)** — needs a Windows binary (un-comment the Windows matrix in \`release.yml\` first); add a \`tinyhumansai/scoop-openhuman\` bucket + +## Tier 2 — Community-supported (PRs welcome, core team reviews) + +- [ ] **AUR (Arch Linux)** — add \`PKGBUILD\` pointing at the GitHub release tarball; list in \`packages/\` +- [ ] **Nix / nixpkgs** — upstream a \`pkgs/tools/openhuman/default.nix\` derivation; document local flake overlay as interim + +## Tier 3 — Planned (no timeline) + +- [ ] **Snap / Snapcraft** — \`snapcraft.yaml\`, publish to Snap Store +- [ ] **Flatpak** — \`org.tinyhumans.Openhuman.yaml\`, publish to Flathub +- [ ] **WinGet** — manifest in \`microsoft/winget-pkgs\` once Windows binary is stable + +## Acceptance criteria + +- [ ] Each official channel has a CI smoke test (install + \`openhuman --version\`) +- [ ] Install commands appear in \`docs/install.md\` +- [ ] Checksums shipped for all artifacts +`; + const issue = await github.rest.issues.create({ + owner, repo, + title, + body, + labels: [label], + }); + core.info(\`Created backlog issue: \${issue.data.html_url}\`); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1131491e1..0bfe1562c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -678,6 +678,48 @@ jobs: 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 }} + VERSION: ${{ needs.prepare-release.outputs.version }} + MATRIX_PLATFORM: ${{ matrix.settings.platform }} + MATRIX_TARGET: ${{ matrix.settings.target }} + run: | + set -euo pipefail + + TARBALL="openhuman-core-${VERSION}-${MATRIX_TARGET}.tar.gz" + WORK=$(mktemp -d) + trap 'rm -rf "$WORK"' EXIT + + if [[ "$MATRIX_PLATFORM" == "macos-latest" ]]; then + # Prefer the notarized+signed binary extracted from inside the .app bundle + 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 + + cp "$BIN" "$WORK/openhuman-core" + chmod +x "$WORK/openhuman-core" + tar -czf "$TARBALL" -C "$WORK" openhuman-core + + # openssl dgst works on both macOS and Linux runners + openssl dgst -sha256 -r "$TARBALL" | awk '{print $1}' > "${TARBALL}.sha256" + + gh release upload "v${VERSION}" "$TARBALL" "${TARBALL}.sha256" \ + --repo tinyhumansai/openhuman --clobber + + echo "[pkg] Uploaded $TARBALL and ${TARBALL}.sha256" + - name: Upload standalone CLI artifacts uses: actions/upload-artifact@v4 with: diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 000000000..d4ec4599e --- /dev/null +++ b/docs/install.md @@ -0,0 +1,199 @@ +# Installing OpenHuman + +## Quick install + +| Package manager | Command | OS | +|---|---|---| +| **Homebrew** | `brew install tinyhumansai/openhuman/openhuman` | macOS, Linux | +| **apt** | `sudo apt install openhuman` (see [setup](#apt-debianubuntu)) | Debian, Ubuntu | +| **npm** | `npm install -g openhuman` | Any (Node ≥ 18) | +| **curl** | `curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh \| bash` | macOS, Linux | + +--- + +## Homebrew (macOS / Linux) + +```bash +brew install tinyhumansai/openhuman/openhuman +``` + +**Update:** +```bash +brew upgrade openhuman +``` + +**Uninstall:** +```bash +brew uninstall openhuman +brew untap tinyhumansai/openhuman # optional: remove tap +``` + +Homebrew installs the binary as `openhuman`. The tap lives at +[tinyhumansai/homebrew-openhuman](https://github.com/tinyhumansai/homebrew-openhuman). + +--- + +## apt (Debian / Ubuntu) + +### 1. Add the repository key and source + +```bash +sudo apt-get install -y gnupg2 curl ca-certificates + +curl -fsSL https://tinyhumansai.github.io/openhuman/apt/KEY.gpg \ + | sudo gpg --dearmor -o /etc/apt/keyrings/openhuman.gpg + +echo "deb [signed-by=/etc/apt/keyrings/openhuman.gpg arch=amd64] \ + https://tinyhumansai.github.io/openhuman/apt stable main" \ + | sudo tee /etc/apt/sources.list.d/openhuman.list +``` + +> **arm64:** replace `arch=amd64` with `arch=arm64` or `arch=amd64,arm64`. + +### 2. Install + +```bash +sudo apt-get update +sudo apt-get install openhuman +``` + +**Update:** +```bash +sudo apt-get update && sudo apt-get upgrade openhuman +``` + +**Uninstall:** +```bash +sudo apt-get remove openhuman +# remove repository (optional): +sudo rm /etc/apt/sources.list.d/openhuman.list /etc/apt/keyrings/openhuman.gpg +``` + +--- + +## npm + +```bash +npm install -g openhuman +``` + +**Update:** +```bash +npm update -g openhuman +``` + +**Uninstall:** +```bash +npm uninstall -g openhuman +``` + +The npm package is a thin wrapper that downloads the platform-native binary on +first install and verifies its SHA-256 checksum before placing it. Node.js ≥ 18 +is required; the binary itself has no Node dependency at runtime. + +--- + +## curl / manual install + +```bash +curl -fsSL \ + https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh \ + | bash +``` + +Pass `--dry-run` to preview actions without installing: + +```bash +bash scripts/install.sh --dry-run --verbose +``` + +**Uninstall (manual):** +```bash +rm "$(which openhuman)" +``` + +--- + +## Support policy + +| Channel | Tier | Maintained by | +|---|---|---| +| Homebrew | **Official** | Core team | +| apt | **Official** | Core team | +| npm | **Official** | Core team | +| curl / install.sh | **Official** | Core team | +| AUR (Arch) | Community | Community PRs | +| Nix | Community | Community PRs | +| Scoop (Windows) | Planned | — | +| Snap / Flatpak | Planned | — | + +See [tinyhumansai/openhuman#distribution-backlog](https://github.com/tinyhumansai/openhuman/issues?q=label%3Adistribution-backlog) for the next channels in the pipeline. + +--- + +## Troubleshooting + +### macOS Gatekeeper warning + +If macOS blocks the binary with *"cannot be opened because the developer cannot be verified"*: + +```bash +# Option 1: approve via System Settings → Privacy & Security → Allow Anyway +# Option 2: remove quarantine flag (Homebrew install should handle this automatically) +xattr -d com.apple.quarantine "$(which openhuman)" +``` + +Binaries installed via Homebrew or the signed `.app` bundle are notarized by +Apple and should not trigger Gatekeeper. + +### apt: "NO_PUBKEY" error + +Re-import the key: +```bash +curl -fsSL https://tinyhumansai.github.io/openhuman/apt/KEY.gpg \ + | sudo gpg --dearmor -o /etc/apt/keyrings/openhuman.gpg +sudo apt-get update +``` + +### npm: binary not found after install + +The postinstall script may have failed silently. Re-run it manually: +```bash +FORCE_REINSTALL=1 node "$(npm root -g)/openhuman/install.js" +``` + +Or reinstall cleanly: +```bash +npm uninstall -g openhuman && npm install -g openhuman +``` + +### Verify checksum manually + +Every release asset ships a companion `.sha256` file: + +```bash +VERSION=0.49.33 +TARGET=x86_64-unknown-linux-gnu +curl -fsSLO "https://github.com/tinyhumansai/openhuman/releases/download/v${VERSION}/openhuman-core-${VERSION}-${TARGET}.tar.gz" +curl -fsSLO "https://github.com/tinyhumansai/openhuman/releases/download/v${VERSION}/openhuman-core-${VERSION}-${TARGET}.tar.gz.sha256" +echo "$(cat openhuman-core-${VERSION}-${TARGET}.tar.gz.sha256) openhuman-core-${VERSION}-${TARGET}.tar.gz" | sha256sum --check +``` + +--- + +## Release artifacts reference + +Each release attaches the following files: + +| Artifact | Platform | +|---|---| +| `openhuman-core--aarch64-apple-darwin.tar.gz` | macOS Apple Silicon | +| `openhuman-core--x86_64-apple-darwin.tar.gz` | macOS Intel | +| `openhuman-core--x86_64-unknown-linux-gnu.tar.gz` | Linux x86-64 | +| `openhuman-core--aarch64-unknown-linux-gnu.tar.gz` | Linux arm64 | +| `OpenHuman__aarch64.dmg` | macOS desktop app (Apple Silicon) | +| `OpenHuman__x64.dmg` | macOS desktop app (Intel) | +| `OpenHuman__amd64.deb` | Linux desktop app (.deb) | +| `OpenHuman__amd64.AppImage` | Linux desktop app (AppImage) | + +Every archive has a corresponding `.sha256` companion file. diff --git a/packages/deb/build.sh b/packages/deb/build.sh new file mode 100755 index 000000000..2e77b1cbf --- /dev/null +++ b/packages/deb/build.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Build a .deb package for the openhuman-core CLI binary. +# Usage: build.sh +# arch: amd64 | arm64 +set -euo pipefail + +BINARY="$1" +VERSION="$2" +ARCH="$3" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="$(mktemp -d)" +trap 'rm -rf "$WORK_DIR"' EXIT + +PKG_NAME="openhuman_${VERSION}_${ARCH}" +PKG_DIR="$WORK_DIR/$PKG_NAME" + +mkdir -p "$PKG_DIR/usr/bin" +mkdir -p "$PKG_DIR/DEBIAN" + +install -m 755 "$BINARY" "$PKG_DIR/usr/bin/openhuman" + +sed \ + -e "s/@VERSION@/${VERSION}/g" \ + -e "s/@ARCH@/${ARCH}/g" \ + "$SCRIPT_DIR/control.in" > "$PKG_DIR/DEBIAN/control" + +OUTPUT="${PKG_NAME}.deb" +dpkg-deb --build --root-owner-group "$PKG_DIR" "$OUTPUT" +echo "[deb] Built: $OUTPUT" diff --git a/packages/deb/control.in b/packages/deb/control.in new file mode 100644 index 000000000..359cfe12d --- /dev/null +++ b/packages/deb/control.in @@ -0,0 +1,10 @@ +Package: openhuman +Version: @VERSION@ +Section: utils +Priority: optional +Architecture: @ARCH@ +Maintainer: OpenHuman +Homepage: https://github.com/tinyhumansai/openhuman +Description: AI-powered assistant for communities + OpenHuman is an AI-powered CLI for crypto communities and + collaborative workspaces. diff --git a/packages/homebrew/openhuman.rb b/packages/homebrew/openhuman.rb new file mode 100644 index 000000000..54fbac6c5 --- /dev/null +++ b/packages/homebrew/openhuman.rb @@ -0,0 +1,39 @@ +# Homebrew formula template — rendered by CI, committed to tinyhumansai/homebrew-openhuman. +# Placeholders replaced by .github/workflows/release-packages.yml before commit. +class Openhuman < Formula + desc "AI-powered assistant for communities — OpenHuman CLI" + homepage "https://github.com/tinyhumansai/openhuman" + version "@VERSION@" + license "MIT" + + on_macos do + on_arm do + url "https://github.com/tinyhumansai/openhuman/releases/download/v@VERSION@/openhuman-core-@VERSION@-aarch64-apple-darwin.tar.gz" + sha256 "@SHA256_MACOS_ARM64@" + end + on_intel do + url "https://github.com/tinyhumansai/openhuman/releases/download/v@VERSION@/openhuman-core-@VERSION@-x86_64-apple-darwin.tar.gz" + sha256 "@SHA256_MACOS_X64@" + end + end + + on_linux do + on_arm do + # ARM64 (aarch64) + url "https://github.com/tinyhumansai/openhuman/releases/download/v@VERSION@/openhuman-core-@VERSION@-aarch64-unknown-linux-gnu.tar.gz" + sha256 "@SHA256_LINUX_ARM64@" + end + on_intel do + url "https://github.com/tinyhumansai/openhuman/releases/download/v@VERSION@/openhuman-core-@VERSION@-x86_64-unknown-linux-gnu.tar.gz" + sha256 "@SHA256_LINUX_X64@" + end + end + + def install + bin.install "openhuman-core" => "openhuman" + end + + test do + system "#{bin}/openhuman", "--version" + end +end diff --git a/packages/npm/.npmignore b/packages/npm/.npmignore new file mode 100644 index 000000000..88c6d143f --- /dev/null +++ b/packages/npm/.npmignore @@ -0,0 +1,6 @@ +# Exclude the downloaded native binary from published package +bin/openhuman-bin +bin/openhuman-bin.exe +bin/*.tar.gz +bin/*.zip +bin/*.sha256 diff --git a/packages/npm/bin/openhuman.js b/packages/npm/bin/openhuman.js new file mode 100644 index 000000000..a9e1ba3e2 --- /dev/null +++ b/packages/npm/bin/openhuman.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node +'use strict'; + +const { spawnSync } = require('child_process'); +const path = require('path'); + +const isWin = process.platform === 'win32'; +const binName = isWin ? 'openhuman-bin.exe' : 'openhuman-bin'; +const binPath = path.join(__dirname, binName); + +const result = spawnSync(binPath, process.argv.slice(2), { + stdio: 'inherit', + windowsHide: false, +}); + +if (result.error) { + if (result.error.code === 'ENOENT') { + process.stderr.write( + 'openhuman binary not found. Try reinstalling: npm install -g openhuman\n' + ); + } else { + process.stderr.write(`openhuman: ${result.error.message}\n`); + } + process.exit(1); +} + +process.exit(result.status ?? 0); diff --git a/packages/npm/install.js b/packages/npm/install.js new file mode 100644 index 000000000..c62318566 --- /dev/null +++ b/packages/npm/install.js @@ -0,0 +1,160 @@ +#!/usr/bin/env node +'use strict'; + +// postinstall: downloads the correct pre-built binary for this platform/arch, +// verifies the SHA-256 checksum, then places it at bin/openhuman-bin[.exe]. +// +// The binary is fetched from the GitHub release that matches package.json version. + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const { execSync } = require('child_process'); + +const REPO = 'tinyhumansai/openhuman'; +const pkg = require('./package.json'); +const VERSION = pkg.version; + +// Maps process.platform + process.arch → Rust target triple +const TARGET_MAP = { + darwin: { x64: 'x86_64-apple-darwin', arm64: 'aarch64-apple-darwin' }, + linux: { x64: 'x86_64-unknown-linux-gnu', arm64: 'aarch64-unknown-linux-gnu' }, + win32: { x64: 'x86_64-pc-windows-msvc' }, +}; + +function getTarget() { + const platform = process.platform; + const arch = process.arch; + const targets = TARGET_MAP[platform]; + if (!targets) throw new Error(`Unsupported platform: ${platform}`); + const target = targets[arch]; + if (!target) throw new Error(`Unsupported arch ${arch} on ${platform}`); + return { platform, target }; +} + +function httpsGet(url) { + return new Promise((resolve, reject) => { + function request(u) { + https.get(u, (res) => { + if (res.statusCode === 301 || res.statusCode === 302) { + return request(res.headers.location); + } + if (res.statusCode !== 200) { + res.resume(); + return reject(new Error(`HTTP ${res.statusCode} fetching ${u}`)); + } + const chunks = []; + res.on('data', (c) => chunks.push(c)); + res.on('end', () => resolve(Buffer.concat(chunks))); + res.on('error', reject); + }).on('error', reject); + } + request(url); + }); +} + +function downloadFile(url, dest) { + return new Promise((resolve, reject) => { + function request(u) { + https.get(u, (res) => { + if (res.statusCode === 301 || res.statusCode === 302) { + return request(res.headers.location); + } + if (res.statusCode !== 200) { + res.resume(); + return reject(new Error(`HTTP ${res.statusCode} fetching ${u}`)); + } + const out = fs.createWriteStream(dest); + res.pipe(out); + out.on('finish', () => out.close(resolve)); + out.on('error', reject); + res.on('error', reject); + }).on('error', reject); + } + request(url); + }); +} + +function sha256hex(filePath) { + return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex'); +} + +async function main() { + // Skip in CI environments that just need the package metadata + if (process.env.SKIP_OPENHUMAN_BINARY_DOWNLOAD) { + console.log('[openhuman] Skipping binary download (SKIP_OPENHUMAN_BINARY_DOWNLOAD set)'); + return; + } + + const { platform, target } = getTarget(); + const isWin = platform === 'win32'; + const ext = isWin ? '.zip' : '.tar.gz'; + const tarball = `openhuman-core-${VERSION}-${target}${ext}`; + const checksumFile = `${tarball}.sha256`; + const baseUrl = `https://github.com/${REPO}/releases/download/v${VERSION}`; + + const binDir = path.join(__dirname, 'bin'); + fs.mkdirSync(binDir, { recursive: true }); + + const tmpTarball = path.join(binDir, tarball); + const binDest = path.join(binDir, isWin ? 'openhuman-bin.exe' : 'openhuman-bin'); + + // Skip if binary already exists and is executable + if (fs.existsSync(binDest)) { + console.log('[openhuman] Binary already installed, skipping download.'); + return; + } + + console.log(`[openhuman] Downloading v${VERSION} for ${target}...`); + + // Download checksum first (small) + const checksumData = await httpsGet(`${baseUrl}/${checksumFile}`); + const expectedChecksum = checksumData.toString('utf8').trim().split(/\s+/)[0]; + + // Download binary archive + await downloadFile(`${baseUrl}/${tarball}`, tmpTarball); + + // Verify checksum + const actualChecksum = sha256hex(tmpTarball); + if (expectedChecksum !== actualChecksum) { + fs.rmSync(tmpTarball, { force: true }); + throw new Error( + `[openhuman] Checksum mismatch!\n expected: ${expectedChecksum}\n got: ${actualChecksum}` + ); + } + console.log('[openhuman] Checksum verified.'); + + // Extract + if (isWin) { + // PowerShell is available on Windows runners + execSync( + `powershell -Command "Expand-Archive -Path '${tmpTarball}' -DestinationPath '${binDir}' -Force"`, + { stdio: 'inherit' } + ); + const extracted = path.join(binDir, 'openhuman-core.exe'); + if (fs.existsSync(extracted)) fs.renameSync(extracted, binDest); + } else { + execSync(`tar -xzf "${tmpTarball}" -C "${binDir}"`, { stdio: 'inherit' }); + const extracted = path.join(binDir, 'openhuman-core'); + if (fs.existsSync(extracted)) { + fs.renameSync(extracted, binDest); + fs.chmodSync(binDest, 0o755); + } + } + + // Clean up archive + fs.rmSync(tmpTarball, { force: true }); + + if (!fs.existsSync(binDest)) { + throw new Error('[openhuman] Extraction failed — binary not found after unpack.'); + } + + console.log(`[openhuman] Installed at ${binDest}`); +} + +main().catch((err) => { + console.error('\n[openhuman] Installation failed:', err.message); + console.error('You can file a bug at https://github.com/tinyhumansai/openhuman/issues'); + process.exit(1); +}); diff --git a/packages/npm/package.json b/packages/npm/package.json new file mode 100644 index 000000000..0aa045e79 --- /dev/null +++ b/packages/npm/package.json @@ -0,0 +1,36 @@ +{ + "name": "openhuman", + "version": "0.0.0", + "description": "AI-powered assistant for communities — OpenHuman CLI", + "keywords": [ + "openhuman", + "ai", + "cli", + "crypto", + "community" + ], + "homepage": "https://github.com/tinyhumansai/openhuman", + "repository": { + "type": "git", + "url": "https://github.com/tinyhumansai/openhuman.git", + "directory": "packages/npm" + }, + "bugs": { + "url": "https://github.com/tinyhumansai/openhuman/issues" + }, + "license": "MIT", + "bin": { + "openhuman": "./bin/openhuman.js" + }, + "scripts": { + "postinstall": "node install.js" + }, + "files": [ + "bin/", + "install.js", + "README.md" + ], + "engines": { + "node": ">=18" + } +} diff --git a/scripts/build-apt-repo.sh b/scripts/build-apt-repo.sh new file mode 100755 index 000000000..d14d4cd9e --- /dev/null +++ b/scripts/build-apt-repo.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Build a signed Debian apt repository from one or more .deb files. +# Requires: dpkg-dev (dpkg-scanpackages), apt-utils (apt-ftparchive), gzip, gpg, python3 +# +# Usage: +# build-apt-repo.sh [ ...] +# +# The GPG signing key must be imported into the agent before calling. +# Set APT_SIGNING_KEY_ID to select the key; leave unset to use the default. +set -euo pipefail + +OUTPUT_DIR="$1"; shift +DEB_FILES=("$@") + +echo "[apt-repo] Building repository at $OUTPUT_DIR" + +# ── Pool ─────────────────────────────────────────────────────────────────────── +mkdir -p "$OUTPUT_DIR/pool/main" +for deb in "${DEB_FILES[@]}"; do + cp "$deb" "$OUTPUT_DIR/pool/main/" + echo "[apt-repo] + pool/main/$(basename "$deb")" +done + +# ── Per-architecture Packages files ─────────────────────────────────────────── +FILTER_PY="$(mktemp --suffix=.py)" +trap 'rm -f "$FILTER_PY"' EXIT + +cat > "$FILTER_PY" << 'PYEOF' +import sys, re + +arch = sys.argv[1] +data = open(sys.argv[2]).read() +out = [] +for block in data.strip().split('\n\n'): + if re.search(r'^Architecture:\s+' + re.escape(arch) + r'\s*$', block, re.MULTILINE): + out.append(block.rstrip()) +if out: + print('\n\n'.join(out) + '\n') +PYEOF + +ALL_PACKAGES="$(mktemp)" +(cd "$OUTPUT_DIR" && dpkg-scanpackages --multiversion pool/main 2>/dev/null) > "$ALL_PACKAGES" + +for arch in amd64 arm64; do + dir="$OUTPUT_DIR/dists/stable/main/binary-${arch}" + mkdir -p "$dir" + python3 "$FILTER_PY" "$arch" "$ALL_PACKAGES" > "$dir/Packages" + gzip -9c "$dir/Packages" > "$dir/Packages.gz" + lines=$(wc -l < "$dir/Packages") + echo "[apt-repo] binary-${arch}/Packages: ${lines} lines" +done +rm -f "$ALL_PACKAGES" + +# ── Release file ─────────────────────────────────────────────────────────────── +RELEASE_CONF="$(mktemp)" +cat > "$RELEASE_CONF" << 'EOF' +APT::FTPArchive::Release::Origin "OpenHuman"; +APT::FTPArchive::Release::Label "OpenHuman"; +APT::FTPArchive::Release::Suite "stable"; +APT::FTPArchive::Release::Codename "stable"; +APT::FTPArchive::Release::Architectures "amd64 arm64"; +APT::FTPArchive::Release::Components "main"; +APT::FTPArchive::Release::Description "OpenHuman official apt repository"; +EOF + +(cd "$OUTPUT_DIR" && apt-ftparchive -c "$RELEASE_CONF" release dists/stable) \ + > "$OUTPUT_DIR/dists/stable/Release" +rm -f "$RELEASE_CONF" +echo "[apt-repo] Release generated" + +# ── Sign ─────────────────────────────────────────────────────────────────────── +GPG_ARGS=(--batch --yes) +[[ -n "${APT_SIGNING_KEY_ID:-}" ]] && GPG_ARGS+=(--local-user "$APT_SIGNING_KEY_ID") + +gpg "${GPG_ARGS[@]}" --clearsign \ + -o "$OUTPUT_DIR/dists/stable/InRelease" \ + "$OUTPUT_DIR/dists/stable/Release" + +gpg "${GPG_ARGS[@]}" -abs \ + -o "$OUTPUT_DIR/dists/stable/Release.gpg" \ + "$OUTPUT_DIR/dists/stable/Release" + +echo "[apt-repo] Release signed" + +# ── Export public key ───────────────────────────────────────────────────────── +gpg --batch --yes --armor --export ${APT_SIGNING_KEY_ID:-} > "$OUTPUT_DIR/KEY.gpg" +echo "[apt-repo] Public key → KEY.gpg" + +echo "[apt-repo] Done. Files:" +find "$OUTPUT_DIR" -type f | sort | sed 's|^| |'