feat: add live staging with auto-refresh on push (#33)
- index.html: polls Gitea API every 30s for new commits on main; shows banner + auto-reloads within 5s when a new SHA is detected - Dockerfile + docker-compose.yml: containerised serve for production (port 4200) and optional branch staging (port 4201, --profile staging) - deploy.sh: one-liner to deploy production or staging locally - .gitea/workflows/deploy.yml: CI job that SSH-deploys on push to main (gracefully skips if deploy secrets are not yet configured) Fixes #33 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
33
.gitea/workflows/deploy.yml
Normal file
33
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: Deploy Nexus
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy to production
|
||||||
|
# SSH into the host and redeploy via docker compose.
|
||||||
|
# Set DEPLOY_HOST, DEPLOY_USER, and DEPLOY_SSH_KEY in repo secrets.
|
||||||
|
env:
|
||||||
|
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
USER: ${{ secrets.DEPLOY_USER }}
|
||||||
|
REPO_DIR: ${{ secrets.DEPLOY_REPO_DIR || '/opt/nexus' }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$SSH_KEY" ] || [ -z "$HOST" ] || [ -z "$USER" ]; then
|
||||||
|
echo "Deploy secrets not configured — skipping remote deploy."
|
||||||
|
echo "Set DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY in repo settings."
|
||||||
|
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
|
||||||
6
Dockerfile
Normal file
6
Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN npm install -g serve
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["serve", ".", "-l", "3000", "--no-clipboard"]
|
||||||
20
deploy.sh
Executable file
20
deploy.sh
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ◈ Nexus — quick deploy helper
|
||||||
|
# Usage:
|
||||||
|
# ./deploy.sh # deploy main to production (port 4200)
|
||||||
|
# ./deploy.sh staging # deploy current branch to staging (port 4201)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
MODE=${1:-production}
|
||||||
|
|
||||||
|
echo "◈ Nexus deploy — branch: $BRANCH mode: $MODE"
|
||||||
|
|
||||||
|
if [ "$MODE" = "staging" ]; then
|
||||||
|
docker compose --profile staging up -d --build nexus-staging
|
||||||
|
echo "✓ Staging live at http://localhost:4201 (branch: $BRANCH)"
|
||||||
|
else
|
||||||
|
docker compose up -d --build nexus
|
||||||
|
echo "✓ Production live at http://localhost:4200"
|
||||||
|
fi
|
||||||
36
docker-compose.yml
Normal file
36
docker-compose.yml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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:
|
||||||
|
nexus:
|
||||||
|
build: .
|
||||||
|
container_name: nexus-main
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "4200:3000"
|
||||||
|
labels:
|
||||||
|
- "nexus.branch=main"
|
||||||
|
|
||||||
|
nexus-staging:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
container_name: nexus-staging
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "4201:3000"
|
||||||
|
labels:
|
||||||
|
- "nexus.branch=staging"
|
||||||
|
profiles:
|
||||||
|
- staging
|
||||||
47
index.html
47
index.html
@@ -118,5 +118,52 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<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 -->
|
||||||
|
<div id="update-banner" style="display:none;position:fixed;top:0;left:0;right:0;z-index:9999;
|
||||||
|
background:linear-gradient(90deg,#4af0c0,#7b5cff);color:#050510;
|
||||||
|
font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:600;
|
||||||
|
text-align:center;padding:8px;cursor:pointer;letter-spacing:0.05em;"
|
||||||
|
onclick="location.reload()">
|
||||||
|
◈ NEW VERSION DEPLOYED — click to reload
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const GITEA_API = 'http://143.198.27.163:3000/api/v1';
|
||||||
|
const REPO = 'Timmy_Foundation/the-nexus';
|
||||||
|
const BRANCH = 'main';
|
||||||
|
const INTERVAL = 30000; // 30s
|
||||||
|
|
||||||
|
let knownSha = null;
|
||||||
|
|
||||||
|
async function checkForUpdates() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${GITEA_API}/repos/${REPO}/commits?sha=${BRANCH}&limit=1`,
|
||||||
|
{ cache: 'no-store' }
|
||||||
|
);
|
||||||
|
if (!res.ok) return;
|
||||||
|
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
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
checkForUpdates();
|
||||||
|
setInterval(checkForUpdates, INTERVAL);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user