Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
c4547d2e52 feat: live staging with auto-refresh on main push (Fixes #33)
- index.html: polls Gitea API every 30s for new commits on main;
  shows countdown banner and auto-reloads when SHA changes
- Dockerfile + docker-compose.yml: serve the site via nginx;
  main on :4200, staging on :4201
- deploy.sh: rebuild and restart a named service
- .gitea/workflows/deploy.yml: SSH-deploy to host on every push to main
  (requires DEPLOY_HOST / DEPLOY_USER / DEPLOY_SSH_KEY repo secrets)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 21:20:58 -04:00
5 changed files with 75 additions and 98 deletions

View File

@@ -12,22 +12,15 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Deploy to production - name: Deploy to host via SSH
# SSH into the host and redeploy via docker compose. uses: appleboy/ssh-action@v1.0.3
# Set DEPLOY_HOST, DEPLOY_USER, and DEPLOY_SSH_KEY in repo secrets. with:
env: host: ${{ secrets.DEPLOY_HOST }}
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} username: ${{ secrets.DEPLOY_USER }}
HOST: ${{ secrets.DEPLOY_HOST }} key: ${{ secrets.DEPLOY_SSH_KEY }}
USER: ${{ secrets.DEPLOY_USER }} script: |
REPO_DIR: ${{ secrets.DEPLOY_REPO_DIR || '/opt/nexus' }} cd ~/the-nexus || git clone http://143.198.27.163:3000/Timmy_Foundation/the-nexus.git ~/the-nexus
run: | cd ~/the-nexus
if [ -z "$SSH_KEY" ] || [ -z "$HOST" ] || [ -z "$USER" ]; then git fetch origin main
echo "Deploy secrets not configured — skipping remote deploy." git reset --hard origin/main
echo "Set DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY in repo settings." ./deploy.sh main
exit 0
fi
echo "$SSH_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
ssh -o StrictHostKeyChecking=no -i /tmp/deploy_key "$USER@$HOST" \
"cd $REPO_DIR && git pull origin main && docker compose up -d --build nexus"
rm /tmp/deploy_key

View File

@@ -1,6 +1,6 @@
FROM node:20-alpine FROM nginx:alpine
WORKDIR /app COPY . /usr/share/nginx/html
COPY . . RUN rm -f /usr/share/nginx/html/Dockerfile \
RUN npm install -g serve /usr/share/nginx/html/docker-compose.yml \
EXPOSE 3000 /usr/share/nginx/html/deploy.sh
CMD ["serve", ".", "-l", "3000", "--no-clipboard"] EXPOSE 80

View File

@@ -1,20 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ◈ Nexus — quick deploy helper # deploy.sh — spin up (or update) the Nexus staging environment
# Usage: # Usage: ./deploy.sh — rebuild and restart nexus-main (port 4200)
# ./deploy.sh # deploy main to production (port 4200) # ./deploy.sh staging — rebuild and restart nexus-staging (port 4201)
# ./deploy.sh staging # deploy current branch to staging (port 4201)
set -euo pipefail set -euo pipefail
BRANCH=$(git rev-parse --abbrev-ref HEAD) SERVICE="${1:-nexus-main}"
MODE=${1:-production}
echo "◈ Nexus deploy — branch: $BRANCH mode: $MODE" case "$SERVICE" in
staging) SERVICE="nexus-staging" ;;
main) SERVICE="nexus-main" ;;
esac
if [ "$MODE" = "staging" ]; then echo "==> Deploying $SERVICE"
docker compose --profile staging up -d --build nexus-staging docker compose build "$SERVICE"
echo "✓ Staging live at http://localhost:4201 (branch: $BRANCH)" docker compose up -d --force-recreate "$SERVICE"
else echo "==> Done. Container: $SERVICE"
docker compose up -d --build nexus
echo "✓ Production live at http://localhost:4200"
fi

View File

@@ -1,36 +1,20 @@
version: '3.9' version: "3.9"
# ◈ The Nexus — staging deployments
#
# Production (main):
# docker compose up -d nexus
# → http://<host>:4200
#
# Branch staging:
# BRANCH=my-feature docker compose up -d nexus-staging
# → http://<host>:4201
#
# To update production after a git pull:
# docker compose up -d --build nexus
services: services:
nexus: nexus-main:
build: . build: .
container_name: nexus-main container_name: nexus-main
restart: unless-stopped restart: unless-stopped
ports: ports:
- "4200:3000" - "4200:80"
labels: labels:
- "nexus.branch=main" - "deployment=main"
nexus-staging: nexus-staging:
build: build: .
context: .
container_name: nexus-staging container_name: nexus-staging
restart: unless-stopped restart: unless-stopped
ports: ports:
- "4201:3000" - "4201:80"
labels: labels:
- "nexus.branch=staging" - "deployment=staging"
profiles:
- staging

View File

@@ -119,50 +119,53 @@
<script type="module" src="./app.js"></script> <script type="module" src="./app.js"></script>
<!-- Live Reload: polls Gitea for new commits, refreshes when main advances --> <!-- Live Refresh: polls Gitea for new commits on main, reloads when SHA changes -->
<div id="update-banner" style="display:none;position:fixed;top:0;left:0;right:0;z-index:9999; <div id="live-refresh-banner" style="
background:linear-gradient(90deg,#4af0c0,#7b5cff);color:#050510; display:none; position:fixed; top:0; left:0; right:0; z-index:9999;
font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:600; background:linear-gradient(90deg,#4af0c0,#7b5cff);
text-align:center;padding:8px;cursor:pointer;letter-spacing:0.05em;" color:#050510; font-family:'JetBrains Mono',monospace; font-size:13px;
onclick="location.reload()"> padding:8px 16px; text-align:center; font-weight:600;
◈ NEW VERSION DEPLOYED — click to reload ">⚡ NEW DEPLOYMENT DETECTED — Reloading in <span id="lr-countdown">5</span>s…</div>
</div>
<script> <script>
(function () { (function() {
const GITEA_API = 'http://143.198.27.163:3000/api/v1'; const GITEA = 'http://143.198.27.163:3000/api/v1';
const REPO = 'Timmy_Foundation/the-nexus'; const REPO = 'Timmy_Foundation/the-nexus';
const BRANCH = 'main'; const BRANCH = 'main';
const INTERVAL = 30000; // 30s const INTERVAL = 30000; // poll every 30s
let knownSha = null; let knownSha = null;
async function checkForUpdates() { async function fetchLatestSha() {
try { try {
const res = await fetch( const r = await fetch(`${GITEA}/repos/${REPO}/branches/${BRANCH}`, { cache: 'no-store' });
`${GITEA_API}/repos/${REPO}/commits?sha=${BRANCH}&limit=1`, if (!r.ok) return null;
{ cache: 'no-store' } const d = await r.json();
); return d.commit && d.commit.id ? d.commit.id : null;
if (!res.ok) return; } catch (e) { return null; }
const commits = await res.json();
if (!commits || !commits[0]) return;
const sha = commits[0].sha;
if (knownSha === null) {
knownSha = sha;
return;
}
if (sha !== knownSha) {
document.getElementById('update-banner').style.display = 'block';
// Auto-reload after 5s
setTimeout(() => location.reload(), 5000);
}
} catch (_) { /* offline or network error — skip */ }
} }
// Start polling once page is loaded async function poll() {
window.addEventListener('load', () => { const sha = await fetchLatestSha();
checkForUpdates(); if (!sha) return;
setInterval(checkForUpdates, INTERVAL); if (knownSha === null) { knownSha = sha; return; }
}); if (sha !== knownSha) {
knownSha = sha;
const banner = document.getElementById('live-refresh-banner');
const countdown = document.getElementById('lr-countdown');
banner.style.display = 'block';
let t = 5;
const tick = setInterval(() => {
t--;
countdown.textContent = t;
if (t <= 0) { clearInterval(tick); location.reload(); }
}, 1000);
}
}
// Start polling after page is interactive
fetchLatestSha().then(sha => { knownSha = sha; });
setInterval(poll, INTERVAL);
})(); })();
</script> </script>
</body> </body>