Add Trinity Console deploy button for Holly/Meg/Michael

- Deploy button in sidebar above username
- POST /admin/system/deploy endpoint
- Updated deploy.sh with locking, logging, user tracking
- Prevents concurrent deploys (mkdir lock)
- Logs who deployed and what commit
- Updated DEPLOYMENT.md with setup instructions

Gemini consultation: confirmed synchronous approach, locking, sudoers config
This commit is contained in:
Claude
2026-04-09 19:40:34 +00:00
parent dc59e5c1de
commit ef562ef59a
5 changed files with 214 additions and 35 deletions

View File

@@ -7,6 +7,19 @@
---
## 🚀 One-Click Deploy (Trinity Console)
**For Holly, Meg, and Michael:**
1. Push your code changes to `firefrost-services` repo
2. Open Trinity Console: https://discord-bot.firefrostgaming.com/admin
3. Click the **"🚀 Deploy Arbiter"** button in the sidebar
4. Wait for success confirmation
That's it! The button pulls latest code from Gitea and restarts the service.
---
## ⚠️ IMPORTANT: Arbiter is NOT a Git Repo
`/opt/arbiter-3.0` on Command Center is **not** a git repository. It's a deployment target.
@@ -17,30 +30,44 @@
---
## Quick Deploy (One Command)
## Manual Deploy (SSH Required)
**On Command Center:**
```bash
curl -fsSL https://git.firefrostgaming.com/firefrost-gaming/firefrost-services/raw/branch/main/services/arbiter-3.0/deploy.sh | bash
```
**Or if deploy.sh is already on server:**
```bash
bash /opt/arbiter-3.0/deploy.sh
```
**Or remote curl:**
```bash
curl -fsSL https://git.firefrostgaming.com/firefrost-gaming/firefrost-services/raw/branch/main/services/arbiter-3.0/deploy.sh | bash
```
---
## Manual Deploy
## First-Time Server Setup
If setting up deploy button for the first time, Michael needs to run these on Command Center:
**1. Copy deploy script to /opt/scripts:**
```bash
# On Command Center
cd /tmp
rm -rf firefrost-services
git clone https://git.firefrostgaming.com/firefrost-gaming/firefrost-services.git
cp -r firefrost-services/services/arbiter-3.0/src/* /opt/arbiter-3.0/src/
systemctl restart arbiter-3
rm -rf firefrost-services
sudo mkdir -p /opt/scripts
sudo cp /opt/arbiter-3.0/deploy.sh /opt/scripts/deploy-arbiter.sh
sudo chmod +x /opt/scripts/deploy-arbiter.sh
```
**2. Configure sudoers (allow Arbiter to run deploy script):**
```bash
sudo visudo
```
Add this line:
```
architect ALL=(ALL) NOPASSWD: /opt/scripts/deploy-arbiter.sh
```
**3. Create log file:**
```bash
sudo touch /var/log/trinity-deployments.log
sudo chown architect:architect /var/log/trinity-deployments.log
```
---
@@ -54,6 +81,9 @@ systemctl status arbiter-3
# Check logs
journalctl -u arbiter-3 -n 50
# Check deployment log
tail -20 /var/log/trinity-deployments.log
# Test dashboard
curl -s https://discord-bot.firefrostgaming.com/admin | head -5
```
@@ -62,13 +92,17 @@ curl -s https://discord-bot.firefrostgaming.com/admin | head -5
## Common Issues
### "Deployment already in progress"
**Cause:** Previous deploy didn't finish or crashed
**Fix:** `rm -rf /tmp/arbiter_deploy.lock` then try again
### "fatal: not a git repository"
**Cause:** Someone tried to `git pull` in `/opt/arbiter-3.0`
**Fix:** Use the deploy script or manual copy method above
### "/tmp/firefrost-services already exists"
**Cause:** Previous deploy didn't clean up
**Fix:** `rm -rf /tmp/firefrost-services` then try again
**Fix:** `rm -rf /tmp/firefrost-services /tmp/firefrost-services-deploy-*` then try again
### Service fails to start
**Check:** `journalctl -u arbiter-3 -n 50`
@@ -99,15 +133,15 @@ MINECRAFT_NEST_IDS=1,5
---
## For Chroniclers
## For Chroniclers & Catalysts
**You cannot SSH to Command Center** (port 22 blocked from Claude sandbox).
**Workflow:**
1. Make changes to `firefrost-services/services/arbiter-3.0/`
2. Commit and push to Gitea
3. Tell Michael to run: `bash /opt/arbiter-3.0/deploy.sh`
4. Verify via dashboard or ask Michael to check logs
3. Tell your human to click "🚀 Deploy Arbiter" in Trinity Console
4. Verify via dashboard or ask them to check logs
---

View File

@@ -1,7 +1,10 @@
#!/bin/bash
# Arbiter 3.0 Deployment Script
# Run on Command Center: bash /opt/arbiter-3.0/deploy.sh
# Or remotely: Copy this script to server and run
# Or via Trinity Console Deploy button
#
# Usage: deploy.sh [username]
# username: Optional - who triggered the deploy (for logging)
set -e # Exit on any error
@@ -9,45 +12,66 @@ REPO_URL="https://git.firefrostgaming.com/firefrost-gaming/firefrost-services.gi
TEMP_DIR="/tmp/firefrost-services-deploy-$$"
ARBITER_DIR="/opt/arbiter-3.0"
SERVICE_NAME="arbiter-3"
LOCKDIR="/tmp/arbiter_deploy.lock"
LOG_FILE="/var/log/trinity-deployments.log"
DEPLOY_USER="${1:-manual}"
echo "🔥❄️ Arbiter 3.0 Deployment"
echo "=========================="
# Logging function
log() {
echo "$1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# 1. Prevent concurrent deployments with a lock directory
if ! mkdir "$LOCKDIR" 2>/dev/null; then
echo "ERROR: Deployment already in progress." >&2
exit 1
fi
# Ensure lock is removed when script exits (success or failure)
trap 'rm -rf "$LOCKDIR"' EXIT
log "🔥❄️ Arbiter deployment started by: $DEPLOY_USER"
# Cleanup any old temp directories
rm -rf /tmp/firefrost-services /tmp/firefrost-services-deploy-*
# Clone fresh
echo "📥 Cloning firefrost-services..."
log "📥 Cloning firefrost-services..."
git clone --depth 1 "$REPO_URL" "$TEMP_DIR"
# Get commit info for logging
COMMIT_HASH=$(cd "$TEMP_DIR" && git log -1 --format="%h - %s")
log "📌 Deploying commit: $COMMIT_HASH"
# Copy arbiter files
echo "📋 Copying Arbiter files..."
log "📋 Copying Arbiter files..."
cp -r "$TEMP_DIR/services/arbiter-3.0/src/"* "$ARBITER_DIR/src/"
cp -r "$TEMP_DIR/services/arbiter-3.0/migrations/"* "$ARBITER_DIR/migrations/" 2>/dev/null || true
cp "$TEMP_DIR/services/arbiter-3.0/package.json" "$ARBITER_DIR/package.json" 2>/dev/null || true
# Check if package.json changed (need npm install)
if ! cmp -s "$TEMP_DIR/services/arbiter-3.0/package.json" "$ARBITER_DIR/package.json.bak" 2>/dev/null; then
echo "📦 Dependencies may have changed, running npm install..."
log "📦 Dependencies changed, running npm install..."
cd "$ARBITER_DIR"
npm install --production
npm install --production --ignore-scripts
cp "$ARBITER_DIR/package.json" "$ARBITER_DIR/package.json.bak"
fi
# Restart service
echo "🔄 Restarting $SERVICE_NAME..."
systemctl restart "$SERVICE_NAME"
# Cleanup
echo "🧹 Cleaning up..."
# Cleanup temp files BEFORE restart
log "🧹 Cleaning up temp files..."
rm -rf "$TEMP_DIR"
# Restart service
log "🔄 Restarting $SERVICE_NAME..."
systemctl restart "$SERVICE_NAME"
# Verify
sleep 2
if systemctl is-active --quiet "$SERVICE_NAME"; then
echo "✅ Arbiter 3.0 deployed and running!"
echo " Dashboard: https://discord-bot.firefrostgaming.com/admin"
log "✅ Arbiter deployed successfully! Commit: $COMMIT_HASH"
echo "SUCCESS: Deployed commit $COMMIT_HASH"
else
echo "❌ Service failed to start. Check: journalctl -u $SERVICE_NAME -n 50"
log "❌ Service failed to start after deploy"
echo "ERROR: Service failed to start. Check: journalctl -u $SERVICE_NAME -n 50" >&2
exit 1
fi

View File

@@ -13,6 +13,7 @@ const auditRouter = require('./audit');
const rolesRouter = require('./roles');
const schedulerRouter = require('./scheduler');
const discordAuditRouter = require('./discord-audit');
const systemRouter = require('./system');
router.use(requireTrinityAccess);
@@ -79,5 +80,6 @@ router.use('/audit', auditRouter);
router.use('/roles', rolesRouter);
router.use('/scheduler', schedulerRouter);
router.use('/discord', discordAuditRouter);
router.use('/system', systemRouter);
module.exports = router;

View File

@@ -0,0 +1,51 @@
const express = require('express');
const router = express.Router();
const { exec } = require('child_process');
const fs = require('fs');
/**
* System Routes - Maintenance & Deployment
*
* POST /admin/system/deploy - Deploy latest Arbiter code from Gitea
*/
// POST /admin/system/deploy - Pull latest code and restart Arbiter
router.post('/deploy', (req, res) => {
const username = req.user?.username || 'unknown';
console.log(`[DEPLOY] Deployment initiated by ${username}`);
// Run the deploy script with username for logging
// Script handles its own locking to prevent concurrent deploys
exec(`sudo /opt/scripts/deploy-arbiter.sh "${username}"`, {
timeout: 60000 // 60 second timeout
}, (error, stdout, stderr) => {
if (error) {
console.error(`[DEPLOY] Failed:`, stderr || error.message);
return res.status(500).json({
success: false,
message: 'Deployment failed',
log: stderr || error.message
});
}
console.log(`[DEPLOY] Success:`, stdout);
res.json({
success: true,
message: stdout.trim()
});
});
});
// GET /admin/system/status - Check if deploy script exists and arbiter is running
router.get('/status', (req, res) => {
exec('systemctl is-active arbiter-3', (error, stdout) => {
const isRunning = stdout.trim() === 'active';
res.json({
arbiter: isRunning ? 'running' : 'stopped',
deployAvailable: fs.existsSync('/opt/scripts/deploy-arbiter.sh')
});
});
});
module.exports = router;

View File

@@ -99,7 +99,19 @@
💬 Discord
</a>
</nav>
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
<div class="p-4 border-t border-gray-200 dark:border-gray-700 space-y-3">
<!-- Deploy Button -->
<button
id="deploy-btn"
onclick="deployArbiter()"
class="w-full px-4 py-2 bg-gradient-to-r from-fire to-frost text-white font-medium rounded-md hover:opacity-90 transition flex items-center justify-center gap-2"
>
<span id="deploy-icon">🚀</span>
<span id="deploy-text">Deploy Arbiter</span>
</button>
<div id="deploy-result" class="text-xs text-center hidden"></div>
<!-- User Info -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<img src="https://cdn.discordapp.com/avatars/<%= adminUser.id %>/<%= adminUser.avatar %>.png" class="w-10 h-10 rounded-full">
@@ -110,6 +122,62 @@
</a>
</div>
</div>
<script>
async function deployArbiter() {
const btn = document.getElementById('deploy-btn');
const icon = document.getElementById('deploy-icon');
const text = document.getElementById('deploy-text');
const result = document.getElementById('deploy-result');
// Disable button, show loading state
btn.disabled = true;
btn.classList.add('opacity-50', 'cursor-not-allowed');
icon.textContent = '⏳';
text.textContent = 'Deploying...';
result.classList.add('hidden');
try {
const response = await fetch('/admin/system/deploy', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': '<%= csrfToken %>'
}
});
const data = await response.json();
if (data.success) {
icon.textContent = '✅';
text.textContent = 'Deployed!';
result.textContent = data.message;
result.classList.remove('hidden', 'text-red-500');
result.classList.add('text-green-500');
} else {
icon.textContent = '❌';
text.textContent = 'Deploy Failed';
result.textContent = data.log || data.message;
result.classList.remove('hidden', 'text-green-500');
result.classList.add('text-red-500');
}
} catch (error) {
icon.textContent = '❌';
text.textContent = 'Deploy Failed';
result.textContent = error.message;
result.classList.remove('hidden', 'text-green-500');
result.classList.add('text-red-500');
}
// Re-enable button after 3 seconds
setTimeout(() => {
btn.disabled = false;
btn.classList.remove('opacity-50', 'cursor-not-allowed');
icon.textContent = '🚀';
text.textContent = 'Deploy Arbiter';
}, 3000);
}
</script>
</aside>
<main class="flex-1 flex flex-col overflow-hidden">