Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 25 additions & 20 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,33 +82,38 @@ jobs:
echo "$http_code"$'\n'"$body"
}

retry_url="${WEBHOOK_URL%/}/webhook/deploy"
normalize_webhook_candidates() {
node -e 'const raw = (process.env.WEBHOOK_URL ?? "").trim(); if (!raw) { process.exit(1); } let parsed; try { parsed = new URL(raw); } catch (error) { console.error(`Invalid DEPLOY_WEBHOOK_URL: ${error.message}`); process.exit(1); } const origin = `${parsed.protocol}//${parsed.host}`; const trimmedPath = parsed.pathname.replace(/\/+$/, ""); const originalPath = trimmedPath.length > 0 ? trimmedPath : "/"; const originalUrl = `${origin}${originalPath}${parsed.search}`; let canonicalPath = trimmedPath; if (!canonicalPath || canonicalPath === "/") { canonicalPath = "/webhook/deploy"; } else if (!canonicalPath.endsWith("/webhook/deploy")) { canonicalPath = `${canonicalPath}/webhook/deploy`; } const canonicalUrl = `${origin}${canonicalPath}`; const candidates = [originalUrl, canonicalUrl].filter((value, index, all) => all.indexOf(value) === index); for (const candidate of candidates) { console.log(candidate); }'
}

mapfile -t webhook_candidates < <(normalize_webhook_candidates)
if [ "${#webhook_candidates[@]}" -eq 0 ]; then
echo "::error::Unable to derive webhook retry candidates from DEPLOY_WEBHOOK_URL"
exit 1
fi

selected_code=""
selected_body=""

result=$(try_webhook_url "$WEBHOOK_URL")
selected_code=$(echo "$result" | head -1)
selected_body=$(echo "$result" | sed '1d')

if [ "$selected_code" -ge 200 ] && [ "$selected_code" -lt 300 ]; then
echo "==> Deploy triggered successfully!"
exit 0
fi
for index in "${!webhook_candidates[@]}"; do
candidate="${webhook_candidates[$index]}"
if [ "$index" -gt 0 ]; then
echo "==> Retrying against canonical webhook path: $candidate" >&2
fi

if [ "$WEBHOOK_URL" != "$retry_url" ]; then
echo "==> Retrying against canonical webhook path: $retry_url" >&2
result=$(try_webhook_url "$retry_url")
result=$(try_webhook_url "$candidate")
selected_code=$(echo "$result" | head -1)
selected_body=$(echo "$result" | sed '1d')
fi

if [ "$selected_code" -ge 200 ] && [ "$selected_code" -lt 300 ]; then
echo "==> Deploy triggered successfully!"
else
echo "::error::Deploy webhook failed with HTTP $selected_code"
echo "::error::Response body: $selected_body"
exit 1
fi
if [ "$selected_code" -ge 200 ] && [ "$selected_code" -lt 300 ]; then
echo "==> Deploy triggered successfully!"
exit 0
fi
done

echo "::error::Deploy webhook failed with HTTP $selected_code"
echo "::error::Response body: $selected_body"
exit 1

- name: Auth config smoke check
env:
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
instead of `${HOME}` mount expansion, with preflight validation for
`config-lucky.yml` and referenced credentials JSON before tunnel restart
(PR #184)
- Deploy webhook rollout now starts target services with `--no-deps` and
removes webhook from nginx startup dependencies, preventing deploy-trigger
self-termination and upstream DNS resolution regressions during rollout
(follow-up to PR #183)
- Deploy workflow webhook retries now normalize URL candidates and de-duplicate
path retries, preventing malformed attempts like
`/webhook/deploy/webhook/deploy` in failure loops
- Guild list/dashboard metrics now return nullable live values from bot/API
enrichment (no forced `0` fallback when metrics are unavailable)
- Sidebar profile identity now resolves as `nick > global_name > username`
Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ services:
depends_on:
- backend
- frontend
- webhook
ports:
- "${NGINX_PORT:-8080}:80"
networks:
Expand Down
27 changes: 24 additions & 3 deletions docs/CI_CD.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ The deploy workflow (`.github/workflows/deploy.yml`) runs on push to `main` and
manual dispatch. It:

1. Validates `DEPLOY_WEBHOOK_SECRET` and `DEPLOY_WEBHOOK_URL`.
2. Sends a signed POST request to the deploy webhook endpoint.
3. If the first URL responds with `405`, retries with `/webhook/deploy`.
2. Normalizes deploy webhook candidates from `DEPLOY_WEBHOOK_URL` (URL-parse +
canonical `/webhook/deploy`) and de-duplicates retries.
3. Sends a signed POST request with retry/backoff across normalized candidates.
4. Verifies OAuth auth-config contract health with retry and classifies failures
as `upstream unavailable (5xx)` vs `contract invalid/unready (200 + bad body)`.
5. Verifies OAuth redirect contract (`/api/auth/discord` 302 redirect shape).
Expand All @@ -66,7 +67,10 @@ Required GitHub secrets: `DEPLOY_WEBHOOK_SECRET`, `DEPLOY_WEBHOOK_URL`.
https://<your-domain>/webhook/deploy
```

If the configured URL returns HTTP 405, the workflow retries once with `/webhook/deploy`.
If the configured URL is missing the deploy path, the workflow appends
`/webhook/deploy` as a canonical retry candidate. Candidate generation is
de-duplicated, so it will never retry with malformed paths such as
`/webhook/deploy/webhook/deploy`.

### Deploy secrets (how to add)

Expand Down Expand Up @@ -121,6 +125,23 @@ If deploy fails with curl exit code `6` (Could not resolve host):
4. Re-run:
- `npm run deploy:homelab`

### Deploy webhook stability troubleshooting

If deploy trigger fails with `502` and nginx logs show `host not found in
upstream "webhook"`:

1. Confirm webhook container is running:
- `docker compose ps webhook`
2. Confirm service discovery from nginx:
- `docker exec lucky-nginx getent hosts webhook backend frontend`
3. Restart only webhook if needed:
- `docker compose up -d webhook`
4. Check recent logs:
- `docker compose logs --tail=200 --no-color webhook nginx`

To reduce recurrence, deploy rollout now restarts target services with
`--no-deps`, and nginx no longer depends on webhook service startup.

### Cloudflared config directory

Set `CLOUDFLARED_CONFIG_DIR` explicitly on the deploy host so tunnel restarts do
Expand Down
2 changes: 1 addition & 1 deletion scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ if ! docker_compose pull bot backend frontend nginx; then
fi

log "Rolling out services..."
docker_compose up -d --remove-orphans bot backend frontend nginx postgres redis
docker_compose up -d --remove-orphans --no-deps bot backend frontend nginx postgres redis

if ! verify_cloudflared_config "$CLOUDFLARED_CONFIG_DIR"; then
print_targeted_logs
Expand Down
Loading