Runner: index.html-Rueckfall wie bisheriger Deploy (Kandidat/Platzhalter/Unterordner)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
54f307d12a
commit
8a028ce75a
7 files changed
+294
-10
No files matched your search
@@ -50,7 +50,8 @@ export function detectProject(files, { mode, index, previewJson } = {}) {
|
||||
const devPort = Number.isInteger(pj.devPort) && pj.devPort >= 1024 && pj.devPort <= 65535 ? pj.devPort : DEFAULT_PORT;
|
||||
const outputDirs = safeRel(pj.output) ? [pj.output] : [...DEFAULT_OUTPUTS];
|
||||
const indexFile = [index, pj.index].find(safeRel) || 'index.html';
|
||||
const hasIndex = files.has(indexFile);
|
||||
// index darf Datei ODER (wie im bisherigen Deploy) Unterordner mit index.html sein
|
||||
const hasIndex = files.has(indexFile) || files.has(`${indexFile.replace(/\/+$/, '')}/index.html`);
|
||||
const hasBuild = hasScript(scripts, 'build');
|
||||
const hasDev = hasScript(scripts, 'dev');
|
||||
const m = mode === 'build' || mode === 'dev' ? mode : 'auto';
|
||||
|
||||
+98
-6
@@ -23,11 +23,12 @@ async function chownR(p) {
|
||||
}
|
||||
|
||||
// Kopiert nur Verzeichnisse und reguläre Dateien (keine Symlinks, nie .git), Rechte 755/644 (R7).
|
||||
async function copyTree(fs, src, dst, { skipGit = false } = {}) {
|
||||
async function copyTree(fs, src, dst, { skipGit = false, skipNodeModules = false } = {}) {
|
||||
await fs.mkdir(dst, { recursive: true });
|
||||
await fs.chmod(dst, 0o755);
|
||||
for (const e of await fs.readdir(src, { withFileTypes: true })) {
|
||||
if (skipGit && e.name === '.git') continue;
|
||||
if (skipNodeModules && e.name === 'node_modules') continue;
|
||||
const s = join(src, e.name);
|
||||
const d = join(dst, e.name);
|
||||
if (e.isDirectory()) await copyTree(fs, s, d);
|
||||
@@ -43,6 +44,76 @@ async function dirSize(fs, p) {
|
||||
return sum;
|
||||
}
|
||||
|
||||
// ---- index.html-Rückfall (1:1 aus server/services/previewDeploy.js, Runner kann Portalcode nicht importieren) ----
|
||||
function placeholderHtml(displayName) {
|
||||
const name = String(displayName || 'Diese Website')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
return `<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>${name} – wird vorbereitet</title>
|
||||
<style>
|
||||
html,body{height:100%;margin:0}
|
||||
body{display:flex;align-items:center;justify-content:center;
|
||||
font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
||||
background:#0f172a;color:#e2e8f0}
|
||||
.box{max-width:32rem;padding:2rem;text-align:center}
|
||||
h1{font-size:1.4rem;margin:0 0 .75rem}
|
||||
p{margin:0;color:#94a3b8;line-height:1.5}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<h1>${name}</h1>
|
||||
<p>Diese Seite wird gerade vorbereitet oder aktualisiert.<br>Bitte in Kürze erneut laden.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
async function findIndexCandidate(fs, dir) {
|
||||
let entries = [];
|
||||
try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return null; }
|
||||
const htmlFiles = entries
|
||||
.filter((e) => e.isFile() && /\.html?$/i.test(e.name) && !e.name.startsWith('.'))
|
||||
.map((e) => e.name)
|
||||
.filter((name) => name.toLowerCase() !== 'index.html');
|
||||
if (!htmlFiles.length) return null;
|
||||
const preferred = ['index.htm', 'home.html', 'homepage.html', 'main.html', 'start.html', 'app.html'];
|
||||
for (const want of preferred) {
|
||||
const hit = htmlFiles.find((name) => name.toLowerCase() === want);
|
||||
if (hit) return hit;
|
||||
}
|
||||
if (htmlFiles.length === 1) return htmlFiles[0];
|
||||
const sized = await Promise.all(htmlFiles.map(async (name) => {
|
||||
try { return { name, size: (await fs.stat(join(dir, name))).size }; } catch { return { name, size: 0 }; }
|
||||
}));
|
||||
sized.sort((a, b) => b.size - a.size);
|
||||
return sized[0].name;
|
||||
}
|
||||
|
||||
// Stellt sicher, dass dir/index.html existiert: Kandidat (Kopie) oder Platzhalter; vorhandene index.html bleibt unberührt.
|
||||
async function ensureIndexHtml(fs, dir, displayName, onLine) {
|
||||
const indexPath = join(dir, 'index.html');
|
||||
try { await fs.access(indexPath); return; } catch { /* keine index.html */ }
|
||||
const candidate = await findIndexCandidate(fs, dir);
|
||||
if (candidate) {
|
||||
await fs.copyFile(join(dir, candidate), indexPath);
|
||||
await fs.chmod(indexPath, 0o644);
|
||||
onLine?.(`keine index.html – "${candidate}" als Startseite übernommen`);
|
||||
return;
|
||||
}
|
||||
await fs.writeFile(indexPath, placeholderHtml(displayName), 'utf8');
|
||||
await fs.chmod(indexPath, 0o644);
|
||||
onLine?.('keine HTML-Datei gefunden – Platzhalter-Startseite geschrieben');
|
||||
}
|
||||
|
||||
const defaultFs = { ...nodeFs, chownR, copyTree, dirSize: (p) => dirSize(nodeFs, p) };
|
||||
|
||||
// ---- git (R11) ----
|
||||
@@ -167,7 +238,7 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
|
||||
}
|
||||
|
||||
// ---- Atomare Veröffentlichung (R11) ----
|
||||
async function publish(job, srcDir, { skipGit, onLine }) {
|
||||
async function publish(job, srcDir, { skipGit, onLine, skipNodeModules = false }) {
|
||||
const target = join(config.previewsRoot, job.sub);
|
||||
const fresh = `${target}.new`;
|
||||
const old = `${target}.old`;
|
||||
@@ -179,7 +250,8 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
|
||||
let hadOld = true;
|
||||
try {
|
||||
await fs.mkdir(config.previewsRoot, { recursive: true });
|
||||
await fs.copyTree(fs, srcDir, fresh, { skipGit });
|
||||
await fs.copyTree(fs, srcDir, fresh, { skipGit, skipNodeModules });
|
||||
await ensureIndexHtml(fs, fresh, job.displayName || job.sub, onLine);
|
||||
try { await fs.rename(target, old); } catch (e) { if (e.code !== 'ENOENT') throw e; hadOld = false; }
|
||||
try { await fs.rename(fresh, target); } catch (e) {
|
||||
if (hadOld) await fs.rename(old, target).catch((e2) => onLine?.(`Rollback fehlgeschlagen: ${e2.message}`));
|
||||
@@ -202,12 +274,26 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
|
||||
if (!st.isDirectory()) continue;
|
||||
const real = await fs.realpath(p);
|
||||
if (real === realWork || !real.startsWith(realWork + sep)) continue;
|
||||
await fs.access(join(p, 'index.html')); // wie bisher: nur Ausgabeordner mit index.html zählen
|
||||
return p;
|
||||
} catch { /* nicht vorhanden */ }
|
||||
} catch { /* nicht vorhanden / ohne index.html */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Webroot der statischen Seite: index-Unterordner (wie bisher), sonst Repo-Wurzel; fehlender/unsicherer Ordner ⇒ Wurzel.
|
||||
async function webrootOf(workDir, sub) {
|
||||
if (!sub) return workDir;
|
||||
try {
|
||||
const p = join(workDir, sub);
|
||||
if (!(await fs.lstat(p)).isDirectory()) return workDir;
|
||||
const real = await fs.realpath(p);
|
||||
const realWork = await fs.realpath(workDir);
|
||||
if (real === realWork || !real.startsWith(realWork + sep)) return workDir;
|
||||
return p;
|
||||
} catch { return workDir; }
|
||||
}
|
||||
|
||||
// ---- Ablauf eines Build-Laufs ----
|
||||
const execute = (run, job) => holdCache(job.repo, () => executeRun(run, job));
|
||||
async function executeRun(run, job) {
|
||||
@@ -224,7 +310,7 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
|
||||
if (prep.error) throw new Error(prep.error);
|
||||
if (d.kind === 'static') {
|
||||
if (d.hint) onLine(d.hint);
|
||||
await publish(job, workDir, { skipGit: true, onLine });
|
||||
await publish(job, await webrootOf(workDir, job.index || ''), { skipGit: true, onLine });
|
||||
run.result = 'static';
|
||||
} else if (d.kind === 'dev') {
|
||||
run.result = 'dev';
|
||||
@@ -240,7 +326,13 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
|
||||
}
|
||||
if (problem) {
|
||||
if (job.mode === 'auto' && d.devCmd) { onLine(`${problem} – Dev-Server wird verwendet`); run.result = 'dev'; }
|
||||
else throw new Error(problem);
|
||||
else if (!out && !r.code && job.mode !== 'build') {
|
||||
// Build lief, erzeugte aber keinen Ausgabeordner mit index.html: wie bisher den Quellstand statisch
|
||||
// veröffentlichen (index-Unterordner, Kandidat/Platzhalter) statt die Subdomain zu verlieren.
|
||||
onLine(`${problem} – Quellstand wird statisch veröffentlicht`);
|
||||
await publish(job, await webrootOf(workDir, job.index || ''), { skipGit: true, skipNodeModules: true, onLine });
|
||||
run.result = 'static';
|
||||
} else throw new Error(problem);
|
||||
} else {
|
||||
await publish(job, out, { skipGit: false, onLine });
|
||||
run.result = 'build';
|
||||
|
||||
@@ -26,6 +26,13 @@ export function validateJob(body) {
|
||||
}
|
||||
index = i;
|
||||
}
|
||||
let displayName;
|
||||
if (body.displayName !== undefined && body.displayName !== '') {
|
||||
const d = body.displayName;
|
||||
// eslint-disable-next-line no-control-regex
|
||||
if (typeof d !== 'string' || d.length > 100 || /[\x00-\x1f\x7f]/.test(d)) return { ok: false, error: 'Ungültiger Anzeigename' };
|
||||
displayName = d;
|
||||
}
|
||||
const env = {};
|
||||
if (body.env !== undefined) {
|
||||
if (!body.env || typeof body.env !== 'object' || Array.isArray(body.env)) return { ok: false, error: 'Ungültige Variablen' };
|
||||
@@ -41,5 +48,6 @@ export function validateJob(body) {
|
||||
// Whitelist: keine ungeprüften Client-Felder im Job
|
||||
const job = { sub, repo, sha, branch, mode, env };
|
||||
if (index !== undefined) job.index = index;
|
||||
if (displayName !== undefined) job.displayName = displayName;
|
||||
return { ok: true, job };
|
||||
}
|
||||
Reference in new issue
Block a user