dimensionlab-website/scripts/deploy-dimensionlab-website.sh
vince 96fe4d26c7
Some checks failed
Dimension Lab website / ci (pull_request) Failing after 35s
Dimension Lab website / deploy (pull_request) Has been skipped
ci: deploy website from Forgejo Actions
2026-06-20 16:45:40 +02:00

291 lines
7 KiB
Bash
Executable file

#!/usr/bin/env bash
set -Eeuo pipefail
APP_NAME="${APP_NAME:-dimensionlab-website}"
SERVICE_NAME="${SERVICE_NAME:-dimensionlab-website.service}"
CONTAINER_NAME="${CONTAINER_NAME:-dimensionlab-website}"
IMAGE_REPO="${IMAGE_REPO:-localhost/dimensionlab-website}"
CONTAINERFILE="${CONTAINERFILE:-apps/web/Containerfile}"
PUBLIC_URL="${PUBLIC_URL:-https://dimensionlab.net/}"
TILE_BATCH_URL="${TILE_BATCH_URL:-https://dimensionlab.net/api/dashboard/tiles}"
DEPLOY_RESTART_STRATEGY="${DEPLOY_RESTART_STRATEGY:-auto}"
DEPLOY_SMOKE_TIMEOUT_SECONDS="${DEPLOY_SMOKE_TIMEOUT_SECONDS:-120}"
DEPLOY_CONTAINER_START_TIMEOUT_SECONDS="${DEPLOY_CONTAINER_START_TIMEOUT_SECONDS:-90}"
dry_run=false
rollback_tag=""
release_tag=""
latest_tag="${IMAGE_REPO}:latest"
container_cli=""
deployment_started=false
usage() {
cat <<USAGE
Usage: $0 [--dry-run]
Build and deploy ${APP_NAME} for the production ${SERVICE_NAME} unit.
Options:
--dry-run Print the deployment actions without changing the host.
USAGE
}
log() {
printf '[deploy:%s] %s\n' "$APP_NAME" "$*"
}
fail() {
printf '[deploy:%s] ERROR: %s\n' "$APP_NAME" "$*" >&2
exit 1
}
run() {
if "$dry_run"; then
printf '[deploy:%s] DRY-RUN:' "$APP_NAME"
printf ' %q' "$@"
printf '\n'
return 0
fi
"$@"
}
for arg in "$@"; do
case "$arg" in
--dry-run)
dry_run=true
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
fail "unknown argument: $arg"
;;
esac
done
deployment_ref() {
printf '%s' "${DEPLOY_REF:-${GITHUB_REF:-${FORGEJO_REF:-}}}"
}
deployment_event() {
printf '%s' "${DEPLOY_EVENT_NAME:-${GITHUB_EVENT_NAME:-${FORGEJO_EVENT_NAME:-}}}"
}
require_main_push() {
local event
local ref
event="$(deployment_event)"
ref="$(deployment_ref)"
if [ -n "$event" ] && [ "$event" != "push" ]; then
fail "refusing to deploy for event '$event'; production deploys only run for push"
fi
if [ -n "$ref" ]; then
[ "$ref" = "refs/heads/main" ] || fail "refusing to deploy ref '$ref'; expected refs/heads/main"
return 0
fi
local branch
branch="$(git branch --show-current 2>/dev/null || true)"
[ "$branch" = "main" ] || fail "refusing to deploy branch '$branch'; expected main"
}
select_container_cli() {
if [ -n "${DEPLOY_CONTAINER_CLI:-}" ]; then
command -v "$DEPLOY_CONTAINER_CLI" >/dev/null 2>&1 || fail "container CLI not found: $DEPLOY_CONTAINER_CLI"
container_cli="$DEPLOY_CONTAINER_CLI"
return 0
fi
if command -v podman >/dev/null 2>&1; then
container_cli="podman"
return 0
fi
if command -v docker >/dev/null 2>&1; then
container_cli="docker"
return 0
fi
fail "podman or docker is required"
}
current_sha() {
if [ -n "${DEPLOY_SHA:-${GITHUB_SHA:-}}" ]; then
printf '%s' "${DEPLOY_SHA:-${GITHUB_SHA:-}}"
return 0
fi
git rev-parse HEAD
}
tag_existing_latest_for_rollback() {
rollback_tag="${IMAGE_REPO}:rollback-$(date -u +%Y%m%d%H%M%S)"
if "$container_cli" image inspect "$latest_tag" >/dev/null 2>&1; then
log "tagging current latest image as $rollback_tag"
run "$container_cli" tag "$latest_tag" "$rollback_tag"
else
log "no existing $latest_tag image found; rollback image tag will not be created"
rollback_tag=""
fi
}
initialize_submodules() {
log "initializing submodules"
run git config --global url."https://git.dimensionlab.net/".insteadOf "ssh://git@git.dimensionlab.net/"
run git submodule update --init --recursive
}
build_image() {
local sha
local short_sha
sha="$(current_sha)"
short_sha="${sha:0:12}"
release_tag="${IMAGE_REPO}:${short_sha}"
[ -f "$CONTAINERFILE" ] || fail "containerfile not found: $CONTAINERFILE"
log "building $release_tag and $latest_tag from $CONTAINERFILE"
run "$container_cli" build -f "$CONTAINERFILE" -t "$release_tag" -t "$latest_tag" .
}
restart_service() {
log "restarting $SERVICE_NAME with strategy $DEPLOY_RESTART_STRATEGY"
case "$DEPLOY_RESTART_STRATEGY" in
systemctl)
run systemctl --user restart "$SERVICE_NAME"
;;
kill-container)
run "$container_cli" stop "$CONTAINER_NAME"
;;
auto)
if command -v systemctl >/dev/null 2>&1 && systemctl --user is-active "$SERVICE_NAME" >/dev/null 2>&1; then
run systemctl --user restart "$SERVICE_NAME"
else
run "$container_cli" stop "$CONTAINER_NAME"
fi
;;
*)
fail "unknown DEPLOY_RESTART_STRATEGY: $DEPLOY_RESTART_STRATEGY"
;;
esac
}
latest_image_id() {
"$container_cli" image inspect "$latest_tag" --format '{{.Id}}' 2>/dev/null || true
}
container_image_id() {
"$container_cli" inspect "$CONTAINER_NAME" --format '{{.Image}}' 2>/dev/null || true
}
container_running() {
local running
running="$("$container_cli" inspect "$CONTAINER_NAME" --format '{{.State.Running}}' 2>/dev/null || true)"
[ "$running" = "true" ]
}
wait_for_container_restart() {
local expected_image
local deadline
if "$dry_run"; then
log "DRY-RUN: would wait for $CONTAINER_NAME to run $latest_tag"
return 0
fi
expected_image="$(latest_image_id)"
[ -n "$expected_image" ] || fail "could not resolve image id for $latest_tag"
deadline=$((SECONDS + DEPLOY_CONTAINER_START_TIMEOUT_SECONDS))
while [ "$SECONDS" -lt "$deadline" ]; do
if container_running && [ "$(container_image_id)" = "$expected_image" ]; then
log "$CONTAINER_NAME is running the new image"
return 0
fi
sleep 2
done
fail "$CONTAINER_NAME did not restart on $latest_tag within ${DEPLOY_CONTAINER_START_TIMEOUT_SECONDS}s"
}
smoke_get() {
local url="$1"
curl -fsS --max-time 10 -o /dev/null "$url"
}
smoke_tiles() {
local response
response="$(
curl -fsS --max-time 20 \
-H "content-type: application/json" \
--data '{"tiles":[{"kind":"status","stripId":"footer-status","id":"system-status"}]}' \
"$TILE_BATCH_URL"
)"
[[ "$response" == *'"state":"ready"'* ]] || fail "tile batch smoke did not return ready state"
}
wait_for_smoke() {
local deadline
if "$dry_run"; then
log "DRY-RUN: would smoke check $PUBLIC_URL and $TILE_BATCH_URL"
return 0
fi
deadline=$((SECONDS + DEPLOY_SMOKE_TIMEOUT_SECONDS))
until smoke_get "$PUBLIC_URL" && smoke_tiles; do
if [ "$SECONDS" -ge "$deadline" ]; then
fail "smoke checks failed for $PUBLIC_URL and $TILE_BATCH_URL"
fi
sleep 3
done
log "smoke checks passed"
}
rollback() {
if [ "$deployment_started" != "true" ] || [ -z "$rollback_tag" ]; then
return 0
fi
printf '[deploy:%s] rolling back to %s\n' "$APP_NAME" "$rollback_tag" >&2
"$container_cli" tag "$rollback_tag" "$latest_tag" || true
restart_service || true
}
on_error() {
local status=$?
rollback
exit "$status"
}
trap on_error ERR
require_main_push
select_container_cli
log "using container CLI: $container_cli"
log "target image: $latest_tag"
log "target service: $SERVICE_NAME"
initialize_submodules
tag_existing_latest_for_rollback
build_image
deployment_started=true
restart_service
wait_for_container_restart
wait_for_smoke
log "deployment finished"