Preview: Router fuer Dev-Server

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 d788dba937
commit cf2841adf5
2 files changed
+334 -4

No files matched your search

+95 -4
View File
@@ -59,6 +59,43 @@ export function publicSubdomainsFromHostsFile(content) {
return result
}
/**
* Dev-Subdomains aus einer von syncPreviewTls geschriebenen preview-hosts.yml:
* alle HTTPS-Router, deren Service preview-runner@docker ist.
*/
export function devSubdomainsFromHostsFile(content) {
const result = new Set()
let current = null
for (const line of String(content || '').split('\n')) {
if (/^ {4}\S/.test(line)) {
const match = line.match(/^ {4}preview-([a-z0-9-]+)-https:\s*$/)
current = match ? match[1] : null
} else if (current && /^ {6}service:\s*preview-runner@docker\s*$/.test(line)) {
result.add(current)
}
}
return result
}
/**
* Projekte mit laufendem/schlafendem Dev-Server (nur mit Preview-Runner).
* Liefert null, wenn Appwrite nicht antwortet.
*/
async function getDevProjects() {
try {
const docs = await listDocuments(config.collections.websiteProjects, [
Query.equal('previewState', ['sleeping', 'starting', 'online']),
Query.limit(500),
])
return docs.filter(
(d) => d.subdomain && /^[a-z0-9-]+$/.test(d.subdomain) && String(d.previewDetected || '').startsWith('dev')
)
} catch (err) {
console.error('[previewDeploy] getDevProjects fehlgeschlagen:', err.message)
return null
}
}
export async function fetchPreviewConfig(repoFullName, branch) {
const [owner, repo] = repoFullName.split('/')
if (!owner || !repo || !config.gitea.apiToken) {
@@ -341,12 +378,25 @@ export async function syncPreviewTls() {
entries = []
}
const slugs = entries
const folderSlugs = entries
.filter((entry) => entry.isDirectory() && /^[a-z0-9-]+$/.test(entry.name))
.map((entry) => entry.name)
.sort()
const hostsFile = path.join(outDir, 'preview-hosts.yml')
// Dev-Server (Preview-Runner): Hosts auch ohne Ordner, Service preview-runner
let devSet = new Set()
let devProjects = []
if (config.previewRunner.enabled) {
const found = await getDevProjects()
if (found) {
devProjects = found
devSet = new Set(found.map((d) => d.subdomain))
} else {
// Appwrite antwortet nicht: Dev-Hosts aus der letzten Datei uebernehmen
devSet = await fs.readFile(hostsFile, 'utf8').then(devSubdomainsFromHostsFile, () => new Set())
}
}
const slugs = [...new Set([...folderSlugs, ...devSet])].sort()
let publicSet = await getPublicSubdomains()
let publicSource = 'appwrite'
if (!publicSet) {
@@ -361,6 +411,7 @@ export async function syncPreviewTls() {
const host = `${name}.${baseHost}`
const id = `preview-${name}`
const isPublic = publicSet.has(name)
const service = devSet.has(name) ? 'preview-runner@docker' : 'preview-nginx@docker'
// Oeffentliche Website: HTTPS-Router OHNE preview-auth -> frei zugaenglich.
// Preview (Standard): mit preview-chain (ForwardAuth) -> login-geschuetzt.
const httpsMiddlewareLines = isPublic
@@ -371,7 +422,7 @@ export async function syncPreviewTls() {
` rule: "Host(\`${host}\`)"`,
' entryPoints:',
' - websecure',
' service: preview-nginx@docker',
` service: ${service}`,
...httpsMiddlewareLines,
' priority: 100',
' tls:',
@@ -381,7 +432,7 @@ export async function syncPreviewTls() {
` rule: "Host(\`${host}\`)"`,
' entryPoints:',
' - web',
' service: preview-nginx@docker',
` service: ${service}`,
' middlewares:',
' - preview-sites-redirect@docker',
' priority: 100',
@@ -398,9 +449,49 @@ export async function syncPreviewTls() {
await fs.mkdir(outDir, { recursive: true })
await fs.writeFile(hostsFile, content, 'utf8')
if (devProjects.length) registerDevSpecs(devProjects)
return { synced: true, hosts: slugs.length, publicSource }
}
// R12: Dev-Auftraege beim Runner hinterlegen (Hintergrund, nie blockierend/werfend)
let registering = null
/** Wartet auf eine laufende Registrierungsrunde (Tests). */
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)
}
})
)
})().finally(() => { registering = null })
}
/**
* Nach einem Server-Neustart startet der Kundenbereich oft vor Appwrite. Dann
* kennt syncPreviewTls die oeffentlichen Seiten nur aus der letzten Datei; der