diff --git a/.gitea/workflows/auto-deploy.yml b/.gitea/workflows/auto-deploy.yml new file mode 100644 index 0000000..4da38cf --- /dev/null +++ b/.gitea/workflows/auto-deploy.yml @@ -0,0 +1,173 @@ +name: Auto-Deploy Changed Stacks + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Detect changed stacks and trigger Komodo webhooks + run: | + set -euo pipefail + + # --------------------------------------------------------------- + # Webhook map: stack-name → Komodo webhook URL + # Komodo webhook URLs are self-authenticating (token is embedded + # in the URL path). Fill in URLs from: + # Komodo UI → Stack → Webhooks tab → copy the full URL + # --------------------------------------------------------------- + declare -A WEBHOOK_MAP + + # heimdall stacks + WEBHOOK_MAP["5etools"]="TODO" + WEBHOOK_MAP["authentik"]="TODO" + WEBHOOK_MAP["bentopdf"]="TODO" + WEBHOOK_MAP["byparr"]="TODO" + WEBHOOK_MAP["convertx"]="TODO" + WEBHOOK_MAP["core"]="TODO" + WEBHOOK_MAP["docker_registry"]="TODO" + WEBHOOK_MAP["gitea"]="TODO" + WEBHOOK_MAP["guardian"]="TODO" + WEBHOOK_MAP["homelab-registry-mcp"]="TODO" + WEBHOOK_MAP["karakeep"]="TODO" + WEBHOOK_MAP["kitchenowl"]="TODO" + WEBHOOK_MAP["ntfy"]="TODO" + WEBHOOK_MAP["overseerr"]="TODO" + WEBHOOK_MAP["profilarr"]="TODO" + WEBHOOK_MAP["prowlarr"]="TODO" + WEBHOOK_MAP["radarr"]="TODO" + WEBHOOK_MAP["sabnzbd"]="TODO" + WEBHOOK_MAP["snapotter"]="TODO" + WEBHOOK_MAP["sonarr"]="TODO" + WEBHOOK_MAP["sparkyfitness"]="TODO" + WEBHOOK_MAP["tautulli"]="TODO" + WEBHOOK_MAP["tracearr"]="TODO" + WEBHOOK_MAP["trek"]="TODO" + WEBHOOK_MAP["vaultwarden"]="TODO" + WEBHOOK_MAP["vscode"]="TODO" + WEBHOOK_MAP["weatherchannel"]="TODO" + WEBHOOK_MAP["wizarr"]="TODO" + WEBHOOK_MAP["zipline"]="TODO" + + # waldorf stacks + WEBHOOK_MAP["immich"]="TODO" + WEBHOOK_MAP["openwebui"]="TODO" + WEBHOOK_MAP["pinchflat"]="TODO" + WEBHOOK_MAP["plex"]="TODO" + WEBHOOK_MAP["tunarr"]="TODO" + + # --------------------------------------------------------------- + # Detect changed paths vs the previous commit + # --------------------------------------------------------------- + CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD) + + echo "=== Changed files ===" + if [[ -z "$CHANGED_FILES" ]]; then + echo " (none)" + else + echo "$CHANGED_FILES" + fi + echo "" + + # --------------------------------------------------------------- + # Parse changed paths → extract {node} and {stack} + # Expected pattern: nodes/{node}/{stack}/... + # --------------------------------------------------------------- + declare -A SEEN_STACKS + declare -a STACKS_TO_DEPLOY + declare -a UNMATCHED_PATHS + + while IFS= read -r filepath; do + [[ -z "$filepath" ]] && continue + + IFS='/' read -ra PARTS <<< "$filepath" + + # Must start with "nodes" and have at least 4 components + # (nodes / {node} / {stack} / file) + [[ "${PARTS[0]}" != "nodes" ]] && continue + [[ "${#PARTS[@]}" -lt 4 ]] && continue + + node="${PARTS[1]}" + stack="${PARTS[2]}" + + [[ -z "$stack" ]] && continue + + # Deduplicate: skip if this stack was already queued + [[ -n "${SEEN_STACKS[$stack]+_}" ]] && continue + SEEN_STACKS["$stack"]=1 + + if [[ -n "${WEBHOOK_MAP[$stack]+_}" ]]; then + STACKS_TO_DEPLOY+=("$stack") + echo " Queued for deploy: $stack (node: $node)" + else + UNMATCHED_PATHS+=("$filepath (stack: $stack)") + fi + done <<< "$CHANGED_FILES" + + echo "" + + # --------------------------------------------------------------- + # Warn about paths that have no webhook entry — not silent + # --------------------------------------------------------------- + if [[ "${#UNMATCHED_PATHS[@]}" -gt 0 ]]; then + echo "=== WARNING: paths with no matching webhook entry ===" + for p in "${UNMATCHED_PATHS[@]}"; do + echo " - $p" + done + echo " Add these stack names to WEBHOOK_MAP in this workflow." + echo "" + fi + + # --------------------------------------------------------------- + # Nothing under nodes/ changed + # --------------------------------------------------------------- + if [[ "${#STACKS_TO_DEPLOY[@]}" -eq 0 ]]; then + echo "No stack changes detected — nothing to deploy." + exit 0 + fi + + # --------------------------------------------------------------- + # POST to each Komodo webhook + # --------------------------------------------------------------- + echo "=== Triggering Komodo webhooks ===" + FAILED=0 + + for stack in "${STACKS_TO_DEPLOY[@]}"; do + url="${WEBHOOK_MAP[$stack]}" + + if [[ "$url" == "TODO" ]]; then + echo " SKIP: $stack — webhook URL not configured yet (fill in WEBHOOK_MAP)" + continue + fi + + echo -n " → $stack ... " + HTTP_STATUS=$(curl --silent --show-error --output /dev/null \ + --write-out "%{http_code}" \ + --max-time 30 \ + --request POST "$url" \ + --header "Content-Type: application/json" \ + --data '{"trigger":"gitea-push"}') + + if [[ "$HTTP_STATUS" =~ ^2 ]]; then + echo "HTTP $HTTP_STATUS OK" + else + echo "HTTP $HTTP_STATUS FAILED" + FAILED=1 + fi + done + + echo "" + if [[ "$FAILED" -ne 0 ]]; then + echo "ERROR: One or more Komodo webhook POSTs returned a non-2xx status." + exit 1 + fi + + echo "All webhooks triggered successfully." diff --git a/README.md b/README.md index d7bcbf5..f988ccb 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,22 @@ --- +## 🚢 Auto-Deploy + +Merging a PR to `main` automatically deploys every stack whose folder changed. + +**How it works:** +1. The Gitea Actions workflow (`.gitea/workflows/auto-deploy.yml`) detects which `nodes/{node}/{stack}/` paths changed +2. It POSTs to the corresponding Komodo webhook for each affected stack — no manual redeploy needed +3. Stacks with no matching webhook entry are logged as warnings (not silent failures); any non-2xx response fails the job loudly + +**Before it goes live you need:** +- A Gitea Actions runner installed on Heimdall (see [SOP-003](documentation/SOPs/SOP-003-Gitea-Actions-Runner-Setup.md)) +- All `TODO` webhook URLs in `WEBHOOK_MAP` replaced with the actual Komodo webhook URLs + (Komodo UI → Stack → Webhooks tab → copy the full self-authenticating URL) + +--- + ## 🤝 Contributing This is a personal homelab, but documentation improvements and issue reports are welcome! diff --git a/documentation/SOPs/SOP-003-Gitea-Actions-Runner-Setup.md b/documentation/SOPs/SOP-003-Gitea-Actions-Runner-Setup.md new file mode 100644 index 0000000..1392c33 --- /dev/null +++ b/documentation/SOPs/SOP-003-Gitea-Actions-Runner-Setup.md @@ -0,0 +1,194 @@ +# SOP-003: Gitea Actions Runner Setup (Heimdall) + +**Status:** Active +**Created:** June 2, 2026 +**Last Updated:** June 2, 2026 +**Owner:** Nathan Castaldi +**Applies To:** Gitea Actions auto-deploy workflow + +--- + +## Purpose + +Install and register a Gitea Actions runner on Heimdall so that the +`.gitea/workflows/auto-deploy.yml` workflow can execute when PRs are merged +to `main`. The runner triggers Komodo webhooks for any stack whose folder +changed in the push. + +--- + +## Prerequisites + +- [ ] SSH access to Heimdall (`10.0.0.151`) +- [ ] Docker and Docker Compose running on Heimdall +- [ ] Gitea instance accessible at `https://git.castaldifamily.com` +- [ ] Admin or repo-owner access to the `nathan/homelab` Gitea repository +- [ ] Komodo webhook URLs retrieved from Komodo UI (see [Step 4](#step-4--add-required-secrets-in-gitea)) + +--- + +## Step 1 — Retrieve a Runner Registration Token + +1. In Gitea, go to **Repository** → `nathan/homelab` → **Settings** → **Actions** → **Runners** +2. Click **Create new Runner** +3. Copy the **registration token** — you will need it in Step 3 + +> **Note:** Tokens are single-use. If you lose it, generate a new one. + +--- + +## Step 2 — Create the Runner Compose File on Heimdall + +SSH into Heimdall and create the runner stack directory: + +```bash +mkdir -p /opt/gitea-runner +``` + +Create `/opt/gitea-runner/compose.yaml`: + +```yaml +name: gitea-runner + +services: + runner: + image: gitea/act_runner:latest + container_name: gitea-runner + restart: unless-stopped + environment: + GITEA_INSTANCE_URL: "https://git.castaldifamily.com" + GITEA_RUNNER_REGISTRATION_TOKEN: "${GITEA_RUNNER_REGISTRATION_TOKEN}" + GITEA_RUNNER_NAME: "heimdall-runner" + GITEA_RUNNER_LABELS: "ubuntu-latest:docker://node:20-bullseye-slim" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - runner-data:/data + +volumes: + runner-data: +``` + +Create `/opt/gitea-runner/.env`: + +```env +GITEA_RUNNER_REGISTRATION_TOKEN= +``` + +> **Security:** The `.env` file contains a credential. Do not commit it to Git. +> The token is consumed on first registration; after that the runner stores its +> own credentials in the `runner-data` volume. + +--- + +## Step 3 — Start the Runner + +```bash +cd /opt/gitea-runner +docker compose up -d +``` + +Watch the startup logs to confirm registration: + +```bash +docker compose logs -f runner +``` + +Expected output (within ~10 seconds): + +``` +INFO Registering runner ... runner=heimdall-runner +INFO Runner registered successfully. +INFO Starting runner ... name=heimdall-runner +``` + +Verify the runner appears in Gitea: + +- Go to **Repository** → **Settings** → **Actions** → **Runners** +- `heimdall-runner` should show **Online** + +--- + +## Step 4 — Add Required Secrets in Gitea + +The auto-deploy workflow does not use a shared secret (Komodo webhook URLs +are self-authenticating). No repository secrets are required for the +workflow itself. + +If you later need to store additional credentials (e.g., a Komodo API key +for a future workflow), add them at: + +> **Repository** → **Settings** → **Secrets and Variables** → **Actions** → **New secret** + +--- + +## Step 5 — Fill In Webhook URLs + +Open `.gitea/workflows/auto-deploy.yml` in the repo and replace each `TODO` +value in `WEBHOOK_MAP` with the actual URL from Komodo: + +1. Log into Komodo UI (`https://komodo.castaldifamily.com`) +2. Navigate to **Stacks** → select the stack +3. Open the **Webhooks** tab +4. Copy the full webhook URL (token is embedded in the path) +5. Paste it as the map value for that stack name + +Commit the updated workflow file to `main` (or merge a PR that includes it). + +> **Never paste webhook URLs into public forks or issue comments.** They are +> equivalent to credentials. + +--- + +## Step 6 — Verify End-to-End + +1. Make a trivial change to any stack folder (e.g., add a blank line to + `nodes/heimdall/sonarr/compose.yaml`) +2. Commit and push directly to `main` (or merge a PR) +3. In Gitea, go to **Repository** → **Actions** — the `Auto-Deploy Changed Stacks` run should appear +4. Click the run → expand the **Detect changed stacks and trigger Komodo webhooks** step +5. Confirm output resembles: + + ``` + === Changed files === + nodes/heimdall/sonarr/compose.yaml + + Queued for deploy: sonarr (node: heimdall) + + === Triggering Komodo webhooks === + → sonarr ... HTTP 200 OK + + All webhooks triggered successfully. + ``` + +6. In Komodo UI, verify the `sonarr` stack shows a recent deployment event + +--- + +## Troubleshooting + +| Symptom | Check | +|---|---| +| Runner shows **Offline** in Gitea | `docker compose logs runner` on Heimdall; check `GITEA_INSTANCE_URL` reachability | +| Job never picks up | Runner label must match `runs-on` in the workflow (`ubuntu-latest`) | +| `HTTP 404` on webhook POST | URL in `WEBHOOK_MAP` is wrong; re-copy from Komodo UI | +| `HTTP 401` / `403` on webhook POST | URL token is stale; regenerate webhook in Komodo UI | +| Job shows `HEAD~1` diff error | Ensure `fetch-depth: 2` is set in the checkout step | +| Stack not in `WEBHOOK_MAP` | Logged as a warning in job output; add the stack name to the map | + +--- + +## Rollback + +To stop the runner without removing its registration: + +```bash +cd /opt/gitea-runner && docker compose stop runner +``` + +To fully deregister and remove: + +```bash +cd /opt/gitea-runner && docker compose down -v +``` + +Then delete the runner from **Gitea → Repository → Settings → Actions → Runners**.