Files
Juan DenisandClaude Opus 5 5bf5e50f62 fix(update): pick the backup engine by DATABASE_URL, not by which files exist
backup_current tested `[ -f serverkit.db ]` FIRST and only fell through to the
PostgreSQL branch when no file was there. An install migrated SQLite ->
PostgreSQL keeps its old serverkit.db, and both deploy paths copy it forward on
file-existence alone (deploy_source, deploy_release), so the stale file is
carried into every future slot and never ages out. Such a box backed up the
abandoned file, skipped pg_dump entirely, and left PRE_UPGRADE_DUMP unset --
which migrate_database reads as "${PRE_UPGRADE_DUMP:-}", so its auto-restore
silently did nothing. PostgreSQL is the one engine that migrates IN PLACE and
cannot roll back by switching slots, so that dump is its only way home: the
exact safety net a61eab1/b48256f added was disabled for it.

migrate_database has always keyed off DATABASE_URL (its sqlite check). Test the
declared engine first so the two functions agree.

Deliberately NOT `grep DATABASE_URL=sqlite && [ -f db ]` as the reviewer
suggested: backend/config.py defaults DATABASE_URL when absent, so any install
whose .env lacks the line would fall to the else branch and update with NO
backup at all -- trading a mismatched backup for none. An explicit `postgres`
URL is a positive signal; the absence of a `sqlite` one is not.

Also:
  * name the engine when it is one we cannot dump. "No SQLite database ..."
    read like a benign nothing-to-do on a MySQL install that in fact had no
    safety net. Prints the scheme only -- a DATABASE_URL carries a password and
    this text lands in the update log.
  * drop the stale "Python 3.11/3.12 is guaranteed by pre-flight" comment,
    contradicted by locate_sqlite_python two lines below it (664a5e6).
  * discord-notify: skip cleanly when DISCORD_WEBHOOK_URL is absent. Secrets
    are not exposed to `pull_request` runs from a fork, so a merged fork PR
    failed the job on an empty curl URL. Not switching to pull_request_target
    for this -- handing a write-scoped token and the webhook to an
    outsider-triggered run is a poor trade for a chat notification.

Tests: a PostgreSQL install carrying a leftover serverkit.db must still dump
and set PRE_UPGRADE_DUMP (fails on the parent commit, rc=1); an undumpable
engine must be named without leaking the password. Neither needs python3, so
both run on all 7 images. Workflow verified end-to-end against a synthetic
event payload: fork PR exits 0 without calling curl, normal PR still builds the
same embed.

Verified on the full CI matrix (ubuntu 22.04/24.04, debian:12, rockylinux:9,
almalinux:9, fedora:40, opensuse/leap:15.5), all six suites: 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 00:06:12 -04:00

97 lines
3.9 KiB
YAML

name: Discord PR Notify
on:
pull_request:
types: [closed] # merged only (filtered in the job `if:`)
jobs:
notify:
# Only when a PR is actually merged; closed-unmerged skips silently.
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Send merged PR to Discord
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
run: |
set -euo pipefail
# Repository secrets are NOT exposed to `pull_request` runs triggered
# from a fork, so a merged fork PR would reach curl with an empty URL
# and fail the job for no useful reason. Skip cleanly instead. The
# fix is deliberately NOT `pull_request_target`: that would hand a
# write-scoped token and this webhook to an outsider-triggered run,
# which is a poor trade for a chat notification.
if [ -z "${DISCORD_WEBHOOK_URL:-}" ]; then
echo "DISCORD_WEBHOOK_URL unavailable (fork PR?) — skipping notification."
exit 0
fi
# Read everything from the event payload via jq: PR text is
# attacker-controlled input, so it must never be interpolated
# into the script (backticks/$() would execute).
TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
URL=$(jq -r '.pull_request.html_url' "$GITHUB_EVENT_PATH")
AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
BASE=$(jq -r '.pull_request.base.ref' "$GITHUB_EVENT_PATH")
BODY=$(jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH")
NUMBER=$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH")
MERGED_AT=$(jq -r '.pull_request.merged_at' "$GITHUB_EVENT_PATH")
echo "title_len=${#TITLE} body_len=${#BODY}"
# NOTE: use [ \t], never [[:space:]] — the runner's awk is mawk,
# which silently mis-parses POSIX character classes.
# Extract bullets from the "### Highlights" section (top 5).
# Stops at the next heading or the <details> block.
HIGHLIGHTS=$(printf '%s\n' "$BODY" | awk '
/^#+[ \t]*Highlights/ { f=1; next }
f && (/^#/ || /^</) { exit }
f && /^- / { print }
' | head -5)
if [ -n "$HIGHLIGHTS" ]; then
DESC="$HIGHLIGHTS"
else
# Lazy PR with no Highlights section: use the first prose
# paragraph of the body, truncated to 300 chars.
FIRST_PARA=$(printf '%s\n' "$BODY" | awk '
/^[ \t]*$/ { if (started) exit; next }
/^(#|<|```|\||- )/ { if (started) exit; next }
{ started=1; print }
' | tr '\n' ' ' | sed 's/ */ /g' | cut -c1-300)
if [ -n "$FIRST_PARA" ]; then
DESC="$FIRST_PARA"
else
DESC="_Merged without a description._"
fi
fi
# Discord embed descriptions cap at 4096 chars; stay under.
DESC=$(printf '%s' "$DESC" | cut -c1-3900)
jq -n \
--arg title "$TITLE" \
--arg url "$URL" \
--arg desc "$DESC" \
--arg author "$AUTHOR" \
--arg author_url "https://github.com/$AUTHOR" \
--arg avatar "https://github.com/$AUTHOR.png" \
--arg footer "PR #$NUMBER merged into $BASE" \
--arg ts "$MERGED_AT" \
'{
username: "ServerKit",
embeds: [{
author: { name: $author, url: $author_url, icon_url: $avatar },
title: $title,
url: $url,
description: $desc,
color: 10181046,
footer: { text: $footer },
timestamp: $ts
}]
}' > payload.json
curl -sS -f -X POST -H "Content-Type: application/json" \
-d @payload.json "$DISCORD_WEBHOOK_URL"