Files
dchain/deploy/single/update.sh
vsecoder 7e7393e4f8 chore: initial commit for v0.0.1
DChain single-node blockchain + React Native messenger client.

Core:
- PBFT consensus with multi-sig validator admission + equivocation slashing
- BadgerDB + schema migration scaffold (CurrentSchemaVersion=0)
- libp2p gossipsub (tx/v1, blocks/v1, relay/v1, version/v1)
- Native Go contracts (username_registry) alongside WASM (wazero)
- WebSocket gateway with topic-based fanout + Ed25519-nonce auth
- Relay mailbox with NaCl envelope encryption (X25519 + Ed25519)
- Prometheus /metrics, per-IP rate limit, body-size cap

Deployment:
- Single-node compose (deploy/single/) with Caddy TLS + optional Prometheus
- 3-node dev compose (docker-compose.yml) with mocked internet topology
- 3-validator prod compose (deploy/prod/) for federation
- Auto-update from Gitea via /api/update-check + systemd timer
- Build-time version injection (ldflags → node --version)
- UI / Swagger toggle flags (DCHAIN_DISABLE_UI, DCHAIN_DISABLE_SWAGGER)

Client (client-app/):
- Expo / React Native / NativeWind
- E2E NaCl encryption, typing indicator, contact requests
- Auto-discovery of canonical contracts, chain_id aware, WS reconnect on node switch

Documentation:
- README.md, CHANGELOG.md, CONTEXT.md
- deploy/single/README.md with 6 operator scenarios
- deploy/UPDATE_STRATEGY.md with 4-layer forward-compat design
- docs/contracts/*.md per contract
2026-04-17 14:16:44 +03:00

167 lines
7.9 KiB
Bash

#!/usr/bin/env bash
# deploy/single/update.sh — pull-and-restart update for a DChain single node.
#
# Modes
# ─────
# 1. RELEASE mode (preferred): DCHAIN_UPDATE_SOURCE_URL is set, node has
# /api/update-check — we trust that endpoint to tell us what's latest,
# then git-checkout the matching tag.
# 2. BRANCH mode (fallback): follow origin/main HEAD.
#
# Both modes share the same safety flow:
# - Fast-forward check (no auto-rewriting local history).
# - Smoke-test the new binary BEFORE killing the running container.
# - Pre-restart best-effort health probe.
# - Poll /api/netstats after restart, fail loud if unhealthy in 60s.
#
# Semver guard
# ────────────
# If UPDATE_ALLOW_MAJOR=false (default), the script refuses to cross a major
# version boundary (v1.x → v2.y). Operator must flip the env var or bump
# manually, avoiding surprise breaking changes from unattended restarts.
#
# Exit codes:
# 0 — up to date or successfully updated
# 1 — new container didn't become healthy
# 2 — smoke test of new image failed
# 3 — git state issue (fetch failed, not fast-forwardable, etc.)
# 4 — semver guard blocked the update
set -euo pipefail
REPO_DIR="${REPO_DIR:-/opt/dchain}"
COMPOSE_FILE="${COMPOSE_FILE:-$REPO_DIR/deploy/single/docker-compose.yml}"
IMAGE_NAME="${IMAGE_NAME:-dchain-node-slim}"
CONTAINER="${CONTAINER:-dchain_node}"
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:8080/api/netstats}"
UPDATE_CHECK_URL="${UPDATE_CHECK_URL:-http://127.0.0.1:8080/api/update-check}"
GIT_REMOTE="${GIT_REMOTE:-origin}"
GIT_BRANCH="${GIT_BRANCH:-main}"
UPDATE_ALLOW_MAJOR="${UPDATE_ALLOW_MAJOR:-false}"
log() { printf '%s %s\n' "$(date -Iseconds)" "$*"; }
die() { log "ERROR: $*"; exit "${2:-1}"; }
command -v docker >/dev/null || die "docker not on PATH" 3
command -v git >/dev/null || die "git not on PATH" 3
command -v curl >/dev/null || die "curl not on PATH" 3
cd "$REPO_DIR" || die "cannot cd to REPO_DIR=$REPO_DIR" 3
# ── Auth header for /api/update-check + /api/netstats on private nodes ────
auth_args=()
[[ -n "${DCHAIN_API_TOKEN:-}" ]] && auth_args=(-H "Authorization: Bearer ${DCHAIN_API_TOKEN}")
# ── 1. Discover target version ────────────────────────────────────────────
target_tag=""
target_commit=""
if [[ -n "${DCHAIN_UPDATE_SOURCE_URL:-}" ]]; then
log "querying release source via $UPDATE_CHECK_URL"
check_json=$(curl -fsS -m 10 "${auth_args[@]}" "$UPDATE_CHECK_URL" 2>/dev/null || echo "")
if [[ -n "$check_json" ]]; then
# Minimal grep-based JSON extraction — avoids adding a jq dependency.
# The shape is stable: we defined it in api_update_check.go.
target_tag=$(printf '%s' "$check_json" | grep -o '"tag":"[^"]*"' | head -1 | sed 's/"tag":"\(.*\)"/\1/')
update_available=$(printf '%s' "$check_json" | grep -o '"update_available":\(true\|false\)' | head -1 | cut -d: -f2)
if [[ "$update_available" != "true" ]]; then
log "up to date according to release source ($target_tag) — nothing to do"
exit 0
fi
log "release source reports new tag: $target_tag"
else
log "release source query failed — falling back to git branch mode"
fi
fi
# ── 2. Fetch git ──────────────────────────────────────────────────────────
log "fetching $GIT_REMOTE"
git fetch --quiet --tags "$GIT_REMOTE" "$GIT_BRANCH" || die "git fetch failed" 3
local_sha=$(git rev-parse HEAD)
if [[ -n "$target_tag" ]]; then
# Release mode: target is the tag we just read from the API.
if ! git rev-parse --verify "refs/tags/$target_tag" >/dev/null 2>&1; then
die "release tag $target_tag unknown to local git — fetch may have missed it" 3
fi
target_commit=$(git rev-parse "refs/tags/$target_tag^{commit}")
else
# Branch mode: target is remote branch HEAD.
target_commit=$(git rev-parse "$GIT_REMOTE/$GIT_BRANCH")
target_tag=$(git describe --tags --abbrev=0 "$target_commit" 2>/dev/null || echo "$target_commit")
fi
if [[ "$local_sha" == "$target_commit" ]]; then
log "up to date at $local_sha — nothing to do"
exit 0
fi
log "updating $local_sha$target_commit ($target_tag)"
# ── 3. Semver guard ───────────────────────────────────────────────────────
# Extract major components from vX.Y.Z tags; refuse to cross boundary unless
# operator opts in. Treats non-semver tags (e.g. raw SHA in branch mode) as
# unversioned — guard is a no-op there.
current_tag=$(cat "$REPO_DIR/.last-update" 2>/dev/null || echo "")
if [[ -z "$current_tag" ]]; then
current_tag=$(git describe --tags --abbrev=0 "$local_sha" 2>/dev/null || echo "")
fi
current_major=$(printf '%s' "$current_tag" | sed -nE 's/^v([0-9]+)\..*/\1/p')
target_major=$(printf '%s' "$target_tag" | sed -nE 's/^v([0-9]+)\..*/\1/p')
if [[ -n "$current_major" && -n "$target_major" && "$current_major" != "$target_major" ]]; then
if [[ "$UPDATE_ALLOW_MAJOR" != "true" ]]; then
die "major version jump $current_tag$target_tag blocked — set UPDATE_ALLOW_MAJOR=true to override" 4
fi
log "semver guard: accepting major jump $current_tag$target_tag (UPDATE_ALLOW_MAJOR=true)"
fi
# ── 4. Fast-forward / checkout ────────────────────────────────────────────
if [[ -n "$target_tag" ]] && git rev-parse --verify "refs/tags/$target_tag" >/dev/null 2>&1; then
# Release mode: check out the tag in detached HEAD.
git checkout --quiet "$target_tag" || die "checkout $target_tag failed" 3
else
git merge --ff-only "$GIT_REMOTE/$GIT_BRANCH" || die "cannot fast-forward — manual merge required" 3
fi
# ── 5. Build image with version metadata ──────────────────────────────────
version_tag="$target_tag"
version_commit="$target_commit"
version_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)
version_dirty=$(git diff --quiet HEAD -- 2>/dev/null && echo false || echo true)
log "building image $IMAGE_NAME:$version_commit ($version_tag)"
docker build --quiet \
--build-arg "VERSION_TAG=$version_tag" \
--build-arg "VERSION_COMMIT=$version_commit" \
--build-arg "VERSION_DATE=$version_date" \
--build-arg "VERSION_DIRTY=$version_dirty" \
-t "$IMAGE_NAME:$version_commit" \
-t "$IMAGE_NAME:latest" \
-f deploy/prod/Dockerfile.slim . \
|| die "docker build failed" 2
# ── 6. Smoke-test the new binary ──────────────────────────────────────────
log "smoke-testing new image"
smoke=$(docker run --rm --entrypoint /usr/local/bin/node "$IMAGE_NAME:$version_commit" --version 2>&1) \
|| die "new image --version failed: $smoke" 2
log "smoke ok: $smoke"
# ── 7. Recreate the container ─────────────────────────────────────────────
log "recreating container $CONTAINER"
docker compose -f "$COMPOSE_FILE" up -d --force-recreate node \
|| die "docker compose up failed" 1
# ── 8. Wait for health ────────────────────────────────────────────────────
log "waiting for health at $HEALTH_URL"
for i in $(seq 1 30); do
if curl -fsS -m 3 "${auth_args[@]}" "$HEALTH_URL" >/dev/null 2>&1; then
log "node healthy after $((i*2))s — update done ($version_tag @ ${version_commit:0:8})"
printf '%s\n' "$version_tag" > .last-update
exit 0
fi
sleep 2
done
log "new container did not become healthy in 60s — dumping logs"
docker logs --tail 80 "$CONTAINER" || true
exit 1