Feat:package manager channels (brew, apt, npm ) (#166)

* feat(release): upload versioned CLI tarballs in release workflow (#128)

Package and upload non-Windows CLI tarballs with SHA-256 checksum companions during release builds so package managers can consume stable release artifacts.

Made-with: Cursor

* feat(packaging): add brew apt npm release channels automation (#128)

Add post-release workflow and channel assets to publish Homebrew formula updates, signed apt repository metadata, and npm package releases with smoke validation.

Made-with: Cursor

* docs: add installation guide for OpenHuman across various package managers
This commit is contained in:
YellowSnnowmann
2026-04-01 19:30:55 +05:30
committed by GitHub
parent 305c58a6ec
commit 6406ec2bc3
11 changed files with 1093 additions and 0 deletions
+454
View File
@@ -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}\`);
+42
View File
@@ -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:
+199
View File
@@ -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-<v>-aarch64-apple-darwin.tar.gz` | macOS Apple Silicon |
| `openhuman-core-<v>-x86_64-apple-darwin.tar.gz` | macOS Intel |
| `openhuman-core-<v>-x86_64-unknown-linux-gnu.tar.gz` | Linux x86-64 |
| `openhuman-core-<v>-aarch64-unknown-linux-gnu.tar.gz` | Linux arm64 |
| `OpenHuman_<v>_aarch64.dmg` | macOS desktop app (Apple Silicon) |
| `OpenHuman_<v>_x64.dmg` | macOS desktop app (Intel) |
| `OpenHuman_<v>_amd64.deb` | Linux desktop app (.deb) |
| `OpenHuman_<v>_amd64.AppImage` | Linux desktop app (AppImage) |
Every archive has a corresponding `.sha256` companion file.
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Build a .deb package for the openhuman-core CLI binary.
# Usage: build.sh <binary_path> <version> <arch>
# 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"
+10
View File
@@ -0,0 +1,10 @@
Package: openhuman
Version: @VERSION@
Section: utils
Priority: optional
Architecture: @ARCH@
Maintainer: OpenHuman <hello@tinyhumans.ai>
Homepage: https://github.com/tinyhumansai/openhuman
Description: AI-powered assistant for communities
OpenHuman is an AI-powered CLI for crypto communities and
collaborative workspaces.
+39
View File
@@ -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
+6
View File
@@ -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
+27
View File
@@ -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);
+160
View File
@@ -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);
});
+36
View File
@@ -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"
}
}
+90
View File
@@ -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 <output_dir> <pkg1.deb> [<pkg2.deb> ...]
#
# 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|^| |'