mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-09-01 17:08:17 +00:00
* fix(owner): Wave 3 mediums — password handling, deploy rollback, newuser races, session revocation - BUG-017: stop trimming MQTT passwords (frontend + backend). A valid broker password may begin/end with whitespace; trimming made it permanently unauthenticatable. Username trimming unchanged. - BUG-013: deploy-website.sh now set -Eeuo pipefail with an EXIT rollback trap armed only after the pin mutates; container-down and bundle-mismatch paths fail loudly and restore the old pin + verify the restored service. - BUG-012: newuser.sh installs an EXIT trap (ERR does not fire on explicit exit 1 from die()) so every post-mutation failure rolls back; rollback is idempotent and a no-op before any mutation flag. Disarm points clear EXIT. - BUG-011: newuser.sh re-reads + re-validates OWNER_MQTT_USERNAME_MAP under the re-acquired lock after the long unlocked discovery window, then merges the new grant into the current map — concurrent provisioning runs can no longer be overwritten by a stale snapshot. - BUG-010: owner sessions are now v3 cookies carrying a credential generation (Redis-backed). When the broker rejects a previously-valid password (revocation detected), the generation bumps and every older session is rejected on the next request. TTL shortened 30d -> 7d. Verified: tsc clean, 310/310 backend tests pass, bash -n on both scripts. * fix(alert-receiver): durable alert delivery with bounded retry + dead-letter (BUG-014) Forwarding was fire-and-forget: failures logged asynchronously after HTTP 202, /healthz stayed green, and alerts could be archived to the local JSONL while operators never saw them. Now: - Every receipt is enqueued for delivery with bounded exponential backoff (ALERT_FORWARD_MAX_ATTEMPTS=5, base 1s, cap 60s) and dead-lettered to receipts.jsonl.dead after exhausting attempts. - /healthz keeps returning 200 (compose wget healthcheck must not restart the container) but the body reports degraded status + detail; new /readyz returns 503 when ALERT_FORWARD_URL is unset (archive-only), no successful forward since startup, or alerts stuck undelivered >5min. Verified: tsc clean, 310/310 tests pass. * fix(ci): classify reviewed public channel keys + fixtures in gitleaks (BUG-009) The nightly full-history secret scan flagged 42 findings spanning the documented community channel keys (VALIDATED_CHANNELS — intentionally public, each verified to decrypt real UK Mesh group text), fake test fixtures, and a deployed-commit SHA in the website live manifests. That made a genuine credential easy to dismiss among expected hits. - Rule-scoped allowlists with exact fingerprints (regexTarget: secret, anchored full-value matches) for the Public channel key, test fixtures (0123456789... / abcdef0123...), and the manifest commit SHA. - Structure-exact line allowlist for channelRegistry.ts VALIDATED_CHANNELS. - Verified locally with gitleaks 8.24.3 full-history scan: 42 -> 0 findings. * fix(viewshed): side-effect completion markers prevent skipped link jobs (BUG-006) store_coverage() commits on an autocommit connection, then the worker queues physical-link jobs and publishes Redis notifications. A Redis failure after the DB commit NACKed the job; on retry already_calculated() saw the coverage row and returned early — link work and frontend notifications were skipped forever. - Record a Redis completion marker (viewshed:side-effects:<node>) only after EVERY side effect succeeds. - On the already_calculated early return, a missing marker triggers an idempotent replay: link jobs are re-enqueued from the stored node position (admission is idempotent) and coverage_update/node_upsert notifications are re-published from the stored coverage row. - Redis read failures are treated as incomplete (replay attempt re-raises and NACKs rather than silently skipping). - Added tests/test_side_effect_markers.py (5 tests) with a conftest that stubs osgeo/psycopg2 so pure-logic worker tests run without GDAL (CI keeps real GDAL via setdefault). Verified: 5/5 new tests pass; full suite 34 passed, 2 GDAL-required terrain tests fail only in stub env (pass in CI image). --------- Co-authored-by: hermes-gadget <hermes-gadget@users.noreply.github.com>
87 lines
3.6 KiB
Bash
Executable File
87 lines
3.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# Safe website deploy: drift-check -> pin -> up -d --no-deps -> verify.
|
|
# Usage: scripts/deploy-website.sh <image-ref> <service> [--force]
|
|
# <service> = website-ukmesh (prod) or website-dev (staging)
|
|
# --force = deploy even if the image drifts from the manifest (e.g. intentional release)
|
|
#
|
|
# Encodes the hard-won pitfalls: never bare `up`, never deploy the wrong image,
|
|
# never trust a build output digest, verify what is SERVED not what was built.
|
|
set -Eeuo pipefail
|
|
cd "$(dirname "$0")/.."
|
|
|
|
IMG="${1:?usage: deploy-website.sh <image-ref> <website-ukmesh|website-dev> [--force]}"
|
|
SVC="${2:?usage: deploy-website.sh <image-ref> <website-ukmesh|website-dev> [--force]}"
|
|
FORCE=0
|
|
[ "${3:-}" = "--force" ] && FORCE=1
|
|
[ "$SVC" = "website-ukmesh" ] || [ "$SVC" = "website-dev" ] || { echo "service must be website-ukmesh or website-dev"; exit 2; }
|
|
[ -f .env ] || { echo "no .env in $(pwd)"; exit 2; }
|
|
|
|
# Rollback state: set only once the live pin has been mutated. The trap fires
|
|
# on ANY exit (error, signal, or early verification failure) so a broken or
|
|
# unverified deployment always restores the previous pin.
|
|
PIN="WEBSITE_DEV_IMAGE"
|
|
[ "$SVC" = "website-ukmesh" ] && PIN="WEBSITE_IMAGE"
|
|
OLD=""
|
|
rollback() {
|
|
if [ -n "$OLD" ] && grep -q "^$PIN=.*" .env && [ "$(grep "^$PIN=" .env | cut -d= -f2)" != "$OLD" ]; then
|
|
echo "ROLLBACK: restoring $PIN=$OLD"
|
|
sed -i "s|^$PIN=.*|$PIN=$OLD|" .env
|
|
docker compose -f docker-compose.yml -f docker-compose.live.yml up -d --no-deps "$SVC" >/dev/null 2>&1 || true
|
|
sleep 6
|
|
if docker ps --format '{{.Names}}' | grep -q "^$SVC$"; then
|
|
echo "ROLLBACK: $SVC is up on old pin"
|
|
else
|
|
echo "ROLLBACK WARNING: $SVC is NOT running after restore — inspect manually" >&2
|
|
fi
|
|
fi
|
|
}
|
|
trap rollback EXIT
|
|
|
|
echo "== 1/5 drift check =="
|
|
# The drift checker exits 1 on intentional drift — capture rc without tripping set -e.
|
|
if DRIFT_OUT=$(scripts/check-website-drift.sh "$IMG" $( [ "$SVC" = "website-dev" ] && echo --staging ) 2>&1); then
|
|
DRIFT_RC=0
|
|
else
|
|
DRIFT_RC=$?
|
|
fi
|
|
echo "$DRIFT_OUT"
|
|
if [ "$DRIFT_RC" = "1" ] && [ "$FORCE" != "1" ]; then
|
|
echo "ABORT: image drifts from deployed state. Re-run with --force for an intentional release."
|
|
exit 1
|
|
fi
|
|
|
|
echo "== 2/5 resolve real digest =="
|
|
DIGEST=$(docker inspect "$IMG" --format '{{.Id}}')
|
|
echo "new digest: $DIGEST"
|
|
grep -q "^$PIN=" .env || { echo "no $PIN in .env"; exit 2; }
|
|
OLD=$(grep "^$PIN=" .env | cut -d= -f2)
|
|
echo "old pin: $OLD"
|
|
echo "new pin: $DIGEST"
|
|
|
|
echo "== 3/5 pin + recreate (--no-deps) =="
|
|
sed -i "s|^$PIN=.*|$PIN=$DIGEST|" .env
|
|
docker compose -f docker-compose.yml -f docker-compose.live.yml up -d --no-deps "$SVC"
|
|
|
|
echo "== 4/5 wait for health =="
|
|
sleep 6
|
|
docker ps --format '{{.Names}} {{.Status}}' | grep "$SVC" || { echo "container not up"; exit 1; }
|
|
|
|
echo "== 5/5 verify SERVED content vs image =="
|
|
PORT=3006; [ "$SVC" = "website-ukmesh" ] && PORT=3004
|
|
PASS=1
|
|
for a in $(docker run --rm --entrypoint sh "$IMG" -c 'grep -oE "assets/[A-Za-z0-9_.-]+\\.(js|css)" /usr/share/nginx/html/index.html' | sort -u); do
|
|
H1=$(docker run --rm --entrypoint sh "$IMG" -c "sha256sum /usr/share/nginx/html/$a" | awk '{print $1}')
|
|
H2=$(curl -s "http://127.0.0.1:$PORT/$a" | sha256sum | awk '{print $1}')
|
|
[ "$H1" = "$H2" ] || { echo "MISMATCH: $a"; PASS=0; }
|
|
done
|
|
if [ "$PASS" != "1" ]; then
|
|
echo "VERIFICATION FAILED: served bundles do not match $IMG on :$PORT — rolling back"
|
|
exit 1
|
|
fi
|
|
echo "VERIFIED: served bundles match $IMG on :$PORT"
|
|
echo "deploy complete: $SVC -> $DIGEST"
|
|
|
|
# Verification succeeded — disarm the rollback trap so the EXIT below keeps the new pin.
|
|
trap - EXIT
|
|
exit 0
|