diff --git a/runner/src/docker.js b/runner/src/docker.js index f7fc10f..221c77d 100644 --- a/runner/src/docker.js +++ b/runner/src/docker.js @@ -9,7 +9,7 @@ const ENV_NAME_RE = /^[A-Z_][A-Z0-9_]*$/; const NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/; const NETWORK = 'preview-sandbox'; // fest (R10), kein Parameter const IMAGES = ['node:20', 'oven/bun:1']; -const MEMORIES = ['1g', '512m']; +const MEMORIES = ['2g', '1g', '512m']; const MAX_LINE = 64 * 1024; // Feste, unkritische Umgebung im Container (R9): Caches landen im gemounteten Cache-Verzeichnis. const FIXED_ENV = ['HOME=/cache/home', 'npm_config_cache=/cache/npm', 'COREPACK_HOME=/cache/corepack']; diff --git a/runner/src/jobs.js b/runner/src/jobs.js index 314f256..9a2144a 100644 --- a/runner/src/jobs.js +++ b/runner/src/jobs.js @@ -197,8 +197,10 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem await fs.chownR(workDir); await fs.chownR(cacheDir); // R7 const remaining = Math.max(1000, deadline - now()); - const args = buildRunArgs({ image: imageFor(pm), name: `preview-build-${id}-${step}`, workDir, cacheDir, workRoot: config.workRoot, cacheRoot: config.cacheRoot, cmd: withCorepack(pm, cmd), memory: '1g' }); - return docker.run(args, { env, timeoutMs: remaining, onLine: (l) => onLine(l) }); + const args = buildRunArgs({ image: imageFor(pm), name: `preview-build-${id}-${step}`, workDir, cacheDir, workRoot: config.workRoot, cacheRoot: config.cacheRoot, cmd: withCorepack(pm, cmd), memory: '2g' }); + const r = await docker.run(args, { env, timeoutMs: remaining, onLine: (l) => onLine(l) }); + if (r && r.code === 137) onLine('Build am Speicherlimit abgebrochen (Exit 137)'); + return r; } const failText = (what, code, limit) => (code === 124 ? `${what}: Zeitlimit von ${Math.round(limit / 60e3)} Minuten überschritten` : `${what} fehlgeschlagen (Exit-Code ${code})`); @@ -342,7 +344,9 @@ 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 && !d.explicitType) { onLine(`${problem} – Dev-Server wird verwendet`); run.result = 'dev'; } + const hasFolder = await fs.lstat(join(config.previewsRoot, job.sub)).then(() => true, () => false); + // Dev-Rueckfall nur beim ersten Deploy; mit bestehender Preview bleibt sie (V4) und das Routing auf nginx + if (job.mode === 'auto' && d.devCmd && !d.explicitType && !hasFolder) { onLine(`${problem} – Dev-Server wird verwendet`); run.result = 'dev'; } else await fallbackOr(problem); } else { await publish(job, out, { skipGit: false, onLine }); diff --git a/runner/test/docker.test.js b/runner/test/docker.test.js index 728a234..222ab8d 100644 --- a/runner/test/docker.test.js +++ b/runner/test/docker.test.js @@ -34,6 +34,8 @@ test('buildRunArgs: Name Pflicht, Memory/Image Allowlist, Network fest', () => { assert.throws(() => buildRunArgs({ ...base, memory: '64g' })); assert.throws(() => buildRunArgs({ ...base, image: 'evil/img' })); assert.ok(buildRunArgs({ ...base, image: 'oven/bun:1', memory: '512m' })); + const b2 = buildRunArgs({ ...base, memory: '2g' }); + assert.ok(pair(b2, '--memory', '2g') && pair(b2, '--memory-swap', '2g')); assert.ok(pair(buildRunArgs({ ...base, network: 'host' }), '--network', 'preview-sandbox')); }); diff --git a/runner/test/jobs.test.js b/runner/test/jobs.test.js index 213be7f..7f08037 100644 --- a/runner/test/jobs.test.js +++ b/runner/test/jobs.test.js @@ -82,6 +82,23 @@ test('auto mit dev-Skript nach Build-Fehler ⇒ result dev, kein Publish', async assert.equal(b.state, 'failed'); }); +test('auto + Build-Fehler + dev-Skript + bestehender Ordner ⇒ failed, Ordner unveraendert', async () => { + const s = setup({ files: { 'package.json': PKG({ dev: 'vite' }) }, dockerImpl: (args) => ({ code: args.includes('build') ? 1 : 0 }) }); + mkdirSync(join(s.config.previewsRoot, 'demo')); writeFileSync(join(s.config.previewsRoot, 'demo', 'index.html'), 'ALT'); + const r = await wait(s.jobs, s.jobs.submit(job())); + assert.equal(r.state, 'failed'); assert.notEqual(r.result, 'dev'); + assert.equal(readFileSync(join(s.config.previewsRoot, 'demo', 'index.html'), 'utf8'), 'ALT'); +}); + +test('Exit 137 im Build: Speicherlimit-Hinweis im Log, Build-Container mit 2g', async () => { + const s = setup({ files: { 'package.json': PKG() }, dockerImpl: (args) => ({ code: args.includes('build') ? 137 : 0 }) }); + const r = await wait(s.jobs, s.jobs.submit(job({ mode: 'build' }))); + assert.equal(r.state, 'failed'); + assert.ok(r.log.some((l) => l.includes('Build am Speicherlimit abgebrochen (Exit 137)'))); + const b = s.calls.find((c) => c.args.includes('preview-build-' + r.runId + '-build')); + assert.ok(b && b.args[b.args.indexOf('--memory') + 1] === '2g'); +}); + test('erzwungener Modus mit Erkennungsfehler ⇒ failed', async () => { const s = setup({ files: { 'index.html': 'x' } }); const r = await wait(s.jobs, s.jobs.submit(job({ mode: 'build' }))); diff --git a/server/services/previewDeploy.js b/server/services/previewDeploy.js index 427e5b4..502a8f7 100644 --- a/server/services/previewDeploy.js +++ b/server/services/previewDeploy.js @@ -87,8 +87,13 @@ async function getDevProjects() { Query.equal('previewState', ['sleeping', 'starting', 'online', 'building', 'failed']), Query.limit(500), ]) + // Oeffentliche Seiten nur bei explizitem previewMode 'dev' zum Runner, sonst immer nginx return docs.filter( - (d) => d.subdomain && /^[a-z0-9-]+$/.test(d.subdomain) && String(d.previewDetected || '').startsWith('dev') + (d) => + d.subdomain && + /^[a-z0-9-]+$/.test(d.subdomain) && + String(d.previewDetected || '').startsWith('dev') && + (d.isPublic !== true || d.previewMode === 'dev') ) } catch (err) { console.error('[previewDeploy] getDevProjects fehlgeschlagen:', err.message) diff --git a/test/previewDevRouter.test.js b/test/previewDevRouter.test.js index 5d3d7f2..6966ca0 100644 --- a/test/previewDevRouter.test.js +++ b/test/previewDevRouter.test.js @@ -148,6 +148,24 @@ test('Dev-Projekt bekommt Runner-Service (auch ohne Ordner), static bleibt nginx assert.deepEqual([...pd.devSubdomainsFromHostsFile(content)].sort(), ['devapp', 'devoff']) }) +test('Oeffentliche Seite mit previewDetected dev geht nur bei explizitem previewMode dev zum Runner', async () => { + const { createDocument, deleteDocument } = await import('../server/services/appwriteAdmin.js') + await createDocument('websiteProjects', { previewMode: 'auto', subdomain: 'livesite', isPublic: true, previewState: 'sleeping', previewDetected: 'dev:npm' }, 'pub1') + await createDocument('websiteProjects', { previewMode: 'dev', subdomain: 'livedev', isPublic: true, previewState: 'sleeping', previewDetected: 'dev:npm' }, 'pub2') + try { + await pd.syncPreviewTls() + await pd.waitForDevRegistration() + const content = await read() + assert.match(routerBlock(content, 'livedev'), /preview-runner@docker/) + assert.ok(!content.includes('preview-livesite-https:') || /preview-nginx@docker/.test(routerBlock(content, 'livesite'))) + if (content.includes('preview-livesite-https:')) assert.doesNotMatch(routerBlock(content, 'livesite'), /preview-runner/) + assert.ok(!pd.devSubdomainsFromHostsFile(content).has('livesite')) + } finally { + await deleteDocument('websiteProjects', 'pub1'); await deleteDocument('websiteProjects', 'pub2') + await pd.syncPreviewTls(); await pd.waitForDevRegistration() + } +}) + test('Appwrite nicht erreichbar: Dev-Hosts aus der letzten Datei bleiben', async () => { await pd.syncPreviewTls() await pd.waitForDevRegistration()