diff --git a/action.yml b/action.yml index eed561b..e77ee55 100644 --- a/action.yml +++ b/action.yml @@ -1,39 +1,206 @@ -name: "platform-deploy-probe" -description: "composite-action capability probe" +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: all secrets as json + description: "toJSON(secrets)" required: false default: "{}" vars_json: - description: all vars as json + description: "toJSON(vars)" required: false default: "{}" - token: - description: forge token - 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 }} - TOK: ${{ inputs.token }} run: | - apk add --no-cache jq >/dev/null 2>&1 || true - R=/srv/sites/.probe-result.txt - { - echo "COMPOSITE_RAN=yes" - echo "secret_keys=$(printf '%s' "$SJSON" | jq -r 'keys|join(",")' 2>&1)" - echo "has_DATABASE_URL=$(printf '%s' "$SJSON" | jq -r 'has("DATABASE_URL")' 2>&1)" - echo "DATABASE_URL_len=$(printf '%s' "$SJSON" | jq -r '.DATABASE_URL // "" | length' 2>&1)" - echo "vars_json=$VJSON" - echo "token_present=$([ -n "$TOK" ] && echo yes || echo no)" - echo "docker=$(docker version --format '{{.Server.Version}}' 2>&1)" - echo "sites_writable=$(touch /srv/sites/.w 2>/dev/null && echo yes && rm -f /srv/sites/.w || echo no)" - echo "caddy_writable=$(touch /srv/platform/caddy/sites/.w.tmp 2>/dev/null && echo yes && rm -f /srv/platform/caddy/sites/.w.tmp || echo no)" - echo "caddy_exec=$(docker exec caddy caddy version 2>&1 | head -1)" - } > "$R" 2>&1 - echo "composite wrote $R"; cat "$R" + 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) + docker rm -f "$SLUG" >/dev/null 2>&1 || true # in case it was previously a container deploy + 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")" + ;; + + 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}"