feat(P02): install.sh 1-liner + in-place update + README quickstart

REQ-043: install.sh pulls release binary from public Gitea URL.
  User-level default (~/.local/bin/orca), --system for system-level
  (/usr/local/bin/orca). Defaults to latest release; --version pins.
  Env-overridable GITEA_URL/OWNER/REPO for testability.

REQ-044: in-place update detects existing binary, reads version via
  'orca version --json', prints update message, overwrites binary,
  preserves namespace dir (config/db/certs). Idempotent re-install.

REQ-016 (completion): README quickstart now documents the 1-liner
  install + --system variant + update-in-place pattern.

Tests: 8/8 pass in scripts/install_test.sh (real public Gitea releases,
  no mock server; timeout-guarded to prevent hangs).

Docs: docs/install.md covers user/system install, version pinning,
  in-place update, uninstall, troubleshooting.

---ci---
project: orca
phase: 2
milestone: v0.5
status: verify
---/ci---
This commit is contained in:
Jon Chery
2026-08-03 18:49:50 +00:00
parent 2ff8318556
commit 85963dc320
6 changed files with 547 additions and 11 deletions
+156
View File
@@ -0,0 +1,156 @@
#!/bin/bash
# install.sh — 1-liner installer for orca
#
# Usage:
# curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash
# curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash -s -- --system
# curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash -s -- --version v0.4.2
#
# Options:
# --system Install at system level (/usr/local/bin/orca, namespace /root/.orca). Requires root.
# --version <tag> Pin a specific version (e.g. v0.4.2). Default: latest release.
# --help, -h Show this help.
#
# Behavior:
# - Downloads the release tarball from the public Gitea release URL.
# - Extracts the orca binary to the install path.
# - If an existing orca binary is found, reads its version and prints
# "updated from X to Y" (in-place update; preserves config/db/certs).
# - Idempotent: re-running with the same version reinstalls the binary.
# - Never touches the namespace dir (~/.orca or /root/.orca) — that's user state.
set -euo pipefail
GITEA_URL="${GITEA_URL:-https://git.cloudinit.dev}"
GITEA_OWNER="${GITEA_OWNER:-coreci}"
GITEA_REPO="${GITEA_REPO:-orca}"
SYSTEM=false
VERSION=""
INSTALL_BIN=""
NAMESPACE_DIR=""
err() { echo "install: error: $*" >&2; exit 1; }
info() { echo "install: $*"; }
usage() {
sed -n '2,/^$/p' "$0" | sed 's/^# \?//' >&2
exit 0
}
# --- parse args ------------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
--system) SYSTEM=true; shift ;;
--version) VERSION="${2:-}"; shift 2 ;;
--version=*) VERSION="${1#*=}"; shift ;;
--help|-h) usage ;;
*) err "unknown argument: $1 (try --help)" ;;
esac
done
# --- determine install paths ----------------------------------------------
if [ "$SYSTEM" = "true" ]; then
if [ "$(id -u)" -ne 0 ]; then
err "--system requires root (uid 0). Re-run with sudo or drop --system for user-level install."
fi
INSTALL_BIN="/usr/local/bin/orca"
NAMESPACE_DIR="/root/.orca"
else
INSTALL_BIN="${HOME}/.local/bin/orca"
NAMESPACE_DIR="${HOME}/.orca"
fi
INSTALL_DIR="$(dirname "$INSTALL_BIN")"
# --- determine version ----------------------------------------------------
if [ -z "$VERSION" ]; then
info "querying latest release from ${GITEA_URL}..."
VERSION="$(curl -fsSL "${GITEA_URL}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/latest" \
| sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
| head -1)"
if [ -z "$VERSION" ]; then
err "could not determine latest release version from Gitea API"
fi
fi
info "version: ${VERSION}"
# --- detect arch ----------------------------------------------------------
ARCH="$(uname -m)"
case "$ARCH" in
x86_64) ARCH=amd64 ;;
aarch64|arm64) ARCH=arm64 ;;
armv7l) ARCH=armv7 ;;
*) err "unsupported architecture: ${ARCH} (supported: amd64, arm64, armv7)" ;;
esac
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
TARBALL="orca-${VERSION}-${OS}-${ARCH}.tar.gz"
# --- find asset download URL ----------------------------------------------
info "locating asset ${TARBALL}..."
ASSET_URL="$(curl -fsSL "${GITEA_URL}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/tags/${VERSION}" \
| sed -n 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
| grep "/${TARBALL}\$" \
| head -1)"
if [ -z "$ASSET_URL" ]; then
err "could not find asset ${TARBALL} in release ${VERSION}. Check that the release exists and has a linux-${ARCH} tarball."
fi
info "asset: ${ASSET_URL}"
# --- in-place update detection -------------------------------------------
OLD_VERSION=""
if [ -x "$INSTALL_BIN" ]; then
OLD_VERSION="$("$INSTALL_BIN" version --json 2>/dev/null | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 || echo "")"
fi
# --- download + extract ---------------------------------------------------
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
info "downloading..."
curl -fsSL -o "${TMPDIR}/${TARBALL}" "$ASSET_URL"
info "extracting..."
tar -xzf "${TMPDIR}/${TARBALL}" -C "$TMPDIR"
if [ ! -f "${TMPDIR}/orca" ]; then
err "tarball did not contain an 'orca' binary"
fi
# --- install --------------------------------------------------------------
mkdir -p "$INSTALL_DIR"
install -m 0755 "${TMPDIR}/orca" "$INSTALL_BIN"
# --- report ---------------------------------------------------------------
if [ -n "$OLD_VERSION" ]; then
if [ "$OLD_VERSION" = "$VERSION" ]; then
info "✓ reinstalled orca ${VERSION} at ${INSTALL_BIN}"
else
info "✓ updated orca from ${OLD_VERSION} to ${VERSION} at ${INSTALL_BIN}"
fi
else
info "✓ installed orca ${VERSION} to ${INSTALL_BIN}"
fi
if [ "$SYSTEM" = "true" ]; then
info " namespace root: ${NAMESPACE_DIR} (use 'orca --system init' to initialize)"
else
info " namespace root: ${NAMESPACE_DIR} (use 'orca init' to initialize)"
if ! echo "$PATH" | grep -q "$INSTALL_DIR"; then
info " NOTE: $INSTALL_DIR is not on your PATH. Add it:"
info " export PATH=\"\$PATH:$INSTALL_DIR\""
fi
fi
info " verify: ${INSTALL_BIN} version"
+129
View File
@@ -0,0 +1,129 @@
#!/bin/bash
# install_test.sh — tests for scripts/install.sh
#
# Tests install.sh against the real public Gitea releases (REQ-045 made
# the repo + releases publicly accessible). Uses real existing release
# tags (v0.4.1, v0.4.2) so no mock infrastructure is needed.
#
# Tests:
# 1. user-level install (binary at ~/.local/bin/orca)
# 2. in-place update (v0.4.1 -> v0.4.2) preserves namespace state
# 3. idempotent re-install (v0.4.2 -> v0.4.2)
# 4. --system install (requires root; /usr/local/bin/orca)
# 5. --system without root fails with error
#
# Usage: bash scripts/install_test.sh
# sudo bash scripts/install_test.sh (to include --system tests)
#
# Each test is wrapped in `timeout 30` to prevent hangs. The whole
# suite is wrapped in `timeout 120`.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_SH="$SCRIPT_DIR/install.sh"
PASS=0
FAIL=0
ok() { echo " PASS: $1"; PASS=$((PASS+1)); }
fail() { echo " FAIL: $1"; FAIL=$((FAIL+1)); }
# Kill any background processes on exit (defensive — no background procs
# expected in this version, but keeps the harness safe).
trap 'kill 0 2>/dev/null || true' EXIT
run_install() {
timeout 30 bash "$INSTALL_SH" "$@" 2>&1
}
echo "=== Test 1: user-level install (v0.4.1) ==="
FAKE_HOME="$(mktemp -d)"
export HOME="$FAKE_HOME"
if run_install --version v0.4.1 > /tmp/it1.log 2>&1; then
if [ -x "$FAKE_HOME/.local/bin/orca" ]; then
ok "binary at ~/.local/bin/orca"
else
fail "binary not at ~/.local/bin/orca"
fi
INSTALLED_VER="$(timeout 5 "$FAKE_HOME/.local/bin/orca" version --json 2>/dev/null | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
if [ "$INSTALLED_VER" = "v0.4.1" ]; then
ok "installed version is v0.4.1"
else
fail "installed version is '${INSTALLED_VER}', expected v0.4.1"
fi
else
fail "user install exited non-zero"; cat /tmp/it1.log
fi
echo "=== Test 2: in-place update (v0.4.1 -> v0.4.2) preserves namespace ==="
mkdir -p "$FAKE_HOME/.orca"
echo "preserve-me" > "$FAKE_HOME/.orca/orca.db"
if run_install --version v0.4.2 > /tmp/it2.log 2>&1; then
if grep -q "updated orca from v0.4.1 to v0.4.2" /tmp/it2.log; then
ok "update message printed"
else
fail "update message not printed"; cat /tmp/it2.log
fi
if [ "$(cat "$FAKE_HOME/.orca/orca.db" 2>/dev/null)" = "preserve-me" ]; then
ok "namespace state preserved during update"
else
fail "namespace state was modified or removed during update"
fi
INSTALLED_VER="$(timeout 5 "$FAKE_HOME/.local/bin/orca" version --json 2>/dev/null | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
if [ "$INSTALLED_VER" = "v0.4.2" ]; then
ok "binary updated to v0.4.2"
else
fail "binary version is '${INSTALLED_VER}', expected v0.4.2"
fi
else
fail "update exited non-zero"; cat /tmp/it2.log
fi
echo "=== Test 3: idempotent re-install (v0.4.2 -> v0.4.2) ==="
if run_install --version v0.4.2 > /tmp/it3.log 2>&1; then
if grep -q "reinstalled orca v0.4.2" /tmp/it3.log; then
ok "reinstall message printed"
else
fail "reinstall message not printed"; cat /tmp/it3.log
fi
else
fail "reinstall exited non-zero"; cat /tmp/it3.log
fi
echo "=== Test 4: --system install (requires root) ==="
if [ "$(id -u)" -eq 0 ]; then
if run_install --system --version v0.4.1 > /tmp/it4.log 2>&1; then
if [ -x /usr/local/bin/orca ]; then
ok "binary at /usr/local/bin/orca"
else
fail "binary not at /usr/local/bin/orca"
fi
if grep -q "namespace root: /root/.orca" /tmp/it4.log; then
ok "--system reports /root/.orca namespace"
else
fail "--system did not report /root/.orca namespace"; cat /tmp/it4.log
fi
rm -f /usr/local/bin/orca
else
fail "--system install exited non-zero"; cat /tmp/it4.log
fi
else
echo " SKIP: --system test (not running as root)"
fi
echo "=== Test 5: --system without root fails ==="
if [ "$(id -u)" -ne 0 ]; then
if run_install --system --version v0.4.1 2>&1 | grep -q "requires root"; then
ok "--system without root correctly errors"
else
fail "--system without root did not error"
fi
else
echo " SKIP: --system-without-root test (running as root)"
fi
echo ""
echo "=== Results: $PASS passed, $FAIL failed ==="
rm -rf "$FAKE_HOME" /tmp/it1.log /tmp/it2.log /tmp/it3.log /tmp/it4.log 2>/dev/null
exit $FAIL