Skip to content

Commit 391d0e8

Browse files
committed
feat: add CI release pipeline, self-update, Apple signing, and Homebrew
Per-commit releases from main: build for darwin-arm64, linux-amd64, linux-arm64. Apple code signing and notarization for macOS. Homebrew formula at arcavenae/tap/kos. Self-update via `kos update`. Build-time version injection via build.rs. New files: build.rs, src/updater.rs, scripts/create-{app,dmg,pkg}.sh, packaging/Info.plist, Formula/kos.rb. New dep: reqwest (blocking, rustls). New subcommands: kos update, kos version. justfile: just install.
1 parent c21186b commit 391d0e8

15 files changed

Lines changed: 2026 additions & 64 deletions

File tree

.github/workflows/ci.yml

Lines changed: 301 additions & 48 deletions
Large diffs are not rendered by default.

Cargo.lock

Lines changed: 1095 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ regex = "1"
3636
# Content hashing for drift detection
3737
sha2 = "0.10"
3838

39+
# Self-update (GitHub releases API)
40+
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
41+
42+
[build-dependencies]
43+
chrono = "0.4"
44+
3945
[dev-dependencies]
4046
insta = { version = "1", features = ["yaml", "json"] }
4147
tempfile = "3"

Formula/kos.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Homebrew formula for kos
2+
# Updated automatically by CI on every push to main
3+
# macOS only (arm64). Linux users: download from GitHub releases.
4+
5+
class Kos < Formula
6+
desc "Knowledge Operating System — graph-based knowledge accumulation for designed systems"
7+
homepage "https://github.com/arcavenae/kos"
8+
url "https://github.com/arcavenae/kos/releases/download/TAG_PLACEHOLDER/kos-darwin-arm64"
9+
version "VERSION_PLACEHOLDER"
10+
sha256 "SHA256_ARM64_PLACEHOLDER"
11+
license "Apache-2.0"
12+
13+
def install
14+
bin.install "kos-darwin-arm64" => "kos"
15+
end
16+
17+
def caveats
18+
<<~EOS
19+
kos updates on every push to main.
20+
Self-update: kos update
21+
EOS
22+
end
23+
24+
test do
25+
assert_match "kos", shell_output("#{bin}/kos --version 2>&1")
26+
end
27+
end

build.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use std::env;
2+
use std::process::Command;
3+
4+
fn main() {
5+
let version = env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".to_string());
6+
7+
let commit = Command::new("git")
8+
.args(["rev-parse", "--short", "HEAD"])
9+
.output()
10+
.ok()
11+
.and_then(|o| {
12+
if o.status.success() {
13+
String::from_utf8(o.stdout)
14+
.ok()
15+
.map(|s| s.trim().to_string())
16+
} else {
17+
None
18+
}
19+
})
20+
.unwrap_or_else(|| "unknown".to_string());
21+
22+
let build_time = chrono::Utc::now().to_rfc3339();
23+
24+
let channel = env::var("KOS_CHANNEL").unwrap_or_else(|_| "main".to_string());
25+
let tag = env::var("KOS_TAG").unwrap_or_else(|_| format!("{version}-{commit}"));
26+
27+
let long_version = format!("{tag} ({commit}, {channel})");
28+
29+
println!("cargo:rustc-env=KOS_VERSION={version}");
30+
println!("cargo:rustc-env=KOS_COMMIT={commit}");
31+
println!("cargo:rustc-env=KOS_BUILD_TIME={build_time}");
32+
println!("cargo:rustc-env=KOS_CHANNEL={channel}");
33+
println!("cargo:rustc-env=KOS_TAG={tag}");
34+
println!("cargo:rustc-env=KOS_LONG_VERSION={long_version}");
35+
36+
println!("cargo:rerun-if-changed=.git/HEAD");
37+
println!("cargo:rerun-if-env-changed=KOS_CHANNEL");
38+
println!("cargo:rerun-if-env-changed=KOS_TAG");
39+
}

deny.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,9 @@ allow = [
1414
"CC0-1.0",
1515
]
1616

17+
[[licenses.exceptions]]
18+
allow = ["CDLA-Permissive-2.0"]
19+
name = "webpki-roots"
20+
1721
[bans]
1822
multiple-versions = "warn"

justfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ build:
1414
build-release:
1515
cargo build --release
1616

17+
# Install to ~/.local/bin
18+
install: build-release
19+
mkdir -p ~/.local/bin
20+
cp target/release/kos ~/.local/bin/kos
21+
@echo "Installed kos to ~/.local/bin/kos"
22+
1723
# Run kos with arguments
1824
run *args:
1925
cargo run -- {{args}}

packaging/Info.plist

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleIdentifier</key>
6+
<string>com.arcavenae.kos</string>
7+
<key>CFBundleName</key>
8+
<string>Kos</string>
9+
<key>CFBundleDisplayName</key>
10+
<string>Kos</string>
11+
<key>CFBundleExecutable</key>
12+
<string>kos</string>
13+
<key>CFBundleVersion</key>
14+
<string>VERSION_PLACEHOLDER</string>
15+
<key>CFBundleShortVersionString</key>
16+
<string>VERSION_PLACEHOLDER</string>
17+
<key>CFBundlePackageType</key>
18+
<string>APPL</string>
19+
<key>CFBundleInfoDictionaryVersion</key>
20+
<string>6.0</string>
21+
<key>LSMinimumSystemVersion</key>
22+
<string>12.0</string>
23+
<key>LSUIElement</key>
24+
<true/>
25+
<key>NSHighResolutionCapable</key>
26+
<true/>
27+
</dict>
28+
</plist>

scripts/create-app.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
# create-app.sh - Create a macOS .app bundle
5+
# Usage: ./scripts/create-app.sh <binary-path> <version> <output-dir>
6+
7+
BINARY_PATH="${1:?Usage: create-app.sh <binary-path> <version> <output-dir>}"
8+
VERSION="${2:?Version required}"
9+
OUTPUT_DIR="${3:?Output directory required}"
10+
11+
if [ ! -f "$BINARY_PATH" ]; then
12+
echo "Error: binary not found: $BINARY_PATH" >&2
13+
exit 1
14+
fi
15+
16+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
18+
19+
APP_BUNDLE="${OUTPUT_DIR}/Kos.app"
20+
21+
rm -rf "$APP_BUNDLE"
22+
mkdir -p "$APP_BUNDLE/Contents/MacOS"
23+
mkdir -p "$APP_BUNDLE/Contents/Resources"
24+
25+
cp "$BINARY_PATH" "$APP_BUNDLE/Contents/MacOS/kos"
26+
chmod +x "$APP_BUNDLE/Contents/MacOS/kos"
27+
28+
sed "s/VERSION_PLACEHOLDER/$VERSION/g" "$PROJECT_ROOT/packaging/Info.plist" \
29+
> "$APP_BUNDLE/Contents/Info.plist"
30+
31+
echo "APPL????" > "$APP_BUNDLE/Contents/PkgInfo"
32+
33+
echo "Created app bundle: $APP_BUNDLE"

scripts/create-dmg.sh

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
# create-dmg.sh - Create a macOS .dmg disk image
5+
# Usage: ./scripts/create-dmg.sh <app-bundle-path> <version> <output-path>
6+
7+
APP_PATH="${1:?Usage: create-dmg.sh <app-bundle-path> <version> <output-path>}"
8+
VERSION="${2:?Version required}"
9+
OUTPUT_PATH="${3:?Output path required}"
10+
11+
if [ ! -d "$APP_PATH" ]; then
12+
echo "Error: app bundle not found: $APP_PATH" >&2
13+
exit 1
14+
fi
15+
16+
STAGING_DIR=$(mktemp -d)
17+
trap 'rm -rf "$STAGING_DIR"' EXIT
18+
19+
cp -R "$APP_PATH" "$STAGING_DIR/"
20+
ln -s /Applications "$STAGING_DIR/Applications"
21+
22+
rm -f "$OUTPUT_PATH"
23+
sync
24+
sleep 1
25+
hdiutil create -volname "Kos $VERSION" \
26+
-srcfolder "$STAGING_DIR" \
27+
-ov -format UDZO \
28+
"$OUTPUT_PATH" || {
29+
echo "Retrying after hdiutil failure..."
30+
sleep 3
31+
hdiutil create -volname "Kos $VERSION" \
32+
-srcfolder "$STAGING_DIR" \
33+
-ov -format UDZO \
34+
"$OUTPUT_PATH"
35+
}
36+
37+
echo "Created dmg: $OUTPUT_PATH"

0 commit comments

Comments
 (0)