Add Merge Dev → Main button for all projects
- New SSE endpoint /api/merge/ for git-only merge workflow - Merge button shown on all projects (disabled when synced) - Deploy button only for projects with app containers - Refactored SSE step runner to shared function Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+29
-9
@@ -56,22 +56,22 @@ function runStep(step: Step): Promise<{ exitCode: number; output: string }> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleDeploy(req: http.IncomingMessage, res: http.ServerResponse, projectKey: string) {
|
||||
const project = PROJECTS[projectKey];
|
||||
if (!project) {
|
||||
res.writeHead(404);
|
||||
res.end("Unknown project");
|
||||
return;
|
||||
}
|
||||
function getMergeSteps(project: ProjectConfig): Step[] {
|
||||
return [
|
||||
{ name: "git fetch", cmd: "git fetch --all", cwd: project.path },
|
||||
{ name: "git checkout main", cmd: "git checkout main", cwd: project.path },
|
||||
{ name: "git merge dev", cmd: "git merge dev --no-edit", cwd: project.path },
|
||||
{ name: "git push", cmd: "git push origin main", cwd: project.path },
|
||||
];
|
||||
}
|
||||
|
||||
async function runSSESteps(res: http.ServerResponse, project: ProjectConfig, steps: Step[]) {
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
});
|
||||
|
||||
const steps = getDeploySteps(project);
|
||||
|
||||
sendSSE(res, "init", {
|
||||
project: project.name,
|
||||
steps: steps.map((s) => s.name),
|
||||
@@ -100,3 +100,23 @@ export async function handleDeploy(req: http.IncomingMessage, res: http.ServerRe
|
||||
|
||||
res.end();
|
||||
}
|
||||
|
||||
export async function handleMerge(req: http.IncomingMessage, res: http.ServerResponse, projectKey: string) {
|
||||
const project = PROJECTS[projectKey];
|
||||
if (!project) {
|
||||
res.writeHead(404);
|
||||
res.end("Unknown project");
|
||||
return;
|
||||
}
|
||||
await runSSESteps(res, project, getMergeSteps(project));
|
||||
}
|
||||
|
||||
export async function handleDeploy(req: http.IncomingMessage, res: http.ServerResponse, projectKey: string) {
|
||||
const project = PROJECTS[projectKey];
|
||||
if (!project) {
|
||||
res.writeHead(404);
|
||||
res.end("Unknown project");
|
||||
return;
|
||||
}
|
||||
await runSSESteps(res, project, getDeploySteps(project));
|
||||
}
|
||||
|
||||
+4
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import http from "node:http";
|
||||
import { getStatusAll, getHealthAll, actionProject } from "./api.js";
|
||||
import { handleDeploy } from "./deploy.js";
|
||||
import { handleDeploy, handleMerge } from "./deploy.js";
|
||||
import { getIndexHtml } from "./web-ui.js";
|
||||
|
||||
const PORT = 5080;
|
||||
@@ -36,6 +36,9 @@ const server = http.createServer(async (req, res) => {
|
||||
} else if (path.startsWith("/api/deploy/") && req.method === "GET") {
|
||||
const projectKey = path.replace("/api/deploy/", "");
|
||||
await handleDeploy(req, res, projectKey);
|
||||
} else if (path.startsWith("/api/merge/") && req.method === "GET") {
|
||||
const projectKey = path.replace("/api/merge/", "");
|
||||
await handleMerge(req, res, projectKey);
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end("Not found");
|
||||
|
||||
+21
-8
@@ -51,6 +51,8 @@ export function getIndexHtml(): string {
|
||||
.btn:disabled { opacity:0.4; cursor:not-allowed; }
|
||||
.btn-deploy { border-color:var(--orange); color:var(--orange); }
|
||||
.btn-deploy:hover { background:rgba(249,115,22,0.12); }
|
||||
.btn-merge { border-color:var(--yellow); color:var(--yellow); }
|
||||
.btn-merge:hover { background:rgba(234,179,8,0.12); }
|
||||
.btn-up { border-color:var(--green); color:var(--green); }
|
||||
.btn-down { border-color:var(--red); color:var(--red); }
|
||||
|
||||
@@ -153,13 +155,15 @@ function render() {
|
||||
return '<div class="svc"><span class="dot '+dot+'"></span>'+esc(s.label)+' '+link+'</div>';
|
||||
}).join('');
|
||||
|
||||
// deploy button
|
||||
// action buttons
|
||||
const isRunning = ds?.running;
|
||||
const canDeploy = p.services.some(s => s.isApp);
|
||||
const deployBtn = canDeploy
|
||||
? '<button class="btn btn-deploy" onclick="deploy(\\''+p.key+'\\')" '+(isRunning?'disabled':'')+'>'+
|
||||
(isRunning ? '<span class="spinning">⚙</span> Deploying...' : '▶ Deploy') + '</button>'
|
||||
(isRunning && ds?.action==='deploy' ? '<span class="spinning">⚙</span> Deploying...' : '▶ Deploy') + '</button>'
|
||||
: '';
|
||||
const mergeBtn = '<button class="btn btn-merge" onclick="merge(\\''+p.key+'\\')" '+(isRunning || p.devAheadOfMain===0?'disabled':'')+'>'+
|
||||
(isRunning && ds?.action==='merge' ? '<span class="spinning">⚙</span> Merging...' : '▶ Merge Dev → Main') + '</button>';
|
||||
|
||||
// deploy panel
|
||||
let panelHtml = '';
|
||||
@@ -183,6 +187,7 @@ function render() {
|
||||
aheadHtml+
|
||||
'</div>'+
|
||||
'<div class="card-actions">'+
|
||||
mergeBtn+
|
||||
deployBtn+
|
||||
'<button class="btn btn-up" onclick="act(\\''+p.key+'\\',\\'up\\')" '+(isRunning?'disabled':'')+'>Start</button>'+
|
||||
'<button class="btn btn-down" onclick="act(\\''+p.key+'\\',\\'down\\')" '+(isRunning?'disabled':'')+'>Stop</button>'+
|
||||
@@ -194,13 +199,13 @@ function render() {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function deploy(key) {
|
||||
if (!confirm('Deploy ' + key + '? (merge dev→main, build, restart)')) return;
|
||||
function runSSE(key, endpoint, actionName, confirmMsg) {
|
||||
if (!confirm(confirmMsg)) return;
|
||||
|
||||
deployState[key] = { steps:[], stepStatuses:[], outputs:[], running:true, error:false };
|
||||
deployState[key] = { steps:[], stepStatuses:[], outputs:[], running:true, error:false, action:actionName };
|
||||
render();
|
||||
|
||||
const es = new EventSource('/api/deploy/' + key);
|
||||
const es = new EventSource(endpoint);
|
||||
|
||||
es.addEventListener('init', (e) => {
|
||||
const d = JSON.parse(e.data);
|
||||
@@ -223,9 +228,9 @@ function deploy(key) {
|
||||
deployState[key].running = false;
|
||||
if (!d.success) {
|
||||
deployState[key].error = true;
|
||||
toast(d.error || 'Deploy failed', 'fail');
|
||||
toast(d.error || actionName + ' failed', 'fail');
|
||||
} else {
|
||||
toast(key + ' deployed!', 'ok');
|
||||
toast(key + ' ' + actionName + ' done!', 'ok');
|
||||
setTimeout(refresh, 1500);
|
||||
}
|
||||
render();
|
||||
@@ -240,6 +245,14 @@ function deploy(key) {
|
||||
};
|
||||
}
|
||||
|
||||
function deploy(key) {
|
||||
runSSE(key, '/api/deploy/' + key, 'deploy', 'Deploy ' + key + '? (merge dev\\u2192main, build, restart)');
|
||||
}
|
||||
|
||||
function merge(key) {
|
||||
runSSE(key, '/api/merge/' + key, 'merge', 'Merge dev \\u2192 main for ' + key + '?');
|
||||
}
|
||||
|
||||
async function act(key, action) {
|
||||
try {
|
||||
const r = await fetch('/api/action', {
|
||||
|
||||
Reference in New Issue
Block a user