Preview: Runner-Verfolger verdraengt sauber, R15-Grenzen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-09-30 21:54:01 +00:00
1 parent 4258b5b9f1
commit d788dba937
2 files changed
+126 -15

No files matched your search

+29 -15
View File
@@ -6,7 +6,7 @@ import { config } from '../config.js'
import { listDocuments, updateDocument, Query } from './appwriteAdmin.js'
import { withLocks } from './keyedLock.js'
import { giteaFetch, splitRepo } from './giteaClient.js'
import { envKey, decryptEnv } from './previewEnv.js'
import { envKey, decryptEnv, maskLog } from './previewEnv.js'
import { saveRunLog } from './previewLogs.js'
import * as runnerClient from './previewRunnerClient.js'
@@ -430,7 +430,7 @@ export async function syncPreviewTlsOnStartup({
// ---- Preview-Runner (PREVIEW_RUNNER=on) ----
// Zeiten des Hintergrund-Verfolgers (fuer Tests ueberschreibbar)
export const runnerTiming = { pollMs: 2000, maxMs: 16 * 60 * 1000 }
export const runnerTiming = { pollMs: 2000, maxMs: 60 * 60 * 1000, unreachableMs: 5 * 60 * 1000 }
// Neuester Lauf je Repo: aeltere Verfolger schreiben nichts mehr (R14)
const latestRun = new Map()
const followers = new Set()
@@ -484,24 +484,31 @@ async function resolveSha(repoFullName, branch, sha) {
return head
}
async function followRun({ repoFullName, project, runId, subdomain, job, env, warning }) {
async function followRun({ repoFullName, project, token, runId, subdomain, job, env, warning }) {
const deadline = Date.now() + runnerTiming.maxMs
const current = () => latestRun.get(repoFullName) === runId
const current = () => latestRun.get(repoFullName) === token
let run = null
let failure = null
let unreachableSince = 0
try {
while (current()) {
await new Promise((r) => setTimeout(r, runnerTiming.pollMs))
if (!current()) return
try {
run = await runnerClient.getRun(runId)
unreachableSince = 0
} catch (err) {
run = null
if (err.status === 404) { failure = 'Der Preview-Lauf ist beim Runner nicht mehr bekannt'; break }
console.error('[previewDeploy] Runner-Abfrage fehlgeschlagen:', err.message)
run = null
unreachableSince ||= Date.now()
if (Date.now() - unreachableSince > runnerTiming.unreachableMs) {
failure = 'Preview-Runner nicht erreichbar'
break
}
}
if (run?.state === 'done' || run?.state === 'failed') break
if (Date.now() > deadline) { failure = 'Zeitüberschreitung beim Preview-Build'; break }
if (Date.now() > deadline) { failure = 'Status unbekannt – bitte neu bauen'; break }
}
if (!current()) return
@@ -529,7 +536,7 @@ async function followRun({ repoFullName, project, runId, subdomain, job, env, wa
} else {
await patchProject(repoFullName, project, {
previewState: 'failed',
previewError: failure || run?.error || 'Preview-Build fehlgeschlagen',
previewError: maskLog(failure || run?.error || 'Preview-Build fehlgeschlagen', env),
})
}
if (!current()) return
@@ -539,19 +546,25 @@ async function followRun({ repoFullName, project, runId, subdomain, job, env, wa
} catch (err) {
console.error(`[previewDeploy] Verfolger ${runId} abgebrochen:`, err.message)
} finally {
if (latestRun.get(repoFullName) === runId && run && (run.state === 'done' || run.state === 'failed')) {
latestRun.delete(repoFullName)
}
if (latestRun.get(repoFullName) === token) latestRun.delete(repoFullName)
}
}
async function runDeployViaRunner(repoFullName, branch, previewConfig, { project, sha } = {}) {
const subdomain = previewConfig.subdomain || repoFullName.split('/').pop()
// Platz fuer diesen Aufruf sofort belegen (vor jedem await): aeltere Verfolger scheiden aus
const token = Symbol('run')
latestRun.set(repoFullName, token)
const current = () => latestRun.get(repoFullName) === token
const release = () => { if (current()) latestRun.delete(repoFullName) }
const doc = project || (await findProject(repoFullName))
const mode = doc?.previewMode || 'auto'
if (mode === 'off') {
await patchProject(repoFullName, doc, { previewState: 'off' })
try {
if (current()) await patchProject(repoFullName, doc, { previewState: 'off' })
} finally { release() }
return { deployed: false, skipped: true, reason: 'Preview ist ausgeschaltet', subdomain }
}
@@ -576,15 +589,16 @@ async function runDeployViaRunner(repoFullName, branch, previewConfig, { project
env,
}
const { runId } = await runnerClient.submitBuild(job)
latestRun.set(repoFullName, runId)
await patchProject(repoFullName, doc, { previewState: 'building', previewError: warning })
const follower = followRun({ repoFullName, project: doc, runId, subdomain, job, env, warning })
if (current()) await patchProject(repoFullName, doc, { previewState: 'building', previewError: warning })
const follower = followRun({ repoFullName, project: doc, token, runId, subdomain, job, env, warning })
followers.add(follower)
follower.finally(() => followers.delete(follower))
return { deployed: false, queued: true, runId, subdomain }
} catch (err) {
console.error(`[previewDeploy] Runner-Auftrag fuer ${repoFullName} fehlgeschlagen:`, err.message)
await patchProject(repoFullName, doc, { previewState: 'failed', previewError: err.message })
try {
if (current()) await patchProject(repoFullName, doc, { previewState: 'failed', previewError: maskLog(err.message, env) })
} finally { release() }
return { deployed: false, error: err.message, subdomain }
}
}