diff --git a/runner/src/detect.js b/runner/src/detect.js index f48276a..d8a49f8 100644 --- a/runner/src/detect.js +++ b/runner/src/detect.js @@ -56,14 +56,18 @@ export function detectProject(files, { mode, index, previewJson } = {}) { const hasDev = hasScript(scripts, 'dev'); const m = mode === 'build' || mode === 'dev' ? mode : 'auto'; - const base = { pm, install: installCmd(pm, files), buildCmd: hasBuild ? [pm, 'run', 'build'] : null, devCmd: hasDev ? devCmdFor(pm, devPort) : null, outputDirs, devPort }; + const base = { index: indexFile, pm, install: installCmd(pm, files), buildCmd: hasBuild ? [pm, 'run', 'build'] : null, devCmd: hasDev ? devCmdFor(pm, devPort) : null, outputDirs, devPort }; const asBuild = () => ({ kind: 'build', ...base }); const asDev = () => ({ kind: 'dev', ...base }); - const asStatic = (hint) => ({ kind: 'static', ...base, install: [], buildCmd: null, devCmd: null, outputDirs: [], index: indexFile, ...(hint ? { hint } : {}) }); + const asStatic = (hint) => ({ kind: 'static', ...base, install: [], buildCmd: null, devCmd: null, outputDirs: [], ...(hint ? { hint } : {}) }); if (m === 'build') return hasBuild ? asBuild() : { ...asBuild(), error: 'Kein build-Skript in package.json' }; if (m === 'dev') return hasDev ? asDev() : { ...asDev(), error: 'Kein dev-Skript in package.json' }; + // R21: .webklar-preview.json-Typ zuerst. 'static' mit index.html bleibt statisch (auch mit build-Skript), + // 'node_build' baut ausdrücklich (kein Dev-Rückfall). Ohne Typ: eigene Erkennung. + if (pj.type === 'static' && hasIndex) return asStatic(); + if (pj.type === 'node_build' && hasBuild) return { ...asBuild(), explicitType: true }; if (hasIndex && !hasBuild) return asStatic(); if (hasBuild) return asBuild(); if (hasDev) return asDev(); diff --git a/runner/src/jobs.js b/runner/src/jobs.js index bb8cc4c..314f256 100644 --- a/runner/src/jobs.js +++ b/runner/src/jobs.js @@ -294,6 +294,16 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem } catch { return workDir; } } + // Erstes Deploy (noch kein -Ordner): Quellstand statisch veröffentlichen statt 403/leer (Verhalten des bisherigen Pfads). + async function firstDeployFallback(job, workDir, d, problem, onLine) { + if (job.mode === 'build') return false; + const target = join(config.previewsRoot, job.sub); + if (await fs.lstat(target).then(() => true, () => false)) return false; + onLine(`${problem} – erstes Deploy, Quellstand wird statisch veröffentlicht (Hinweis: Build fehlgeschlagen)`); + await publish(job, await webrootOf(workDir, job.index || d.index || ''), { skipGit: true, skipNodeModules: true, onLine }); + return true; + } + // ---- Ablauf eines Build-Laufs ---- const execute = (run, job) => holdCache(job.repo, () => executeRun(run, job)); async function executeRun(run, job) { @@ -307,10 +317,17 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem const d = prep.detected; run.detected = d; if (d.error) throw new Error(d.error); - if (prep.error) throw new Error(prep.error); + // Build-/Installationsproblem: erstes Deploy ⇒ Quellstand statisch (wie bisher), sonst Fehler (alte Preview bleibt) + const fallbackOr = async (problem) => { + if (!(await firstDeployFallback(job, workDir, d, problem, onLine))) throw new Error(problem); + run.result = 'static'; + run.hint = 'Build fehlgeschlagen – Quellstand statisch veröffentlicht'; + }; + if (prep.error) { await fallbackOr(prep.error); final = 'done'; } + else if (d.kind === 'static') { if (d.hint) onLine(d.hint); - await publish(job, await webrootOf(workDir, job.index || ''), { skipGit: true, onLine }); + await publish(job, await webrootOf(workDir, d.index || job.index || ''), { skipGit: true, onLine }); run.result = 'static'; } else if (d.kind === 'dev') { run.result = 'dev'; @@ -325,14 +342,8 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem if (!out) problem = `Kein Ausgabeordner gefunden (gesucht: ${d.outputDirs.join(', ')})`; } if (problem) { - if (job.mode === 'auto' && d.devCmd) { onLine(`${problem} – Dev-Server wird verwendet`); run.result = 'dev'; } - 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); + if (job.mode === 'auto' && d.devCmd && !d.explicitType) { onLine(`${problem} – Dev-Server wird verwendet`); run.result = 'dev'; } + else await fallbackOr(problem); } else { await publish(job, out, { skipGit: false, onLine }); run.result = 'build'; diff --git a/runner/test/index-fallback.test.js b/runner/test/index-fallback.test.js index e674f13..93b7a4e 100644 --- a/runner/test/index-fallback.test.js +++ b/runner/test/index-fallback.test.js @@ -91,10 +91,71 @@ test('Build-Ausgabe mit index.html wird unverändert veröffentlicht', async () assert.deepEqual(readdirSync(s.out).sort(), ['index.html', 'o.html']); }); -test('fehlgeschlagener Build bleibt ein Fehler (alter Stand bleibt)', async () => { - const s = setup({ files: { 'package.json': JSON.stringify({ scripts: { build: 'x' } }) }, dockerImpl: (args) => ({ code: args.includes('build') ? 1 : 0 }) }); +const BUILD_PKG = JSON.stringify({ scripts: { build: 'x' } }); +const failBuild = (args) => ({ code: args.includes('build') ? 1 : 0 }); + +test('bestehender -Ordner + fehlgeschlagener Build ⇒ failed, alter Stand unverändert', async () => { + const s = setup({ files: { 'package.json': BUILD_PKG, 'x.html': 'QUELLE' }, dockerImpl: failBuild }); + mkdirSync(s.out, { recursive: true }); writeFileSync(join(s.out, 'index.html'), 'ALT'); const r = await wait(s.jobs, s.jobs.submit(job())); assert.equal(r.state, 'failed'); + assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'ALT'); +}); + +test('erstes Deploy + fehlgeschlagener Build ⇒ Quellstand statisch (Kandidat), result static, Hinweis', async () => { + const s = setup({ files: { 'package.json': BUILD_PKG, 'x.html': 'QUELLE' }, dockerImpl: failBuild }); + const r = await wait(s.jobs, s.jobs.submit(job())); + assert.equal(r.state, 'done'); assert.equal(r.result, 'static'); assert.match(r.hint, /Build fehlgeschlagen/); + assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'QUELLE'); + assert.ok(r.log.some((l) => /erstes Deploy/.test(l))); +}); + +test('erstes Deploy + Installationsfehler ⇒ statischer Quellstand', async () => { + const s = setup({ files: { 'package.json': BUILD_PKG }, dockerImpl: () => ({ code: 1 }) }); + const r = await wait(s.jobs, s.jobs.submit(job({ displayName: 'Shop' }))); + assert.equal(r.result, 'static'); + assert.match(readFileSync(join(s.out, 'index.html'), 'utf8'), /

Shop<\/h1>/); +}); + +test('R21: json type static + index.html + build-Skript ⇒ static, kein Container', async () => { + const s = setup({ files: { 'package.json': BUILD_PKG, 'index.html': 'S', '.webklar-preview.json': '{"type":"static"}' }, dockerImpl: () => { throw new Error('kein Container'); } }); + const r = await wait(s.jobs, s.jobs.submit(job())); + assert.equal(r.state, 'done'); assert.equal(r.result, 'static'); +}); + +test('R21: json type node_build ⇒ build (auch mit index.html); kein Dev-Rückfall', async () => { + const mk = (extra) => setup({ files: { 'package.json': JSON.stringify({ scripts: { build: 'x', dev: 'y' } }), 'index.html': 'S', '.webklar-preview.json': '{"type":"node_build"}', ...extra }, + dockerImpl: (args) => { if (args.includes('build')) { mkdirSync(join(workOf(args), 'dist'), { recursive: true }); writeFileSync(join(workOf(args), 'dist', 'index.html'), 'B'); } return { code: 0 }; } }); + let s = mk(); + let r = await wait(s.jobs, s.jobs.submit(job())); + assert.equal(r.result, 'build'); assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'B'); + s = setup({ files: { 'package.json': JSON.stringify({ scripts: { build: 'x', dev: 'y' } }), '.webklar-preview.json': '{"type":"node_build"}' }, dockerImpl: failBuild }); + mkdirSync(s.out, { recursive: true }); + r = await wait(s.jobs, s.jobs.submit(job())); + assert.equal(r.state, 'failed'); +}); + +test('R21: ohne json-Typ + index.html + build-Skript ⇒ build', async () => { + const s = setup({ files: { 'package.json': BUILD_PKG, 'index.html': 'S' }, dockerImpl: (args) => { if (args.includes('build')) { mkdirSync(join(workOf(args), 'dist'), { recursive: true }); writeFileSync(join(workOf(args), 'dist', 'index.html'), 'B'); } return { code: 0 }; } }); + const r = await wait(s.jobs, s.jobs.submit(job())); + assert.equal(r.result, 'build'); +}); + +test('R21: Modus build aus dem Portal schlägt json type static', async () => { + const s = setup({ files: { 'package.json': BUILD_PKG, 'index.html': 'S', '.webklar-preview.json': '{"type":"static"}' }, dockerImpl: (args) => { if (args.includes('build')) { mkdirSync(join(workOf(args), 'dist'), { recursive: true }); writeFileSync(join(workOf(args), 'dist', 'index.html'), 'B'); } return { code: 0 }; } }); + const r = await wait(s.jobs, s.jobs.submit(job({ mode: 'build' }))); + assert.equal(r.result, 'build'); +}); + +test('json index-Unterordner wird beim Veröffentlichen genutzt (static und Build-Rückfall)', async () => { + let s = setup({ files: { 'public/index.html': 'PUB', 'root.html': 'r', '.webklar-preview.json': '{"index":"public"}' } }); + await wait(s.jobs, s.jobs.submit(job())); + assert.deepEqual(readdirSync(s.out), ['index.html']); + s = setup({ files: { 'package.json': BUILD_PKG, 'public/home.html': 'H', '.webklar-preview.json': '{"index":"public"}' }, dockerImpl: failBuild }); + const r = await wait(s.jobs, s.jobs.submit(job())); + assert.equal(r.result, 'static'); + assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'H'); + assert.ok(!existsSync(join(s.out, 'package.json'))); }); test('validateJob: displayName optional, ≤100 Zeichen, keine Steuerzeichen', () => { diff --git a/runner/test/jobs.test.js b/runner/test/jobs.test.js index a8f56ed..213be7f 100644 --- a/runner/test/jobs.test.js +++ b/runner/test/jobs.test.js @@ -250,6 +250,7 @@ test('I8: npm ci scheitert ⇒ Wiederholung mit npm install (Protokoll, Build l test('I8: auch npm install scheitert ⇒ Installationsfehler', async () => { const s = setup({ files: { 'package.json': PKG(), 'package-lock.json': '{}' }, dockerImpl: () => ({ code: 1 }) }); + mkdirSync(join(s.config.previewsRoot, 'demo')); const r = await wait(s.jobs, s.jobs.submit(job())); assert.equal(r.state, 'failed'); assert.match(r.error, /Installation fehlgeschlagen/); assert.equal(s.calls.length, 2); diff --git a/test/previewIndexParity.test.js b/test/previewIndexParity.test.js index 01441e5..130e9b6 100644 --- a/test/previewIndexParity.test.js +++ b/test/previewIndexParity.test.js @@ -12,6 +12,7 @@ const FIXTURES = { mehrere: { 'a.html': 'aaaa', 'z.html': 'zzzzzzzz', 'css/s.css': 'c' }, bevorzugt: { 'about.html': 'AAAAAAAAAA', 'main.html': 'm' }, index: { 'index.html': 'I', 'other.html': 'o' }, + typStatic: { 'index.html': 'I', 'package.json': '{"scripts":{"build":"x"}}', '.webklar-preview.json': '{"type":"static"}', 'a.css': 'c' }, unterordner: { 'site/home.html': 'H', 'site/a.css': 'c', 'root.html': 'r' }, }