Preview: Builds ueber den Runner
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
40a8aad3f9
commit
4258b5b9f1
7 files changed
+526
-5
No files matched your search
@@ -3,8 +3,12 @@ import { promisify } from 'node:util'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { config } from '../config.js'
|
||||
import { listDocuments, Query } from './appwriteAdmin.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 { saveRunLog } from './previewLogs.js'
|
||||
import * as runnerClient from './previewRunnerClient.js'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
@@ -423,7 +427,172 @@ export async function syncPreviewTlsOnStartup({
|
||||
return null
|
||||
}
|
||||
|
||||
export async function runDeploy(repoFullName, branch, previewConfig) {
|
||||
// ---- Preview-Runner (PREVIEW_RUNNER=on) ----
|
||||
|
||||
// Zeiten des Hintergrund-Verfolgers (fuer Tests ueberschreibbar)
|
||||
export const runnerTiming = { pollMs: 2000, maxMs: 16 * 60 * 1000 }
|
||||
// Neuester Lauf je Repo: aeltere Verfolger schreiben nichts mehr (R14)
|
||||
const latestRun = new Map()
|
||||
const followers = new Set()
|
||||
const SHA_RE = /^[0-9a-f]{7,40}$/
|
||||
const KEY_MISSING = 'Preview-Variablen nicht entschlüsselbar (PREVIEW_ENV_KEY fehlt)'
|
||||
const KEY_BROKEN = 'Preview-Variablen nicht entschlüsselbar'
|
||||
|
||||
/** Wartet auf alle laufenden Hintergrund-Verfolger (Tests, geordnetes Beenden). */
|
||||
export async function waitForRunFollowers() {
|
||||
while (followers.size) await Promise.allSettled([...followers])
|
||||
}
|
||||
|
||||
// previewDetected: static | build:<pm> | dev:<pm> (der Runner liefert kein Framework)
|
||||
export function detectedLabel(detected, result) {
|
||||
const kind = detected?.kind || result
|
||||
if (kind === 'static') return 'static'
|
||||
const pm = detected?.pm || 'npm'
|
||||
return kind === 'dev' ? `dev:${pm}` : `build:${pm}`
|
||||
}
|
||||
|
||||
async function findProject(repoFullName) {
|
||||
try {
|
||||
const docs = await listDocuments(config.collections.websiteProjects, [
|
||||
Query.equal('repoFullName', repoFullName),
|
||||
Query.limit(1),
|
||||
])
|
||||
return docs[0] || null
|
||||
} catch (err) {
|
||||
console.error('[previewDeploy] Projekt-Lookup fehlgeschlagen:', err.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function patchProject(repoFullName, project, data) {
|
||||
const doc = project?.$id ? project : await findProject(repoFullName)
|
||||
if (!doc?.$id) return null
|
||||
try {
|
||||
return await updateDocument(config.collections.websiteProjects, doc.$id, data)
|
||||
} catch (err) {
|
||||
console.error('[previewDeploy] Projekt-Update fehlgeschlagen:', err.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveSha(repoFullName, branch, sha) {
|
||||
if (SHA_RE.test(String(sha || ''))) return String(sha)
|
||||
const [owner, repo] = splitRepo(repoFullName)
|
||||
const data = await giteaFetch(`/repos/${owner}/${repo}/branches/${encodeURIComponent(branch)}`)
|
||||
const head = data?.commit?.id || data?.commit?.sha
|
||||
if (!SHA_RE.test(String(head || ''))) throw new Error('Commit des Branches nicht ermittelbar')
|
||||
return head
|
||||
}
|
||||
|
||||
async function followRun({ repoFullName, project, runId, subdomain, job, env, warning }) {
|
||||
const deadline = Date.now() + runnerTiming.maxMs
|
||||
const current = () => latestRun.get(repoFullName) === runId
|
||||
let run = null
|
||||
let failure = null
|
||||
try {
|
||||
while (current()) {
|
||||
await new Promise((r) => setTimeout(r, runnerTiming.pollMs))
|
||||
if (!current()) return
|
||||
try {
|
||||
run = await runnerClient.getRun(runId)
|
||||
} catch (err) {
|
||||
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
|
||||
}
|
||||
if (run?.state === 'done' || run?.state === 'failed') break
|
||||
if (Date.now() > deadline) { failure = 'Zeitüberschreitung beim Preview-Build'; break }
|
||||
}
|
||||
if (!current()) return
|
||||
|
||||
if (run?.log?.length) {
|
||||
try { await saveRunLog((project?.$id || (await findProject(repoFullName))?.$id), run.log, env) } catch (err) {
|
||||
console.error('[previewDeploy] Protokoll speichern fehlgeschlagen:', err.message)
|
||||
}
|
||||
}
|
||||
if (!current()) return
|
||||
|
||||
const now = new Date().toISOString()
|
||||
if (!failure && run?.state === 'done') {
|
||||
const detected = detectedLabel(run.detected, run.result)
|
||||
const base = { previewDetected: detected, previewError: warning || '' }
|
||||
if (run.result === 'dev') {
|
||||
await patchProject(repoFullName, project, { ...base, previewState: 'sleeping' })
|
||||
try { await runnerClient.startDev(subdomain, job) } catch (err) {
|
||||
console.error('[previewDeploy] Dev-Registrierung fehlgeschlagen:', err.message)
|
||||
}
|
||||
} else {
|
||||
await patchProject(repoFullName, project, {
|
||||
...base, previewState: 'online', previewBuiltSha: run.sha || job.sha, previewBuiltAt: now,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
await patchProject(repoFullName, project, {
|
||||
previewState: 'failed',
|
||||
previewError: failure || run?.error || 'Preview-Build fehlgeschlagen',
|
||||
})
|
||||
}
|
||||
if (!current()) return
|
||||
try { await syncPreviewTls() } catch (err) {
|
||||
console.error('[previewDeploy] TLS-Sync fehlgeschlagen:', err.message)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runDeployViaRunner(repoFullName, branch, previewConfig, { project, sha } = {}) {
|
||||
const subdomain = previewConfig.subdomain || repoFullName.split('/').pop()
|
||||
const doc = project || (await findProject(repoFullName))
|
||||
const mode = doc?.previewMode || 'auto'
|
||||
|
||||
if (mode === 'off') {
|
||||
await patchProject(repoFullName, doc, { previewState: 'off' })
|
||||
return { deployed: false, skipped: true, reason: 'Preview ist ausgeschaltet', subdomain }
|
||||
}
|
||||
|
||||
let env = {}
|
||||
let warning = ''
|
||||
if (doc?.previewEnv) {
|
||||
const key = envKey()
|
||||
if (!key) warning = KEY_MISSING
|
||||
else {
|
||||
try { env = decryptEnv(doc.previewEnv, key) } catch { warning = KEY_BROKEN }
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const job = {
|
||||
sub: subdomain,
|
||||
repo: repoFullName,
|
||||
sha: await resolveSha(repoFullName, branch, sha),
|
||||
branch,
|
||||
mode: ['build', 'dev'].includes(mode) ? mode : 'auto',
|
||||
...(previewConfig.index ? { index: previewConfig.index } : {}),
|
||||
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 })
|
||||
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 })
|
||||
return { deployed: false, error: err.message, subdomain }
|
||||
}
|
||||
}
|
||||
|
||||
export async function runDeploy(repoFullName, branch, previewConfig, options = {}) {
|
||||
if (config.previewRunner.enabled) {
|
||||
return runDeployViaRunner(repoFullName, branch, previewConfig, options)
|
||||
}
|
||||
if (!config.preview.deployRoot) {
|
||||
return { deployed: false, reason: 'PREVIEW_DEPLOY_ROOT nicht gesetzt' }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user