- Cargo workspace with hiy-server (axum 0.7 + sqlx SQLite + tokio) - SQLite schema: apps, deploys, env_vars (inline migrations, no daemon) - Background build worker: sequential queue, streams stdout/stderr to DB - REST API: CRUD for apps, deploys, env vars; GitHub webhook with HMAC-SHA256 - SSE endpoint for live build log streaming - Monospace HTMX-free dashboard: app list + per-app detail, log viewer, env editor - builder/build.sh: clone/pull → detect strategy (Dockerfile/buildpack/static) → docker build → swap container → update Caddy via admin API → prune images - infra/docker-compose.yml + Dockerfile.server for local dev (no Pi needed) - proxy/Caddyfile: auto-HTTPS off for local, comment removed for production - .env.example Compiles clean (zero warnings). Run locally: cp .env.example .env && cargo run --bin hiy-server https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
152 lines
6 KiB
Bash
Executable file
152 lines
6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# HIY Build Engine
|
|
# Environment variables injected by hiy-server:
|
|
# APP_ID, APP_NAME, REPO_URL, BRANCH, PORT, ENV_FILE, SHA, BUILD_DIR
|
|
set -euo pipefail
|
|
|
|
log() { echo "[hiy] $*"; }
|
|
|
|
log "=== HostItYourself Build Engine ==="
|
|
log "App: $APP_NAME ($APP_ID)"
|
|
log "Repo: $REPO_URL"
|
|
log "Branch: $BRANCH"
|
|
log "Build dir: $BUILD_DIR"
|
|
|
|
# ── 1. Clone or pull ───────────────────────────────────────────────────────────
|
|
mkdir -p "$BUILD_DIR"
|
|
cd "$BUILD_DIR"
|
|
|
|
if [ -d ".git" ]; then
|
|
log "Updating existing clone…"
|
|
git fetch origin "$BRANCH" --depth=50
|
|
git checkout "$BRANCH"
|
|
git reset --hard "origin/$BRANCH"
|
|
else
|
|
log "Cloning repository…"
|
|
git clone --depth=50 --branch "$BRANCH" "$REPO_URL" .
|
|
fi
|
|
|
|
ACTUAL_SHA=$(git rev-parse HEAD)
|
|
log "SHA: $ACTUAL_SHA"
|
|
|
|
# ── 2. Detect build strategy ──────────────────────────────────────────────────
|
|
IMAGE_TAG="hiy/${APP_ID}:${ACTUAL_SHA}"
|
|
CONTAINER_NAME="hiy-${APP_ID}"
|
|
|
|
if [ -f "Dockerfile" ]; then
|
|
log "Strategy: Dockerfile"
|
|
docker build --tag "$IMAGE_TAG" .
|
|
|
|
elif [ -f "package.json" ] || [ -f "yarn.lock" ]; then
|
|
log "Strategy: Node.js (Cloud Native Buildpack)"
|
|
if ! command -v pack &>/dev/null; then
|
|
log "ERROR: 'pack' CLI not found. Install it: https://buildpacks.io/docs/tools/pack/"
|
|
exit 1
|
|
fi
|
|
pack build "$IMAGE_TAG" --builder paketobuildpacks/builder-jammy-base
|
|
|
|
elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
|
|
log "Strategy: Python (Cloud Native Buildpack)"
|
|
if ! command -v pack &>/dev/null; then
|
|
log "ERROR: 'pack' CLI not found. Install it: https://buildpacks.io/docs/tools/pack/"
|
|
exit 1
|
|
fi
|
|
pack build "$IMAGE_TAG" --builder paketobuildpacks/builder-jammy-base
|
|
|
|
elif [ -f "go.mod" ]; then
|
|
log "Strategy: Go (Cloud Native Buildpack)"
|
|
if ! command -v pack &>/dev/null; then
|
|
log "ERROR: 'pack' CLI not found. Install it: https://buildpacks.io/docs/tools/pack/"
|
|
exit 1
|
|
fi
|
|
pack build "$IMAGE_TAG" --builder paketobuildpacks/builder-jammy-base
|
|
|
|
elif [ -d "static" ] || [ -d "public" ]; then
|
|
STATIC_DIR="static"
|
|
[ -d "public" ] && STATIC_DIR="public"
|
|
log "Strategy: Static files (Caddy) from ./$STATIC_DIR"
|
|
cat > Dockerfile.hiy <<EOF
|
|
FROM caddy:2-alpine
|
|
COPY $STATIC_DIR /srv
|
|
EOF
|
|
docker build --file Dockerfile.hiy --tag "$IMAGE_TAG" .
|
|
rm -f Dockerfile.hiy
|
|
|
|
else
|
|
log "ERROR: Could not detect build strategy."
|
|
log "Add a Dockerfile, package.json, requirements.txt, go.mod, or a static/ directory."
|
|
exit 1
|
|
fi
|
|
|
|
# ── 3. Ensure Docker network ───────────────────────────────────────────────────
|
|
docker network create hiy-net 2>/dev/null || true
|
|
|
|
# ── 4. Stop & remove previous container ───────────────────────────────────────
|
|
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
|
log "Stopping old container…"
|
|
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
docker rm "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
# ── 5. Start new container ────────────────────────────────────────────────────
|
|
log "Starting container $CONTAINER_NAME…"
|
|
docker run --detach \
|
|
--name "$CONTAINER_NAME" \
|
|
--network hiy-net \
|
|
--env-file "$ENV_FILE" \
|
|
--expose "$PORT" \
|
|
--label "hiy.app=${APP_ID}" \
|
|
--label "hiy.port=${PORT}" \
|
|
--restart unless-stopped \
|
|
--memory="512m" \
|
|
--cpus="0.5" \
|
|
"$IMAGE_TAG"
|
|
|
|
# ── 6. Update Caddy via its admin API ─────────────────────────────────────────
|
|
CADDY_API="${CADDY_API_URL:-http://localhost:2019}"
|
|
DOMAIN_SUFFIX="${DOMAIN_SUFFIX:-localhost}"
|
|
|
|
if curl --silent --fail "${CADDY_API}/config/" >/dev/null 2>&1; then
|
|
CONTAINER_IP=$(docker inspect "$CONTAINER_NAME" \
|
|
--format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')
|
|
UPSTREAM="${CONTAINER_IP}:${PORT}"
|
|
log "Updating Caddy: ${APP_ID}.${DOMAIN_SUFFIX} → ${UPSTREAM}"
|
|
|
|
ROUTE_JSON=$(cat <<EOF
|
|
{
|
|
"match": [{"host": ["${APP_ID}.${DOMAIN_SUFFIX}"]}],
|
|
"handle": [{"handler": "reverse_proxy", "upstreams": [{"dial": "${UPSTREAM}"}]}]
|
|
}
|
|
EOF
|
|
)
|
|
# Upsert the route for this app.
|
|
ROUTES=$(curl --silent "${CADDY_API}/config/apps/http/servers/hiy/routes" 2>/dev/null || echo "[]")
|
|
# Remove existing route for the same host, then append the new one.
|
|
UPDATED=$(echo "$ROUTES" | python3 -c "
|
|
import sys, json
|
|
routes = json.load(sys.stdin)
|
|
new_host = '${APP_ID}.${DOMAIN_SUFFIX}'
|
|
routes = [r for r in routes if new_host not in r.get('match',[{}])[0].get('host',[])]
|
|
routes.append(json.loads(sys.argv[1]))
|
|
print(json.dumps(routes))
|
|
" "$ROUTE_JSON")
|
|
|
|
curl --silent --fail "${CADDY_API}/config/apps/http/servers/hiy/routes" \
|
|
--header "Content-Type: application/json" \
|
|
--request PUT \
|
|
--data "$UPDATED" && log "Caddy updated." \
|
|
|| log "WARNING: Caddy update failed (app is running; fix routing manually)."
|
|
else
|
|
log "Caddy admin API not reachable; skipping route update."
|
|
log "Container $CONTAINER_NAME is running. Expose manually if needed."
|
|
fi
|
|
|
|
# ── 7. Prune old images ───────────────────────────────────────────────────────
|
|
log "Pruning old images (keeping last 3)…"
|
|
docker images "hiy/${APP_ID}" --format "{{.ID}}\t{{.CreatedAt}}" \
|
|
| sort --reverse --key=2 \
|
|
| tail -n +4 \
|
|
| awk '{print $1}' \
|
|
| xargs --no-run-if-empty docker rmi 2>/dev/null || true
|
|
|
|
log "=== Build complete: $APP_NAME @ $ACTUAL_SHA ==="
|