Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Deploy scripts

Helpers for deploying and verifying the Polis Protocol stack. Each script is idempotent and prints the command it's about to run before running it, so nothing surprising happens.

## Prerequisites

Before running any deploy script:

1. Complete account setup in [`docs/ENVIRONMENT.md`](../docs/ENVIRONMENT.md).
2. Install CLI tools: `terraform`, `fly`, `doppler`, `gh`, `forge`, `jq`, `curl`.
3. Run `./scripts/preflight.sh` — validates your local environment.

## Order of operations

Deploys are layered; later ones depend on earlier ones. Target order for a fresh deploy:

```
preflight.sh → validates tools & credentials
deploy-discourse.sh → Hetzner VPS + Discourse container (~25 min)
deploy-contracts.sh → PolisArchive.sol on Base Sepolia (~3 min)
deploy-bff.sh → BFF on Fly.io (needs Discourse done first)
smoke-test.sh → curl health checks end-to-end
```

## Scripts

| Script | Purpose | Idempotent | Duration |
|--------|---------|------------|----------|
| `preflight.sh` | Check tools + credentials are set | ✓ | ~5s |
| `deploy-discourse.sh` | Provision Hetzner + initial Discourse config | partial (terraform is, SSH step is not) | ~25 min first time |
| `deploy-contracts.sh` | Forge deploy PolisArchive to Base (Sepolia or mainnet) | ✗ (each run = new deployment) | ~3 min |
| `deploy-bff.sh` | Fly deploy BFF with secrets from Doppler | ✓ | ~5 min |
| `smoke-test.sh` | curl health/GraphQL/contract endpoints | ✓ | ~30s |

## Environment

Scripts read from:

1. **Shell env** — exported vars (`export HCLOUD_TOKEN=...`)
2. **Doppler** — if `doppler` is configured, secrets are pulled at runtime
3. **`.env.local`** — at repo root, gitignored

Preferred flow: **Doppler** for prod secrets, `.env.local` for dev overrides, shell env for one-off experiments.

## Rollback

Each deploy script prints the rollback command on exit. For quick reference:

- **Discourse:** `terraform -chdir=infra/terraform/hetzner destroy` (destroys VPS, keeps backups)
- **Contracts:** immutable — deploy a new version and call `transferAdmin` if needed
- **BFF:** `fly releases --app polis-bff-ipe list` → `fly deploy --image <previous>`
102 changes: 102 additions & 0 deletions scripts/deploy-bff.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# deploy-bff.sh — deploy the BFF (Fastify + GraphQL) to Fly.io.
# Pulls secrets from Doppler and pushes to Fly.
# Idempotent: can be re-run safely to update secrets or deploy a new build.

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BFF_DIR="$REPO_ROOT/packages/bff"
FLY_APP="${FLY_APP:-polis-bff-ipe}"
FLY_REGION="${FLY_REGION:-gru}"

YELLOW='\033[1;33m'
GREEN='\033[0;32m'
RED='\033[0;31m'
DIM='\033[2m'
RESET='\033[0m'

log() { echo -e "${YELLOW}▸${RESET} $1"; }
ok() { echo -e "${GREEN}✓${RESET} $1"; }
fail() { echo -e "${RED}✗${RESET} $1" >&2; }

# ── 1. Validate tooling ────────────────────────────────────────────
command -v fly >/dev/null 2>&1 || { fail "fly CLI not installed"; exit 1; }
command -v doppler >/dev/null 2>&1 || { fail "doppler CLI not installed"; exit 1; }
fly auth whoami >/dev/null 2>&1 || { fail "fly not logged in (run: fly auth login)"; exit 1; }
doppler me >/dev/null 2>&1 || { fail "doppler not logged in (run: doppler login)"; exit 1; }

# ── 2. Build locally first (catch issues before deploy) ────────────
log "building BFF locally (catch errors before deploy)"
cd "$REPO_ROOT"
pnpm --filter '@polisprotocol/bff' build

# ── 3. Ensure Fly app exists ────────────────────────────────────────
if ! fly status --app "$FLY_APP" >/dev/null 2>&1; then
log "creating Fly app $FLY_APP in region $FLY_REGION"
cd "$BFF_DIR"
fly launch --no-deploy --name "$FLY_APP" --region "$FLY_REGION" --copy-config --yes
else
ok "Fly app $FLY_APP exists"
fi

# ── 4. Sync secrets from Doppler to Fly ────────────────────────────
log "syncing secrets from Doppler to Fly"
REQUIRED_SECRETS=(
DATABASE_URL
REDIS_URL
DISCOURSE_URL
DISCOURSE_API_KEY
DISCOURSE_API_USERNAME
DISCOURSE_SSO_SECRET
SESSION_SECRET
SIWE_DOMAIN
CORS_ORIGIN
)

FLY_SECRET_ARGS=()
MISSING=()
for key in "${REQUIRED_SECRETS[@]}"; do
val=$(doppler secrets get "$key" --plain 2>/dev/null || true)
if [ -z "$val" ]; then
MISSING+=("$key")
else
FLY_SECRET_ARGS+=("$key=$val")
fi
done

if [ ${#MISSING[@]} -gt 0 ]; then
fail "missing secrets in Doppler: ${MISSING[*]}"
echo "Set them with:"
for key in "${MISSING[@]}"; do
echo " doppler secrets set $key=<value>"
done
exit 1
fi

cd "$BFF_DIR"
fly secrets set "${FLY_SECRET_ARGS[@]}" --app "$FLY_APP" --stage
ok "secrets staged"

# ── 5. Deploy ───────────────────────────────────────────────────────
log "fly deploy"
fly deploy --app "$FLY_APP" --remote-only --ha=false

# ── 6. Smoke test ───────────────────────────────────────────────────
log "smoke test /health endpoint"
URL="https://$FLY_APP.fly.dev/health"
for i in 1 2 3 4 5; do
if curl -sf "$URL" >/dev/null; then
ok "$URL returned 200"
break
fi
echo " attempt $i/5 failed, retrying in 3s..."
sleep 3
done

echo
echo "Deployed: https://$FLY_APP.fly.dev"
echo "GraphQL: https://$FLY_APP.fly.dev/graphql"
echo
echo -e "${DIM}Rollback: fly releases --app $FLY_APP list${RESET}"
echo -e "${DIM} fly deploy --app $FLY_APP --image <previous-image>${RESET}"
99 changes: 99 additions & 0 deletions scripts/deploy-contracts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# deploy-contracts.sh — deploy PolisArchive.sol to Base Sepolia or mainnet.
# Not idempotent — each run creates a new deployment. For upgrades to an
# existing contract, use transferAdmin or deploy a new version and migrate.

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CONTRACTS_DIR="$REPO_ROOT/packages/contracts"

YELLOW='\033[1;33m'
GREEN='\033[0;32m'
RED='\033[0;31m'
DIM='\033[2m'
RESET='\033[0m'

log() { echo -e "${YELLOW}▸${RESET} $1"; }
ok() { echo -e "${GREEN}✓${RESET} $1"; }
fail() { echo -e "${RED}✗${RESET} $1" >&2; }

# ── 1. Pick network ─────────────────────────────────────────────────
NETWORK="${1:-sepolia}"
case "$NETWORK" in
sepolia)
RPC_VAR="BASE_SEPOLIA_RPC_URL"
CHAIN_ID=84532
EXPLORER="https://sepolia.basescan.org"
;;
mainnet)
RPC_VAR="BASE_RPC_URL"
CHAIN_ID=8453
EXPLORER="https://basescan.org"
;;
*)
fail "Usage: $0 [sepolia|mainnet]"
exit 1
;;
esac

log "target network: Base $NETWORK (chainId=$CHAIN_ID)"

# ── 2. Load env ─────────────────────────────────────────────────────
# Precedence: Doppler > shell env > .env file in contracts dir
if [ -f "$CONTRACTS_DIR/.env" ]; then
# shellcheck disable=SC1091
set -a; source "$CONTRACTS_DIR/.env"; set +a
fi

: "${PRIVATE_KEY:?PRIVATE_KEY not set — add to $CONTRACTS_DIR/.env or export in shell}"
: "${!RPC_VAR:?$RPC_VAR not set}"
: "${BASESCAN_API_KEY:?BASESCAN_API_KEY not set (needed for --verify)}"

RPC_URL="${!RPC_VAR}"

# ── 3. Final confirmation for mainnet ──────────────────────────────
if [ "$NETWORK" = "mainnet" ]; then
echo
echo "⚠ You are deploying to Base MAINNET. This costs real ETH."
read -p "Type 'DEPLOY TO MAINNET' to proceed: " -r reply
if [ "$reply" != "DEPLOY TO MAINNET" ]; then
echo "Aborted."
exit 0
fi
fi

# ── 4. Build + deploy ───────────────────────────────────────────────
log "forge build"
cd "$CONTRACTS_DIR"
forge build --sizes

log "forge script DeployPolisArchive.s.sol"
forge script script/DeployPolisArchive.s.sol \
--rpc-url "$RPC_URL" \
--broadcast \
--verify \
--etherscan-api-key "$BASESCAN_API_KEY" \
-vvv

# ── 5. Extract deployed address ─────────────────────────────────────
BROADCAST_FILE="$CONTRACTS_DIR/broadcast/DeployPolisArchive.s.sol/$CHAIN_ID/run-latest.json"
if [ -f "$BROADCAST_FILE" ]; then
ADDR=$(jq -r '.transactions[] | select(.contractName=="PolisArchive") | .contractAddress' "$BROADCAST_FILE" | head -1)
ok "deployed: $ADDR"
echo
echo "Explorer: $EXPLORER/address/$ADDR"
echo
echo "Save to Doppler:"
if [ "$NETWORK" = "mainnet" ]; then
echo " doppler secrets set ARCHIVE_CONTRACT=$ADDR"
else
echo " doppler secrets set ARCHIVE_CONTRACT_SEPOLIA=$ADDR"
fi
else
fail "broadcast file not found at $BROADCAST_FILE"
exit 1
fi

echo
echo -e "${DIM}Next: register your city with ./scripts/deploy-contracts.sh register $ADDR${RESET}"
68 changes: 68 additions & 0 deletions scripts/deploy-discourse.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# deploy-discourse.sh — provision Hetzner VPS and bootstrap Discourse.
# First run takes ~25 min (terraform + cloud-init + launcher rebuild).
# Subsequent runs are fast — terraform is idempotent, SSH step is a no-op if already set up.

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TF_DIR="$REPO_ROOT/infra/terraform/hetzner"

YELLOW='\033[1;33m'
GREEN='\033[0;32m'
RED='\033[0;31m'
DIM='\033[2m'
RESET='\033[0m'

log() { echo -e "${YELLOW}▸${RESET} $1"; }
ok() { echo -e "${GREEN}✓${RESET} $1"; }
fail() { echo -e "${RED}✗${RESET} $1" >&2; }

# ── 1. Validate inputs ──────────────────────────────────────────────
if [ ! -f "$TF_DIR/terraform.tfvars" ]; then
fail "terraform.tfvars not found at $TF_DIR/terraform.tfvars"
echo "Copy the example and fill it in:"
echo " cp $TF_DIR/terraform.tfvars.example $TF_DIR/terraform.tfvars"
echo " \$EDITOR $TF_DIR/terraform.tfvars"
exit 1
fi

# ── 2. Terraform apply ──────────────────────────────────────────────
log "terraform init"
terraform -chdir="$TF_DIR" init -upgrade -input=false

log "terraform plan"
terraform -chdir="$TF_DIR" plan -out=tfplan -input=false

echo
read -p "Apply this plan? (yes/no) " -r reply
if [ "$reply" != "yes" ]; then
echo "Aborted."
exit 0
fi

log "terraform apply"
terraform -chdir="$TF_DIR" apply -input=false tfplan
rm -f "$TF_DIR/tfplan"

SERVER_IP=$(terraform -chdir="$TF_DIR" output -raw server_ip)
ok "server provisioned: $SERVER_IP"

# ── 3. Post-install Discourse ───────────────────────────────────────
echo
log "Next step (manual): SSH in and run the Discourse container."
echo
echo " ssh root@$SERVER_IP"
echo " cd /var/discourse"
echo " ./launcher rebuild app # takes ~20 min the first time"
echo
echo "After the rebuild completes, visit https://forum.<your-domain>/ and:"
echo " 1. Create the admin account"
echo " 2. Admin → API → generate API key (user: system)"
echo " 3. Admin → Customize → Categories → create your polis.config categories"
echo " 4. Admin → Settings → Login → enable DiscourseConnect, set URL + SSO secret"
echo
echo "Store the API key in Doppler:"
echo " doppler secrets set DISCOURSE_API_KEY=<paste>"
echo
echo -e "${DIM}Rollback: terraform -chdir=$TF_DIR destroy${RESET}"
Loading
Loading