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:
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
51
services/arbiter-3.0/src/routes/admin/system.js
Normal file
51
services/arbiter-3.0/src/routes/admin/system.js
Normal 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;
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user