Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5cb668853 | ||
|
|
56637f2005 |
No files matched your search
@@ -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 NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
|
||||||
const NETWORK = 'preview-sandbox'; // fest (R10), kein Parameter
|
const NETWORK = 'preview-sandbox'; // fest (R10), kein Parameter
|
||||||
const IMAGES = ['node:20', 'oven/bun:1'];
|
const IMAGES = ['node:20', 'oven/bun:1'];
|
||||||
const MEMORIES = ['1g', '512m'];
|
const MEMORIES = ['2g', '1g', '512m'];
|
||||||
const MAX_LINE = 64 * 1024;
|
const MAX_LINE = 64 * 1024;
|
||||||
// Feste, unkritische Umgebung im Container (R9): Caches landen im gemounteten Cache-Verzeichnis.
|
// 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'];
|
const FIXED_ENV = ['HOME=/cache/home', 'npm_config_cache=/cache/npm', 'COREPACK_HOME=/cache/corepack'];
|
||||||
|
|||||||
+8
-4
@@ -197,10 +197,12 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
|
|||||||
await fs.chownR(workDir);
|
await fs.chownR(workDir);
|
||||||
await fs.chownR(cacheDir); // R7
|
await fs.chownR(cacheDir); // R7
|
||||||
const remaining = Math.max(1000, deadline - now());
|
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' });
|
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' });
|
||||||
return docker.run(args, { env, timeoutMs: remaining, onLine: (l) => onLine(l) });
|
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})`);
|
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.
|
// Klonen + Erkennung + Installation (R1); Aufrufer hält den Slot.
|
||||||
async function prepare(job, { onLine, workDir, id = crypto.randomUUID(), deadline = now() + config.buildTimeoutMs }) {
|
async function prepare(job, { onLine, workDir, id = crypto.randomUUID(), deadline = now() + config.buildTimeoutMs }) {
|
||||||
@@ -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 (!out) problem = `Kein Ausgabeordner gefunden (gesucht: ${d.outputDirs.join(', ')})`;
|
||||||
}
|
}
|
||||||
if (problem) {
|
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 fallbackOr(problem);
|
||||||
} else {
|
} else {
|
||||||
await publish(job, out, { skipGit: false, onLine });
|
await publish(job, out, { skipGit: false, onLine });
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ test('buildRunArgs: Name Pflicht, Memory/Image Allowlist, Network fest', () => {
|
|||||||
assert.throws(() => buildRunArgs({ ...base, memory: '64g' }));
|
assert.throws(() => buildRunArgs({ ...base, memory: '64g' }));
|
||||||
assert.throws(() => buildRunArgs({ ...base, image: 'evil/img' }));
|
assert.throws(() => buildRunArgs({ ...base, image: 'evil/img' }));
|
||||||
assert.ok(buildRunArgs({ ...base, image: 'oven/bun:1', memory: '512m' }));
|
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'));
|
assert.ok(pair(buildRunArgs({ ...base, network: 'host' }), '--network', 'preview-sandbox'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,23 @@ test('auto mit dev-Skript nach Build-Fehler ⇒ result dev, kein Publish', async
|
|||||||
assert.equal(b.state, 'failed');
|
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 () => {
|
test('erzwungener Modus mit Erkennungsfehler ⇒ failed', async () => {
|
||||||
const s = setup({ files: { 'index.html': 'x' } });
|
const s = setup({ files: { 'index.html': 'x' } });
|
||||||
const r = await wait(s.jobs, s.jobs.submit(job({ mode: 'build' })));
|
const r = await wait(s.jobs, s.jobs.submit(job({ mode: 'build' })));
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ router.post('/:id/preview/dev/start', wrap(async (req, res) => {
|
|||||||
requireRunner()
|
requireRunner()
|
||||||
const doc = await loadProject(req.params.id)
|
const doc = await loadProject(req.params.id)
|
||||||
if ((doc.previewMode || 'auto') === 'off') throw fail(409, 'Preview ist ausgeschaltet')
|
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
|
// Gespeicherten Stand verwenden (R17), nie raten
|
||||||
if (!doc.repoFullName || !doc.previewBranch || !SHA_RE.test(String(doc.previewBuiltSha || ''))) {
|
if (!doc.repoFullName || !doc.previewBranch || !SHA_RE.test(String(doc.previewBuiltSha || ''))) {
|
||||||
throw fail(409, 'Noch kein Stand gespeichert – bitte zuerst neu bauen')
|
throw fail(409, 'Noch kein Stand gespeichert – bitte zuerst neu bauen')
|
||||||
|
|||||||
@@ -77,6 +77,15 @@ export function devSubdomainsFromHostsFile(content) {
|
|||||||
return result
|
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).
|
* Projekte mit laufendem/schlafendem Dev-Server (nur mit Preview-Runner).
|
||||||
* Liefert null, wenn Appwrite nicht antwortet.
|
* Liefert null, wenn Appwrite nicht antwortet.
|
||||||
@@ -87,9 +96,7 @@ async function getDevProjects() {
|
|||||||
Query.equal('previewState', ['sleeping', 'starting', 'online', 'building', 'failed']),
|
Query.equal('previewState', ['sleeping', 'starting', 'online', 'building', 'failed']),
|
||||||
Query.limit(500),
|
Query.limit(500),
|
||||||
])
|
])
|
||||||
return docs.filter(
|
return docs.filter((d) => d.subdomain && /^[a-z0-9-]+$/.test(d.subdomain) && isRunnerDev(d))
|
||||||
(d) => d.subdomain && /^[a-z0-9-]+$/.test(d.subdomain) && String(d.previewDetected || '').startsWith('dev')
|
|
||||||
)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[previewDeploy] getDevProjects fehlgeschlagen:', err.message)
|
console.error('[previewDeploy] getDevProjects fehlgeschlagen:', err.message)
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -6,13 +6,11 @@
|
|||||||
import { config } from '../config.js'
|
import { config } from '../config.js'
|
||||||
import { listDocuments, updateDocument, Query } from './appwriteAdmin.js'
|
import { listDocuments, updateDocument, Query } from './appwriteAdmin.js'
|
||||||
import * as runnerClient from './previewRunnerClient.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
|
export const STALE_BUILD_MS = 70 * 60 * 1000
|
||||||
const STALE_TEXT = 'Status unbekannt – bitte neu bauen'
|
const STALE_TEXT = 'Status unbekannt – bitte neu bauen'
|
||||||
|
|
||||||
const isDev = (d) => String(d.previewDetected || '').startsWith('dev')
|
|
||||||
|
|
||||||
export async function reconcilePreviews({ now = Date.now() } = {}) {
|
export async function reconcilePreviews({ now = Date.now() } = {}) {
|
||||||
if (!config.previewRunner.enabled) return { skipped: true }
|
if (!config.previewRunner.enabled) return { skipped: true }
|
||||||
const result = { staleFailed: 0, stateUpdated: 0, reregistered: 0 }
|
const result = { staleFailed: 0, stateUpdated: 0, reregistered: 0 }
|
||||||
@@ -44,7 +42,7 @@ export async function reconcilePreviews({ now = Date.now() } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// (b) + (c)
|
// (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
|
if (!devDocs.length) return result
|
||||||
let st
|
let st
|
||||||
try { st = await runnerClient.status() } catch (err) {
|
try { st = await runnerClient.status() } catch (err) {
|
||||||
|
|||||||
@@ -192,6 +192,16 @@ test('Rebuild und Dev start/stop', async () => {
|
|||||||
assert.ok(rs.calls.some((c) => c.url === '/dev/web/stop'))
|
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 () => {
|
test('Schalter aus: Schreib-Routen 409, GET geht', async () => {
|
||||||
cfg.previewRunner.enabled = false
|
cfg.previewRunner.enabled = false
|
||||||
for (const [m, p, b] of [['PATCH', '/pw/preview', { mode: 'auto' }], ['PUT', '/pw/preview/env', { set: {} }],
|
for (const [m, p, b] of [['PATCH', '/pw/preview', { mode: 'auto' }], ['PUT', '/pw/preview/env', { set: {} }],
|
||||||
|
|||||||
@@ -148,6 +148,26 @@ test('Dev-Projekt bekommt Runner-Service (auch ohne Ordner), static bleibt nginx
|
|||||||
assert.deepEqual([...pd.devSubdomainsFromHostsFile(content)].sort(), ['devapp', 'devoff'])
|
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')
|
||||||
|
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.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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('Appwrite nicht erreichbar: Dev-Hosts aus der letzten Datei bleiben', async () => {
|
test('Appwrite nicht erreichbar: Dev-Hosts aus der letzten Datei bleiben', async () => {
|
||||||
await pd.syncPreviewTls()
|
await pd.syncPreviewTls()
|
||||||
await pd.waitForDevRegistration()
|
await pd.waitForDevRegistration()
|
||||||
|
|||||||
@@ -212,6 +212,20 @@ test('I6: Dev-Zustand folgt dem Runner; Modus off bleibt unberührt', async () =
|
|||||||
assert.equal(res.reregistered, 0)
|
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 () => {
|
test('I4: dem Runner unbekannte Dev-Spezifikation wird erneut hinterlegt', async () => {
|
||||||
const { createDocument } = await import('../server/services/appwriteAdmin.js')
|
const { createDocument } = await import('../server/services/appwriteAdmin.js')
|
||||||
await createDocument('websiteProjects', {
|
await createDocument('websiteProjects', {
|
||||||
|
|||||||
Reference in new issue
Block a user