igneum/tools/site-deploy-from-mirror.sh

95 lines
9.6 KiB
Bash
Executable file

#!/usr/bin/env bash
# Deploy the public site from the BOX MIRROR's master (GitHub dark, 7 October 2026): a GIT-LESS export of the site tree at build/master
# (or the commit given), the site built in it (node site/build.mjs, the downloads snapshot read from the dl host), then the Vercel CLI
# from this Mac with the igneum team config (~/.config/igneum/vercel, --scope igneum; the box holds no token by R6) against the
# site project (.vercel/project.json of ~/Projects/igneum/site). Reads nothing from GitHub. Prints the commit served and the
# production URL with the UTC time; the worktree is removed after.
#
# tools/site-deploy-from-mirror.sh deploy the mirror's master (the ONLY ref a site deploy takes: main, 7 October
# 2026 21:2x UK, after a branch deploy was overwritten by the next master deploy)
# tools/site-deploy-from-mirror.sh --self-test-checks the post-deploy checks against the live site as it stands (known-failed first)
#
# POST-DEPLOY CHECKS (main, 7 October 2026 22:1x UK), before the edge time is printed: /api/live's network must equal LIVE_NETWORK
# (igneum-devnet-3), the index must carry INDEX_STRINGS (the launch-first chip line, the git.igneum.network link), LEGAL_PAGE must
# carry "Not legal advice" (the litepaper: no commit on master puts it on the index) and /miners must carry at least MINERS_MIN_ROWS table rows; any mismatch prints "DEPLOY RED <field>: ..." and exits 1. The
# edge serves the previous deployment for some seconds after the CLI returns, so the checks retry for up to 90 s before the verdict.
set -euo pipefail
LIVE_NETWORK="${LIVE_NETWORK:-igneum-devnet-3}"
INDEX_STRINGS=("At launch the strongest chip in our public model" "git.igneum.network/igneum-network/")
LEGAL_PAGE="${LEGAL_PAGE:-/litepaper}"; LEGAL_STRING="Not legal advice" # the legal line lives on the litepaper in master's tree (22:16 UK: no commit put it on the index)
MINERS_MIN_ROWS="${MINERS_MIN_ROWS:-20}" # the current-class table alone (the datacentre rows sit in their own table since 8 Oct 2026)
SERVED_PAGES=(swap "faucet|Devnet 3 faucet" "build|Built to prove every block" "grants|Not legal advice.") # clean URLs every deploy must answer 200 (8 Oct 2026: the DEX lane's /swap, the builder lane's /faucet; /light /receipt /oracle /build /grants join as they land)
SITE="${SITE_URL:-https://igneum.network}"
# the /miners row count: the rows of the current-class table (table#bench-current, the regrouped bench of 8 Oct 2026; the datacentre
# and earlier tables sit under their own ids); known-failed: an empty table reads 0
miners_rows() { printf '%s' "$1" | python3 -c '
import sys,re
h=sys.stdin.read(); m=re.search(r"<table[^>]*id=\"bench-current\".*?</table>", h, re.S)
print(len(re.findall(r"<tr class=\"row\"", m.group(0))) if m else 0)'; }
post_checks() { # one pass: prints the first mismatch as "field: detail" and returns 1, or returns 0 silently
local j n idx m rows
local nc=(-H 'Cache-Control: no-cache' -H 'Pragma: no-cache')
j=$(curl -fsS "${nc[@]}" --max-time 20 "$SITE/api/live?x=$RANDOM" 2>/dev/null) || { echo "api/live: not reachable"; return 1; }
n=$(printf '%s' "$j" | python3 -c 'import sys,json; print(json.load(sys.stdin)["state"]["network"])' 2>/dev/null || echo "?")
[ "$n" = "$LIVE_NETWORK" ] || { echo "api/live network: $n (want $LIVE_NETWORK)"; return 1; }
idx=$(curl -fsS "${nc[@]}" --max-time 20 "$SITE/?x=$RANDOM" 2>/dev/null) || { echo "index: not reachable"; return 1; }
for m in "${INDEX_STRINGS[@]}"; do printf '%s' "$idx" | /usr/bin/grep -cF -- "$m" >/dev/null || { echo "index string missing: \"$m\""; return 1; }; done
# the page is read into a variable first: `curl | grep -q` under pipefail reads false on a MATCH (grep closes the pipe, curl exits 56;
# three false DEPLOY REDs on 7 and 8 October 2026)
local legal; legal=$(curl -fsS "${nc[@]}" --max-time 20 "$SITE$LEGAL_PAGE?x=$RANDOM" 2>/dev/null) || { echo "$LEGAL_PAGE: not reachable"; return 1; }
printf '%s' "$legal" | /usr/bin/grep -ciF -- "$LEGAL_STRING" >/dev/null || { echo "legal string missing on $LEGAL_PAGE: \"$LEGAL_STRING\""; return 1; }
local miners; miners=$(curl -fsS "${nc[@]}" --max-time 20 "$SITE/miners?x=$RANDOM" 2>/dev/null) || { echo "miners: not reachable"; return 1; }
rows=$(miners_rows "$miners")
[ "${rows:-0}" -ge "$MINERS_MIN_ROWS" ] || { echo "miners rows: ${rows:-0} (want at least $MINERS_MIN_ROWS)"; return 1; }
# the builder-programme pages (8 October 2026): every clean URL in SERVED_PAGES answers 200; a page a lane lands is added here
# on its deploy, so a later deploy that loses one reads RED with the path
# each entry is "path" or "path|string": the page answers 200 and, when given, carries the string (the lane's own read-back line)
local pg path want body code
for pg in "${SERVED_PAGES[@]}"; do
path="${pg%%|*}"; want=""; case "$pg" in *"|"*) want="${pg#*|}" ;; esac
body=$(curl -s -w '\n%{http_code}' "${nc[@]}" --max-time 20 "$SITE/$path?x=$RANDOM" 2>/dev/null) || body=$'\n000'
code="${body##*$'\n'}"; body="${body%$'\n'*}"
[ "$code" = 200 ] || { echo "page /$path: HTTP $code (want 200)"; return 1; }
[ -z "$want" ] || printf '%s' "$body" | /usr/bin/grep -cF -- "$want" >/dev/null || { echo "page /$path: string missing: \"$want\""; return 1; }
done
echo "ok: api/live $n, ${#INDEX_STRINGS[@]} index strings, the legal line on $LEGAL_PAGE, $rows miners rows, ${#SERVED_PAGES[@]} pages 200"
}
if [ "${1:-}" = --self-test-checks ]; then
# known-failed first: a network constant the live site cannot carry must read RED with the field; then the live read as it stands
LIVE_NETWORK="igneum-no-such-network" post_checks >/dev/null 2>&1 && { echo "post-deploy self-test: FAIL: a wrong network passed"; exit 1; }
out=$(LIVE_NETWORK="igneum-no-such-network" post_checks 2>&1 || true); case "$out" in "api/live network:"*) ;; *) echo "post-deploy self-test: FAIL: the wrong-network line was '$out'"; exit 1 ;; esac
# an array is set inside a subshell, not as a command prefix (a prefix makes it the string "(x)")
( SERVED_PAGES=("no-such-page-7f3a"); post_checks >/dev/null 2>&1 ) && { echo "post-deploy self-test: FAIL: a missing page passed"; exit 1; }
out=$( SERVED_PAGES=("no-such-page-7f3a"); post_checks 2>&1 || true ); case "$out" in "page /no-such-page-7f3a:"*) ;; *) echo "post-deploy self-test: FAIL: the missing-page line was '$out'"; exit 1 ;; esac
INDEX_STRINGS=("no such string on any page 7f3a") LIVE_NETWORK="$(curl -fsS --max-time 20 "$SITE/api/live" | python3 -c 'import sys,json; print(json.load(sys.stdin)["state"]["network"])')" post_checks >/dev/null 2>&1 && { echo "post-deploy self-test: FAIL: a missing index string passed"; exit 1; }
[ "$(miners_rows '<table id="bench-current"><tr class="hdr"></tr></table><table id="bench-earlier"><tr class="row"></tr></table>')" = 0 ] || { echo "post-deploy self-test: FAIL: an empty current table did not read 0 rows"; exit 1; }
[ "$(miners_rows '<table id="bench-current"><tr class="row"></tr><tr class="detail"></tr><tr class="row"></tr></table>')" = 2 ] || { echo "post-deploy self-test: FAIL: two rows did not read 2"; exit 1; }
echo "post-deploy self-test: a wrong network, a missing index string and a missing page read RED with the field; an empty current table reads 0 rows, two rows read 2"
echo "live site now: $(post_checks 2>&1 || true)"
exit 0
fi
case "${1:-}" in "") ;; build/master|master) ;; *) echo "a site deploy takes the mirror's master only (main, 7 Oct 2026); no branch or commit argument" >&2; exit 2 ;; esac
cd "$(git rev-parse --show-toplevel)"
REF="${1:-build/master}"
git fetch -q build master
SHA=$(git rev-parse "$REF"); SHORT=${SHA:0:8}
# a GIT-LESS export (main, 7 Oct 2026 19:4x BST: the worktree deploy G72XWh48k was BLOCKED by the team's commit-author check, which
# cannot resolve 337424239+igneum-labs@users.noreply.github.com while the GitHub account is suspended; an export carries no Git
# metadata, so the check does not apply). The deploy directory must hold no .git; the check below refuses otherwise.
W=$(mktemp -d "${TMPDIR:-/tmp}/site-deploy.XXXXXX")
trap 'rm -rf "$W"' EXIT
git archive --format=tar "$SHA" site | tar -x -C "$W"
[ -e "$W/.git" ] || [ -e "$W/site/.git" ] && { echo "the deploy directory carries a .git; refusing (the author check would block it)" >&2; exit 1; }
LINK="$HOME/Projects/igneum/site/.vercel/project.json"; [ -f "$LINK" ] || { echo "no site project link at $LINK" >&2; exit 1; }
# the Vercel project's Root Directory is "site": the deploy runs from the export's root with site/ inside it
mkdir -p "$W/.vercel"; cp "$LINK" "$W/.vercel/project.json"
echo "site export at $SHORT ($(git log -1 --format='%ci %s' "$SHA" | cut -c1-90)); no .git in $W"
( cd "$W/site" && node build.mjs ) 2>&1 | tail -3
echo "deploying $(TZ=UTC date +%H:%M:%SZ)"
OUT=$( cd "$W" && npx --yes vercel@latest --global-config "$HOME/.config/igneum/vercel" --scope igneum deploy --prod --yes 2>&1 ) || { echo "$OUT" | tail -5 >&2; exit 1; }
URL=$(echo "$OUT" | grep -oE 'https://[a-z0-9.-]*vercel\.app' | tail -1)
# the post-deploy checks, retried while the edge still serves the previous deployment (up to 90 s)
verdict=""; for i in $(seq 1 30); do verdict=$(post_checks 2>&1) && break; sleep 5; done # up to 150 s: the edge serves the previous deployment per path for a while (22:27 BST: the litepaper lagged the API by over 90 s)
case "$verdict" in ok:*) echo "post-deploy checks $verdict" ;; *) echo "DEPLOY RED $verdict (commit $SHORT at the edge $URL, $(TZ=Europe/London date +%H:%M) BST)" >&2; exit 1 ;; esac
echo "SITE DEPLOYED commit $SHORT at $(TZ=UTC date +%H:%M:%SZ) UTC ($(TZ=Europe/London date +%H:%M) BST): $URL -> https://igneum.network"
echo "$OUT" | grep -iE "Production:|Aliased|error" | head -3