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:
2026-07-18 00:54:53 -04:00
parent 5e4e24db99
commit 86e3724310
57 changed files with 27431 additions and 27787 deletions
@@ -1,404 +1,305 @@
//! Blitz harness for the bash installer (`install.sh`), the second client that
//! can brick a machine. Runs the REAL shipped `install.sh` against a fake
//! `curl` that can serve the good artifact, truncate it, or serve a right-length
//! garbage body, and asserts the same invariant as the Rust blitz:
//! Harness for the bootstrap installer (`install.sh` at the repo root), the
//! second client that can brick a machine. Runs the REAL shipped script
//! against a fake `curl` that serves GitHub-Releases-shaped JSON, the
//! archive, and SHA256SUMS from local fixtures, and asserts:
//!
//! > After any install attempt, `$BIN_DIR/grok` resolves to a binary that runs,
//! > OR is still the previous-good binary — never a partial/garbage binary.
//! > After any install attempt, `$KIGI_SHARE_DIR/bin/kigi` resolves to a
//! > binary that runs, OR the install failed cleanly with nothing activated —
//! > never a partial/garbage binary.
//!
//! Also covers shell-rc rewrite: stowed/symlinked `~/.bashrc` etc. must survive
//! reinstall without being replaced by a plain file.
//!
//! The installer lives in the sibling `kigi-tui` crate; it is resolved by
//! relative path. If it cannot be found (e.g. a sandbox that does not vendor it)
//! the test skips rather than fail — under the repo's `cargo nextest` workflow
//! the path resolves and the installer is exercised end to end.
//! Each test uses its own tempdir home and passes PATH/KIGI_SHARE_DIR to the
//! child process explicitly, so no process-global state is touched and no
//! `#[serial]` is needed.
#![cfg(unix)]
mod common;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
fn script_path(name: &str) -> Option<PathBuf> {
dunce::canonicalize(
Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../kigi-tui/scripts/{name}")),
)
.ok()
.filter(|p| p.exists())
}
use common::{archive_name, host_platform, make_release_archive, sha256_hex, small_good_artifact};
fn install_sh_path() -> Option<PathBuf> {
script_path("install.sh")
// crates/codegen/kigi-update → repo root.
dunce::canonicalize(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../install.sh"))
.ok()
.filter(|p| p.exists())
}
fn host_platform() -> String {
let os = if cfg!(target_os = "macos") {
"macos"
} else {
"linux"
};
let arch = if cfg!(target_arch = "x86_64") {
"x86_64"
} else {
"aarch64"
};
format!("{os}-{arch}")
/// GitHub release JSON with download URLs whose suffixes the fake curl
/// dispatches on (the host is irrelevant).
fn release_json(version: &str) -> String {
let name = archive_name(version);
serde_json::json!({
"tag_name": format!("v{version}"),
"draft": false,
"prerelease": false,
"assets": [
{ "name": name, "browser_download_url": format!("https://example.test/dl/v{version}/{name}") },
{ "name": "SHA256SUMS", "browser_download_url": format!("https://example.test/dl/v{version}/SHA256SUMS") },
],
})
.to_string()
}
const GOOD_SCRIPT: &str = "#!/bin/sh\nexit 0\n";
const INSTALLER_BLOCK_START: &str = "# >>> grok installer >>>";
/// Write a fake `curl` that intercepts every download `install.sh` performs.
/// `$FAKE_MODE` (full|truncate|garbage) selects the corruption.
fn write_fake_curl(dir: &Path) {
let body = format!(
r#"#!/bin/bash
mode="${{FAKE_MODE:-full}}"
fullsize={fullsize}
head=0; out=""; want_code=0; url=""
while [ $# -gt 0 ]; do
case "$1" in
--head) head=1 ;;
-o) shift; out="$1" ;;
-w) shift; [ "$1" = '%{{http_code}}' ] && want_code=1 ;;
-*) : ;;
*) url="$1" ;;
esac
shift
done
if [ "$head" = 1 ]; then
if [ "$want_code" = 1 ]; then printf '200'; else printf 'HTTP/1.1 200 OK\r\nContent-Length: %s\r\n\r\n' "$fullsize"; fi
exit 0
fi
if [ -n "$out" ]; then
case "$mode" in
full) printf '%s' '{good}' > "$out" ;;
truncate) printf '\0\0\0\0' > "$out" ;;
garbage) head -c "$fullsize" /dev/zero | tr '\0' 'X' > "$out" ;;
esac
exit 0
fi
printf '0.1.181'
exit 0
"#,
fullsize = GOOD_SCRIPT.len(),
good = GOOD_SCRIPT,
);
let path = dir.join("curl");
std::fs::write(&path, body).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
/// Fixture directory holding the fake curl + canned responses.
struct Fixture {
dir: tempfile::TempDir,
home: tempfile::TempDir,
}
/// Seed a valid previous-good binary + symlink in the isolated home.
fn seed_previous_good(home: &Path, platform: &str) -> PathBuf {
let downloads = home.join(".kigi").join("downloads");
let bin = home.join(".kigi").join("bin");
std::fs::create_dir_all(&downloads).unwrap();
std::fs::create_dir_all(&bin).unwrap();
let prev = downloads.join(format!("grok-{platform}"));
std::fs::write(&prev, GOOD_SCRIPT).unwrap();
std::fs::set_permissions(&prev, std::fs::Permissions::from_mode(0o755)).unwrap();
let link = bin.join("grok");
let _ = std::fs::remove_file(&link);
std::os::unix::fs::symlink(format!("../downloads/grok-{platform}"), &link).unwrap();
dunce::canonicalize(&prev).unwrap()
}
/// Re-resolve `$BIN_DIR/grok` from disk and re-run it: the active grok must
/// always execute, and never be a `.tmp`/partial file.
fn assert_active_grok_runs(home: &Path) {
let link = home.join(".kigi").join("bin").join("grok");
assert!(link.is_symlink(), "grok must remain a symlink");
let resolved =
dunce::canonicalize(&link).unwrap_or_else(|e| panic!("grok symlink dangles: {e}"));
let name = resolved.file_name().unwrap().to_string_lossy().to_string();
assert!(
!name.contains(".tmp"),
"active grok must not be a temp file: {name}"
);
let ok = Command::new(&resolved)
.arg("--version")
.status()
.map(|s| s.success())
.unwrap_or(false);
assert!(ok, "active grok must run: {}", resolved.display());
}
fn run_installer(install_sh: &Path, home: &Path, fakebin: &Path, mode: &str, shell: &str) -> bool {
let path_env = format!("{}:/usr/bin:/bin", fakebin.display());
let status = Command::new("/bin/bash")
.arg(install_sh)
.arg("0.1.181")
.env_clear()
.env("HOME", home)
.env("PATH", path_env)
.env("SHELL", shell)
.env("KIGI_BIN_DIR", home.join(".kigi").join("bin"))
.env("KIGI_CHANNEL", "stable")
.env("FAKE_MODE", mode)
.status()
.expect("spawn bash install.sh");
status.success()
}
fn installer_block_count(body: &str) -> usize {
body.matches(INSTALLER_BLOCK_START).count()
}
fn assert_single_installer_block(path: &Path, preserved: Option<&str>) {
let body = std::fs::read_to_string(path).unwrap_or_else(|e| {
panic!("read {}: {e}", path.display());
});
let n = installer_block_count(&body);
assert_eq!(
n,
1,
"{} must contain exactly one grok installer block, got {n}:\n{body}",
path.display()
);
if let Some(marker) = preserved {
assert!(
body.contains(marker),
"{} must keep pre-existing content ({marker:?}):\n{body}",
path.display()
);
}
}
#[derive(Clone, Copy)]
enum RcLayout {
Missing,
Plain,
StowAbsolute,
StowRelative,
/// `$root/user/.bashrc` → `../packages/bash/bashrc` (physical relative arm).
StowRelativeDotDot,
}
struct ShellRcCase {
name: &'static str,
script: &'static str,
shell: &'static str,
rc_name: &'static str,
stow_name: &'static str,
layout: RcLayout,
reinstall: bool,
}
/// Returns `(installer_home, rc_path, stow_target, expected_link_value)`.
fn setup_rc(
root: &Path,
case: &ShellRcCase,
) -> (PathBuf, PathBuf, Option<PathBuf>, Option<PathBuf>) {
let marker = "# user shell rc\n";
match case.layout {
RcLayout::Missing => {
let home = root.to_path_buf();
(home.clone(), home.join(case.rc_name), None, None)
}
RcLayout::Plain => {
let home = root.to_path_buf();
let rc_link = home.join(case.rc_name);
std::fs::write(&rc_link, marker).unwrap();
(home, rc_link, None, None)
}
RcLayout::StowAbsolute | RcLayout::StowRelative => {
let home = root.to_path_buf();
let stow_dir = home.join("dotfiles");
std::fs::create_dir_all(&stow_dir).unwrap();
let target = stow_dir.join(case.stow_name);
std::fs::write(&target, marker).unwrap();
let link_value = if matches!(case.layout, RcLayout::StowAbsolute) {
target.clone()
} else {
PathBuf::from(format!("dotfiles/{}", case.stow_name))
};
let rc_link = home.join(case.rc_name);
std::os::unix::fs::symlink(&link_value, &rc_link).unwrap();
(home, rc_link, Some(target), Some(link_value))
}
RcLayout::StowRelativeDotDot => {
// $HOME = root/user; package is a sibling of user (relative needs `..`).
let home = root.join("user");
std::fs::create_dir_all(&home).unwrap();
let target = root.join("packages/bash/bashrc");
std::fs::create_dir_all(target.parent().unwrap()).unwrap();
std::fs::write(&target, marker).unwrap();
let link_value = PathBuf::from("../packages/bash/bashrc");
let rc_link = home.join(case.rc_name);
std::os::unix::fs::symlink(&link_value, &rc_link).unwrap();
(home, rc_link, Some(target), Some(link_value))
}
}
}
fn run_shell_rc_case(case: &ShellRcCase) {
let Some(script) = script_path(case.script) else {
eprintln!(
"skipping {}: {} not found relative to crate",
case.name, case.script
);
return;
};
let platform = host_platform();
let fakedir = tempfile::tempdir().unwrap();
write_fake_curl(fakedir.path());
let root = tempfile::tempdir().unwrap();
let (home_path, rc_path, stow_target, expected_link) = setup_rc(root.path(), case);
seed_previous_good(&home_path, &platform);
assert!(
run_installer(&script, &home_path, fakedir.path(), "full", case.shell),
"{}: first install should succeed",
case.name
);
if case.reinstall {
assert!(
run_installer(&script, &home_path, fakedir.path(), "full", case.shell),
"{}: reinstall should succeed",
case.name
);
}
match case.layout {
RcLayout::Missing | RcLayout::Plain => {
assert!(
rc_path.is_file() && !rc_path.is_symlink(),
"{}: {} must be a regular file",
case.name,
case.rc_name
);
let preserved = match case.layout {
RcLayout::Plain => Some("# user shell rc"),
_ => None,
};
assert_single_installer_block(&rc_path, preserved);
}
RcLayout::StowAbsolute | RcLayout::StowRelative | RcLayout::StowRelativeDotDot => {
assert!(
rc_path.is_symlink(),
"{}: {} must remain a symlink after install",
case.name,
case.rc_name
);
let link = std::fs::read_link(&rc_path).unwrap();
assert_eq!(
link,
*expected_link.as_ref().unwrap(),
"{}: symlink target must be unchanged",
case.name
);
let target = stow_target.as_ref().unwrap();
assert_single_installer_block(target, Some("# user shell rc"));
}
}
assert_active_grok_runs(&home_path);
}
#[test]
fn install_sh_blitz_keeps_grok_runnable_under_corruption() {
let Some(install_sh) = install_sh_path() else {
eprintln!("skipping: install.sh not found relative to crate; run under cargo");
return;
};
let platform = host_platform();
let fakedir = tempfile::tempdir().unwrap();
write_fake_curl(fakedir.path());
// Each entry: (mode, should the installer succeed?). Loop a few rounds so a
// re-install over an existing good install is also exercised.
let cases = [
("full", true),
("truncate", false),
("garbage", false),
("full", true),
("truncate", false),
("garbage", false),
("full", true),
];
for (mode, expect_ok) in cases {
impl Fixture {
/// `binary` becomes the `kigi` entry of the served archive; `sums_hash`
/// overrides the manifest hash when `Some` (to simulate corruption).
fn new(version: &str, binary: &[u8], sums_hash: Option<&str>) -> Self {
let dir = tempfile::tempdir().unwrap();
let home = tempfile::tempdir().unwrap();
seed_previous_good(home.path(), &platform);
let ok = run_installer(&install_sh, home.path(), fakedir.path(), mode, "/bin/bash");
assert_eq!(
ok, expect_ok,
"install.sh mode={mode} exit success mismatch"
let archive = make_release_archive(binary);
let hash = match sums_hash {
Some(h) => h.to_string(),
None => sha256_hex(&archive),
};
std::fs::write(dir.path().join("release.json"), release_json(version)).unwrap();
std::fs::write(dir.path().join("archive.tar.gz"), &archive).unwrap();
std::fs::write(
dir.path().join("SHA256SUMS"),
format!("{hash} {}\n", archive_name(version)),
)
.unwrap();
let d = dir.path().to_string_lossy().replace('\'', "'\\''");
let curl = format!(
r#"#!/bin/sh
echo "$@" >> '{d}/curl-args.log'
out=""
url=""
prev=""
for a in "$@"; do
if [ "$prev" = "-o" ]; then out="$a"; fi
case "$a" in
-*) ;;
*) url="$a" ;;
esac
prev="$a"
done
serve() {{
if [ -n "$out" ]; then cat "$1" > "$out"; else cat "$1"; fi
}}
case "$url" in
*/SHA256SUMS) serve '{d}/SHA256SUMS' ;;
*.tar.gz) serve '{d}/archive.tar.gz' ;;
*/latest|*/tags/v*) serve '{d}/release.json' ;;
*) echo "fake curl: unmatched url: $url" >&2; exit 22 ;;
esac
"#
);
let curl_path = dir.path().join("curl");
std::fs::write(&curl_path, curl).unwrap();
std::fs::set_permissions(&curl_path, std::fs::Permissions::from_mode(0o755)).unwrap();
// The invariant holds regardless of which path was taken: the active
// grok always runs (new good binary on success, previous-good on
// rejection).
assert_active_grok_runs(home.path());
Self { dir, home }
}
fn run(&self, args: &[&str]) -> std::process::Output {
let script = install_sh_path().expect("install.sh present at repo root");
let path = format!(
"{}:{}",
self.dir.path().display(),
std::env::var("PATH").unwrap_or_default()
);
Command::new("sh")
.arg(&script)
.args(args)
.env("PATH", path)
.env("KIGI_SHARE_DIR", self.home.path())
.env("HOME", self.home.path())
.output()
.expect("install.sh must spawn")
}
fn curl_log(&self) -> String {
std::fs::read_to_string(self.dir.path().join("curl-args.log")).unwrap_or_default()
}
fn active_kigi(&self) -> PathBuf {
self.home.path().join("bin").join("kigi")
}
}
/// Shell-rc rewrite matrix: stow absolute/relative/`..`, plain, first-create, enterprise.
fn stderr_of(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stderr).to_string()
}
#[test]
fn install_sh_shell_rc_rewrite_matrix() {
let cases = [
ShellRcCase {
name: "stow absolute bashrc reinstall",
script: "install.sh",
shell: "/bin/bash",
rc_name: ".bashrc",
stow_name: "bashrc",
layout: RcLayout::StowAbsolute,
reinstall: true,
},
ShellRcCase {
name: "stow relative bashrc reinstall",
script: "install.sh",
shell: "/bin/bash",
rc_name: ".bashrc",
stow_name: "bashrc",
layout: RcLayout::StowRelative,
reinstall: true,
},
ShellRcCase {
name: "stow relative ../ bashrc reinstall",
script: "install.sh",
shell: "/bin/bash",
rc_name: ".bashrc",
stow_name: "bashrc",
layout: RcLayout::StowRelativeDotDot,
reinstall: true,
},
ShellRcCase {
name: "plain bashrc reinstall",
script: "install.sh",
shell: "/bin/bash",
rc_name: ".bashrc",
stow_name: "bashrc",
layout: RcLayout::Plain,
reinstall: true,
},
ShellRcCase {
name: "missing bashrc first install",
script: "install.sh",
shell: "/bin/bash",
rc_name: ".bashrc",
stow_name: "bashrc",
layout: RcLayout::Missing,
reinstall: false,
},
ShellRcCase {
name: "enterprise stow absolute bashrc reinstall",
script: "install-enterprise.sh",
shell: "/bin/bash",
rc_name: ".bashrc",
stow_name: "bashrc",
layout: RcLayout::StowAbsolute,
reinstall: true,
},
];
for case in &cases {
run_shell_rc_case(case);
fn install_sh_happy_path_installs_versioned_binary_and_symlink() {
if install_sh_path().is_none() {
eprintln!("skipping: install.sh not found (vendored sandbox)");
return;
}
let fx = Fixture::new("0.1.5", &small_good_artifact(), None);
let out = fx.run(&[]);
assert!(
out.status.success(),
"install.sh must succeed: stderr={}",
stderr_of(&out)
);
// Managed layout: versioned binary + relative symlink, same as the
// self-updater produces.
let versioned = fx
.home
.path()
.join("downloads")
.join(format!("kigi-0.1.5-{}", host_platform()));
assert!(versioned.exists(), "versioned binary installed");
assert_eq!(std::fs::read(&versioned).unwrap(), small_good_artifact());
let link = fx.active_kigi();
assert!(link.is_symlink(), "bin/kigi is a symlink");
assert_eq!(
std::fs::read_link(&link).unwrap(),
Path::new("..")
.join("downloads")
.join(format!("kigi-0.1.5-{}", host_platform())),
"symlink must be relative (survives bind-mounted homes)"
);
// The active link runs.
let status = Command::new(&link).arg("--version").status().unwrap();
assert!(status.success(), "installed kigi must run");
// Resolved the latest endpoint (no pinned version).
assert!(
fx.curl_log().contains("/latest"),
"must resolve via /latest: {}",
fx.curl_log()
);
}
#[test]
fn install_sh_pinned_version_uses_tag_endpoint() {
if install_sh_path().is_none() {
eprintln!("skipping: install.sh not found (vendored sandbox)");
return;
}
let fx = Fixture::new("0.1.5", &small_good_artifact(), None);
let out = fx.run(&["--version", "v0.1.5"]);
assert!(
out.status.success(),
"pinned install must succeed: stderr={}",
stderr_of(&out)
);
assert!(
fx.curl_log().contains("/tags/v0.1.5"),
"must resolve via /tags/v0.1.5: {}",
fx.curl_log()
);
assert!(fx.active_kigi().is_symlink());
}
#[test]
fn install_sh_rejects_checksum_mismatch_and_activates_nothing() {
if install_sh_path().is_none() {
eprintln!("skipping: install.sh not found (vendored sandbox)");
return;
}
let fx = Fixture::new("0.1.5", &small_good_artifact(), Some(&"0".repeat(64)));
let out = fx.run(&[]);
assert!(
!out.status.success(),
"checksum mismatch must fail the install"
);
assert!(
stderr_of(&out).contains("SHA256 mismatch"),
"stderr: {}",
stderr_of(&out)
);
let link = fx.active_kigi();
assert!(
!link.exists() && !link.is_symlink(),
"nothing may be activated after a checksum failure"
);
}
#[test]
fn install_sh_rejects_invalid_version_argument() {
if install_sh_path().is_none() {
eprintln!("skipping: install.sh not found (vendored sandbox)");
return;
}
let fx = Fixture::new("0.1.5", &small_good_artifact(), None);
let out = fx.run(&["--version", "not-a-version"]);
assert!(!out.status.success());
assert!(
stderr_of(&out).contains("invalid version"),
"stderr: {}",
stderr_of(&out)
);
assert!(
fx.curl_log().is_empty(),
"invalid arguments must fail before any network access"
);
}
#[test]
fn install_sh_fails_when_release_lacks_platform_asset() {
if install_sh_path().is_none() {
eprintln!("skipping: install.sh not found (vendored sandbox)");
return;
}
let fx = Fixture::new("0.1.5", &small_good_artifact(), None);
// Rewrite release.json without the platform archive asset.
let json = serde_json::json!({
"tag_name": "v0.1.5",
"assets": [
{ "name": "SHA256SUMS", "browser_download_url": "https://example.test/dl/v0.1.5/SHA256SUMS" },
],
});
std::fs::write(fx.dir.path().join("release.json"), json.to_string()).unwrap();
let out = fx.run(&[]);
assert!(!out.status.success());
assert!(
stderr_of(&out).contains("no asset"),
"stderr: {}",
stderr_of(&out)
);
let link = fx.active_kigi();
assert!(!link.exists() && !link.is_symlink());
}
#[test]
fn install_sh_fails_when_archive_lacks_kigi_binary() {
if install_sh_path().is_none() {
eprintln!("skipping: install.sh not found (vendored sandbox)");
return;
}
let fx = Fixture::new("0.1.5", &small_good_artifact(), None);
// Replace the archive with one that has no `kigi` entry; keep the
// manifest consistent so the checksum gate passes and the extraction
// check is what trips.
let archive = common::make_tar_gz(&[("LICENSE", b"license only")]);
std::fs::write(
fx.dir.path().join("SHA256SUMS"),
format!("{} {}\n", sha256_hex(&archive), archive_name("0.1.5")),
)
.unwrap();
std::fs::write(fx.dir.path().join("archive.tar.gz"), &archive).unwrap();
let out = fx.run(&[]);
assert!(!out.status.success());
assert!(
stderr_of(&out).contains("does not contain a 'kigi' binary"),
"stderr: {}",
stderr_of(&out)
);
let link = fx.active_kigi();
assert!(!link.exists() && !link.is_symlink());
}