ci: deploy website from Forgejo Actions
This commit is contained in:
parent
29077923e8
commit
96fe4d26c7
3 changed files with 402 additions and 0 deletions
72
.forgejo/workflows/dimensionlab-website.yml
Normal file
72
.forgejo/workflows/dimensionlab-website.yml
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
name: Dimension Lab website
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: dimensionlab-website-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: docker
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://data.forgejo.org/actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: false
|
||||
|
||||
- name: Initialize submodules
|
||||
run: |
|
||||
git config --global url."https://git.dimensionlab.net/".insteadOf "ssh://git@git.dimensionlab.net/"
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Install Bun
|
||||
run: |
|
||||
curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.14"
|
||||
"$HOME/.bun/bin/bun" --version
|
||||
|
||||
- name: Check, test, and build
|
||||
run: |
|
||||
export BUN_INSTALL="$HOME/.bun"
|
||||
export PATH="$BUN_INSTALL/bin:$PATH"
|
||||
bun install --frozen-lockfile
|
||||
bun run check
|
||||
bun run test
|
||||
bun run build
|
||||
|
||||
deploy:
|
||||
needs: ci
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
runs-on: docker
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://data.forgejo.org/actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: false
|
||||
|
||||
- name: Initialize submodules
|
||||
run: |
|
||||
git config --global url."https://git.dimensionlab.net/".insteadOf "ssh://git@git.dimensionlab.net/"
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Deploy production website
|
||||
env:
|
||||
DEPLOY_CONTAINER_CLI: docker
|
||||
DEPLOY_EVENT_NAME: ${{ github.event_name }}
|
||||
DEPLOY_REF: ${{ github.ref }}
|
||||
DEPLOY_RESTART_STRATEGY: kill-container
|
||||
DEPLOY_SHA: ${{ github.sha }}
|
||||
run: scripts/deploy-dimensionlab-website.sh
|
||||
|
|
@ -253,6 +253,45 @@ describe("workspace boundaries", () => {
|
|||
"rm -rf build && vite build && bun build src/server/index.ts --target bun --outdir build",
|
||||
);
|
||||
});
|
||||
|
||||
test("defines Forgejo CI and main-branch deploy automation", () => {
|
||||
const workflow = readFileSync(
|
||||
join(root, ".forgejo/workflows/dimensionlab-website.yml"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(workflow).toContain("name: Dimension Lab website");
|
||||
expect(workflow).toContain("pull_request:");
|
||||
expect(workflow).toContain("push:");
|
||||
expect(workflow).toContain("branches:");
|
||||
expect(workflow).toContain("- main");
|
||||
expect(workflow).toContain("runs-on: docker");
|
||||
expect(workflow).toContain("bun install --frozen-lockfile");
|
||||
expect(workflow).toContain("bun run check");
|
||||
expect(workflow).toContain("bun run test");
|
||||
expect(workflow).toContain("bun run build");
|
||||
expect(workflow).toContain("needs: ci");
|
||||
expect(workflow).toContain("github.event_name == 'push'");
|
||||
expect(workflow).toContain("github.ref == 'refs/heads/main'");
|
||||
expect(workflow).toContain("scripts/deploy-dimensionlab-website.sh");
|
||||
});
|
||||
|
||||
test("keeps production deployment behind a guarded script", () => {
|
||||
const deployScript = readFileSync(
|
||||
join(root, "scripts/deploy-dimensionlab-website.sh"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(deployScript).toContain("refs/heads/main");
|
||||
expect(deployScript).toContain("dimensionlab-website.service");
|
||||
expect(deployScript).toContain("localhost/dimensionlab-website");
|
||||
expect(deployScript).toContain("apps/web/Containerfile");
|
||||
expect(deployScript).toContain("rollback-");
|
||||
expect(deployScript).toContain("https://dimensionlab.net");
|
||||
expect(deployScript).toContain("/api/dashboard/tiles");
|
||||
expect(deployScript).toContain("--dry-run");
|
||||
expect(deployScript).toContain("DEPLOY_RESTART_STRATEGY");
|
||||
});
|
||||
});
|
||||
|
||||
type WorkspacePackageExport =
|
||||
|
|
|
|||
291
scripts/deploy-dimensionlab-website.sh
Executable file
291
scripts/deploy-dimensionlab-website.sh
Executable file
|
|
@ -0,0 +1,291 @@
|
|||
#!/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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue