init platform/launch
This commit is contained in:
parent
87470cc453
commit
0a8730039f
1 changed files with 273 additions and 0 deletions
273
action.yml
Normal file
273
action.yml
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
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.extract.team"
|
||||
OUR_IP="51.161.128.94"
|
||||
|
||||
echo "::group::setup"
|
||||
# platform-ci-base bakes in git/jq/dig/rsync/bun — skip apk on the fast path,
|
||||
# fall back to installing only if a tool is missing (e.g. stock docker:24 image).
|
||||
command -v git >/dev/null 2>&1 && command -v jq >/dev/null 2>&1 && command -v dig >/dev/null 2>&1 \
|
||||
|| apk add --no-cache git jq bind-tools rsync >/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}"
|
||||
rm -f "/srv/platform/caddy/sites/${SLUG}-get.caddy" 2>/dev/null || true # install subdomain, if any
|
||||
[ -f "$FRAG" ] && rm -f "$FRAG"
|
||||
reload_caddy || true; echo " fragment(s) removed + caddy reloaded"
|
||||
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
|
||||
|
||||
# ---- extra domains (EXTRA_DOMAINS=alias1.com,alias2.com in .env) ----
|
||||
# Serve the SAME site on additional domains, each with its own auto-TLS cert.
|
||||
# Point each domain's A-record at the platform IP. www is NOT auto-added for
|
||||
# extras — list "www.alias.com" explicitly if you want it.
|
||||
if [ -n "${EXTRA_DOMAINS:-}" ]; then
|
||||
OLDIFS=$IFS; IFS=', '
|
||||
for ed in ${EXTRA_DOMAINS}; do
|
||||
[ -z "$ed" ] && continue
|
||||
SITE_ADDR="$SITE_ADDR, $ed"
|
||||
echo " extra domain: + $ed"
|
||||
dig +short A "$ed" 2>/dev/null | grep -q "$OUR_IP" \
|
||||
|| echo "::warning::$ed has no A→$OUR_IP yet — add the DNS record; TLS issues once it resolves"
|
||||
done
|
||||
IFS=$OLDIFS
|
||||
fi
|
||||
|
||||
# ---- domain collision pre-check: clear error instead of a raw caddy
|
||||
# "ambiguous site definition" when another deploy already serves this domain ----
|
||||
for dom in $(printf '%s' "$SITE_ADDR" | tr ',' ' '); do
|
||||
for frag in /srv/platform/caddy/sites/*.caddy; do
|
||||
[ -f "$frag" ] || continue
|
||||
base=$(basename "$frag")
|
||||
{ [ "$base" = "${SLUG}.caddy" ] || [ "$base" = "_platform.caddy" ]; } && continue
|
||||
for a in $(sed -n '1,/{/p' "$frag" | tr -d '{' | tr ',' ' '); do
|
||||
if [ "$a" = "$dom" ]; then
|
||||
echo "::error::domain '$dom' is already served by another deploy ($base). Change DOMAIN in your .env, or ask the admin to free it (remove $base)."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
case "$DT" in
|
||||
static)
|
||||
# build IN-PLACE — the job runs on platform-ci-base which has bun, so no
|
||||
# throwaway helper + docker-cp round-trip. (Trade-off: the dev's build runs
|
||||
# in the privileged job container; consistent with the Phase-6 socket-accepted
|
||||
# model. Revert to the sandboxed helper if that matters more than ~3s.)
|
||||
echo "::group::build (bun, in-place)"
|
||||
cd /tmp/repo
|
||||
if [ -f bun.lock ] || [ -f bun.lockb ]; then bun install --frozen-lockfile; else bun install; fi
|
||||
bun run build
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::publish static → /srv/sites/$DOMAIN"
|
||||
[ -d "/tmp/repo/${BUILD_DIR}" ] || { echo "::error::build output '${BUILD_DIR}' not found"; exit 1; }
|
||||
TMP="/srv/sites/.tmp-${SLUG}-${SHA}"
|
||||
rm -rf "$TMP"; mkdir -p "$TMP"
|
||||
cp -a "/tmp/repo/${BUILD_DIR}/." "$TMP/"
|
||||
rm -rf "/srv/sites/${DOMAIN}"; mv "$TMP" "/srv/sites/${DOMAIN}"
|
||||
echo " $(find "/srv/sites/${DOMAIN}" -type f | wc -l) files published"
|
||||
echo "::endgroup::"
|
||||
|
||||
write_fragment "$(printf '%s {\n\timport accesslog\n\troot * /srv/sites/%s\n\tfile_server\n\ttry_files {path} /index.html\n}\n' "$SITE_ADDR" "$DOMAIN")"
|
||||
|
||||
# ---- optional install-script subdomain: curl get.<domain> | sh ----
|
||||
# Opt-in via .env INSTALL_SH=<file in build output> (e.g. install.sh).
|
||||
# Serves that one file as text/plain at get.<domain> (self-managed
|
||||
# fragment ${SLUG}-get.caddy, separate from the main one). Unset the var
|
||||
# → the subdomain is removed on the next deploy. Dev must add the DNS
|
||||
# A-record get.<domain> → our IP themselves.
|
||||
GFRAG="/srv/platform/caddy/sites/${SLUG}-get.caddy"
|
||||
if [ -n "${INSTALL_SH:-}" ]; then
|
||||
GDOM="get.${DOMAIN}"
|
||||
[ -f "/srv/sites/${DOMAIN}/${INSTALL_SH}" ] \
|
||||
|| echo "::warning::${INSTALL_SH} not in build output — ${GDOM} will 404 until you add public/${INSTALL_SH}"
|
||||
dig +short A "$GDOM" 2>/dev/null | grep -q "$OUR_IP" \
|
||||
|| echo "::warning::no A record ${GDOM} → ${OUR_IP} yet — add it; TLS issues once it resolves"
|
||||
GNEW=$(printf '%s {\n\troot * /srv/sites/%s\n\trewrite * /%s\n\theader Content-Type text/plain\n\tfile_server\n}\n' "$GDOM" "$DOMAIN" "$INSTALL_SH")
|
||||
if [ ! -f "$GFRAG" ] || [ "$(cat "$GFRAG")" != "$GNEW" ]; then
|
||||
printf '%s' "$GNEW" > "$GFRAG"
|
||||
reload_caddy && echo " install subdomain → https://${GDOM} (serves /${INSTALL_SH})" \
|
||||
|| { echo "::error::caddy reload failed for $GFRAG"; rm -f "$GFRAG"; reload_caddy || true; }
|
||||
else
|
||||
echo " install subdomain fragment unchanged"
|
||||
fi
|
||||
elif [ -f "$GFRAG" ]; then
|
||||
rm -f "$GFRAG"; reload_caddy || true; echo " install subdomain removed (INSTALL_SH unset)"
|
||||
fi
|
||||
|
||||
# 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}"
|
||||
# BuildKit ON: enables --mount=type=cache in the repo's Dockerfile and
|
||||
# layer reuse from the previous :latest (inline cache) → fast rebuilds when
|
||||
# deps/layers are unchanged. First build of a repo just misses the cache.
|
||||
DOCKER_BUILDKIT=1 docker build \
|
||||
--cache-from "platform/${SLUG}:latest" \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
-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 sites --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\timport accesslog\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}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue