diff --git a/scripts/preview-runner-setup.mjs b/scripts/preview-runner-setup.mjs index 8ca18df..078edd1 100644 --- a/scripts/preview-runner-setup.mjs +++ b/scripts/preview-runner-setup.mjs @@ -15,6 +15,7 @@ export const PREVIEW_RUNNER_ATTRIBUTES = [ { key: 'previewState', type: 'string', size: 20, required: false }, { key: 'previewDetected', type: 'string', size: 40, required: false }, { key: 'previewBuiltSha', type: 'string', size: 40, required: false }, + { key: 'previewBranch', type: 'string', size: 100, required: false }, { key: 'previewBuiltAt', type: 'string', size: 40, required: false }, { key: 'previewError', type: 'string', size: 500, required: false }, ] diff --git a/server/services/previewDeploy.js b/server/services/previewDeploy.js index ec4fdac..a6c2962 100644 --- a/server/services/previewDeploy.js +++ b/server/services/previewDeploy.js @@ -461,34 +461,46 @@ export async function waitForDevRegistration() { while (registering) await registering } -function registerDevSpecs(projects) { - if (registering) return - registering = (async () => { - const key = envKey() - await Promise.allSettled( - projects.map(async (project) => { - const sub = project.subdomain - try { - const repo = project.repoFullName - if (!repo) return - const branch = project.previewBranch || 'main' - let sha = project.previewBuiltSha - if (!SHA_RE.test(String(sha || ''))) { - try { sha = await resolveSha(repo, branch, null) } catch (err) { - console.error(`[previewDeploy] Dev-Registrierung ${sub} uebersprungen:`, err.message) - return - } - } - let env = {} - if (project.previewEnv && key) { - try { env = decryptEnv(project.previewEnv, key) } catch { env = {} } - } - await runnerClient.startDev(sub, { sub, repo, sha, branch, mode: 'dev', env }) - } catch (err) { - console.error(`[previewDeploy] Dev-Registrierung ${sub} fehlgeschlagen:`, err.message) +let pendingProjects = null + +async function registerDevRound(projects) { + const key = envKey() + await Promise.allSettled( + projects.map(async (project) => { + const sub = project.subdomain + try { + const repo = project.repoFullName + const { previewBranch: branch, previewBuiltSha: sha } = project + // Kein Raten (R17): ohne gespeicherten Branch/Stand wird uebersprungen + if (!repo || !branch || !SHA_RE.test(String(sha || ''))) { + console.error(`[previewDeploy] Dev-Registrierung ${sub} uebersprungen: Branch oder Stand unbekannt`) + return } - }) - ) + let env = {} + if (project.previewEnv) { + try { env = decryptEnv(project.previewEnv, key) } catch { + console.error(`[previewDeploy] Dev-Registrierung ${sub} uebersprungen: Variablen nicht entschluesselbar`) + return + } + } + await runnerClient.startDev(sub, { sub, repo, sha, branch, mode: 'dev', env }) + } catch (err) { + console.error(`[previewDeploy] Dev-Registrierung ${sub} fehlgeschlagen:`, err.message) + } + }) + ) +} + +function registerDevSpecs(projects) { + // Laeuft schon eine Runde: genau eine Wiederholung danach (mit dem neuesten Stand) + if (registering) { pendingProjects = projects; return } + registering = (async () => { + let next = projects + while (next) { + pendingProjects = null + await registerDevRound(next) + next = pendingProjects + } })().finally(() => { registering = null }) } @@ -613,7 +625,12 @@ async function followRun({ repoFullName, project, token, runId, subdomain, job, const now = new Date().toISOString() if (!failure && run?.state === 'done') { const detected = detectedLabel(run.detected, run.result) - const base = { previewDetected: detected, previewError: warning || '' } + const base = { + previewDetected: detected, + previewError: warning || '', + previewBranch: job.branch, + previewBuiltSha: run.sha || job.sha, + } if (run.result === 'dev') { await patchProject(repoFullName, project, { ...base, previewState: 'sleeping' }) try { await runnerClient.startDev(subdomain, job) } catch (err) { @@ -621,7 +638,7 @@ async function followRun({ repoFullName, project, token, runId, subdomain, job, } } else { await patchProject(repoFullName, project, { - ...base, previewState: 'online', previewBuiltSha: run.sha || job.sha, previewBuiltAt: now, + ...base, previewState: 'online', previewBuiltAt: now, }) } } else { diff --git a/test/previewDevRouter.test.js b/test/previewDevRouter.test.js index 5cdf095..4bd93a3 100644 --- a/test/previewDevRouter.test.js +++ b/test/previewDevRouter.test.js @@ -34,6 +34,7 @@ before(async () => { await new Promise((r) => runner.listen(0, '127.0.0.1', r)) gitea = http.createServer((req, res) => { res.setHeader('Content-Type', 'application/json') + giteaHits++ if (giteaDown) { res.statusCode = 500; return res.end('{}') } res.end(JSON.stringify({ commit: { id: HEAD } })) }) @@ -62,7 +63,7 @@ before(async () => { await createDocument('websiteProjects', { ...base, subdomain: 'statisch', previewState: 'online', previewDetected: 'build:npm' }, 'p2') await createDocument('websiteProjects', { ...base, subdomain: 'devapp', repoFullName: 'JUSN/devapp', previewState: 'sleeping', previewDetected: 'dev:npm', - previewBuiltSha: BUILT, previewEnv: envLib.encryptEnv({ API_KEY: 'geheim-wert' }, key), + previewBuiltSha: BUILT, previewBranch: 'feature-x', previewEnv: envLib.encryptEnv({ API_KEY: 'geheim-wert' }, key), }, 'p3') await createDocument('websiteProjects', { ...base, subdomain: 'devoff', repoFullName: 'JUSN/devoff', previewState: 'failed', previewDetected: 'dev:npm', @@ -182,18 +183,21 @@ test('Registrierung: Runner bekommt Spec mit entschluesselten Variablen', async assert.equal(puts.length, 1) assert.equal(puts[0].url, '/dev/devapp') assert.deepEqual(puts[0].body, { - sub: 'devapp', repo: 'JUSN/devapp', sha: BUILT, branch: 'main', mode: 'dev', env: { API_KEY: 'geheim-wert' }, + sub: 'devapp', repo: 'JUSN/devapp', sha: BUILT, branch: 'feature-x', mode: 'dev', env: { API_KEY: 'geheim-wert' }, }) }) -test('Registrierung: ohne PREVIEW_ENV_KEY env = {}', async () => { +test('Registrierung: nicht entschluesselbare Variablen (Schluessel fehlt) => uebersprungen', async () => { const saved = process.env.PREVIEW_ENV_KEY delete process.env.PREVIEW_ENV_KEY + const errors = console.error + console.error = () => {} try { await pd.syncPreviewTls() await pd.waitForDevRegistration() - } finally { process.env.PREVIEW_ENV_KEY = saved } - assert.deepEqual(puts[0].body.env, {}) + } finally { process.env.PREVIEW_ENV_KEY = saved; console.error = errors } + assert.equal(puts.length, 0) + assert.match(await read(), /preview-runner@docker/) }) test('Registrierung: Runner-Fehler bricht den Abgleich nicht, Datei ist geschrieben', async () => { @@ -210,30 +214,45 @@ test('Registrierung: Runner-Fehler bricht den Abgleich nicht, Datei ist geschrie assert.match(await read(), /preview-runner@docker/) }) -test('Registrierung: ohne gebauten Stand wird der Branch-Kopf genutzt, bei Fehlschlag uebersprungen', async () => { +let giteaHits = 0 + +test('Registrierung: fehlender Stand oder Branch => uebersprungen, kein Gitea-Aufruf', async () => { const { updateDocument } = await import('../server/services/appwriteAdmin.js') - await updateDocument('websiteProjects', 'p3', { previewBuiltSha: '' }) + const errors = console.error + console.error = () => {} + try { + for (const patch of [{ previewBuiltSha: '' }, { previewBranch: '' }]) { + await updateDocument('websiteProjects', 'p3', patch) + giteaHits = 0 + puts.length = 0 + await pd.syncPreviewTls() + await pd.waitForDevRegistration() + assert.equal(puts.length, 0) + assert.equal(giteaHits, 0) + assert.match(await read(), /preview-runner@docker/) + await updateDocument('websiteProjects', 'p3', { previewBuiltSha: BUILT, previewBranch: 'feature-x' }) + } + } finally { console.error = errors } +}) + +test('Registrierung: kaputte verschluesselte Variablen => uebersprungen', async () => { + const { updateDocument } = await import('../server/services/appwriteAdmin.js') + const good = envLib.encryptEnv({ API_KEY: 'geheim-wert' }, key) + await updateDocument('websiteProjects', 'p3', { previewEnv: 'v1:xx:yy:zz' }) + const errors = console.error + console.error = () => {} try { await pd.syncPreviewTls() await pd.waitForDevRegistration() - assert.equal(puts[0].body.sha, HEAD) - puts.length = 0 - giteaDown = true - const errors = console.error - console.error = () => {} - try { - await pd.syncPreviewTls() - await pd.waitForDevRegistration() - } finally { console.error = errors } - assert.equal(puts.length, 0) - assert.match(await read(), /preview-runner@docker/) // Router trotzdem da } finally { - await updateDocument('websiteProjects', 'p3', { previewBuiltSha: BUILT }) + console.error = errors + await updateDocument('websiteProjects', 'p3', { previewEnv: good }) } + assert.equal(puts.length, 0) }) -test('Registrierung: keine zwei Runden gleichzeitig', async () => { - await Promise.all([pd.syncPreviewTls(), pd.syncPreviewTls()]) +test('Registrierung: laeuft eine Runde, folgt genau eine Wiederholung', async () => { + await Promise.all([pd.syncPreviewTls(), pd.syncPreviewTls(), pd.syncPreviewTls()]) await pd.waitForDevRegistration() - assert.equal(puts.length, 1) + assert.equal(puts.length, 2) }) diff --git a/test/previewRunner.test.js b/test/previewRunner.test.js index b1ab5e0..58d4c20 100644 --- a/test/previewRunner.test.js +++ b/test/previewRunner.test.js @@ -157,6 +157,8 @@ test('dev: sleeping + Registrierung per PUT /dev/', async () => { await pd.waitForRunFollowers() const d = aw.get('websiteProjects', id) assert.equal(d.previewState, 'sleeping') + assert.equal(d.previewBranch, 'main') + assert.equal(d.previewBuiltSha, SHA) assert.equal(d.previewDetected, 'dev:pnpm') const put = rs.calls.find((c) => c.method === 'PUT') assert.equal(put.url, `/dev/${repo.split('/')[1]}`) diff --git a/test/previewRunnerSetup.test.js b/test/previewRunnerSetup.test.js index bb556b7..9a7aa08 100644 --- a/test/previewRunnerSetup.test.js +++ b/test/previewRunnerSetup.test.js @@ -17,10 +17,10 @@ function mockClient(existing = []) { } } -test('Schema: genau die 7 Attribute mit Groessen, alle optional', () => { +test('Schema: genau die 8 Attribute mit Groessen, alle optional', () => { assert.deepEqual( PREVIEW_RUNNER_ATTRIBUTES.map((a) => a.key), - ['previewMode', 'previewEnv', 'previewState', 'previewDetected', 'previewBuiltSha', 'previewBuiltAt', 'previewError'] + ['previewMode', 'previewEnv', 'previewState', 'previewDetected', 'previewBuiltSha', 'previewBranch', 'previewBuiltAt', 'previewError'] ) assert.ok(PREVIEW_RUNNER_ATTRIBUTES.every((a) => a.required === false && a.type === 'string')) const sizes = Object.fromEntries(PREVIEW_RUNNER_ATTRIBUTES.map((a) => [a.key, a.size])) @@ -30,6 +30,7 @@ test('Schema: genau die 7 Attribute mit Groessen, alle optional', () => { previewState: 20, previewDetected: 40, previewBuiltSha: 40, + previewBranch: 100, previewBuiltAt: 40, previewError: 500, }) @@ -38,16 +39,16 @@ test('Schema: genau die 7 Attribute mit Groessen, alle optional', () => { test('main legt nur fehlende Attribute an', async () => { const client = mockClient(['previewMode']) const result = await main({ check: false, client, log: () => {} }) - assert.equal(client.created.length, 6) + assert.equal(client.created.length, 7) assert.ok(!client.created.includes('previewMode')) - assert.equal(result.missing.length, 6) + assert.equal(result.missing.length, 7) }) test('main --check legt nichts an und meldet Fehlende', async () => { const client = mockClient(['previewMode']) const result = await main({ check: true, client, log: () => {} }) assert.equal(client.created.length, 0) - assert.equal(result.missing.length, 6) + assert.equal(result.missing.length, 7) }) test('Konfiguration: previewRunner ist standardmaessig aus', () => {