Preview: isRunnerDev einheitlich (Reconcile, Dev-Start), 137-Fehlertext

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-09-30 22:32:09 +00:00
1 parent 56637f2005
commit a5cb668853
7 files changed
+42 -15

No files matched your search

+1 -1
View File
@@ -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 }) {
+1
View File
@@ -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')
+10 -8
View File
@@ -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
+2 -4
View File
@@ -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) {
+10
View File
@@ -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: {} }],
+4 -2
View File
@@ -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()
}
+14
View File
@@ -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', {