Deploy button disabled when container runs latest code

- Track deployed git hash per project in deployed-hashes.json
- Compare main branch hash with last deployed hash
- Deploy button disabled when no new code to deploy
- Save hash after successful deploy
- Also: CentralStore dev synced with main

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ondrej Trochta
2026-03-07 23:48:48 +01:00
parent e3e82cafc0
commit c7473378f5
5 changed files with 51 additions and 6 deletions
+1
View File
@@ -1,3 +1,4 @@
node_modules/
dist/
*.js.map
deployed-hashes.json
+5 -2
View File
@@ -11,6 +11,7 @@ export interface ProjectStatusDTO {
devAheadOfMain: number;
lastCommit: string;
lastCommitDev: string;
deployNeeded: boolean;
services: ServiceStatusDTO[];
}
@@ -32,15 +33,17 @@ for (const [key, cfg] of Object.entries(PROJECTS)) {
export function getStatusAll(): ProjectStatusDTO[] {
const containers = getRunningContainers();
return getAllProjects().map((p) => {
const git = getGitStatus(p);
const key = PROJECT_KEYS[p.name] ?? p.name.toLowerCase();
const git = getGitStatus(p, key);
return {
name: p.name,
key: PROJECT_KEYS[p.name] ?? p.name.toLowerCase(),
key,
branch: git.currentBranch,
behindRemote: git.behindRemote,
devAheadOfMain: git.devAheadOfMain,
lastCommit: git.lastCommit,
lastCommitDev: git.lastCommitDev,
deployNeeded: git.deployNeeded,
services: p.services.map((s) => {
const c = containers.find((ct) => ct.name === s.container);
return {
+7 -2
View File
@@ -2,6 +2,7 @@ import http from "node:http";
import { spawn } from "node:child_process";
import { PROJECTS, type ProjectConfig } from "./config.js";
import { exec } from "./shell.js";
import { saveDeployedHash } from "./git.js";
type StepStatus = "pending" | "running" | "done" | "error";
@@ -65,7 +66,7 @@ function getMergeSteps(project: ProjectConfig): Step[] {
];
}
async function runSSESteps(res: http.ServerResponse, project: ProjectConfig, steps: Step[]) {
async function runSSESteps(res: http.ServerResponse, project: ProjectConfig, steps: Step[], onSuccess?: () => void) {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
@@ -95,6 +96,7 @@ async function runSSESteps(res: http.ServerResponse, project: ProjectConfig, ste
}
if (!failed) {
onSuccess?.();
sendSSE(res, "done", { success: true });
}
@@ -118,5 +120,8 @@ export async function handleDeploy(req: http.IncomingMessage, res: http.ServerRe
res.end("Unknown project");
return;
}
await runSSESteps(res, project, getDeploySteps(project));
await runSSESteps(res, project, getDeploySteps(project), () => {
const hash = exec("git rev-parse main", project.path).stdout;
if (hash) saveDeployedHash(projectKey, hash);
});
}
+36 -1
View File
@@ -1,5 +1,30 @@
import { exec, execLive } from "./shell.js";
import type { ProjectConfig } from "./config.js";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const DEPLOYED_FILE = join(__dirname, "..", "deployed-hashes.json");
function loadDeployedHashes(): Record<string, string> {
if (!existsSync(DEPLOYED_FILE)) return {};
try {
return JSON.parse(readFileSync(DEPLOYED_FILE, "utf-8"));
} catch {
return {};
}
}
export function saveDeployedHash(projectKey: string, hash: string) {
const data = loadDeployedHashes();
data[projectKey] = hash;
writeFileSync(DEPLOYED_FILE, JSON.stringify(data, null, 2));
}
export function getDeployedHash(projectKey: string): string | null {
return loadDeployedHashes()[projectKey] ?? null;
}
export interface GitStatus {
currentBranch: string;
@@ -7,9 +32,12 @@ export interface GitStatus {
devAheadOfMain: number;
lastCommit: string;
lastCommitDev: string;
mainHash: string;
deployedHash: string | null;
deployNeeded: boolean;
}
export function getGitStatus(project: ProjectConfig): GitStatus {
export function getGitStatus(project: ProjectConfig, projectKey?: string): GitStatus {
const branch = exec("git branch --show-current", project.path).stdout || "unknown";
exec("git fetch --all --quiet", project.path);
const behind = exec(`git rev-list HEAD..origin/${branch} --count`, project.path);
@@ -21,12 +49,19 @@ export function getGitStatus(project: ProjectConfig): GitStatus {
const lastCommitDev = exec("git log dev -1 --oneline 2>/dev/null", project.path).stdout || "";
const mainHash = exec("git rev-parse main 2>/dev/null", project.path).stdout || "";
const deployedHash = projectKey ? getDeployedHash(projectKey) : null;
const deployNeeded = devAheadCount > 0 || (!!mainHash && mainHash !== deployedHash);
return {
currentBranch: branch,
behindRemote: behindCount,
devAheadOfMain: devAheadCount,
lastCommit,
lastCommitDev,
mainHash,
deployedHash,
deployNeeded,
};
}
+2 -1
View File
@@ -158,8 +158,9 @@ function render() {
// action buttons
const isRunning = ds?.running;
const canDeploy = p.services.some(s => s.isApp);
const deployDisabled = isRunning || !p.deployNeeded;
const deployBtn = canDeploy
? '<button class="btn btn-deploy" onclick="deploy(\\''+p.key+'\\')" '+(isRunning?'disabled':'')+'>'+
? '<button class="btn btn-deploy" onclick="deploy(\\''+p.key+'\\')" '+(deployDisabled?'disabled':'')+'>'+
(isRunning && ds?.action==='deploy' ? '<span class="spinning">&#9881;</span> Deploying...' : '&#9654; Deploy') + '</button>'
: '';
const mergeBtn = '<button class="btn btn-merge" onclick="merge(\\''+p.key+'\\')" '+(isRunning || p.devAheadOfMain===0?'disabled':'')+'>'+