#!/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 Pin a specific version (e.g. v0.4.2). Default: latest release. # --check Dry-run: print the version + asset URL + install path without writing. # --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 the resolved release (latest or pinned) has no matching binary # asset, walks backward through recent releases to find one that does, # and prints a warning. (REQ-098 — the v0.8.x releases shipped with # zero binary assets, causing install to resolve to v0.4.5.) # - 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="" CHECK=false INSTALL_BIN="" NAMESPACE_DIR="" warn() { printf " \033[1;33m!\033[0m %s\n" "$*" >&2 } 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 ;; --check) CHECK=true; 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" elif [ -w /usr/local/bin ] || [ "$(id -u)" -eq 0 ]; then INSTALL_BIN="/usr/local/bin/orca" NAMESPACE_DIR="${HOME}/.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 (with fallback walk — REQ-098) -------------- # # The v0.8.x releases shipped with zero binary assets attached, causing # install to error out on the latest release. If the resolved release # (latest or --version) lacks the matching tarball, walk backward through # recent releases to find one that carries it, and print a warning. find_asset_url() { # $1 = tag. Prints the browser_download_url for the matching tarball, or empty. # Match by asset NAME (not URL path) — Gitea attachment URLs are opaque # UUIDs that don't contain the tarball name. local tag="$1" local json json="$(curl -fsSL "${GITEA_URL}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/tags/${tag}" 2>/dev/null)" || return 1 if command -v python3 >/dev/null 2>&1; then echo "$json" | python3 -c " import json,sys r=json.load(sys.stdin) for a in r.get('assets',[]): if a.get('name')=='$TARBALL': print(a.get('browser_download_url','')) break " 2>/dev/null || true else echo "$json" | sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"$TARBALL".*"browser_download_url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 || true fi } find_asset_in_releases() { # Walk recent releases, find the newest with a matching asset name. # Outputs two lines: URL and VERSION (caller captures both). local json json="$(curl -fsSL "${GITEA_URL}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/releases?limit=50" 2>/dev/null)" || return 1 if command -v python3 >/dev/null 2>&1; then echo "$json" | python3 -c " import json,sys,re rels=json.load(sys.stdin) for r in rels: tag=r.get('tag_name','') for a in r.get('assets',[]): name=a.get('name','') m=re.match(r'orca-(v[0-9.]+)-' + '${OS}' + '-' + '${ARCH}' + r'\.tar\.gz$', name) if m: print(a.get('browser_download_url','')) print(m.group(1)) sys.exit(0) " fi } info "locating asset ${TARBALL} in release ${VERSION}..." ASSET_URL="$(find_asset_url "$VERSION" 2>/dev/null)" || true if [ -z "$ASSET_URL" ]; then info "WARNING: release ${VERSION} has no ${TARBALL} asset. Walking back through recent releases..." FALLBACK_OUT="$(find_asset_in_releases || true)" ASSET_URL="$(echo "$FALLBACK_OUT" | head -1)" if [ -n "$ASSET_URL" ]; then VERSION="$(echo "$FALLBACK_OUT" | tail -1)" info "WARNING: falling back to ${VERSION} which has orca-${VERSION}-${OS}-${ARCH}.tar.gz." else err "could not find any release with a ${OS}-${ARCH} tarball in the last 50 releases. Check that a release exists with a linux-${ARCH} binary." fi 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 # --- --check dry-run (D-194) --------------------------------------------- # Print what would be installed without writing anything. if [ "$CHECK" = "true" ]; then info "dry-run (--check): no files will be written" info " would install: orca ${VERSION}" info " asset: ${ASSET_URL}" info " binary path: ${INSTALL_BIN}" info " namespace root: ${NAMESPACE_DIR}" if [ -n "$OLD_VERSION" ]; then info " current: ${OLD_VERSION} (would update to ${VERSION})" else info " current: (not installed)" fi exit 0 fi # --- download + extract --------------------------------------------------- TMPDIR="$(mktemp -d)" trap 'rm -rf "$TMPDIR"' EXIT info "downloading..." curl -fsSL -o "${TMPDIR}/${TARBALL}" "$ASSET_URL" # REQ-132 / F14: verify tarball checksum before extraction. # Fetch SHA256SUMS from the same release. For Gitea release-download # URLs (e.g. /releases/download/vX.Y.Z/...) the SHA256SUMS is a sibling. # For Gitea attachment URLs (e.g. /attachments/) we must look up # the SHA256SUMS asset by name from the release API. SHA256SUMS_URL="" if echo "$ASSET_URL" | grep -q "/releases/download/"; then SHA256SUMS_URL="$(dirname "$ASSET_URL")/SHA256SUMS" elif command -v python3 >/dev/null 2>&1; then # Look up SHA256SUMS asset by name from the release API. SHA256SUMS_URL="$(curl -fsSL \ "${GITEA_URL}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/tags/${VERSION}" 2>/dev/null \ | python3 -c " import json,sys r=json.load(sys.stdin) for a in r.get('assets',[]): if a.get('name')=='SHA256SUMS': print(a.get('browser_download_url','')) break " 2>/dev/null || true)" fi if [ -n "$SHA256SUMS_URL" ] && curl -fsSL -o "${TMPDIR}/SHA256SUMS" "$SHA256SUMS_URL" 2>/dev/null; then info "verifying checksum..." (cd "$TMPDIR" && grep -F "$TARBALL" SHA256SUMS | sha256sum -c -) || { err "checksum verification failed (REQ-132); refusing to install" exit 1 } else warn "no SHA256SUMS found at $SHA256SUMS_URL; skipping checksum (insecure)" fi 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 the install dir is not on PATH, add it to .bashrc automatically. if ! echo "$PATH" | grep -q "$INSTALL_DIR"; then info " Adding $INSTALL_DIR to PATH via ~/.bashrc..." SHELL_RC="${HOME}/.bashrc" if [ -f "$SHELL_RC" ]; then # Append only if not already present (idempotent). if ! grep -qF "$INSTALL_DIR" "$SHELL_RC"; then echo "" >> "$SHELL_RC" echo "# Added by orca install.sh" >> "$SHELL_RC" echo "export PATH=\"\$PATH:$INSTALL_DIR\"" >> "$SHELL_RC" info " ✓ Added 'export PATH=\"\$PATH:$INSTALL_DIR\"' to ~/.bashrc" info " Run 'source ~/.bashrc' or start a new shell to pick up the change." fi else info " NOTE: ~/.bashrc not found. Add to PATH manually:" info " export PATH=\"\$PATH:$INSTALL_DIR\"" fi fi fi info " verify: ${INSTALL_BIN} version"