AkurAI Build
Menu

Public repository / 8d7067ab5b862b25460ebbdbf35b265ac7e4f995

AkurAI-Framework / deploy.sh

Latest change ee7487eee78cd52cdafdcb22b6ff4bf57093d6e9 - feat: add --name flag to serve, standardize /api/health across all framework apps by Ólafur Búi Ólafsson

#!/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-Framework 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 website's `site/content/changelog.md` before publishing to
# EC2, so the live page at https://akurai-framework.olibuijr.com/changelog
# always reflects the version just shipped. Developer-facing release history now
# lives in AkurAI Notes, not in a repo-root `CHANGELOG.md`.
set -euo pipefail

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

SITE_CHANGELOG="site/content/changelog.md"

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

# ── Live publish ────────────────────────────────────────────────────────────
# Build the static musl binary and ship the running site to the AkurAI AWS box
# behind nginx + TLS, via the reusable `akurai-ec2` ops CLI. Idempotent. Shared
# by `./deploy.sh ec2` and the tail of every release so the live /changelog is
# never stale. Missing `akurai-ec2` is a warning, not a failure — a release that
# is already tagged and pushed must not be undone by an absent ops CLI.
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-framework.olibuijr.com" PORT=8094 NAME="akurai-framework"
  local MUSL="target/x86_64-unknown-linux-musl/release/akurai"

  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 || die "musl build failed"

  # Ship the framework's own site/ (landing + docs + changelog + components),
  # which the framework serves itself — not a scaffolded starter.
  [ -d site/frontend ] || die "site/ not found"
  akurai-ec2 deploy-binary "$NAME" "$MUSL" "$PORT" site
  akurai-ec2 nginx-proxy "$DOMAIN" "$PORT"
  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"

  # ── Post-deploy validation (mandatory gate) ──────────────────────────
  # Ship the local validate script and run it on the box. Failing validation
  # is a deploy-stopper — the operator must investigate.
  if [ -f "$ROOT/scripts/validate.sh" ]; then
    say "Post-deploy validation"
    akurai-ec2 ship "$ROOT/scripts/validate.sh" "/opt/$NAME/scripts/validate.sh"
    if akurai-ec2 ssh "bash /opt/$NAME/scripts/validate.sh"; then
      say "Validate: PASS ✓"
    else
      die "Validate: FAIL — post-deploy checks failed. Investigate before re-deploying."
    fi
  fi

  say "Published → https://$DOMAIN  (incl. /changelog)"
}

# ── Changelog cut ───────────────────────────────────────────────────────────
# Stamp the "Unreleased" section of a changelog into a dated version section,
# leaving a fresh empty Unreleased on top.
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
# workspace.package.version is the single source crates inherit via version.workspace = true
sed -i -E "s/^version = \"$CUR\"/version = \"$NEW\"/" Cargo.toml
# Keep the site's displayed version in sync (page.json drives the header/footer).
# Bumps both the top-level "version" field and the muted version badge so they
# never drift from the release.
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 refreshes the lockfile's workspace package versions lazily. Do it now so
# the release commit captures the bumped lockfile instead of leaving it dirty
# after the publish build.
cargo metadata --format-version 1 >/dev/null || die "failed to refresh Cargo.lock"

# ── 3. CHANGELOG (live site) ────────────────────────────────────────────────
DATE="$(date +%Y-%m-%d)"
say "CHANGELOG: cutting $SITE_CHANGELOG for $NEW"
cut_changelog "$SITE_CHANGELOG" "## 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 (so /changelog reflects v$NEW) ─────────────────────────
say "Publish: live site"
ec2_publish

say "Released v$NEW ✓"