From 8a028ce75ac26b9b4655d8287518f8c43d22a5af Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 22:15:27 +0000 Subject: [PATCH] Runner: index.html-Rueckfall wie bisheriger Deploy (Kandidat/Platzhalter/Unterordner) Co-Authored-By: Claude Sonnet 5.5 --- runner/src/detect.js | 3 +- runner/src/jobs.js | 104 ++++++++++++++++++++++++++-- runner/src/validate.js | 8 +++ runner/test/index-fallback.test.js | 107 +++++++++++++++++++++++++++++ server/services/previewDeploy.js | 8 ++- test/previewIndexParity.test.js | 59 ++++++++++++++++ test/previewRunner.test.js | 15 +++- 7 files changed, 294 insertions(+), 10 deletions(-) create mode 100644 runner/test/index-fallback.test.js create mode 100644 test/previewIndexParity.test.js diff --git a/runner/src/detect.js b/runner/src/detect.js index 5886985..f48276a 100644 --- a/runner/src/detect.js +++ b/runner/src/detect.js @@ -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'; diff --git a/runner/src/jobs.js b/runner/src/jobs.js index cf88bde..bb8cc4c 100644 --- a/runner/src/jobs.js +++ b/runner/src/jobs.js @@ -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, '>'); + return ` + + + + + +${name} – wird vorbereitet + + + +
+

${name}

+

Diese Seite wird gerade vorbereitet oder aktualisiert.
Bitte in Kürze erneut laden.

+
+ + +`; +} + +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'; diff --git a/runner/src/validate.js b/runner/src/validate.js index d568abb..0327490 100644 --- a/runner/src/validate.js +++ b/runner/src/validate.js @@ -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 }; } diff --git a/runner/test/index-fallback.test.js b/runner/test/index-fallback.test.js new file mode 100644 index 0000000..e674f13 --- /dev/null +++ b/runner/test/index-fallback.test.js @@ -0,0 +1,107 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, statSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { createJobs } from '../src/jobs.js'; +import { validateJob } from '../src/validate.js'; + +function setup({ files, dockerImpl }) { + const root = mkdtempSync(join(tmpdir(), 'idx-')); + const config = { giteaUrl: 'https://git.x', giteaToken: 'tok_SECRET_123456', previewsRoot: join(root, 'previews'), workRoot: join(root, 'work'), cacheRoot: join(root, 'cache'), minFreeBytes: 1000, buildTimeoutMs: 900000 }; + const git = async ({ dir }) => { for (const [f, c] of Object.entries(files)) { mkdirSync(join(dir, f, '..'), { recursive: true }); writeFileSync(join(dir, f), c); } mkdirSync(join(dir, '.git'), { recursive: true }); writeFileSync(join(dir, '.git', 'config'), 'x'); }; + const docker = { run: async (args) => (dockerImpl ? dockerImpl(args) : { code: 0 }) }; + const jobs = createJobs({ config, docker, git, mem: () => 1e10, fs: { chownR: async () => {} }, setTimer: () => {} }); + return { config, jobs, out: join(config.previewsRoot, 'demo') }; +} +const job = (o = {}) => ({ sub: 'demo', repo: 'own/rep', sha: 'abcdef1', branch: 'main', mode: 'auto', env: {}, ...o }); +const wait = async (jobs, id) => { for (let i = 0; i < 400; i++) { const r = jobs.get(id); if (r.state === 'done' || r.state === 'failed') return r; await new Promise((r2) => setTimeout(r2, 5)); } throw new Error('timeout'); }; +const workOf = (args) => args.find((a) => a.endsWith(':/work')).split(':')[0]; + +test('Doener-Fall: nur Doener_Nation_Website.html ⇒ index.html ist Byte-Kopie, Original bleibt', async () => { + const html = Buffer.from('Döneräö\n'); + const s = setup({ files: { 'Doener_Nation_Website.html': html, '.webklar-preview.json': '{}' } }); + const r = await wait(s.jobs, s.jobs.submit(job())); + assert.equal(r.state, 'done'); + assert.deepEqual(readFileSync(join(s.out, 'index.html')), html); + assert.deepEqual(readFileSync(join(s.out, 'Doener_Nation_Website.html')), html); + assert.equal(statSync(join(s.out, 'index.html')).mode & 0o777, 0o644); +}); + +test('vorhandene index.html bleibt unberührt', async () => { + const s = setup({ files: { 'index.html': 'ORIG', 'home.html': 'HOME' } }); + await wait(s.jobs, s.jobs.submit(job())); + assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'ORIG'); +}); + +test('bevorzugter Name gewinnt, sonst größte Datei', async () => { + let s = setup({ files: { 'a.html': 'aaaa', 'Main.html': 'm', 'zz.html': 'zzzzzzzz' } }); + await wait(s.jobs, s.jobs.submit(job())); + assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'm'); + s = setup({ files: { 'a.html': 'aaaa', 'b.htm': 'bbbbbbbbb', 'c.html': 'c' } }); + await wait(s.jobs, s.jobs.submit(job())); + assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'bbbbbbbbb'); +}); + +test('index-Unterordner wird Webroot; fehlender Ordner ⇒ Repo-Wurzel', async () => { + let s = setup({ files: { 'public/index.html': 'PUB', 'x.html': 'root' } }); + let r = await wait(s.jobs, s.jobs.submit(job({ index: 'public' }))); + assert.equal(r.state, 'done'); + assert.deepEqual(readdirSync(s.out), ['index.html']); + s = setup({ files: { 'x.html': 'root' } }); + await wait(s.jobs, s.jobs.submit(job({ index: 'gibtsnicht' }))); + assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'root'); +}); + +test('kein HTML ⇒ Platzhalter mit Anzeigename (maskiert)', async () => { + const s = setup({ files: { 'readme.txt': 'x' } }); + await wait(s.jobs, s.jobs.submit(job({ displayName: 'A & ' }))); + const h = readFileSync(join(s.out, 'index.html'), 'utf8'); + assert.match(h, /

A & <B><\/h1>/); + assert.match(h, /A & <B> – wird vorbereitet<\/title>/); + const s2 = setup({ files: { 'readme.txt': 'x' } }); + await wait(s2.jobs, s2.jobs.submit(job())); + assert.match(readFileSync(join(s2.out, 'index.html'), 'utf8'), /<h1>demo<\/h1>/); +}); + +test('Build ohne index.html im Ausgabeordner ⇒ Quellstand statisch + Kandidat/Platzhalter', async () => { + const s = setup({ + files: { 'package.json': JSON.stringify({ scripts: { build: 'x' } }), 'seite.html': 'SEITE' }, + dockerImpl: (args) => { if (args.includes('build')) { mkdirSync(join(workOf(args), 'dist'), { recursive: true }); writeFileSync(join(workOf(args), 'dist', 'app.js'), 'x'); mkdirSync(join(workOf(args), 'node_modules'), { recursive: true }); writeFileSync(join(workOf(args), 'node_modules', 'm.js'), 'x'); } return { code: 0 }; }, + }); + const r = await wait(s.jobs, s.jobs.submit(job())); + assert.equal(r.state, 'done'); assert.equal(r.result, 'static'); + assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'SEITE'); + assert.ok(!existsSync(join(s.out, 'node_modules'))); + const s2 = setup({ + files: { 'package.json': JSON.stringify({ scripts: { build: 'x' } }) }, + dockerImpl: (args) => { if (args.includes('build')) mkdirSync(join(workOf(args), 'dist'), { recursive: true }); return { code: 0 }; }, + }); + await wait(s2.jobs, s2.jobs.submit(job({ displayName: 'Shop' }))); + assert.match(readFileSync(join(s2.out, 'index.html'), 'utf8'), /<h1>Shop<\/h1>/); +}); + +test('Build-Ausgabe mit index.html wird unverändert veröffentlicht', async () => { + const s = setup({ + files: { 'package.json': JSON.stringify({ scripts: { build: 'x' } }) }, + dockerImpl: (args) => { if (args.includes('build')) { mkdirSync(join(workOf(args), 'dist'), { recursive: true }); writeFileSync(join(workOf(args), 'dist', 'index.html'), 'B'); writeFileSync(join(workOf(args), 'dist', 'o.html'), 'O'); } return { code: 0 }; }, + }); + const r = await wait(s.jobs, s.jobs.submit(job())); + assert.equal(r.result, 'build'); + 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 r = await wait(s.jobs, s.jobs.submit(job())); + assert.equal(r.state, 'failed'); +}); + +test('validateJob: displayName optional, ≤100 Zeichen, keine Steuerzeichen', () => { + const good = { sub: 'a', repo: 'o/r', sha: 'abcdef1', branch: 'main' }; + assert.equal(validateJob({ ...good, displayName: 'Döner Nation' }).job.displayName, 'Döner Nation'); + assert.equal('displayName' in validateJob(good).job, false); + assert.equal(validateJob({ ...good, displayName: 'x'.repeat(101) }).ok, false); + assert.equal(validateJob({ ...good, displayName: 'a\nb' }).ok, false); + assert.equal(validateJob({ ...good, displayName: 5 }).ok, false); +}); diff --git a/server/services/previewDeploy.js b/server/services/previewDeploy.js index e3698b9..427e5b4 100644 --- a/server/services/previewDeploy.js +++ b/server/services/previewDeploy.js @@ -661,7 +661,7 @@ async function followRun({ repoFullName, project, token, runId, subdomain, job, } if (run.result === 'dev') { await patchProject(repoFullName, project, { ...base, previewState: 'sleeping' }) - try { await runnerClient.startDev(subdomain, { ...job, mode: 'dev' }) } catch (err) { + try { await runnerClient.startDev(subdomain, { ...job, mode: 'dev', displayName: undefined }) } catch (err) { console.error('[previewDeploy] Dev-Registrierung fehlgeschlagen:', err.message) } } else { @@ -714,6 +714,8 @@ async function runDeployViaRunner(repoFullName, branch, previewConfig, { project } } + const displayName = String(previewConfig.displayName || doc?.projectName || '').slice(0, 100).replace(/[\x00-\x1f\x7f]/g, ' ').trim() + try { const job = { sub: subdomain, @@ -722,6 +724,7 @@ async function runDeployViaRunner(repoFullName, branch, previewConfig, { project branch, mode: ['build', 'dev'].includes(mode) ? mode : 'auto', ...(previewConfig.index ? { index: previewConfig.index } : {}), + ...(displayName ? { displayName } : {}), env, } const { runId } = await runnerClient.submitBuild(job) @@ -816,3 +819,6 @@ export async function removeDeploy(subdomain) { } return { removed: true, subdomain, tls } } + +// Nur fuer Tests (Vergleich Altpfad gegen Runner-Veroeffentlichung) +export const previewDeployInternals = { deployStatic, ensureIndexHtml, placeholderHtml, findIndexCandidate } diff --git a/test/previewIndexParity.test.js b/test/previewIndexParity.test.js new file mode 100644 index 0000000..01441e5 --- /dev/null +++ b/test/previewIndexParity.test.js @@ -0,0 +1,59 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { previewDeployInternals as legacy } from '../server/services/previewDeploy.js' +import { createJobs } from '../runner/src/jobs.js' + +const FIXTURES = { + doener: { 'Doener_Nation_Website.html': '<html>Döner</html>', '.webklar-preview.json': '{"type":"static"}' }, + leer: { 'readme.txt': 'x' }, + 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' }, + unterordner: { 'site/home.html': 'H', 'site/a.css': 'c', 'root.html': 'r' }, +} + +function walk(dir, base = dir) { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => { + const p = path.join(dir, e.name) + return e.isDirectory() ? walk(p, base) : [path.relative(base, p)] + }).sort() +} + +async function viaRunner(files, index) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'parity-r-')) + const config = { giteaUrl: 'https://g', giteaToken: 'tok_SECRET_123456', previewsRoot: path.join(root, 'p'), workRoot: path.join(root, 'w'), cacheRoot: path.join(root, 'c'), minFreeBytes: 1, buildTimeoutMs: 1e6 } + const git = async ({ dir }) => { + for (const [f, c] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(dir, f)), { recursive: true }); fs.writeFileSync(path.join(dir, f), c) } + fs.mkdirSync(path.join(dir, '.git'), { recursive: true }); fs.writeFileSync(path.join(dir, '.git/config'), 'x') + } + const jobs = createJobs({ config, docker: { run: async () => ({ code: 0 }) }, git, mem: () => 1e10, fs: { chownR: async () => {} }, setTimer: () => {} }) + const id = jobs.submit({ sub: 'demo', repo: 'o/r', sha: 'abcdef1', branch: 'main', mode: 'auto', env: {}, displayName: 'Demo', ...(index ? { index } : {}) }) + for (let i = 0; i < 400 && !['done', 'failed'].includes(jobs.get(id).state); i++) await new Promise((r) => setTimeout(r, 5)) + assert.equal(jobs.get(id).state, 'done') + return path.join(config.previewsRoot, 'demo') +} + +async function viaLegacy(files, index) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'parity-l-')) + const src = path.join(root, 'src') + for (const [f, c] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(src, f)), { recursive: true }); fs.writeFileSync(path.join(src, f), c) } + fs.mkdirSync(path.join(src, '.git'), { recursive: true }); fs.writeFileSync(path.join(src, '.git/config'), 'x') + const target = path.join(root, 'out') + await legacy.deployStatic(src, target, index || '') + await legacy.ensureIndexHtml(target, 'Demo') + return target +} + +for (const [name, files] of Object.entries(FIXTURES)) { + for (const index of name === 'unterordner' ? ['', 'site', 'fehlt'] : ['']) { + test(`Parität Altpfad/Runner: ${name}${index ? ` (index=${index})` : ''}`, async () => { + const a = await viaLegacy(files, index) + const b = await viaRunner(files, index) + assert.deepEqual(walk(b), walk(a)) + for (const f of walk(a)) assert.deepEqual(fs.readFileSync(path.join(b, f)), fs.readFileSync(path.join(a, f)), f) + }) + } +} diff --git a/test/previewRunner.test.js b/test/previewRunner.test.js index 58d4c20..67c05fa 100644 --- a/test/previewRunner.test.js +++ b/test/previewRunner.test.js @@ -104,7 +104,7 @@ test('Schalter aus: kein Runner-Aufruf, alter Pfad', async () => { }) test('an: Auftrag mit entschlüsselten Variablen, sofortige Rückgabe, Ergebnis online', async () => { - const { id, repo, doc } = await project({ previewEnv: envLib.encryptEnv({ API_URL: 'https://x.test' }, key) }) + const { id, repo, doc } = await project({ projectName: 'Mein Projekt', previewEnv: envLib.encryptEnv({ API_URL: 'https://x.test' }, key) }) const r = await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) assert.equal(r.queued, true) assert.equal(r.deployed, false) @@ -112,7 +112,7 @@ test('an: Auftrag mit entschlüsselten Variablen, sofortige Rückgabe, Ergebnis assert.equal(aw.get('websiteProjects', id).previewState, 'building') const [sub] = submits() assert.equal(sub.auth, 'Bearer sekret') - assert.deepEqual(sub.body, { sub: repo.split('/')[1], repo, sha: SHA, branch: 'main', mode: 'auto', env: { API_URL: 'https://x.test' } }) + assert.deepEqual(sub.body, { sub: repo.split('/')[1], repo, sha: SHA, branch: 'main', mode: 'auto', displayName: 'Mein Projekt', env: { API_URL: 'https://x.test' } }) await pd.waitForRunFollowers() const d = aw.get('websiteProjects', id) assert.equal(d.previewState, 'online') @@ -123,6 +123,17 @@ test('an: Auftrag mit entschlüsselten Variablen, sofortige Rückgabe, Ergebnis assert.equal((await logs.listRunLogs(id)).length, 1) }) +test('displayName: previewConfig.displayName hat Vorrang vor projectName, sonst projectName', async () => { + const a = await project({ projectName: 'Projektname' }) + await pd.runDeploy(a.repo, 'main', { ...cfg(a.repo), displayName: 'Doener Nation' }, { project: a.doc, sha: SHA }) + await pd.waitForRunFollowers() + assert.equal(submits().at(-1).body.displayName, 'Doener Nation') + const b = await project({ projectName: 'Projektname' }) + await pd.runDeploy(b.repo, 'main', cfg(b.repo), { project: b.doc, sha: SHA }) + await pd.waitForRunFollowers() + assert.equal(submits().at(-1).body.displayName, 'Projektname') +}) + test('Sha fehlt: Branch-Kopf per Gitea-API', async () => { const { repo, doc } = await project() await pd.runDeploy(repo, 'main', cfg(repo), { project: doc })