mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-07-26 10:55:41 +00:00
ci: automated Telegram-gated Supabase deploy (edge functions + migrations)
Adds a deploy-supabase job to the quality gate so the self-hosted Supabase side ships the same way as the frontend/backend instead of by hand: - gated by the existing telegram-gate (owner approves once, in Telegram) - runs on main-push in parallel with deploy-app, so edge functions, DB migrations and the new frontend go live in one post-approval window - rsyncs the edge-function dirs into the edge-runtime volume (hot-reloaded, no restart; --no-delete preserves the telegram-* functions) - applies pending migrations via deploy/apply-migrations.sh: idempotent and transactional, tracked in supabase_migrations.schema_migrations, skips non-versioned files (COMBINED_*), never half-records a failed migration - post-deploy health check verifies the fund-custody guards took effect (anon lost EXECUTE on the escrow/withdraw RPCs; both freeze triggers exist) - Telegram alert on success/failure Requires repo secrets SUPABASE_VPS_HOST/USER/SSH_KEY/PORT.
This commit is contained in:
@@ -896,6 +896,118 @@ jobs:
|
||||
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-d "chat_id=${CEO_CHAT_ID}" --data-urlencode "text=$MSG"
|
||||
|
||||
# ========================================
|
||||
# DEPLOY - SUPABASE (edge functions + migrations)
|
||||
# Same Telegram-gated cutover as the frontend/backend: syncs the edge functions
|
||||
# into the self-hosted edge-runtime volume (hot-reloaded, no restart) and applies
|
||||
# any un-recorded migrations transactionally against the fund-custody Postgres.
|
||||
# Runs in parallel with deploy-app so functions, migrations and the new frontend
|
||||
# go live in the same post-approval window (minimal fail-closed gap).
|
||||
# ========================================
|
||||
deploy-supabase:
|
||||
name: Deploy Supabase (functions + migrations)
|
||||
runs-on: pwap-runner
|
||||
needs: [telegram-gate]
|
||||
if: |
|
||||
always() &&
|
||||
needs.telegram-gate.result == 'success' &&
|
||||
github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Pack supabase tree
|
||||
run: |
|
||||
# __tests__ are Deno tests — kept out of the runtime bundle entirely.
|
||||
tar czf supabase-deploy.tgz -C web/supabase \
|
||||
--exclude='__tests__' functions migrations deploy
|
||||
|
||||
- name: Upload bundle to staging
|
||||
uses: appleboy/scp-action@v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.SUPABASE_VPS_HOST }}
|
||||
username: ${{ secrets.SUPABASE_VPS_USER }}
|
||||
key: ${{ secrets.SUPABASE_VPS_SSH_KEY }}
|
||||
port: ${{ secrets.SUPABASE_VPS_SSH_PORT || 22 }}
|
||||
source: "supabase-deploy.tgz"
|
||||
target: /opt/supabase-self-hosted/deploy-staging
|
||||
|
||||
- name: Sync edge functions + apply migrations (health-checked)
|
||||
id: apply
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.SUPABASE_VPS_HOST }}
|
||||
username: ${{ secrets.SUPABASE_VPS_USER }}
|
||||
key: ${{ secrets.SUPABASE_VPS_SSH_KEY }}
|
||||
port: ${{ secrets.SUPABASE_VPS_SSH_PORT || 22 }}
|
||||
command_timeout: 15m
|
||||
script: |
|
||||
set -euo pipefail
|
||||
BASE=/opt/supabase-self-hosted/deploy-staging
|
||||
STAGING="$BASE/tree"
|
||||
VOL=/opt/supabase-self-hosted/docker/volumes/functions
|
||||
DB=supabase-db
|
||||
|
||||
echo "── 0. Unpack bundle ──"
|
||||
rm -rf "$STAGING" && mkdir -p "$STAGING"
|
||||
tar xzf "$BASE/supabase-deploy.tgz" -C "$STAGING"
|
||||
|
||||
echo "── 1. Sync edge functions (additive, no --delete: telegram-* preserved) ──"
|
||||
# __tests__ are Deno test dirs — never ship them into the runtime volume.
|
||||
rsync -a --exclude='__tests__' "$STAGING/functions/" "$VOL/"
|
||||
echo " synced: $(ls "$STAGING/functions" | grep -v __tests__ | tr '\n' ' ')"
|
||||
|
||||
echo "── 2. Pre-flight: required edge-runtime secrets ──"
|
||||
EF_ENV="$(docker exec supabase-edge-functions env 2>/dev/null || true)"
|
||||
miss=""
|
||||
for k in SUPABASE_URL SUPABASE_SERVICE_ROLE_KEY PLATFORM_PRIVATE_KEY; do
|
||||
grep -q "^$k=" <<<"$EF_ENV" || miss="$miss $k"
|
||||
done
|
||||
if [ -n "$miss" ]; then
|
||||
echo "::warning::edge-runtime missing secrets:$miss (fund functions may fail until set)"
|
||||
fi
|
||||
|
||||
echo "── 3. Apply pending migrations (transactional, tracked) ──"
|
||||
bash "$STAGING/deploy/apply-migrations.sh" "$STAGING/migrations" "$DB"
|
||||
|
||||
echo "── 4. Verify the fund-custody guards actually took effect ──"
|
||||
q() { docker exec -i "$DB" psql -U postgres -tAq -c "$1"; }
|
||||
fail=0
|
||||
for fn in "release_escrow_internal" "lock_escrow_internal" "refund_escrow_internal" "request_withdraw"; do
|
||||
# any signature match; expect NO anon EXECUTE
|
||||
oid="$(q "SELECT oid FROM pg_proc WHERE proname='$fn' LIMIT 1;")"
|
||||
if [ -n "$oid" ]; then
|
||||
priv="$(q "SELECT has_function_privilege('anon', $oid, 'EXECUTE');")"
|
||||
echo " anon EXECUTE $fn = $priv"
|
||||
[ "$priv" = "f" ] || { echo "::error::anon still has EXECUTE on $fn"; fail=1; }
|
||||
fi
|
||||
done
|
||||
trg="$(q "SELECT tgname FROM pg_trigger WHERE tgname IN ('trg_freeze_trade_financials','trg_freeze_offer_financials');")"
|
||||
n_trg="$(printf '%s' "$trg" | grep -c . || true)"
|
||||
echo " freeze triggers present: ${trg:-<none>} (count=$n_trg)"
|
||||
[ "$n_trg" = "2" ] || { echo "::error::trade/offer freeze triggers missing (expected 2, got $n_trg)"; fail=1; }
|
||||
|
||||
rm -rf "$BASE"
|
||||
[ "$fail" = "0" ] && echo "✅ Supabase deploy verified" || { echo "❌ post-deploy verification failed"; exit 1; }
|
||||
|
||||
- name: Telegram notify
|
||||
if: always()
|
||||
env:
|
||||
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
||||
OUTCOME: ${{ steps.apply.outcome }}
|
||||
run: |
|
||||
if [ "$OUTCOME" = "success" ]; then
|
||||
MSG="✅ pwap Supabase: edge functions synced + migrations applied & verified (${{ github.sha }})."
|
||||
else
|
||||
MSG="🚨 pwap Supabase deploy FAILED (${{ github.sha }}) — functions/migrations may be partially applied. Check the run."
|
||||
fi
|
||||
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-d chat_id="${CEO_CHAT_ID}" -d text="$MSG" >/dev/null || true
|
||||
|
||||
# ========================================
|
||||
# SECURITY CHECKS (BLOCKING)
|
||||
# npm audit (high + critical) + TruffleHog secret scan
|
||||
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# apply-migrations.sh — idempotent, tracked migration applier for the self-hosted
|
||||
# Supabase Postgres. Runs ON the Supabase host (invoked by the deploy-supabase CI
|
||||
# job over SSH). Applies every versioned migration in $MIGRATIONS_DIR that is not
|
||||
# yet recorded in supabase_migrations.schema_migrations, in ascending version
|
||||
# order, each inside a single transaction together with its own tracking-row
|
||||
# insert — so a failed migration rolls back cleanly and is never half-recorded.
|
||||
#
|
||||
# Safe to re-run: already-recorded versions are skipped. Non-versioned files
|
||||
# (e.g. COMBINED_*.sql consolidated snapshots) are ignored.
|
||||
#
|
||||
# Usage: apply-migrations.sh <migrations_dir> [db_container]
|
||||
set -euo pipefail
|
||||
|
||||
MIGRATIONS_DIR="${1:?migrations dir required}"
|
||||
DB_CONTAINER="${2:-supabase-db}"
|
||||
PSQL=(docker exec -i "$DB_CONTAINER" psql -U postgres -v ON_ERROR_STOP=1 --single-transaction -q)
|
||||
PSQL_Q=(docker exec -i "$DB_CONTAINER" psql -U postgres -tAq)
|
||||
|
||||
echo "▶ apply-migrations: dir=$MIGRATIONS_DIR container=$DB_CONTAINER"
|
||||
|
||||
# Ensure the tracking schema/table exists (matches the Supabase CLI layout).
|
||||
"${PSQL[@]}" >/dev/null <<'SQL'
|
||||
CREATE SCHEMA IF NOT EXISTS supabase_migrations;
|
||||
CREATE TABLE IF NOT EXISTS supabase_migrations.schema_migrations (
|
||||
version text PRIMARY KEY,
|
||||
statements text[],
|
||||
name text
|
||||
);
|
||||
SQL
|
||||
|
||||
# Snapshot the already-applied versions once.
|
||||
applied="$("${PSQL_Q[@]}" -c "SELECT version FROM supabase_migrations.schema_migrations;")"
|
||||
is_applied() { grep -qxF "$1" <<<"$applied"; }
|
||||
|
||||
pending=0 done=0
|
||||
# Sort by filename so version order == chronological order (14-digit timestamps
|
||||
# and zero-padded legacy 0NN prefixes both sort correctly).
|
||||
for f in $(ls "$MIGRATIONS_DIR"/*.sql 2>/dev/null | sort); do
|
||||
base="$(basename "$f")"
|
||||
# Versioned migrations only: <digits>_<name>.sql. Skips COMBINED_*, README, etc.
|
||||
if [[ ! "$base" =~ ^([0-9]+)_(.+)\.sql$ ]]; then
|
||||
echo " ↷ skip (not a versioned migration): $base"
|
||||
continue
|
||||
fi
|
||||
version="${BASH_REMATCH[1]}"
|
||||
name="${BASH_REMATCH[2]}"
|
||||
|
||||
if is_applied "$version"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
pending=$((pending+1))
|
||||
if [[ "${DRY_RUN:-}" == "1" ]]; then
|
||||
echo " → [dry-run] WOULD apply $version ($name)"
|
||||
continue
|
||||
fi
|
||||
echo " → applying $version ($name)"
|
||||
# Migration body + its tracking insert in ONE transaction (--single-transaction):
|
||||
# if the body errors, ON_ERROR_STOP aborts and the whole tx (including the insert)
|
||||
# rolls back, so the version is never recorded for a failed apply.
|
||||
# version is always digits and name always [a-z0-9_] (captured from the filename
|
||||
# regex above), so direct single-quoting is safe — no injection surface.
|
||||
{
|
||||
cat "$f"
|
||||
printf "\nINSERT INTO supabase_migrations.schema_migrations(version,name) VALUES ('%s','%s');\n" \
|
||||
"$version" "$name"
|
||||
} | "${PSQL[@]}"
|
||||
echo " ✓ applied and recorded $version"
|
||||
done=$((done+1))
|
||||
done
|
||||
|
||||
if [[ $pending -eq 0 ]]; then
|
||||
echo "✔ up to date — no pending migrations"
|
||||
else
|
||||
echo "✔ applied $done migration(s)"
|
||||
fi
|
||||
Reference in New Issue
Block a user