F8: distribution and GitHub-Releases self-update
- .github/workflows/release.yml: on tag v* build all 5 targets (macOS
arm64/x86_64, Linux arm64/x86_64 incl. free arm runners, Windows
x86_64) with the release-dist profile, archive kigi-<version>-<triple>
with LICENSE/NOTICE/THIRD-PARTY-NOTICES, generate SHA256SUMS, publish
the release (prerelease for tags containing '-'), with a tag↔workspace
version guard.
- install.sh / install.ps1 (repo root): platform detection, latest or
--version download from GitHub Releases, SHA-256 verification against
SHA256SUMS, install into the kigi home's downloads/ + bin/kigi symlink
(the same layout the self-updater manages), smoke test, PATH guidance.
- kigi-update rewritten onto the GitHub Releases API (documented wire
shape; stable=/latest, alpha=semver-max across the list, pinned=/tags):
SHA-256 gate before any binary swap, tar.gz/zip extraction per
platform, atomic bin/kigi symlink swap, channel/rollback semantics and
the KIGI_AUTO_UPDATE gate preserved verbatim; every x.ai/GCS/npm
endpoint deleted, npm/gh-release installers removed, legacy grok/agent
links retired on install. kigi-env owns the update base URL with a
KIGI_UPDATE_BASE_URL override (this is what the test artifact server
injects).
- .cargo/config.toml: removed the non-portable neoverse-v2 CPU pin on
Linux arm64 (fleet-specific); RELRO/NX hardening link-args now apply
to the gnu targets too, matching the release-dist profile's contract.
- THIRD-PARTY-NOTICES regenerated via cargo-about (about.toml +
template); the M0 hand-built file is dropped and README points at the
generated one. docs/RELEASE.md carries the release checklist.
- Deleted xAI-era leftovers: kigi-tui/scripts/install*.{sh,ps1} (x.ai
CDN) and the @xai-official/grok npm skeleton (PRD F8: no npm).
Gates: fmt clean; workspace check/clippy 0/0 (--locked, -D warnings);
kigi-update 58 lib + 86 integration tests green; deny ok;
release-dist build of kigi-bin succeeds and reports 'kigi 0.1.0'.
This commit is contained in:
+199
@@ -0,0 +1,199 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Kigi installer (macOS / Linux) — PRD F8.
|
||||
#
|
||||
# Downloads the matching platform artifact from this repo's GitHub Releases,
|
||||
# verifies its SHA-256 against the release's SHA256SUMS manifest, and installs
|
||||
# the binary as ~/.kigi/bin/kigi (the same managed layout the self-updater
|
||||
# maintains: versioned binary in ~/.kigi/downloads/, atomic symlink in bin/).
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://raw.githubusercontent.com/ZacharyZhang-NY/Kigi-CLI/main/install.sh | sh
|
||||
# sh install.sh --version v0.1.0 # pin a specific release
|
||||
#
|
||||
# Environment:
|
||||
# KIGI_SHARE_DIR install root (default: ~/.kigi)
|
||||
# KIGI_UPDATE_BASE_URL GitHub-Releases-shaped API base (default:
|
||||
# https://api.github.com/repos/ZacharyZhang-NY/Kigi-CLI/releases)
|
||||
#
|
||||
# Fails fast on any error; never leaves a partial binary as the active kigi.
|
||||
|
||||
set -eu
|
||||
|
||||
REPO="ZacharyZhang-NY/Kigi-CLI"
|
||||
API_BASE="${KIGI_UPDATE_BASE_URL:-https://api.github.com/repos/${REPO}/releases}"
|
||||
KIGI_HOME="${KIGI_SHARE_DIR:-$HOME/.kigi}"
|
||||
|
||||
err() {
|
||||
printf 'install.sh: error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
sed -n '2,20p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//'
|
||||
}
|
||||
|
||||
# ── Arguments ────────────────────────────────────────────────────────────────
|
||||
VERSION=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--version)
|
||||
[ $# -ge 2 ] || err "--version requires an argument (e.g. --version v0.1.0)"
|
||||
VERSION="$2"
|
||||
shift
|
||||
;;
|
||||
--version=*)
|
||||
VERSION="${1#--version=}"
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
err "unknown argument: $1 (supported: --version vX.Y.Z)"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
VERSION="${VERSION#v}"
|
||||
if [ -n "$VERSION" ]; then
|
||||
case "$VERSION" in
|
||||
[0-9]*.[0-9]*.[0-9]*) ;;
|
||||
*) err "invalid version '$VERSION' (expected X.Y.Z or vX.Y.Z)" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ── Platform detection ───────────────────────────────────────────────────────
|
||||
OS="$(uname -s)"
|
||||
ARCH="$(uname -m)"
|
||||
case "$OS" in
|
||||
Darwin)
|
||||
PLATFORM_OS="macos"
|
||||
case "$ARCH" in
|
||||
arm64|aarch64) TRIPLE="aarch64-apple-darwin"; PLATFORM_ARCH="aarch64" ;;
|
||||
x86_64) TRIPLE="x86_64-apple-darwin"; PLATFORM_ARCH="x86_64" ;;
|
||||
*) err "unsupported macOS architecture: $ARCH" ;;
|
||||
esac
|
||||
;;
|
||||
Linux)
|
||||
PLATFORM_OS="linux"
|
||||
case "$ARCH" in
|
||||
arm64|aarch64) TRIPLE="aarch64-unknown-linux-gnu"; PLATFORM_ARCH="aarch64" ;;
|
||||
x86_64|amd64) TRIPLE="x86_64-unknown-linux-gnu"; PLATFORM_ARCH="x86_64" ;;
|
||||
*) err "unsupported Linux architecture: $ARCH" ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
err "unsupported OS: $OS (Windows: use install.ps1)"
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Downloader ───────────────────────────────────────────────────────────────
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
fetch() { curl -fsSL -o "$2" "$1"; }
|
||||
fetch_stdout() { curl -fsSL "$1"; }
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
fetch() { wget -q -O "$2" "$1"; }
|
||||
fetch_stdout() { wget -q -O - "$1"; }
|
||||
else
|
||||
err "either curl or wget is required"
|
||||
fi
|
||||
|
||||
# ── SHA-256 tool ─────────────────────────────────────────────────────────────
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256_of() { sha256sum "$1" | cut -d' ' -f1; }
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
sha256_of() { shasum -a 256 "$1" | cut -d' ' -f1; }
|
||||
else
|
||||
err "either sha256sum or shasum is required to verify the download"
|
||||
fi
|
||||
|
||||
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/kigi-install.XXXXXX")"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT INT TERM
|
||||
|
||||
# ── Resolve the release ──────────────────────────────────────────────────────
|
||||
if [ -n "$VERSION" ]; then
|
||||
RELEASE_URL="$API_BASE/tags/v$VERSION"
|
||||
else
|
||||
RELEASE_URL="$API_BASE/latest"
|
||||
fi
|
||||
printf 'Resolving release from %s\n' "$RELEASE_URL"
|
||||
RELEASE_JSON="$(fetch_stdout "$RELEASE_URL")" \
|
||||
|| err "could not fetch release metadata from $RELEASE_URL"
|
||||
|
||||
TAG="$(printf '%s' "$RELEASE_JSON" \
|
||||
| tr ',' '\n' \
|
||||
| sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
|
||||
| head -n 1)"
|
||||
[ -n "$TAG" ] || err "release metadata has no tag_name (endpoint: $RELEASE_URL)"
|
||||
RESOLVED_VERSION="${TAG#v}"
|
||||
if [ -n "$VERSION" ] && [ "$RESOLVED_VERSION" != "$VERSION" ]; then
|
||||
err "requested version $VERSION but release tag is $TAG"
|
||||
fi
|
||||
|
||||
ASSET="kigi-${RESOLVED_VERSION}-${TRIPLE}.tar.gz"
|
||||
|
||||
# Pull every browser_download_url out of the JSON, then select by asset name.
|
||||
URLS="$(printf '%s' "$RELEASE_JSON" \
|
||||
| tr ',' '\n' \
|
||||
| sed -n 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
||||
ARCHIVE_URL="$(printf '%s\n' "$URLS" | grep -F "/$ASSET" | head -n 1 || true)"
|
||||
SUMS_URL="$(printf '%s\n' "$URLS" | grep -F "/SHA256SUMS" | head -n 1 || true)"
|
||||
[ -n "$ARCHIVE_URL" ] || err "release $TAG has no asset $ASSET (this platform may not be published yet)"
|
||||
[ -n "$SUMS_URL" ] || err "release $TAG has no SHA256SUMS asset; refusing to install unverified binaries"
|
||||
|
||||
# ── Download + verify ────────────────────────────────────────────────────────
|
||||
printf 'Downloading kigi v%s (%s)...\n' "$RESOLVED_VERSION" "$TRIPLE"
|
||||
fetch "$ARCHIVE_URL" "$TMP_DIR/$ASSET" || err "download failed: $ARCHIVE_URL"
|
||||
fetch "$SUMS_URL" "$TMP_DIR/SHA256SUMS" || err "download failed: $SUMS_URL"
|
||||
|
||||
EXPECTED=""
|
||||
while IFS=' ' read -r hash name; do
|
||||
name="${name#\*}"
|
||||
if [ "$name" = "$ASSET" ]; then
|
||||
EXPECTED="$hash"
|
||||
fi
|
||||
done < "$TMP_DIR/SHA256SUMS"
|
||||
[ -n "$EXPECTED" ] || err "SHA256SUMS has no entry for $ASSET"
|
||||
|
||||
ACTUAL="$(sha256_of "$TMP_DIR/$ASSET")"
|
||||
if [ "$ACTUAL" != "$EXPECTED" ]; then
|
||||
err "SHA256 mismatch for $ASSET: expected $EXPECTED, got $ACTUAL"
|
||||
fi
|
||||
printf 'Checksum verified.\n'
|
||||
|
||||
# ── Extract + install ────────────────────────────────────────────────────────
|
||||
tar -xzf "$TMP_DIR/$ASSET" -C "$TMP_DIR" || err "failed to extract $ASSET"
|
||||
[ -f "$TMP_DIR/kigi" ] || err "archive $ASSET does not contain a 'kigi' binary"
|
||||
chmod 0755 "$TMP_DIR/kigi"
|
||||
|
||||
DOWNLOADS_DIR="$KIGI_HOME/downloads"
|
||||
BIN_DIR="$KIGI_HOME/bin"
|
||||
mkdir -p "$DOWNLOADS_DIR" "$BIN_DIR"
|
||||
|
||||
# Versioned binary + atomic symlink swap — the exact layout the self-updater
|
||||
# maintains, so `kigi update` takes over seamlessly from here.
|
||||
VERSIONED="kigi-${RESOLVED_VERSION}-${PLATFORM_OS}-${PLATFORM_ARCH}"
|
||||
mv -f "$TMP_DIR/kigi" "$DOWNLOADS_DIR/$VERSIONED"
|
||||
|
||||
TMP_LINK="$BIN_DIR/kigi.install.$$"
|
||||
ln -s "../downloads/$VERSIONED" "$TMP_LINK"
|
||||
mv -f "$TMP_LINK" "$BIN_DIR/kigi"
|
||||
|
||||
# Smoke-test the installed binary through the managed link.
|
||||
"$BIN_DIR/kigi" --version >/dev/null 2>&1 \
|
||||
|| err "installed binary failed to run; your PATH still has no working kigi"
|
||||
|
||||
printf '\nkigi v%s installed to %s\n' "$RESOLVED_VERSION" "$BIN_DIR/kigi"
|
||||
|
||||
case ":$PATH:" in
|
||||
*":$BIN_DIR:"*)
|
||||
printf 'Run `kigi` to get started.\n'
|
||||
;;
|
||||
*)
|
||||
printf '\n%s is not on your PATH. Add it with:\n\n' "$BIN_DIR"
|
||||
printf ' export PATH="%s:$PATH" # sh / bash / zsh (add to your shell rc)\n' "$BIN_DIR"
|
||||
printf ' fish_add_path %s # fish\n\n' "$BIN_DIR"
|
||||
printf 'Then run `kigi` to get started.\n'
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user