ci-templates/action.yml

209 lines
8.5 KiB
YAML

name: "platform-deploy"
description: "Build & deploy a dev project (static / docker / docker-db) onto the platform"
inputs:
repo:
description: "owner/repo"
required: true
ref:
description: "branch name"
required: true
sha:
description: "commit sha"
required: false
default: ""
token:
description: "forge token for checkout"
required: true
secrets_json:
description: "toJSON(secrets)"
required: false
default: "{}"
vars_json:
description: "toJSON(vars)"
required: false
default: "{}"
runs:
using: composite
steps:
- shell: sh
env:
IN_REPO: ${{ inputs.repo }}
IN_REF: ${{ inputs.ref }}
IN_SHA: ${{ inputs.sha }}
IN_TOKEN: ${{ inputs.token }}
SJSON: ${{ inputs.secrets_json }}
VJSON: ${{ inputs.vars_json }}
run: |
set -eu
FORGE_HOST="git.154.83.149.72.nip.io"
OUR_IP="154.83.149.72"
echo "::group::setup"
apk add --no-cache git jq bind-tools >/dev/null 2>&1 || true
REPO="$IN_REPO"; REF="$IN_REF"; SHA="${IN_SHA:-manual}"
SLUG=$(printf '%s' "$REPO" | tr 'A-Z/' 'a-z-')
FRAG="/srv/platform/caddy/sites/${SLUG}.caddy"
echo "repo=$REPO ref=$REF slug=$SLUG"
echo "::endgroup::"
echo "::group::checkout"
rm -rf /tmp/repo
git clone --depth 1 -b "$REF" \
"https://x-access-token:${IN_TOKEN}@${FORGE_HOST}/${REPO}.git" /tmp/repo
cd /tmp/repo
[ -f package.json ] || { echo "::error::no package.json in repo root"; exit 1; }
[ -f .env ] || { echo "::error::no .env in repo root (need DOMAIN=...)"; exit 1; }
echo "::endgroup::"
echo "::group::parse .env"
: > /tmp/envp
while IFS='=' read -r k v; do
case "$k" in ''|\#*) continue ;; esac
v=$(printf '%s' "$v" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' \
-e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'$/\1/")
printf '%s=%s\n' "$k" "$v" >> /tmp/envp
done < .env
set -a; . /tmp/envp; set +a
echo "::endgroup::"
# ---- kill-switch: repo variable ENABLED overrides .env ----
ADMIN_ENABLED=$(printf '%s' "$VJSON" | jq -r '.ENABLED // empty')
EN="${ENABLED:-true}"; [ -n "$ADMIN_ENABLED" ] && EN="$ADMIN_ENABLED"
DOMAIN="${DOMAIN:-}"
reload_caddy() {
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
}
write_fragment() { # $1 = new content
NEW="$1"
if [ -f "$FRAG" ] && [ "$(cat "$FRAG")" = "$NEW" ]; then
echo " caddy fragment unchanged; no reload"; return 0
fi
PREV=""; [ -f "$FRAG" ] && PREV=$(cat "$FRAG")
printf '%s' "$NEW" > "$FRAG"
if reload_caddy; then
echo " caddy reloaded ($FRAG)"
else
echo "::error::caddy reload failed — reverting fragment"
if [ -n "$PREV" ]; then printf '%s' "$PREV" > "$FRAG"; else rm -f "$FRAG"; fi
reload_caddy || true
exit 1
fi
}
# ---- disabled path ----
if [ "$EN" = "false" ]; then
echo "🛑 ENABLED=false → tearing down $SLUG"
docker rm -f "$SLUG" >/dev/null 2>&1 && echo " container removed" || echo " no container"
[ -n "$DOMAIN" ] && rm -rf "/srv/sites/${DOMAIN}"
if [ -f "$FRAG" ]; then rm -f "$FRAG"; reload_caddy || true; echo " fragment removed + caddy reloaded"; fi
echo "✅ $SLUG disabled (404)"
exit 0
fi
[ -n "$DOMAIN" ] || { echo "::error::DOMAIN not set in .env"; exit 1; }
# ---- deploy type ----
DT="${DEPLOY:-}"
if [ -z "$DT" ]; then
if [ -f Dockerfile ]; then DT=docker; else DT=static; fi
fi
BUILD_DIR="${BUILD_DIR:-dist}"
APP_PORT="${APP_PORT:-80}"
echo "deploy_type=$DT domain=$DOMAIN build_dir=$BUILD_DIR"
# ---- DNS sanity (warn only) ----
if ! dig +short A "$DOMAIN" 2>/dev/null | grep -q "$OUR_IP"; then
echo "::warning::$DOMAIN does not resolve to $OUR_IP yet — TLS issuance will retry once DNS is set"
fi
# ---- auto-www ----
SITE_ADDR="$DOMAIN"; ALT=""
WWW_MODE="${WWW:-auto}"
if [ "$WWW_MODE" != "false" ]; then
case "$DOMAIN" in
www.*) CAND="${DOMAIN#www.}" ;;
*.*.*) CAND="" ;;
*.*) CAND="www.${DOMAIN}" ;;
*) CAND="" ;;
esac
if [ -n "$CAND" ] && dig +short A "$CAND" 2>/dev/null | grep -q "$OUR_IP"; then
ALT="$CAND"; SITE_ADDR="$DOMAIN, $CAND"; echo " auto-www: + $CAND"
elif [ -n "$CAND" ]; then
echo " auto-www: $CAND has no A→$OUR_IP, serving $DOMAIN only"
fi
fi
case "$DT" in
static)
echo "::group::build (bun)"
BH="build-${SLUG}-${SHA}"
docker rm -f "$BH" >/dev/null 2>&1 || true
trap 'docker rm -f "$BH" >/dev/null 2>&1 || true' EXIT
docker run -d --name "$BH" -w /app oven/bun:1-alpine sleep 1200 >/dev/null
docker cp /tmp/repo/. "$BH:/app/"
if docker exec -w /app "$BH" sh -c '[ -f bun.lock ] || [ -f bun.lockb ]'; then
docker exec -w /app "$BH" sh -c 'bun install --frozen-lockfile'
else
docker exec -w /app "$BH" sh -c 'bun install'
fi
docker exec -w /app "$BH" sh -c 'bun run build'
echo "::endgroup::"
echo "::group::publish static → /srv/sites/$DOMAIN"
TMP="/srv/sites/.tmp-${SLUG}-${SHA}"
rm -rf "$TMP"; mkdir -p "$TMP"
docker cp "$BH:/app/${BUILD_DIR}/." "$TMP/" \
|| { echo "::error::build output '${BUILD_DIR}' not found"; exit 1; }
rm -rf "/srv/sites/${DOMAIN}"; mv "$TMP" "/srv/sites/${DOMAIN}"
docker rm -f "$BH" >/dev/null 2>&1 || true; trap - EXIT
echo " $(find "/srv/sites/${DOMAIN}" -type f | wc -l) files published"
echo "::endgroup::"
write_fragment "$(printf '%s {\n\troot * /srv/sites/%s\n\tfile_server\n\ttry_files {path} /index.html\n}\n' "$SITE_ADDR" "$DOMAIN")"
# Remove any prior container LAST — zero-downtime: a pre-existing
# container kept serving the domain (via its old fragment) all through
# the build above, until the file_server fragment took over just now.
docker rm -f "$SLUG" >/dev/null 2>&1 && echo " removed old container (switched to static)" || true
;;
docker|docker-db)
rm -rf "/srv/sites/${DOMAIN}" # in case it was previously a static deploy
[ -f Dockerfile ] || { echo "::error::DEPLOY=$DT needs a Dockerfile in repo root"; exit 1; }
echo "::group::docker build"
IMG="platform/${SLUG}:${SHA}"
docker build -t "$IMG" -t "platform/${SLUG}:latest" /tmp/repo
echo "::endgroup::"
echo "::group::run container"
# runtime secrets: only keys listed in .env RUNTIME_KEYS, pulled from secrets_json
RTENV="/tmp/runtime.env"; : > "$RTENV"
RK="${RUNTIME_KEYS:-}"
OLDIFS=$IFS; IFS=', '
for key in $RK; do
[ -z "$key" ] && continue
val=$(printf '%s' "$SJSON" | jq -r --arg k "$key" '.[$k] // empty')
if [ -n "$val" ]; then printf '%s=%s\n' "$key" "$val" >> "$RTENV"; echo " + runtime env: $key"; fi
done
IFS=$OLDIFS
set -- --name "$SLUG" --network web --restart unless-stopped --env-file "$RTENV" \
--label "platform.project=${REPO}" --label "platform.slug=${SLUG}" --label "platform.domain=${DOMAIN}"
if [ "$DT" = "docker-db" ]; then
printf '%s\n' "DATABASE_URL=file:/data/app.db" >> "$RTENV"
set -- "$@" -v "${SLUG}-data:/data"
echo " + sqlite volume ${SLUG}-data, DATABASE_URL=file:/data/app.db"
fi
docker rm -f "$SLUG" >/dev/null 2>&1 || true
docker run -d "$@" "$IMG" >/dev/null
rm -f "$RTENV"
echo " container $SLUG up → port $APP_PORT"
echo "::endgroup::"
write_fragment "$(printf '%s {\n\treverse_proxy %s:%s\n}\n' "$SITE_ADDR" "$SLUG" "$APP_PORT")"
;;
*)
echo "::error::unknown DEPLOY=$DT (use static | docker | docker-db)"; exit 1 ;;
esac
echo "✅ deployed https://${DOMAIN}${ALT:+ + https://$ALT}"