AkurAI Build
Menu

Public repository / 9f0fd7a16e2dff7518389ff0ddbd603ebc6264ed

AkurAI-Notes / deploy.sh

Latest change 4ec0c6d0dc48e92b0b469b84d64b411e08c72692 - E07: deploy.sh release engine (Framework fork) + CHANGELOG + versioning lockstep by Claude Code

#!/usr/bin/env bash
# deploy.sh — AkurAI-Notes release engine.
#
#   ./deploy.sh [patch|minor|major]   release: gate -> bump -> changelog -> tag ->
#                                      push -> publish live (default: patch)
#   ./deploy.sh ec2                    publish the live site only (no version bump)
#
# A release cuts the repo CHANGELOG.md, tags, pushes, and publishes three
# systemd services behind two nginx location blocks:
#   /   -> :8099 (web app + REST API)
#   /mcp -> :8100 (MCP JSON-RPC server)
set -euo pipefail

REPO="olibuijr/AkurAI-Notes"
HOST="akurai-mail"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"

say() { printf '\033[1;34m▸ %s\033[0m\n' "$*"; }
die() { printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2; exit 1; }

# ── Live publish ────────────────────────────────────────────────────────────
ec2_publish() {
  if ! command -v akurai-ec2 >/dev/null; then
    say "akurai-ec2 not on PATH -- skipping live publish (run './deploy.sh ec2' later)"
    return 0
  fi
  local DOMAIN="akurai-notes.olibuijr.com" PORT=8099 NAME="akurai-notes"
  local MUSL="target/x86_64-unknown-linux-musl/release/akurai-notes"

  build_musl
  ship_binary
  ensure_env_file
  ensure_embedding_model
  install_systemd_units
  write_nginx_vhost
  run_tls
  healthcheck
}

build_musl() {
  say "Build: static musl binary"
  rustup target add x86_64-unknown-linux-musl >/dev/null 2>&1 || true
  cargo build --release --target x86_64-unknown-linux-musl -p akurai-notes \
    || die "musl build failed"
}

ship_binary() {
  [ -d site/frontend ] || die "site/frontend not found"
  akurai-ec2 deploy-binary "$NAME" "$MUSL" "$PORT" site
}

ensure_env_file() {
  say "Env: /etc/akurai-notes/notes.env"
  ssh "$HOST" 'sudo bash -s' <<'ENV'
set -euo pipefail
mkdir -p /etc/akurai-notes
F=/etc/akurai-notes/notes.env
if [ ! -f "$F" ] || ! grep -q '^AKURAI_NOTES_IDP_CLIENT_ID=' "$F"; then
  PENDING=/etc/akurai-notes/notes.env.pending
  touch "$F"; chmod 0600 "$F"

  if [ -f "$PENDING" ]; then
    cat "$PENDING" | tee -a "$F" >/dev/null
  fi

  grep -q '^AKURAI_NOTES_DB='            "$F" || echo "AKURAI_NOTES_DB=/var/lib/akurai-notes/notes.db"           | tee -a "$F" >/dev/null
  grep -q '^AKURAI_NOTES_IDP_ISSUER='   "$F" || echo 'AKURAI_NOTES_IDP_ISSUER=https://auth.olibuijr.com'         | tee -a "$F" >/dev/null
  grep -q '^AKURAI_NOTES_IDP_REDIRECT=' "$F" || echo 'AKURAI_NOTES_IDP_REDIRECT=https://akurai-notes.olibuijr.com/auth/callback' | tee -a "$F" >/dev/null
  grep -q '^AKURAI_NOTES_ALLOW='        "$F" || echo 'AKURAI_NOTES_ALLOW=olibuijr@olibuijr.com'                  | tee -a "$F" >/dev/null
  grep -q '^AKURAI_NOTES_EMBED_URL='    "$F" || echo 'AKURAI_NOTES_EMBED_URL=http://127.0.0.1:8081/v1/embeddings' | tee -a "$F" >/dev/null
  grep -q '^AKURAI_NOTES_EMBED_MODEL='  "$F" || echo 'AKURAI_NOTES_EMBED_MODEL=qwen3-embedding-0.6b'              | tee -a "$F" >/dev/null

  if ! grep -q '^AKURAI_NOTES_MCP_TOKEN=' "$F"; then
    T="mcp_$(openssl rand -hex 32)"
    echo "AKURAI_NOTES_MCP_TOKEN=$T" | tee -a "$F" >/dev/null
    echo "  [generated] AKURAI_NOTES_MCP_TOKEN (value in $F)"
  fi

  grep -q '^AKURAI_NOTES_MCP_OWNER_SUB=' "$F" || echo 'AKURAI_NOTES_MCP_OWNER_SUB=olibuijr@olibuijr.com' | tee -a "$F" >/dev/null
fi
ENV
}

ensure_embedding_model() {
  say "Model: Qwen3-Embedding GGUF"
  ssh "$HOST" 'bash -s' <<'MODEL'
set -euo pipefail
DIR=/opt/akurai-notes-embed
MODEL=Qwen3-Embedding-0.6B-Q4_K_M.gguf
URL=https://huggingface.co/Qwen/Qwen3-Embedding-0.6B-GGUF/resolve/main/$MODEL
[ -d "$DIR" ] || sudo mkdir -p "$DIR" && sudo chown -R akurai:akurai "$DIR"

if [ ! -f "$DIR/$MODEL" ] || [ "$(stat -c%s "$DIR/$MODEL")" -lt 200000000 ]; then
  echo "Fetching $MODEL from $URL ..."
  curl -fL "$URL" -o "/tmp/$MODEL" && sudo mv "/tmp/$MODEL" "$DIR/$MODEL"
fi
sudo chown akurai:akurai "$DIR/$MODEL"
MODEL
}

install_systemd_units() {
  say "Systemd: 3 units (app, mcp, embed)"
  ssh "$HOST" 'sudo bash -s' <<'UNITS'
set -euo pipefail

cat > /etc/systemd/system/akurai-notes.service <<EOF
[Unit]
Description=AkurAI-Notes (web app + REST API)
After=network-online.target akurai-notes-embed.service
Wants=network-online.target

[Service]
Type=simple
User=akurai
Group=akurai
ExecStart=/opt/akurai-notes/akurai-notes serve \
  --host 127.0.0.1 --port 8099 \
  --db /var/lib/akurai-notes/notes.db \
  --frontend-dir /opt/akurai-notes/site/frontend
EnvironmentFile=/etc/akurai-notes/notes.env
Restart=always
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/var/lib/akurai-notes

[Install]
WantedBy=multi-user.target
EOF

cat > /etc/systemd/system/akurai-notes-mcp.service <<EOF
[Unit]
Description=AkurAI-Notes MCP server (POST /mcp JSON-RPC over HTTPS)
After=network-online.target akurai-notes.service
Wants=network-online.target

[Service]
Type=simple
User=akurai
Group=akurai
ExecStart=/opt/akurai-notes/akurai-notes mcp \
  --host 127.0.0.1 --port 8100 \
  --db /var/lib/akurai-notes/notes.db \
  --owner-sub \$AKURAI_NOTES_MCP_OWNER_SUB
EnvironmentFile=/etc/akurai-notes/notes.env
Restart=always
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/var/lib/akurai-notes

[Install]
WantedBy=multi-user.target
EOF

cat > /etc/systemd/system/akurai-notes-embed.service <<EOF
[Unit]
Description=AkurAI-Notes on-device embedding server (CPU GGUF, OpenAI-compat)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=akurai
Group=akurai
ExecStart=/usr/local/bin/llama-server \
  --model /opt/akurai-notes-embed/Qwen3-Embedding-0.6B-Q4_K_M.gguf \
  --host 127.0.0.1 --port 8081 \
  --embedding
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

if ! id -u akurai >/dev/null 2>&1; then
  useradd -r -m -d /opt/akurai -s /usr/sbin/nologin akurai
fi
mkdir -p /opt/akurai-notes /var/lib/akurai-notes /etc/akurai-notes
chown -R akurai:akurai /opt/akurai-notes /var/lib/akurai-notes /etc/akurai-notes

systemctl daemon-reload
systemctl enable --now akurai-notes-embed.service || say "  (akurai-notes-embed.service not started -- install llama-server separately)"
systemctl restart  akurai-notes.service
systemctl restart  akurai-notes-mcp.service
echo "  [done] systemd units installed + restarted"
UNITS
}

write_nginx_vhost() {
  say "Nginx: vhost with / and /mcp locations"
  ssh "$HOST" 'sudo bash -s' <<'NGINX'
set -euo pipefail
cat > /etc/nginx/sites-available/akurai-notes.olibuijr.com <<EOF
# Managed by AkurAI-Notes deploy.sh -- do not edit by hand.
upstream akurai_notes_app  { server 127.0.0.1:8099; }
upstream akurai_notes_mcp { server 127.0.0.1:8100; }

server {
    listen 80;
    server_name akurai-notes.olibuijr.com;
    return 301 https://\$host\$request_uri;
}

server {
    listen 443 ssl http2;
    server_name akurai-notes.olibuijr.com;

    ssl_certificate     /etc/letsencrypt/live/akurai-notes.olibuijr.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/akurai-notes.olibuijr.com/privkey.pem;

    location / {
        proxy_pass http://akurai_notes_app;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }

    location /mcp {
        proxy_pass http://akurai_notes_mcp;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Authorization \$http_authorization;
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;
        proxy_buffering off;
    }
}
EOF
ln -sf /etc/nginx/sites-available/akurai-notes.olibuijr.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
echo "  [done] nginx vhost installed + reloaded"
NGINX
}

run_tls() {
  say "TLS: requesting cert (skips gracefully if DNS hasn't propagated)"
  akurai-ec2 tls "$DOMAIN" || say "  TLS deferred -- run 'akurai-ec2 tls $DOMAIN' once DNS resolves"
}

healthcheck() {
  say "Healthcheck: verifying services"
  sleep 2

  ssh "$HOST" 'curl -fsS http://127.0.0.1:8099/api/health' | jq .ok | grep -q true \
    || die "/api/health failed"

  ssh "$HOST" 'curl -fsS http://127.0.0.1:8100/mcp' | grep -qE '405|method' \
    || die "MCP /mcp didn't answer 405 on bare GET (expected)"

  ssh "$HOST" 'curl -fsS http://127.0.0.1:8081/v1/embeddings \
    -H "Content-Type: application/json" \
    -d "{\"input\":\"x\",\"model\":\"qwen3-embedding-0.6b\"}" 2>/dev/null | \
    jq -re ".data[0].embedding | length"' 2>/dev/null | head -1 \
    || say "  embed server not ready yet -- semantic search will use substring fallback until it's up"

  curl -fsS https://akurai-notes.olibuijr.com/api/health | jq .ok | grep -q true \
    || say "  Public URL not healthy yet -- DNS/TLS may still be propagating"

  say "Published -> https://$DOMAIN"
}

# ── Changelog cut ───────────────────────────────────────────────────────────
cut_changelog() {
  local file="$1" unreleased="$2" verhead="$3"
  if [ ! -f "$file" ]; then say "  (no $file -- skipping)"; return 0; fi
  awk -v unrel="$unreleased" -v vh="$verhead" '
    $0 == unrel { print unrel; print ""; print vh; next }
    { print }
  ' "$file" > "$file.tmp" && mv "$file.tmp" "$file"
}

# ── `ec2` mode: publish only, no version change ─────────────────────────────
if [ "${1:-}" = "ec2" ]; then
  ec2_publish
  exit 0
fi

BUMP="${1:-patch}"
case "$BUMP" in patch|minor|major) ;; *) echo "usage: $0 [patch|minor|major|ec2]" >&2; exit 2 ;; esac

# ── 1. Quality gate ────────────────────────────────────────────────────────
say "Gate: rustfmt"
cargo fmt --all -- --check || die "formatting failed (run: cargo fmt --all)"

say "Gate: clippy (-D warnings)"
cargo clippy --all-targets --all-features -- -D warnings || die "clippy failed"

say "Gate: tests"
cargo test --all || die "tests failed"

if [ -f tests/e2e/package.json ]; then
  say "Gate: Playwright e2e"
  ( cd tests/e2e && npx playwright test ) || die "e2e failed"
fi

if [ -d bench ]; then
  say "Gate: performance budgets"
  cargo bench --bench budgets 2>/dev/null || say "  (no budget bench yet -- skipping)"
fi

# ── 2. Version bump (lockstep) ─────────────────────────────────────────────
CUR="$(tr -d '[:space:]' < VERSION)"
IFS='.' read -r MA MI PA <<< "$CUR"
case "$BUMP" in
  major) MA=$((MA+1)); MI=0; PA=0 ;;
  minor) MI=$((MI+1)); PA=0 ;;
  patch) PA=$((PA+1)) ;;
esac
NEW="$MA.$MI.$PA"
say "Version: $CUR -> $NEW ($BUMP)"
printf '%s\n' "$NEW" > VERSION
sed -i -E "s/^version = \"$CUR\"/version = \"$NEW\"/" Cargo.toml
if [ -f site/backend/page.json ]; then
  sed -i -E "s/\"version\": \"v[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"v$NEW\"/" site/backend/page.json
  sed -i -E "s/\"label\": \"v[0-9]+\.[0-9]+\.[0-9]+\"/\"label\": \"v$NEW\"/" site/backend/page.json
fi

# ── 3. CHANGELOG (repo only) ───────────────────────────────────────────────
DATE="$(date +%Y-%m-%d)"
say "CHANGELOG: cutting [$NEW]"
cut_changelog "CHANGELOG.md" "## [Unreleased]" "## [$NEW] - $DATE"

# ── 4. Commit, tag, push ───────────────────────────────────────────────────
say "Git: commit + tag v$NEW"
git add -A
git commit -q -m "release: v$NEW"
git tag -a "v$NEW" -m "v$NEW"

if ! git remote get-url origin >/dev/null 2>&1; then
  if gh repo view "$REPO" >/dev/null 2>&1; then
    git remote add origin "https://github.com/$REPO.git"
  else
    say "Creating GitHub repo $REPO (private)"
    gh repo create "$REPO" --private --source=. --remote=origin
  fi
fi

say "Git: push"
git push -u origin HEAD
git push origin "v$NEW"

# ── 5. Publish live ────────────────────────────────────────────────────────
say "Publish: live site"
ec2_publish

say "Released v$NEW ✓"