diff --git a/runner/src/jobs.js b/runner/src/jobs.js index 9a2144a..1c11f68 100644 --- a/runner/src/jobs.js +++ b/runner/src/jobs.js @@ -202,7 +202,7 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem 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})`); + const failText = (what, code, limit) => (code === 137 ? 'Build am Speicherlimit abgebrochen (Exit 137)' : code === 124 ? `${what}: Zeitlimit von ${Math.round(limit / 60e3)} Minuten überschritten` : `${what} fehlgeschlagen (Exit-Code ${code})`); // Klonen + Erkennung + Installation (R1); Aufrufer hält den Slot. async function prepare(job, { onLine, workDir, id = crypto.randomUUID(), deadline = now() + config.buildTimeoutMs }) { diff --git a/server/routes/admin/preview.js b/server/routes/admin/preview.js index df1d94b..f0925e7 100644 --- a/server/routes/admin/preview.js +++ b/server/routes/admin/preview.js @@ -150,6 +150,7 @@ router.post('/:id/preview/dev/start', wrap(async (req, res) => { requireRunner() const doc = await loadProject(req.params.id) if ((doc.previewMode || 'auto') === 'off') throw fail(409, 'Preview ist ausgeschaltet') + if (doc.isPublic === true && doc.previewMode !== 'dev') throw fail(409, 'Öffentliche Seite: Dev-Server nur im Modus Dev-Server') // Gespeicherten Stand verwenden (R17), nie raten if (!doc.repoFullName || !doc.previewBranch || !SHA_RE.test(String(doc.previewBuiltSha || ''))) { throw fail(409, 'Noch kein Stand gespeichert – bitte zuerst neu bauen') diff --git a/server/services/previewDeploy.js b/server/services/previewDeploy.js index 502a8f7..bf5efa2 100644 --- a/server/services/previewDeploy.js +++ b/server/services/previewDeploy.js @@ -77,6 +77,15 @@ export function devSubdomainsFromHostsFile(content) { return result } +/** + * Gehoert das Projekt zum Runner (Dev-Server)? Oeffentliche Seiten nur bei explizitem previewMode 'dev'. + */ +export function isRunnerDev(d) { + return String(d?.previewDetected || '').startsWith('dev') && + d.previewMode !== 'off' && + (d.isPublic !== true || d.previewMode === 'dev') +} + /** * Projekte mit laufendem/schlafendem Dev-Server (nur mit Preview-Runner). * Liefert null, wenn Appwrite nicht antwortet. @@ -87,14 +96,7 @@ 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.isPublic !== true || d.previewMode === 'dev') - ) + return docs.filter((d) => d.subdomain && /^[a-z0-9-]+$/.test(d.subdomain) && isRunnerDev(d)) } catch (err) { console.error('[previewDeploy] getDevProjects fehlgeschlagen:', err.message) return null diff --git a/server/services/previewReconcile.js b/server/services/previewReconcile.js index 7f55050..503c67b 100644 --- a/server/services/previewReconcile.js +++ b/server/services/previewReconcile.js @@ -6,13 +6,11 @@ import { config } from '../config.js' import { listDocuments, updateDocument, Query } from './appwriteAdmin.js' import * as runnerClient from './previewRunnerClient.js' -import { hasLiveFollower, registerDevRound } from './previewDeploy.js' +import { hasLiveFollower, registerDevRound, isRunnerDev } from './previewDeploy.js' export const STALE_BUILD_MS = 70 * 60 * 1000 const STALE_TEXT = 'Status unbekannt – bitte neu bauen' -const isDev = (d) => String(d.previewDetected || '').startsWith('dev') - export async function reconcilePreviews({ now = Date.now() } = {}) { if (!config.previewRunner.enabled) return { skipped: true } const result = { staleFailed: 0, stateUpdated: 0, reregistered: 0 } @@ -44,7 +42,7 @@ export async function reconcilePreviews({ now = Date.now() } = {}) { } // (b) + (c) - const devDocs = docs.filter((d) => isDev(d) && d.previewMode !== 'off' && d.previewState !== 'building' && d.subdomain) + const devDocs = docs.filter((d) => isRunnerDev(d) && d.previewState !== 'building' && d.subdomain) if (!devDocs.length) return result let st try { st = await runnerClient.status() } catch (err) { diff --git a/test/previewAdmin.test.js b/test/previewAdmin.test.js index 32a83f3..efd2644 100644 --- a/test/previewAdmin.test.js +++ b/test/previewAdmin.test.js @@ -192,6 +192,16 @@ test('Rebuild und Dev start/stop', async () => { assert.ok(rs.calls.some((c) => c.url === '/dev/web/stop')) }) +test('dev/start: oeffentliche Seite ausserhalb Modus dev => 409', async () => { + const { createDocument } = await import('../server/services/appwriteAdmin.js') + await createDocument('websiteProjects', { projectName: 'Pub', repoFullName: 'JUSN/pub', subdomain: 'pub', previewBranch: 'main', previewBuiltSha: SHA, previewState: 'online', previewDetected: 'static', isPublic: true, previewMode: 'auto' }, 'ppub') + rs.calls = [] + const r = await call('POST', '/ppub/preview/dev/start') + assert.equal(r.status, 409) + assert.deepEqual(await r.json(), { error: 'Öffentliche Seite: Dev-Server nur im Modus Dev-Server' }) + assert.equal(rs.calls.filter((c) => c.url.startsWith('/dev/')).length, 0) +}) + test('Schalter aus: Schreib-Routen 409, GET geht', async () => { cfg.previewRunner.enabled = false for (const [m, p, b] of [['PATCH', '/pw/preview', { mode: 'auto' }], ['PUT', '/pw/preview/env', { set: {} }], diff --git a/test/previewDevRouter.test.js b/test/previewDevRouter.test.js index 6966ca0..7252dcd 100644 --- a/test/previewDevRouter.test.js +++ b/test/previewDevRouter.test.js @@ -152,15 +152,17 @@ test('Oeffentliche Seite mit previewDetected dev geht nur bei explizitem preview 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') + await fs.mkdir(path.join(tmp, 'previews', 'livesite'), { recursive: true }) // veroeffentlichter Ordner 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.match(routerBlock(content, 'livesite'), /preview-nginx@docker/) + assert.doesNotMatch(routerBlock(content, 'livesite'), /preview-runner/) assert.ok(!pd.devSubdomainsFromHostsFile(content).has('livesite')) } finally { + await fs.rm(path.join(tmp, 'previews', 'livesite'), { recursive: true, force: true }) await deleteDocument('websiteProjects', 'pub1'); await deleteDocument('websiteProjects', 'pub2') await pd.syncPreviewTls(); await pd.waitForDevRegistration() } diff --git a/test/previewFinalFixes.test.js b/test/previewFinalFixes.test.js index 8f0c398..8595299 100644 --- a/test/previewFinalFixes.test.js +++ b/test/previewFinalFixes.test.js @@ -212,6 +212,20 @@ test('I6: Dev-Zustand folgt dem Runner; Modus off bleibt unberührt', async () = assert.equal(res.reregistered, 0) }) +test('Abgleich: oeffentliche auto-Seite mit altem dev:* wird nicht angefasst und nicht registriert', async () => { + const { createDocument } = await import('../server/services/appwriteAdmin.js') + await createDocument('websiteProjects', { + previewDetected: 'dev:npm', previewBranch: 'main', previewBuiltSha: SHA, isPublic: true, previewMode: 'auto', + repoFullName: 'WEBklar/d-pub', subdomain: 'd-pub', previewState: 'online', + }, 'dpub') + rs.status = { dev: [], known: [], queue: 0 } + rs.calls = [] + const res = await rec.reconcilePreviews() + assert.equal(aw.get('websiteProjects', 'dpub').previewState, 'online') + assert.equal(rs.calls.filter((c) => c.method === 'PUT' && c.url === '/dev/d-pub').length, 0) + assert.ok(res.reregistered === 0 || !rs.calls.some((c) => c.url === '/dev/d-pub')) +}) + test('I4: dem Runner unbekannte Dev-Spezifikation wird erneut hinterlegt', async () => { const { createDocument } = await import('../server/services/appwriteAdmin.js') await createDocument('websiteProjects', {