feat: push-to-deploy pipeline on Hermes VPS (task #47)
vps/ directory — all versioned, installed on VPS with one command: - vps/deploy.sh: pull from Hermes Gitea → pnpm build → deploy bundle → health check → auto-rollback on failure - vps/webhook.js: Node.js webhook receiver (port 9000, HMAC-SHA256) validates Gitea signature, runs deploy.sh, skips non-main branches - vps/timmy-deploy-hook.service: systemd unit for webhook receiver - vps/timmy-health.service + .timer: health watchdog every 5 min, auto-restarts timmy-tower if /api/health returns non-200 - vps/install.sh: one-time VPS setup — installs scripts, sets WEBHOOK_SECRET in .env, adds nginx /webhook/deploy block, enables services Gitea webhook configured on admin/timmy-tower (id: 1): - URL: http://143.198.27.163/webhook/deploy - HMAC secret stored in .local/deploy-webhook-secret (gitignored) One-time install command: WEBHOOK_SECRET=$(cat .local/deploy-webhook-secret) \ ssh root@143.198.27.163 'bash -s' < vps/install.sh replit.md: removed stale bore-tunnel push instructions; documented sovereign deploy workflow, monitoring commands, and rollback procedure
This commit is contained in:
156
replit.md
156
replit.md
@@ -296,98 +296,90 @@ DB tables: `sessions` (state machine, balance, macaroon), `session_requests` (pe
|
||||
|
||||
## Pushing to Gitea
|
||||
|
||||
Gitea runs on the local Mac behind a bore tunnel. The bore port changes every session.
|
||||
|
||||
### One-time setup
|
||||
```bash
|
||||
bash scripts/push-to-gitea.sh <PORT> # saves port to .bore-port, then pushes
|
||||
```
|
||||
|
||||
### Every subsequent push this session
|
||||
```bash
|
||||
bash scripts/push-to-gitea.sh # reads port from .bore-port automatically
|
||||
```
|
||||
|
||||
### When bore restarts (new port)
|
||||
Bore assigns a new random port on each restart. You must pass it once — after that `.bore-port` remembers it:
|
||||
```bash
|
||||
bash scripts/push-to-gitea.sh <NEW_PORT> # overwrites .bore-port, then pushes
|
||||
```
|
||||
|
||||
**How to find the bore port:** The port is shown in the Mac terminal where bore is running:
|
||||
```
|
||||
bore local 3000 --to bore.pub
|
||||
→ "listening at bore.pub:NNNNN"
|
||||
```
|
||||
|
||||
**Credentials (GITEA_TOKEN):** The script never hard-codes a token. Set it one of two ways:
|
||||
Hermes VPS Gitea is the primary remote. No bore tunnel or Tailscale needed.
|
||||
|
||||
```bash
|
||||
# Option A — env var (add to shell profile for persistence)
|
||||
export GITEA_TOKEN=<your-token>
|
||||
|
||||
# Option B — gitignored credentials file (one-time setup)
|
||||
echo <your-token> > .gitea-credentials
|
||||
bash scripts/push-to-gitea.sh # push to Hermes Gitea from any session
|
||||
```
|
||||
|
||||
Get your token from Gitea → User Settings → Applications → Generate Token.
|
||||
The script authenticates as the `replit` user using the token in `.gitea-credentials`.
|
||||
The `gitea` remote points to `http://143.198.27.163:3000/admin/timmy-tower.git`.
|
||||
|
||||
**Rules:**
|
||||
- Always create a branch and open a PR — never push directly to `main` (Gitea enforces this)
|
||||
- The `.bore-port` and `.gitea-credentials` files are gitignored — never committed
|
||||
### Gitea — Hermes VPS (primary remote)
|
||||
|
||||
### Gitea repos
|
||||
- `replit/token-gated-economy` — TypeScript API server (this repo)
|
||||
- `perplexity/the-matrix` — Three.js 3D world frontend
|
||||
|
||||
## Deployment
|
||||
|
||||
### Canonical deployment config — artifact.toml (not .replit)
|
||||
|
||||
The API server's authoritative deployment configuration lives in
|
||||
`artifacts/api-server/.replit-artifact/artifact.toml`. This file controls the
|
||||
production build command and the run command for the always-on VM deployment.
|
||||
|
||||
```
|
||||
deploymentTarget = "vm"
|
||||
buildCommand = "pnpm --filter @workspace/api-server run build"
|
||||
runCommand = "node artifacts/api-server/dist/index.js"
|
||||
```
|
||||
|
||||
The root `.replit` file may show an older `deploymentTarget = "autoscale"` and
|
||||
`run = "dist/index.cjs"` — these are legacy entries left from when Replit
|
||||
platform protection blocked agent edits. **artifact.toml is the source of
|
||||
truth**; `.replit` entries for this artifact should be ignored.
|
||||
|
||||
### Hermes Gitea (backup / deployment source)
|
||||
|
||||
All workspace code is mirrored to a self-hosted Gitea instance on hermes backed by PostgreSQL.
|
||||
This is the second git remote — independent of the Mac Tailscale Gitea — so no single machine holds all the code.
|
||||
All code lives on Hermes Gitea. The old Mac bore-tunnel Gitea is obsolete.
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Web UI | `http://143.198.27.163:3000/admin/timmy-tower` |
|
||||
| SSH (git) | `ssh://git@143.198.27.163:2222` |
|
||||
| DB backend | PostgreSQL (`gitea` DB on hermes) |
|
||||
| Admin user | `admin` |
|
||||
| Token store | `/root/.gitea-replit-token` on VPS |
|
||||
| Admin creds | `admin` / `hermes_gitea_admin_2024` |
|
||||
| replit user | `replit` / token in `.gitea-credentials` |
|
||||
|
||||
**Push from Replit** (any session):
|
||||
```bash
|
||||
bash scripts/push-to-hermes.sh
|
||||
bash scripts/push-to-gitea.sh
|
||||
```
|
||||
This script fetches the API token from the VPS over SSH (never stored in git), adds the `hermes` remote, and pushes all branches + tags.
|
||||
No bore tunnel, no Tailscale needed — Hermes is publicly accessible.
|
||||
|
||||
**Postgres backup** — the Gitea metadata lives in the `gitea` PostgreSQL DB. Backup with:
|
||||
**Postgres backup:**
|
||||
```bash
|
||||
# On hermes
|
||||
# On hermes VPS
|
||||
sudo -u postgres pg_dump gitea > /root/gitea-backup-$(date +%Y%m%d).sql
|
||||
```
|
||||
The bare git objects live in `/var/lib/gitea/repositories/` and can be backed up with rsync or tar.
|
||||
|
||||
### VPS deployment (hermes — 143.198.27.163)
|
||||
## Deployment
|
||||
|
||||
The production instance runs on the user's VPS via systemd, outside Replit:
|
||||
### Sovereign push-to-deploy pipeline
|
||||
|
||||
Push to `main` on Hermes Gitea → webhook fires → VPS pulls, builds, restarts.
|
||||
No Replit required. Deploy from any device with git access.
|
||||
|
||||
```
|
||||
git push gitea main → Gitea webhook → VPS deploy.sh → service restart
|
||||
```
|
||||
|
||||
**Deploy infrastructure lives in `vps/`** (versioned in this repo):
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `vps/deploy.sh` | Clones repo, builds with pnpm, deploys bundle, health-checks, rolls back on failure |
|
||||
| `vps/webhook.js` | Node.js webhook receiver — validates HMAC, runs deploy.sh |
|
||||
| `vps/timmy-deploy-hook.service` | systemd unit for webhook receiver |
|
||||
| `vps/timmy-health.service/.timer` | systemd timer — health-checks every 5 min, auto-restarts |
|
||||
| `vps/install.sh` | One-time setup: installs all of the above on the VPS |
|
||||
|
||||
**One-time install** (run once from a machine with VPS SSH access):
|
||||
```bash
|
||||
# With pre-configured Gitea webhook secret:
|
||||
WEBHOOK_SECRET=$(cat .local/deploy-webhook-secret) \
|
||||
ssh root@143.198.27.163 'bash -s' < vps/install.sh
|
||||
```
|
||||
|
||||
**Gitea webhook** — already configured on `admin/timmy-tower` (id: 1):
|
||||
- URL: `http://143.198.27.163/webhook/deploy`
|
||||
- Secret: stored in `.local/deploy-webhook-secret` (gitignored)
|
||||
- Trigger: push to any branch (filtered to `main` in webhook.js)
|
||||
|
||||
**Monitoring:**
|
||||
```bash
|
||||
# Watch deploy logs live
|
||||
ssh root@143.198.27.163 'tail -f /opt/timmy-tower/deploy.log'
|
||||
|
||||
# Watch health check logs
|
||||
ssh root@143.198.27.163 'tail -f /opt/timmy-tower/health.log'
|
||||
|
||||
# Manual deploy (bypasses webhook)
|
||||
ssh root@143.198.27.163 'bash /opt/timmy-tower/deploy.sh'
|
||||
|
||||
# Webhook service status
|
||||
ssh root@143.198.27.163 'systemctl status timmy-deploy-hook'
|
||||
```
|
||||
|
||||
**Rollback:** `git revert HEAD && git push gitea main` — triggers a re-deploy automatically.
|
||||
|
||||
### VPS production instance
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
@@ -395,23 +387,27 @@ The production instance runs on the user's VPS via systemd, outside Replit:
|
||||
| Service | `systemctl status timmy-tower` |
|
||||
| Deploy dir | `/opt/timmy-tower/` |
|
||||
| Env file | `/opt/timmy-tower/.env` |
|
||||
| Deploy log | `/opt/timmy-tower/deploy.log` |
|
||||
| DB | `postgres://timmy:...@localhost:5432/timmy_tower` |
|
||||
| Nostr npub | `npub1e3gu2j08t6hymjd5sz9dmy4u5pcl22mj5hl60avkpj5tdpaq3dasjax6tv` |
|
||||
| AI backend | OpenRouter (`https://openrouter.ai/api/v1`) via Anthropic SDK compat layer |
|
||||
|
||||
To redeploy after a build:
|
||||
```bash
|
||||
# From Replit — rebuild and copy bundle
|
||||
pnpm --filter @workspace/api-server run build
|
||||
cat artifacts/api-server/dist/index.js | ssh root@143.198.27.163 "cat > /opt/timmy-tower/index.js"
|
||||
ssh root@143.198.27.163 "systemctl restart timmy-tower"
|
||||
```
|
||||
|
||||
External packages that must be present in `/opt/timmy-tower/node_modules/`:
|
||||
External packages required in `/opt/timmy-tower/node_modules/`:
|
||||
- `nostr-tools` (^2.23.3)
|
||||
- `cookie-parser` (^1.4.7)
|
||||
|
||||
These are externalized by esbuild (not in the allowlist in `build.ts`).
|
||||
These are externalized by esbuild (not in the bundle allowlist in `build.ts`).
|
||||
|
||||
### Replit deployment (secondary)
|
||||
|
||||
The Replit VM deployment is still active for development/staging use.
|
||||
Artifact config: `artifacts/api-server/.replit-artifact/artifact.toml`
|
||||
|
||||
```
|
||||
deploymentTarget = "vm"
|
||||
buildCommand = "pnpm --filter @workspace/api-server run build"
|
||||
runCommand = "node artifacts/api-server/dist/index.js"
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
||||
126
vps/install.sh
Normal file
126
vps/install.sh
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# vps/install.sh — One-time setup of push-to-deploy pipeline on Hermes VPS
|
||||
#
|
||||
# Run once from the VPS (or from a machine with SSH access):
|
||||
# bash vps/install.sh
|
||||
# OR remotely:
|
||||
# ssh root@143.198.27.163 'bash -s' < vps/install.sh
|
||||
#
|
||||
# What it does:
|
||||
# 1. Ensures Node 24 + pnpm are available
|
||||
# 2. Installs deploy/webhook/health scripts to /opt/timmy-tower/
|
||||
# 3. Generates a WEBHOOK_SECRET and adds to .env
|
||||
# 4. Adds nginx location block for /webhook/deploy
|
||||
# 5. Enables + starts systemd services: timmy-deploy-hook, timmy-health.timer
|
||||
# 6. Prints the webhook secret so you can configure the Gitea webhook
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOY_DIR="/opt/timmy-tower"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
NGINX_CONF="/etc/nginx/sites-enabled/default"
|
||||
WEBHOOK_PORT=9000
|
||||
|
||||
log() { echo "[install] $*"; }
|
||||
ok() { echo "[install] ✓ $*"; }
|
||||
err() { echo "[install] ✗ $*" >&2; exit 1; }
|
||||
|
||||
# ── 1. Ensure Node 24 ─────────────────────────────────────────────────────────
|
||||
log "Checking Node.js..."
|
||||
NODE_VER=$(node --version 2>/dev/null || echo "none")
|
||||
if [[ "$NODE_VER" == none ]]; then
|
||||
err "Node.js is not installed. Install Node 24 first: https://nodejs.org"
|
||||
fi
|
||||
ok "Node.js $NODE_VER"
|
||||
|
||||
# ── 2. Ensure pnpm ────────────────────────────────────────────────────────────
|
||||
log "Checking pnpm..."
|
||||
if ! command -v pnpm &>/dev/null; then
|
||||
log "Installing pnpm via corepack..."
|
||||
corepack enable
|
||||
corepack prepare pnpm@latest --activate
|
||||
fi
|
||||
ok "pnpm $(pnpm --version)"
|
||||
|
||||
# ── 3. Copy scripts to deploy dir ─────────────────────────────────────────────
|
||||
log "Copying scripts to $DEPLOY_DIR..."
|
||||
cp "$SCRIPT_DIR/deploy.sh" "$DEPLOY_DIR/deploy.sh"
|
||||
cp "$SCRIPT_DIR/webhook.js" "$DEPLOY_DIR/webhook.js"
|
||||
cp "$SCRIPT_DIR/health-check.sh" "$DEPLOY_DIR/health-check.sh"
|
||||
chmod +x "$DEPLOY_DIR/deploy.sh" "$DEPLOY_DIR/health-check.sh"
|
||||
ok "Scripts installed."
|
||||
|
||||
# ── 4. Set WEBHOOK_SECRET in .env ────────────────────────────────────────────
|
||||
# Priority: $WEBHOOK_SECRET env var → existing .env entry → generate new one
|
||||
ENV_FILE="$DEPLOY_DIR/.env"
|
||||
if [ -n "${WEBHOOK_SECRET:-}" ]; then
|
||||
# Caller provided secret — write or replace in .env
|
||||
if grep -q "^WEBHOOK_SECRET=" "$ENV_FILE" 2>/dev/null; then
|
||||
sed -i "s|^WEBHOOK_SECRET=.*|WEBHOOK_SECRET=$WEBHOOK_SECRET|" "$ENV_FILE"
|
||||
else
|
||||
echo "WEBHOOK_SECRET=$WEBHOOK_SECRET" >> "$ENV_FILE"
|
||||
fi
|
||||
ok "WEBHOOK_SECRET set from environment."
|
||||
elif grep -q "^WEBHOOK_SECRET=" "$ENV_FILE" 2>/dev/null; then
|
||||
WEBHOOK_SECRET=$(grep "^WEBHOOK_SECRET=" "$ENV_FILE" | cut -d= -f2-)
|
||||
log "WEBHOOK_SECRET already in .env — keeping existing value."
|
||||
else
|
||||
WEBHOOK_SECRET=$(openssl rand -hex 32)
|
||||
echo "WEBHOOK_SECRET=$WEBHOOK_SECRET" >> "$ENV_FILE"
|
||||
ok "WEBHOOK_SECRET generated and saved to .env"
|
||||
log "NOTE: Update the Gitea webhook secret to match: $WEBHOOK_SECRET"
|
||||
fi
|
||||
|
||||
# ── 5. Install systemd services ───────────────────────────────────────────────
|
||||
log "Installing systemd units..."
|
||||
cp "$SCRIPT_DIR/timmy-deploy-hook.service" /etc/systemd/system/
|
||||
cp "$SCRIPT_DIR/timmy-health.service" /etc/systemd/system/
|
||||
cp "$SCRIPT_DIR/timmy-health.timer" /etc/systemd/system/
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now timmy-deploy-hook
|
||||
systemctl enable --now timmy-health.timer
|
||||
ok "Services enabled: timmy-deploy-hook, timmy-health.timer"
|
||||
|
||||
# ── 6. Nginx — add /webhook/deploy proxy block ───────────────────────────────
|
||||
if grep -q "webhook/deploy" "$NGINX_CONF" 2>/dev/null; then
|
||||
log "nginx already has /webhook/deploy block — skipping"
|
||||
else
|
||||
log "Adding nginx proxy for /webhook/deploy..."
|
||||
BLOCK="
|
||||
# Timmy deploy webhook (managed by install.sh)
|
||||
location /webhook/deploy {
|
||||
proxy_pass http://127.0.0.1:${WEBHOOK_PORT}/deploy;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_read_timeout 10s;
|
||||
}"
|
||||
|
||||
# Insert before the closing } of the server block
|
||||
sed -i "s|^}|${BLOCK}\n}|" "$NGINX_CONF"
|
||||
nginx -t && systemctl reload nginx
|
||||
ok "nginx updated and reloaded."
|
||||
fi
|
||||
|
||||
# ── 7. Print summary ─────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo " Push-to-deploy pipeline installed on Hermes VPS"
|
||||
echo "================================================================"
|
||||
echo ""
|
||||
echo " Webhook endpoint: http://143.198.27.163/webhook/deploy"
|
||||
echo " WEBHOOK_SECRET: $WEBHOOK_SECRET"
|
||||
echo ""
|
||||
echo " Configure this Gitea webhook on admin/timmy-tower:"
|
||||
echo " URL: http://143.198.27.163/webhook/deploy"
|
||||
echo " Secret: $WEBHOOK_SECRET"
|
||||
echo " Events: Push"
|
||||
echo " Branch: main (filter in webhook.js)"
|
||||
echo ""
|
||||
echo " Useful commands:"
|
||||
echo " tail -f $DEPLOY_DIR/deploy.log # watch deploy logs"
|
||||
echo " tail -f $DEPLOY_DIR/health.log # watch health logs"
|
||||
echo " systemctl status timmy-deploy-hook # webhook service status"
|
||||
echo " bash $DEPLOY_DIR/deploy.sh # manual deploy"
|
||||
echo "================================================================"
|
||||
Reference in New Issue
Block a user