#!/usr/bin/env bash # Forgejo origin lifecycle on DigitalOcean. Sourced by ./run (defines cmd_forge_*). # ./run forge:down stop service, snapshot, destroy droplet (~$6/mo -> ~$0.30/mo idle) # ./run forge:up recreate from newest snapshot, refresh ~/.vault/mc_forge_creds # # DO bills powered-off droplets; only destroy stops billing, so "down" = # snapshot + destroy and "up" = create-from-snapshot. The droplet gets a NEW ip # each time, so forge:up refreshes the vault creds file (the single source of # truth for the forge URL). _FORGE_TAG="forgejo" _FORGE_SIZE="s-1vcpu-1gb" _FORGE_REGION="nyc3" _FORGE_KEY_ID="57416789" _FORGE_SNAP_PREFIX="mc-forge-snap" _VAULT_PAT="$HOME/.vault/do_pat_mc" _VAULT_CREDS="$HOME/.vault/mc_forge_creds" _forge_pat() { cat "$_VAULT_PAT" 2>/dev/null; } _forge_curl() { # _forge_curl METHOD PATH [JSON-body] local method="$1" path="$2" data="${3:-}" pat pat="$(_forge_pat)" if [ -n "$data" ]; then curl -s -X "$method" -H "Authorization: Bearer $pat" -H "Content-Type: application/json" -d "$data" "https://api.digitalocean.com/v2${path}" else curl -s -X "$method" -H "Authorization: Bearer $pat" "https://api.digitalocean.com/v2${path}" fi } _forge_droplet_id() { _forge_curl GET "/droplets?tag_name=${_FORGE_TAG}" \ | python3 -c "import sys,json;d=json.load(sys.stdin).get('droplets',[]);print(d[0]['id'] if d else '')" } _forge_project_id() { _forge_curl GET "/projects" \ | python3 -c "import sys,json;print(next((p['id'] for p in json.load(sys.stdin)['projects'] if p['name']=='mc:dev'),''))" } _forge_wait_action() { local aid="$1" st for _ in $(seq 1 120); do st=$(_forge_curl GET "/actions/$aid" | python3 -c "import sys,json;print(json.load(sys.stdin)['action']['status'])" 2>/dev/null) [ "$st" = completed ] && return 0 [ "$st" = errored ] && { echo "action $aid errored" >&2; return 1; } sleep 5 done echo "action $aid timed out" >&2 return 1 } cmd_forge() { cat <<'EOF' Forgejo origin lifecycle (DigitalOcean). Needs ~/.vault/do_pat_mc. ./run forge:down stop + snapshot + destroy (~$6/mo -> ~$0.30/mo idle) ./run forge:up restore from newest snapshot, refresh vault creds EOF } cmd_forge_down() { local id ip aid snap id="$(_forge_droplet_id)" [ -n "$id" ] || { echo "no live mc-forge droplet (already down?)" >&2; return 1; } ip=$(grep -E '^FORGE_IP=' "$_VAULT_CREDS" 2>/dev/null | cut -d= -f2) echo "[1/4] stopping forgejo service on ${ip:-?}" [ -n "$ip" ] && ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -i ~/.ssh/id_mc_fleet root@"$ip" 'systemctl stop forgejo' 2>/dev/null || true echo "[2/4] powering off droplet $id" aid=$(_forge_curl POST "/droplets/$id/actions" '{"type":"power_off"}' | python3 -c "import sys,json;print(json.load(sys.stdin)['action']['id'])") _forge_wait_action "$aid" || return 1 snap="${_FORGE_SNAP_PREFIX}-$(date +%Y%m%d%H%M%S)" echo "[3/4] snapshotting -> $snap" aid=$(_forge_curl POST "/droplets/$id/actions" "{\"type\":\"snapshot\",\"name\":\"$snap\"}" | python3 -c "import sys,json;print(json.load(sys.stdin)['action']['id'])") _forge_wait_action "$aid" || return 1 echo "[4/4] destroying droplet $id" _forge_curl DELETE "/droplets/$id" >/dev/null echo "forge down — snapshot $snap kept (~\$0.30/mo). './run forge:up' to restore." } cmd_forge_up() { local snapid did ip code projid admin_user admin_pass snapid=$(_forge_curl GET "/snapshots?resource_type=droplet" \ | python3 -c "import sys,json;s=[x for x in json.load(sys.stdin)['snapshots'] if x['name'].startswith('${_FORGE_SNAP_PREFIX}')];s.sort(key=lambda x:x['created_at']);print(s[-1]['id'] if s else '')") [ -n "$snapid" ] || { echo "no ${_FORGE_SNAP_PREFIX}-* snapshot found" >&2; return 1; } echo "[1/4] creating droplet from snapshot $snapid" did=$(_forge_curl POST "/droplets" "{\"name\":\"mc-forge\",\"region\":\"${_FORGE_REGION}\",\"size\":\"${_FORGE_SIZE}\",\"image\":${snapid},\"ssh_keys\":[${_FORGE_KEY_ID}],\"tags\":[\"magic-civilization\",\"${_FORGE_TAG}\"]}" \ | python3 -c "import sys,json;d=json.load(sys.stdin).get('droplet');print(d['id'] if d else '')") [ -n "$did" ] || { echo "create failed" >&2; return 1; } echo "[2/4] waiting for active + ip (droplet $did)" for _ in $(seq 1 40); do ip=$(_forge_curl GET "/droplets/$did" | python3 -c "import sys,json;d=json.load(sys.stdin)['droplet'];ips=[n['ip_address'] for n in d['networks']['v4'] if n['type']=='public'];print(ips[0] if ips and d['status']=='active' else '')") [ -n "$ip" ] && break sleep 8 done [ -n "$ip" ] || { echo "droplet never reported an ip" >&2; return 1; } echo "[3/4] waiting for forgejo http at $ip:3000" for _ in $(seq 1 30); do code=$(curl -s -o /dev/null -m 5 -w "%{http_code}" "http://$ip:3000/" 2>/dev/null); [ "$code" = 200 ] && break; sleep 4; done projid="$(_forge_project_id)" [ -n "$projid" ] && _forge_curl POST "/projects/$projid/resources" "{\"resources\":[\"do:droplet:$did\"]}" >/dev/null echo "[4/4] refreshing $_VAULT_CREDS with new ip" admin_user=$(grep -E '^ADMIN_USER=' "$_VAULT_CREDS" 2>/dev/null | cut -d= -f2); admin_user=${admin_user:-mcadmin} admin_pass=$(grep -E '^ADMIN_PASS=' "$_VAULT_CREDS" 2>/dev/null | cut -d= -f2) umask 177 cat > "$_VAULT_CREDS" <