567 lines
18 KiB
Bash
Executable File
567 lines
18 KiB
Bash
Executable File
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
GITEA_URL="https://git.timothykim.net"
|
|
GITEA_ORG="hantim"
|
|
TEMPLATE_REPO="static-site-template"
|
|
|
|
SPINNER='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
|
spin_wait() {
|
|
local secs=$1 msg=$2 start=$3 i=0
|
|
while [ $i -lt $((secs * 4)) ]; do
|
|
local elapsed=$(( start + i / 4 ))
|
|
local sc=${SPINNER:$(( (i) % ${#SPINNER} )):1}
|
|
printf "\r %s %s (%ds)" "$sc" "$msg" "$elapsed"
|
|
sleep 0.25
|
|
i=$((i + 1))
|
|
done
|
|
}
|
|
|
|
usage() {
|
|
echo "Usage: ./tools/app.sh <command> <domain>"
|
|
echo ""
|
|
echo "Commands:"
|
|
echo " dns Set up Vultr DNS zone and A records"
|
|
echo " repo Create Gitea repo from template"
|
|
echo " files Create local compose, workflow, nginx conf"
|
|
echo " cert Issue SSL certificate via deploy SSH"
|
|
echo " garage Create Garage media bucket and grant access"
|
|
echo " build Trigger initial build workflow"
|
|
echo " verify Check if site is live"
|
|
echo " monitor Create UptimeRobot HTTPS monitor"
|
|
echo " all Run all steps (dns, repo, files, cert, garage, commit, build, verify, monitor)"
|
|
echo ""
|
|
echo "Requires: bws, jq, dig"
|
|
exit 1
|
|
}
|
|
|
|
COMMAND="${1:-}"
|
|
APP="${2:-}"
|
|
|
|
if [ -z "$COMMAND" ] || [ -z "$APP" ]; then
|
|
usage
|
|
fi
|
|
|
|
if ! [[ "$APP" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then
|
|
echo "Error: app name must be alphanumeric (hyphens, dots, underscores allowed)."
|
|
exit 1
|
|
fi
|
|
|
|
CONTAINER_NAME="${APP//./_}"
|
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
|
|
# --- Dependency checks (only what each command needs) ---
|
|
|
|
require_dig() {
|
|
if ! command -v dig &>/dev/null; then
|
|
echo "Error: dig is not installed."
|
|
echo " On macOS: brew install bind"
|
|
echo " On Linux: dnf install bind-utils"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_bws() {
|
|
if ! command -v bws &>/dev/null; then
|
|
echo "Error: Bitwarden Secrets Manager CLI (bws) is not installed."
|
|
echo " https://github.com/bitwarden/sdk-sm/releases"
|
|
exit 1
|
|
fi
|
|
if ! command -v jq &>/dev/null; then
|
|
echo "Error: jq is not installed."
|
|
exit 1
|
|
fi
|
|
|
|
BWS_TOKEN_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hantim/bws-token"
|
|
if [ -z "${BWS_ACCESS_TOKEN:-}" ]; then
|
|
if [ -f "$BWS_TOKEN_FILE" ]; then
|
|
BWS_ACCESS_TOKEN=$(cat "$BWS_TOKEN_FILE")
|
|
else
|
|
echo "==> Bitwarden Secrets Manager access token required."
|
|
echo " Generate one for your machine account at:"
|
|
echo " https://vault.bitwarden.com/#/sm/machine-accounts"
|
|
read -rp " Paste access token: " BWS_ACCESS_TOKEN
|
|
if [ -z "$BWS_ACCESS_TOKEN" ]; then
|
|
echo "ERROR: Access token cannot be empty."
|
|
exit 1
|
|
fi
|
|
mkdir -p "$(dirname "$BWS_TOKEN_FILE")"
|
|
echo "$BWS_ACCESS_TOKEN" > "$BWS_TOKEN_FILE"
|
|
chmod 600 "$BWS_TOKEN_FILE"
|
|
fi
|
|
fi
|
|
export BWS_ACCESS_TOKEN
|
|
}
|
|
|
|
bws_get() {
|
|
local name="$1"
|
|
local value
|
|
value=$(bws secret list | jq -r --arg name "$name" '.[] | select(.key == $name) | .value')
|
|
if [ -z "$value" ]; then
|
|
echo "ERROR: Secret '$name' not found in Bitwarden Secrets Manager." >&2
|
|
exit 1
|
|
fi
|
|
echo "$value"
|
|
}
|
|
|
|
resolve_server_ip() {
|
|
require_dig
|
|
SERVER_IP=$(dig +short hantim.net | head -1)
|
|
if [ -z "$SERVER_IP" ]; then
|
|
echo "Error: Could not resolve hantim.net to get server IP."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
setup_deploy_key() {
|
|
require_bws
|
|
echo "==> Fetching deploy key..."
|
|
DEPLOY_KEY=$(bws_get "hantim-deploy-ssh-private-key")
|
|
DEPLOY_KEY_FILE=$(mktemp)
|
|
echo "$DEPLOY_KEY" > "$DEPLOY_KEY_FILE"
|
|
chmod 600 "$DEPLOY_KEY_FILE"
|
|
trap "rm -f $DEPLOY_KEY_FILE" EXIT
|
|
}
|
|
|
|
# --- Commands ---
|
|
|
|
cmd_dns() {
|
|
resolve_server_ip
|
|
require_bws
|
|
|
|
echo "==> Fetching Vultr API key..."
|
|
VULTR_API_KEY=$(bws_get "hantim-vultr-api-key")
|
|
VULTR_API="https://api.vultr.com/v2"
|
|
VULTR_AUTH="Authorization: Bearer $VULTR_API_KEY"
|
|
|
|
echo "==> Checking DNS zone for $APP on Vultr..."
|
|
ZONE_CHECK=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
"$VULTR_API/domains/$APP" \
|
|
-H "$VULTR_AUTH")
|
|
|
|
if [ "$ZONE_CHECK" != "200" ]; then
|
|
echo " Creating DNS zone for $APP..."
|
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
-X POST "$VULTR_API/domains" \
|
|
-H "$VULTR_AUTH" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"domain\": \"$APP\"}")
|
|
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
|
|
echo "Error: Failed to create DNS zone (HTTP $HTTP_CODE)"
|
|
exit 1
|
|
fi
|
|
echo " Created DNS zone for $APP"
|
|
fi
|
|
|
|
echo "==> Setting A records for $APP..."
|
|
RECORDS=$(curl -s "$VULTR_API/domains/$APP/records" -H "$VULTR_AUTH")
|
|
|
|
echo "$RECORDS" | jq -r '.records[] | select(.type == "A" and .name == "") | .id' | while read -r id; do
|
|
echo " Deleting existing A record for $APP (id: $id)"
|
|
curl -s -X DELETE "$VULTR_API/domains/$APP/records/$id" -H "$VULTR_AUTH"
|
|
done
|
|
|
|
echo "$RECORDS" | jq -r '.records[] | select(.type == "A" and .name == "www") | .id' | while read -r id; do
|
|
echo " Deleting existing A record for www.$APP (id: $id)"
|
|
curl -s -X DELETE "$VULTR_API/domains/$APP/records/$id" -H "$VULTR_AUTH"
|
|
done
|
|
|
|
echo " Creating A record: $APP -> $SERVER_IP"
|
|
RESP=$(mktemp)
|
|
HTTP_CODE=$(curl -s -o "$RESP" -w "%{http_code}" \
|
|
-X POST "$VULTR_API/domains/$APP/records" \
|
|
-H "$VULTR_AUTH" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"name\": \"\", \"type\": \"A\", \"data\": \"$SERVER_IP\", \"ttl\": 3600}")
|
|
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ] && [ "$HTTP_CODE" != "204" ]; then
|
|
echo "Error: Failed to create A record for $APP (HTTP $HTTP_CODE)"
|
|
cat "$RESP"
|
|
rm -f "$RESP"
|
|
exit 1
|
|
fi
|
|
rm -f "$RESP"
|
|
|
|
echo " Creating A record: www.$APP -> $SERVER_IP"
|
|
RESP=$(mktemp)
|
|
HTTP_CODE=$(curl -s -o "$RESP" -w "%{http_code}" \
|
|
-X POST "$VULTR_API/domains/$APP/records" \
|
|
-H "$VULTR_AUTH" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"name\": \"www\", \"type\": \"A\", \"data\": \"$SERVER_IP\", \"ttl\": 3600}")
|
|
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ] && [ "$HTTP_CODE" != "204" ]; then
|
|
echo "Error: Failed to create A record for www.$APP (HTTP $HTTP_CODE)"
|
|
cat "$RESP"
|
|
rm -f "$RESP"
|
|
exit 1
|
|
fi
|
|
rm -f "$RESP"
|
|
|
|
echo "==> Waiting for DNS to propagate..."
|
|
ELAPSED=0
|
|
for i in $(seq 1 30); do
|
|
RESOLVED=$(dig +short "$APP" @ns1.vultr.com 2>/dev/null)
|
|
if [ "$RESOLVED" = "$SERVER_IP" ]; then
|
|
printf "\r\033[K DNS is live.\n"
|
|
return
|
|
fi
|
|
if [ "$i" = "30" ]; then
|
|
printf "\r\033[K"
|
|
echo "Error: DNS for $APP did not resolve to $SERVER_IP after 5 minutes."
|
|
echo " If this is a new domain, make sure nameservers are set to Vultr on directnic.com."
|
|
exit 1
|
|
fi
|
|
spin_wait 10 "Waiting for DNS..." "$ELAPSED"
|
|
ELAPSED=$((ELAPSED + 10))
|
|
done
|
|
}
|
|
|
|
cmd_repo() {
|
|
require_bws
|
|
|
|
echo "==> Fetching Gitea token..."
|
|
GITEA_TOKEN=$(bws_get "hantim-new-app-script")
|
|
|
|
REPO_CHECK=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
"$GITEA_URL/api/v1/repos/$GITEA_ORG/$APP" \
|
|
-H "Authorization: token $GITEA_TOKEN")
|
|
if [ "$REPO_CHECK" = "200" ]; then
|
|
echo " Gitea repo $GITEA_ORG/$APP already exists, skipping."
|
|
return
|
|
fi
|
|
|
|
echo "==> Creating Gitea repo $GITEA_ORG/$APP from template $TEMPLATE_REPO..."
|
|
HTTP_CODE=$(curl -s -o /tmp/new-app-response.json -w "%{http_code}" \
|
|
-X POST "$GITEA_URL/api/v1/repos/$GITEA_ORG/$TEMPLATE_REPO/generate" \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"owner\": \"$GITEA_ORG\",
|
|
\"name\": \"$APP\",
|
|
\"git_content\": true,
|
|
\"labels\": false,
|
|
\"webhooks\": false
|
|
}")
|
|
|
|
if [ "$HTTP_CODE" != "201" ]; then
|
|
echo "Error: Failed to create repo (HTTP $HTTP_CODE)"
|
|
cat /tmp/new-app-response.json
|
|
rm -f /tmp/new-app-response.json
|
|
exit 1
|
|
fi
|
|
rm -f /tmp/new-app-response.json
|
|
echo " Created: $GITEA_URL/$GITEA_ORG/$APP"
|
|
}
|
|
|
|
cmd_files() {
|
|
local all_exist=true
|
|
for f in "docker/$APP/compose.yml" ".gitea/workflows/deploy-$APP.yml" "docker/nginx/conf.d/$APP.conf"; do
|
|
if [ ! -e "$REPO_ROOT/$f" ]; then
|
|
all_exist=false
|
|
break
|
|
fi
|
|
done
|
|
if [ "$all_exist" = true ]; then
|
|
echo " All files already exist, skipping."
|
|
return
|
|
fi
|
|
|
|
if [ ! -e "$REPO_ROOT/docker/$APP/compose.yml" ]; then
|
|
echo "==> Creating docker/$APP/compose.yml..."
|
|
mkdir -p "$REPO_ROOT/docker/$APP"
|
|
cat > "$REPO_ROOT/docker/$APP/compose.yml" <<EOF
|
|
services:
|
|
app:
|
|
image: git.timothykim.net/hantim/$APP:latest
|
|
restart: unless-stopped
|
|
container_name: $CONTAINER_NAME
|
|
networks:
|
|
- shared
|
|
|
|
networks:
|
|
shared:
|
|
external: true
|
|
EOF
|
|
else
|
|
echo " docker/$APP/compose.yml already exists, skipping."
|
|
fi
|
|
|
|
if [ ! -e "$REPO_ROOT/.gitea/workflows/deploy-$APP.yml" ]; then
|
|
echo "==> Creating .gitea/workflows/deploy-$APP.yml..."
|
|
mkdir -p "$REPO_ROOT/.gitea/workflows"
|
|
cat > "$REPO_ROOT/.gitea/workflows/deploy-$APP.yml" <<'OUTER'
|
|
name: Deploy APP_PLACEHOLDER
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- 'docker/APP_PLACEHOLDER/**'
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Deploy via SSH
|
|
run: |
|
|
mkdir -p ~/.ssh
|
|
echo "${{ vars.DEPLOY_HOST_KEY }}" > ~/.ssh/known_hosts
|
|
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
|
|
chmod 600 ~/.ssh/deploy_key
|
|
ssh -o StrictHostKeyChecking=yes -i ~/.ssh/deploy_key deploy@${{ vars.DEPLOY_HOST }} deploy-APP_PLACEHOLDER
|
|
OUTER
|
|
sed -i "s/APP_PLACEHOLDER/$APP/g" "$REPO_ROOT/.gitea/workflows/deploy-$APP.yml"
|
|
else
|
|
echo " .gitea/workflows/deploy-$APP.yml already exists, skipping."
|
|
fi
|
|
|
|
if [ ! -e "$REPO_ROOT/docker/nginx/conf.d/$APP.conf" ]; then
|
|
echo "==> Creating docker/nginx/conf.d/$APP.conf..."
|
|
cat > "$REPO_ROOT/docker/nginx/conf.d/$APP.conf" <<NGINX
|
|
# Redirect HTTP to HTTPS, bare domain to www
|
|
server {
|
|
listen 80;
|
|
listen [::]:80;
|
|
server_name $APP www.$APP;
|
|
|
|
location /.well-known/acme-challenge/ {
|
|
root /var/www/certbot;
|
|
}
|
|
|
|
location / {
|
|
return 301 https://www.$APP\$request_uri;
|
|
}
|
|
}
|
|
|
|
# Redirect bare HTTPS domain to www
|
|
server {
|
|
listen 443 ssl;
|
|
listen [::]:443 ssl;
|
|
http2 on;
|
|
server_name $APP;
|
|
|
|
ssl_certificate /etc/letsencrypt/live/$APP/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/$APP/privkey.pem;
|
|
|
|
return 301 https://www.$APP\$request_uri;
|
|
}
|
|
|
|
# Main site
|
|
server {
|
|
listen 443 ssl;
|
|
listen [::]:443 ssl;
|
|
http2 on;
|
|
server_name www.$APP;
|
|
|
|
ssl_certificate /etc/letsencrypt/live/$APP/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/$APP/privkey.pem;
|
|
|
|
include /etc/nginx/conf.d/security-headers.inc;
|
|
include /etc/nginx/conf.d/goatcounter.inc;
|
|
|
|
location /media/ {
|
|
proxy_pass http://garage:3902/;
|
|
proxy_set_header Host $APP.web.garage;
|
|
}
|
|
|
|
location / {
|
|
proxy_pass http://$CONTAINER_NAME:80;
|
|
proxy_set_header Host \$host;
|
|
proxy_set_header X-Real-IP \$remote_addr;
|
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
}
|
|
}
|
|
NGINX
|
|
else
|
|
echo " docker/nginx/conf.d/$APP.conf already exists, skipping."
|
|
fi
|
|
}
|
|
|
|
cmd_cert() {
|
|
resolve_server_ip
|
|
setup_deploy_key
|
|
|
|
echo "==> Issuing SSL certificate for $APP via deploy user..."
|
|
ssh -o StrictHostKeyChecking=accept-new -i "$DEPLOY_KEY_FILE" deploy@"$SERVER_IP" "cert-$APP"
|
|
echo " Certificate issued."
|
|
}
|
|
|
|
cmd_garage() {
|
|
require_bws
|
|
|
|
echo "==> Fetching Garage secrets..."
|
|
GARAGE_ADMIN_TOKEN=$(bws_get "hantim-garage-admin-token")
|
|
GARAGE_MEDIA_KEY_ID=$(bws_get "hantim-garage-media-key-id")
|
|
|
|
GARAGE_API="https://garage.hantim.net"
|
|
GARAGE_AUTH="Authorization: Bearer $GARAGE_ADMIN_TOKEN"
|
|
|
|
echo "==> Creating Garage media bucket for $APP..."
|
|
BUCKET_RESP=$(curl -s -X POST "$GARAGE_API/v2/CreateBucket" \
|
|
-H "$GARAGE_AUTH" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"globalAlias\": \"$APP\"}")
|
|
|
|
BUCKET_ID=$(echo "$BUCKET_RESP" | jq -r '.id')
|
|
if [ -z "$BUCKET_ID" ] || [ "$BUCKET_ID" = "null" ]; then
|
|
BUCKET_ID=$(curl -s "$GARAGE_API/v2/GetBucketInfo?globalAlias=$APP" \
|
|
-H "$GARAGE_AUTH" | jq -r '.id')
|
|
if [ -z "$BUCKET_ID" ] || [ "$BUCKET_ID" = "null" ]; then
|
|
echo "Error: Failed to create or find bucket for $APP"
|
|
echo "$BUCKET_RESP"
|
|
exit 1
|
|
fi
|
|
echo " Bucket already exists, continuing..."
|
|
fi
|
|
|
|
echo " Allowing media-key access..."
|
|
curl -sf -X POST "$GARAGE_API/v2/AllowBucketKey" \
|
|
-H "$GARAGE_AUTH" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"bucketId\": \"$BUCKET_ID\", \"accessKeyId\": \"$GARAGE_MEDIA_KEY_ID\", \"permissions\": {\"read\": true, \"write\": true, \"owner\": false}}" > /dev/null
|
|
|
|
echo " Enabling website access..."
|
|
curl -sf -X POST "$GARAGE_API/v2/UpdateBucket?id=$BUCKET_ID" \
|
|
-H "$GARAGE_AUTH" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"websiteAccess": {"enabled": true, "indexDocument": "index.html"}}' > /dev/null
|
|
|
|
echo " Bucket '$APP' ready."
|
|
echo ""
|
|
echo " Upload media:"
|
|
echo " aws --endpoint-url https://s3.hantim.net s3 cp <file> s3://$APP/<path>"
|
|
echo " aws --endpoint-url https://s3.hantim.net s3 sync static/media/ s3://$APP/"
|
|
echo " Served at: https://www.$APP/media/"
|
|
}
|
|
|
|
cmd_build() {
|
|
require_bws
|
|
|
|
echo "==> Fetching Gitea token..."
|
|
GITEA_TOKEN=$(bws_get "hantim-new-app-script")
|
|
|
|
echo "==> Triggering initial build for $APP..."
|
|
ELAPSED=0
|
|
TRIGGER_CODE=""
|
|
for i in $(seq 1 60); do
|
|
TRIGGER_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
-X POST "$GITEA_URL/api/v1/repos/$GITEA_ORG/$APP/actions/workflows/build.yml/dispatches" \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"ref": "main"}')
|
|
[ "$TRIGGER_CODE" != "404" ] && printf "\r\033[K" && break
|
|
spin_wait 2 "Waiting for repo to be ready..." "$ELAPSED"
|
|
ELAPSED=$((ELAPSED + 2))
|
|
done
|
|
|
|
if [ "$TRIGGER_CODE" = "204" ]; then
|
|
echo " Build triggered."
|
|
else
|
|
echo " Warning: Could not trigger build (HTTP $TRIGGER_CODE). Check manually:"
|
|
echo " $GITEA_URL/$GITEA_ORG/$APP/actions"
|
|
fi
|
|
}
|
|
|
|
cmd_verify() {
|
|
echo "==> Checking if https://www.$APP is live..."
|
|
ELAPSED=0
|
|
for i in $(seq 1 18); do
|
|
if curl -sf "https://www.$APP" > /dev/null 2>&1; then
|
|
printf "\r\033[K Site is live at https://www.$APP\n"
|
|
return
|
|
fi
|
|
spin_wait 10 "Waiting for site..." "$ELAPSED"
|
|
ELAPSED=$((ELAPSED + 10))
|
|
done
|
|
printf "\r\033[K"
|
|
|
|
echo "WARNING: https://www.$APP is not responding after 3 minutes."
|
|
echo " Check that:"
|
|
echo " - DNS for $APP and www.$APP points to the server"
|
|
echo " - The build completed: $GITEA_URL/$GITEA_ORG/$APP/actions"
|
|
echo " - The container is running on the server: docker ps"
|
|
}
|
|
|
|
cmd_monitor() {
|
|
require_bws
|
|
|
|
echo "==> Fetching UptimeRobot API key..."
|
|
UPTIMEROBOT_API_KEY=$(bws_get "hantim-uptimerobot-api-key")
|
|
UPTIMEROBOT_API="https://api.uptimerobot.com/v3"
|
|
UPTIMEROBOT_AUTH="Authorization: Bearer $UPTIMEROBOT_API_KEY"
|
|
|
|
MONITOR_URL="https://www.$APP"
|
|
|
|
echo "==> Checking for existing UptimeRobot monitor for $MONITOR_URL..."
|
|
EXISTING=$(curl -s -G "$UPTIMEROBOT_API/monitors" \
|
|
-H "$UPTIMEROBOT_AUTH" \
|
|
--data-urlencode "search=$APP")
|
|
|
|
MATCH=$(echo "$EXISTING" | jq -r --arg url "$MONITOR_URL" \
|
|
'.data[]? | select(.url == $url) | .id')
|
|
if [ -n "$MATCH" ]; then
|
|
echo " Monitor already exists (id: $MATCH), skipping."
|
|
return
|
|
fi
|
|
|
|
echo "==> Fetching alert contacts..."
|
|
ALERT_CONTACTS=$(curl -s "$UPTIMEROBOT_API/user/alert-contacts" \
|
|
-H "$UPTIMEROBOT_AUTH" | jq '[.[] | {alertContactId: .id, threshold: 5, recurrence: 30}]')
|
|
|
|
echo "==> Creating UptimeRobot HTTPS monitor for $MONITOR_URL..."
|
|
RESP=$(curl -s -X POST "$UPTIMEROBOT_API/monitors" \
|
|
-H "$UPTIMEROBOT_AUTH" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$(jq -n --arg name "$APP" --arg url "$MONITOR_URL" --argjson contacts "$ALERT_CONTACTS" \
|
|
'{friendlyName: $name, url: $url, type: 1, interval: 300, timeout: 30, assignedAlertContacts: $contacts, tagNames: ["hantim"]}')")
|
|
|
|
MONITOR_ID=$(echo "$RESP" | jq -r '.id // empty')
|
|
if [ -n "$MONITOR_ID" ]; then
|
|
echo " Monitor created (id: $MONITOR_ID)."
|
|
else
|
|
ERROR_MSG=$(echo "$RESP" | jq -r 'if .message then (.message | if type == "array" then join("; ") else . end) else .error // "unknown error" end')
|
|
echo " Warning: Failed to create monitor: $ERROR_MSG"
|
|
fi
|
|
}
|
|
|
|
cmd_all() {
|
|
cmd_dns
|
|
cmd_repo
|
|
cmd_cert
|
|
cmd_files
|
|
|
|
echo "==> Committing and pushing hantim-server..."
|
|
cd "$REPO_ROOT"
|
|
git add "docker/$APP/compose.yml" ".gitea/workflows/deploy-$APP.yml" "docker/nginx/conf.d/$APP.conf"
|
|
if git diff --cached --quiet; then
|
|
echo " No changes to commit, skipping."
|
|
else
|
|
git commit -m "add $APP"
|
|
git push
|
|
fi
|
|
|
|
cmd_garage
|
|
cmd_build
|
|
cmd_verify
|
|
cmd_monitor
|
|
}
|
|
|
|
# --- Dispatch ---
|
|
|
|
case "$COMMAND" in
|
|
dns) cmd_dns ;;
|
|
repo) cmd_repo ;;
|
|
files) cmd_files ;;
|
|
cert) cmd_cert ;;
|
|
garage) cmd_garage ;;
|
|
build) cmd_build ;;
|
|
verify) cmd_verify ;;
|
|
monitor) cmd_monitor ;;
|
|
all) cmd_all ;;
|
|
*)
|
|
echo "Unknown command: $COMMAND"
|
|
echo ""
|
|
usage
|
|
;;
|
|
esac
|