Published your App
Replit-Commit-Author: Deployment Replit-Commit-Session-Id: 90c7a60b-2c61-4699-b5c6-6a1ac7469a4d Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: bca5769b-f33f-4202-85e3-b4f84e426350 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/90c7a60b-2c61-4699-b5c6-6a1ac7469a4d/G03TLre Replit-Commit-Deployment-Build-Id: 6750cd6c-5980-4b2b-bcd1-ceb093d94078 Replit-Helium-Checkpoint-Created: true
This commit is contained in:
85
vps/deploy.sh
Normal file
85
vps/deploy.sh
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# /opt/timmy-tower/deploy.sh
|
||||
# Pull latest from Hermes Gitea, build, deploy, health-check, rollback on fail.
|
||||
# Invoked by the webhook receiver (webhook.js) or manually by an operator.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOY_DIR="/opt/timmy-tower"
|
||||
WORK_DIR="/tmp/timmy-deploy-$$"
|
||||
LOG="$DEPLOY_DIR/deploy.log"
|
||||
TOKEN_FILE="/root/.gitea-replit-token"
|
||||
GITEA_HOST="143.198.27.163:3000"
|
||||
GITEA_REPO="admin/timmy-tower"
|
||||
HEALTH_URL="http://localhost:8088/api/health"
|
||||
|
||||
log() { echo "[$(date -u +%FT%TZ)] [deploy] $*" | tee -a "$LOG"; }
|
||||
fail() { log "ERROR: $*"; exit 1; }
|
||||
|
||||
log "========================================"
|
||||
log "Deploy started"
|
||||
|
||||
# ── 1. Backup current bundle ──────────────────────────────────────────────────
|
||||
if [ -f "$DEPLOY_DIR/index.js" ]; then
|
||||
cp "$DEPLOY_DIR/index.js" "$DEPLOY_DIR/index.js.bak"
|
||||
log "Previous bundle backed up → index.js.bak"
|
||||
fi
|
||||
|
||||
# ── 2. Clone repo from Hermes Gitea ──────────────────────────────────────────
|
||||
TOKEN=$(tr -d '[:space:]' < "$TOKEN_FILE")
|
||||
log "Cloning repo from Hermes Gitea..."
|
||||
git clone --depth=1 \
|
||||
"http://admin:${TOKEN}@${GITEA_HOST}/${GITEA_REPO}.git" \
|
||||
"$WORK_DIR" 2>&1 | tail -3 | tee -a "$LOG"
|
||||
|
||||
# ── 3. Install deps + build ───────────────────────────────────────────────────
|
||||
cd "$WORK_DIR"
|
||||
|
||||
log "Ensuring pnpm is available..."
|
||||
if ! command -v pnpm &>/dev/null; then
|
||||
corepack enable
|
||||
corepack prepare pnpm@latest --activate
|
||||
fi
|
||||
|
||||
log "Installing dependencies..."
|
||||
pnpm install --frozen-lockfile 2>&1 | tail -5 | tee -a "$LOG" \
|
||||
|| fail "pnpm install failed"
|
||||
|
||||
log "Building api-server..."
|
||||
pnpm --filter @workspace/api-server run build 2>&1 | tail -15 | tee -a "$LOG" \
|
||||
|| fail "build failed"
|
||||
|
||||
# ── 4. Deploy bundle ──────────────────────────────────────────────────────────
|
||||
BUNDLE="$WORK_DIR/artifacts/api-server/dist/index.js"
|
||||
[ -f "$BUNDLE" ] || fail "Bundle not found at $BUNDLE after build"
|
||||
|
||||
cp "$BUNDLE" "$DEPLOY_DIR/index.js"
|
||||
log "Bundle deployed."
|
||||
|
||||
# ── 5. Restart service ────────────────────────────────────────────────────────
|
||||
log "Restarting timmy-tower..."
|
||||
systemctl restart timmy-tower
|
||||
sleep 6
|
||||
|
||||
# ── 6. Health check — rollback on failure ────────────────────────────────────
|
||||
if curl -sf --max-time 15 "$HEALTH_URL" > /dev/null; then
|
||||
log "Health check passed ✓"
|
||||
else
|
||||
log "Health check FAILED — rolling back"
|
||||
if [ -f "$DEPLOY_DIR/index.js.bak" ]; then
|
||||
cp "$DEPLOY_DIR/index.js.bak" "$DEPLOY_DIR/index.js"
|
||||
systemctl restart timmy-tower
|
||||
log "Rollback complete. Previous bundle restored."
|
||||
else
|
||||
log "No backup available — service may be down"
|
||||
fi
|
||||
cd /; rm -rf "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── 7. Cleanup ────────────────────────────────────────────────────────────────
|
||||
cd /
|
||||
rm -rf "$WORK_DIR"
|
||||
log "Deploy complete"
|
||||
log "========================================"
|
||||
24
vps/health-check.sh
Normal file
24
vps/health-check.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# /opt/timmy-tower/health-check.sh
|
||||
# Run by systemd timer every 5 minutes.
|
||||
# Restarts timmy-tower if /api/health returns non-200.
|
||||
# =============================================================================
|
||||
HEALTH_URL="http://localhost:8088/api/health"
|
||||
LOG="/opt/timmy-tower/health.log"
|
||||
|
||||
log() { echo "[$(date -u +%FT%TZ)] [health] $*" | tee -a "$LOG"; }
|
||||
|
||||
if curl -sf --max-time 10 "$HEALTH_URL" > /dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Health check FAILED — restarting timmy-tower"
|
||||
systemctl restart timmy-tower
|
||||
sleep 5
|
||||
|
||||
if curl -sf --max-time 10 "$HEALTH_URL" > /dev/null; then
|
||||
log "Service recovered after restart"
|
||||
else
|
||||
log "CRITICAL: service did not recover after restart — manual intervention needed"
|
||||
fi
|
||||
18
vps/timmy-deploy-hook.service
Normal file
18
vps/timmy-deploy-hook.service
Normal file
@@ -0,0 +1,18 @@
|
||||
[Unit]
|
||||
Description=Timmy Tower Deploy Webhook Receiver
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/timmy-tower
|
||||
ExecStart=/usr/bin/node /opt/timmy-tower/webhook.js
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
EnvironmentFile=/opt/timmy-tower/.env
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=timmy-deploy-hook
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
11
vps/timmy-health.service
Normal file
11
vps/timmy-health.service
Normal file
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Timmy Tower Health Check (one-shot)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
ExecStart=/opt/timmy-tower/health-check.sh
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=timmy-health
|
||||
10
vps/timmy-health.timer
Normal file
10
vps/timmy-health.timer
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Timmy Tower Health Check — every 5 minutes
|
||||
|
||||
[Timer]
|
||||
OnBootSec=60
|
||||
OnUnitActiveSec=5min
|
||||
Unit=timmy-health.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
114
vps/webhook.js
Normal file
114
vps/webhook.js
Normal file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env node
|
||||
// =============================================================================
|
||||
// /opt/timmy-tower/webhook.js
|
||||
// Lightweight webhook receiver: validates Gitea HMAC signature, runs deploy.sh
|
||||
// Runs as systemd service: timmy-deploy-hook
|
||||
// =============================================================================
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const crypto = require('crypto');
|
||||
const { execFile } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = 9000;
|
||||
const HOST = '127.0.0.1';
|
||||
const SECRET = process.env.WEBHOOK_SECRET || '';
|
||||
const DEPLOY_SCRIPT = path.join(__dirname, 'deploy.sh');
|
||||
const LOG_FILE = path.join(__dirname, 'deploy.log');
|
||||
|
||||
let deploying = false;
|
||||
|
||||
function log(msg) {
|
||||
const line = `[${new Date().toISOString()}] [webhook] ${msg}\n`;
|
||||
process.stdout.write(line);
|
||||
try { fs.appendFileSync(LOG_FILE, line); } catch (_) {}
|
||||
}
|
||||
|
||||
function verifySignature(payload, signature) {
|
||||
if (!SECRET) {
|
||||
log('WARN: WEBHOOK_SECRET not set — skipping signature check');
|
||||
return true;
|
||||
}
|
||||
const hmac = crypto.createHmac('sha256', SECRET);
|
||||
hmac.update(payload);
|
||||
const expected = 'sha256=' + hmac.digest('hex');
|
||||
try {
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(expected, 'utf8'),
|
||||
Buffer.from(signature, 'utf8')
|
||||
);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.method === 'GET' && req.url === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true, deploying }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== 'POST' || req.url !== '/deploy') {
|
||||
res.writeHead(404).end('Not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks = [];
|
||||
req.on('data', chunk => chunks.push(chunk));
|
||||
req.on('end', () => {
|
||||
const body = Buffer.concat(chunks);
|
||||
const sig = req.headers['x-gitea-signature']
|
||||
|| req.headers['x-hub-signature-256']
|
||||
|| '';
|
||||
|
||||
if (!verifySignature(body, sig)) {
|
||||
log(`Rejected deploy: invalid HMAC signature from ${req.socket.remoteAddress}`);
|
||||
res.writeHead(401).end('Unauthorized');
|
||||
return;
|
||||
}
|
||||
|
||||
let payload = {};
|
||||
try { payload = JSON.parse(body.toString()); } catch (_) {}
|
||||
|
||||
const branch = (payload.ref || '').replace('refs/heads/', '');
|
||||
if (branch && branch !== 'main') {
|
||||
log(`Skipping push to non-main branch: ${branch}`);
|
||||
res.writeHead(200).end('Skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
if (deploying) {
|
||||
log('Deploy already in progress — request queued');
|
||||
res.writeHead(202).end('Deploy already in progress');
|
||||
return;
|
||||
}
|
||||
|
||||
const pusher = payload.pusher ? payload.pusher.login || payload.pusher.name : 'unknown';
|
||||
const commit = payload.after ? payload.after.slice(0, 8) : 'unknown';
|
||||
log(`Deploy triggered — pusher: ${pusher}, commit: ${commit}`);
|
||||
|
||||
res.writeHead(202).end('Deploy started');
|
||||
|
||||
deploying = true;
|
||||
execFile('bash', [DEPLOY_SCRIPT], { timeout: 600_000 }, (err, stdout, stderr) => {
|
||||
deploying = false;
|
||||
if (err) {
|
||||
log(`Deploy FAILED: ${err.message}`);
|
||||
} else {
|
||||
log(`Deploy succeeded (commit ${commit})`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
log(`Webhook receiver listening on ${HOST}:${PORT}`);
|
||||
log(`Deploy script: ${DEPLOY_SCRIPT}`);
|
||||
log(`HMAC secret: ${SECRET ? 'configured' : 'NOT SET (insecure)'}`);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => { log('SIGTERM received — shutting down'); process.exit(0); });
|
||||
process.on('SIGINT', () => { log('SIGINT received — shutting down'); process.exit(0); });
|
||||
Reference in New Issue
Block a user