Menu
Public repository / 1733e7c7f9cb6fd1bd4760c59b7727822aab6036
AkurAI-Notes / deploy.sh
Latest change 1733e7c7f9cb6fd1bd4760c59b7727822aab6036 - release: v0.0.6 by Claude Code
#!/usr/bin/env bash
# Deploy rules of conduct:
# - Read AGENTS.md and canonical Notes/docs before changing or deploying.
# - Never print, commit, or copy secrets; use passvault/env files only.
# - Preserve env files, service users, user data, and databases; snapshot state before risky swaps.
# - Verify ports, DNS, TLS, and systemd unit names before changing routes or services.
# - Use managed services only (systemd/pm2); no nohup, background shells, or ad hoc daemons.
# - Run gates and health checks; if deploy fails, stop and roll back rather than improvising.
# - Keep deploy behavior unchanged unless the task explicitly asks for deploy logic changes.
# 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 two
# systemd services behind two nginx location blocks:
# / -> :8099 (web app + REST API)
# /mcp -> :8101 (MCP JSON-RPC server; 8100 is AkurAI-Fulltrui)
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
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"
say "Ship: binary + site assets"
ssh "$HOST" 'sudo bash -s' <<'PREP'
set -euo pipefail
mkdir -p /opt/akurai-notes/bin /opt/akurai-notes/app
chown -R ubuntu:ubuntu /opt/akurai-notes
systemctl stop akurai-notes-mcp.service 2>/dev/null || true
systemctl stop akurai-notes.service 2>/dev/null || true
if [ -f /opt/akurai-notes/bin/akurai-notes ]; then
cp -p /opt/akurai-notes/bin/akurai-notes /opt/akurai-notes/bin/akurai-notes.prev
fi
PREP
scp -q "$MUSL" "$HOST:/tmp/akurai-notes.new"
ssh "$HOST" 'sudo install -m 755 -o ubuntu -g ubuntu /tmp/akurai-notes.new /opt/akurai-notes/bin/akurai-notes && rm -f /tmp/akurai-notes.new'
(cd site && tar czf - .) | ssh "$HOST" 'rm -rf /opt/akurai-notes/app && mkdir -p /opt/akurai-notes/app && tar xzf - -C /opt/akurai-notes/app && sudo chown -R ubuntu:ubuntu /opt/akurai-notes/app'
}
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
PENDING=/etc/akurai-notes/notes.env.pending
BOOTSTRAP=0
if [ ! -f "$F" ] || ! grep -q '^AKURAI_NOTES_IDP_CLIENT_ID=' "$F"; then
BOOTSTRAP=1
fi
touch "$F"
chmod 0600 "$F"
append_if_missing() {
key="$1"
value="$2"
if ! grep -q "^${key}=" "$F"; then
printf '%s=%s\n' "$key" "$value" >> "$F"
chmod 0600 "$F"
fi
}
set_kv() {
key="$1"
value="$2"
tmp="${F}.tmp"
grep -v "^${key}=" "$F" > "$tmp" || true
printf '%s=%s\n' "$key" "$value" >> "$tmp"
mv "$tmp" "$F"
chmod 0600 "$F"
}
if [ "$BOOTSTRAP" -eq 1 ]; then
if [ -f "$PENDING" ]; then
cat "$PENDING" >> "$F"
fi
append_if_missing AKURAI_NOTES_DB /var/lib/akurai-notes/notes.db
append_if_missing AKURAI_NOTES_IDP_ISSUER https://auth.olibuijr.com
append_if_missing AKURAI_NOTES_IDP_REDIRECT https://akurai-notes.olibuijr.com/auth/callback
append_if_missing AKURAI_NOTES_ALLOW olibuijr@olibuijr.com
if ! grep -q '^AKURAI_NOTES_MCP_TOKEN=' "$F"; then
T="mcp_$(openssl rand -hex 32)"
printf 'AKURAI_NOTES_MCP_TOKEN=%s\n' "$T" >> "$F"
chmod 0600 "$F"
echo " [generated] AKURAI_NOTES_MCP_TOKEN (value in $F)"
fi
append_if_missing AKURAI_NOTES_MCP_OWNER_SUB olibuijr@olibuijr.com
fi
set_kv AKURAI_NOTES_EMBED_URL http://127.0.0.1:4219/v1/embeddings
set_kv AKURAI_NOTES_EMBED_MODEL intfloat/multilingual-e5-small
ROUTER_KEY="$(awk -F= '/^AKURAI_ROUTER_API_KEY=/{print substr($0, index($0, "=") + 1)}' /etc/akurai-router/router.env 2>/dev/null | head -n 1)"
if [ -n "$ROUTER_KEY" ]; then
set_kv AKURAI_NOTES_EMBED_API_KEY "$ROUTER_KEY"
fi
ENV
}
install_systemd_units() {
say "Systemd: 2 units (app, mcp)"
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
Wants=network-online.target
[Service]
Type=simple
User=ubuntu
Group=ubuntu
ExecStart=/opt/akurai-notes/bin/akurai-notes serve \
--host 127.0.0.1 --port 8099 \
--db /var/lib/akurai-notes/notes.db \
--dir /opt/akurai-notes/app/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=ubuntu
Group=ubuntu
ExecStart=/opt/akurai-notes/bin/akurai-notes mcp \
--host 127.0.0.1 --port 8101 \
--db /var/lib/akurai-notes/notes.db
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
mkdir -p /opt/akurai-notes /var/lib/akurai-notes /etc/akurai-notes
chown -R ubuntu:ubuntu /opt/akurai-notes /var/lib/akurai-notes
systemctl disable --now akurai-notes-embed.service 2>/dev/null || true
rm -f /etc/systemd/system/akurai-notes-embed.service
systemctl daemon-reload
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:8101; }
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 -sS -o /dev/null -w "%{http_code}" http://127.0.0.1:8101/mcp')" = "405" ] \
|| die "MCP /mcp didn't answer 405 on bare GET (expected)"
if ! ssh "$HOST" 'bash -s' <<'EMBED_HEALTH'
set -euo pipefail
F=/etc/akurai-notes/notes.env
URL="$(awk -F= '/^AKURAI_NOTES_EMBED_URL=/{print substr($0, index($0, "=") + 1)}' "$F" | head -n 1)"
MODEL="$(awk -F= '/^AKURAI_NOTES_EMBED_MODEL=/{print substr($0, index($0, "=") + 1)}' "$F" | head -n 1)"
KEY="$(awk -F= '/^AKURAI_NOTES_EMBED_API_KEY=/{print substr($0, index($0, "=") + 1)}' "$F" | head -n 1)"
[ -n "$URL" ] && [ -n "$MODEL" ] && [ -n "$KEY" ]
payload="$(printf '{"input":"x","model":"%s"}' "$MODEL")"
code="$(curl -sS -o /tmp/akurai-notes-embed-health.json -w '%{http_code}' \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d "$payload" "$URL" || true)"
rm -f /tmp/akurai-notes-embed-health.json
[ "$code" = "200" ]
EMBED_HEALTH
then
say " Router embedding upstream not ready yet -- semantic search will use substring fallback until it is up"
fi
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 -p notes-core -p notes-api -p notes-mcp -p akurai-notes -- --check \
|| die "formatting failed (run: cargo fmt -p notes-core -p notes-api -p notes-mcp -p akurai-notes)"
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
cargo metadata --format-version 1 >/dev/null || die "lockfile update failed"
# ── 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 ✓"