Preview-Router: öffentliche Seiten überstehen Server-Neustart

Nach einem Neustart startet der Kundenbereich oft vor Appwrite. Bisher
lieferte getPublicSubdomains dann ein leeres Set, syncPreviewTls setzte
alle Preview-Hosts hinter die Login-Weiche (preview-chain) und wiederholte
das nie. Öffentliche Seiten wie anelia-wolf und emmas-schatzkiste waren
bis zum nächsten Deploy nur noch mit Login erreichbar bzw. 403.

- Antwortet Appwrite nicht, übernimmt syncPreviewTls die öffentlichen
  Seiten aus der zuletzt geschriebenen preview-hosts.yml. Neue Subdomains
  bleiben login-geschützt, ohne alte Datei ist alles geschützt.
- Beim Start wird der Abgleich alle 15 s wiederholt (max. 10 min), bis er
  mit Appwrite-Daten geklappt hat.
- Tests: Datei lesen, Abgleich mit/ohne Appwrite, Startup-Wiederholung.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-26 22:19:33 +00:00
1 parent ba9916cfe6
commit d5eb908cb6
3 files changed
+218 -10

No files matched your search

+69 -6
View File
@@ -10,8 +10,8 @@ const execFileAsync = promisify(execFile)
/**
* Subdomains, die als "oeffentliche Website" (ohne Login) laufen sollen.
* Diese bekommen in syncPreviewTls einen Router OHNE preview-auth-ForwardAuth.
* Faellt bei Appwrite-Fehlern bewusst auf ein leeres Set zurueck -> alle Seiten
* bleiben dann login-geschuetzt (sicherer Default).
* Liefert null, wenn Appwrite nicht antwortet (z.B. direkt nach einem
* Server-Neustart, solange Appwrite noch hochfaehrt).
*/
async function getPublicSubdomains() {
try {
@@ -22,10 +22,38 @@ async function getPublicSubdomains() {
return new Set(docs.map((d) => d.subdomain).filter(Boolean))
} catch (err) {
console.error('[previewDeploy] getPublicSubdomains fehlgeschlagen:', err.message)
return new Set()
return null
}
}
/**
* Oeffentliche Subdomains aus einer von syncPreviewTls geschriebenen
* preview-hosts.yml: alle HTTPS-Router ohne preview-chain.
*/
export function publicSubdomainsFromHostsFile(content) {
const result = new Set()
let current = null
let isProtected = false
const finish = () => {
if (current && !isProtected) result.add(current)
current = null
}
for (const line of String(content || '').split('\n')) {
if (/^ {4}\S/.test(line)) {
finish()
const match = line.match(/^ {4}preview-([a-z0-9-]+)-https:\s*$/)
if (match) {
current = match[1]
isProtected = false
}
} else if (current && line.includes('preview-chain@docker')) {
isProtected = true
}
}
finish()
return result
}
export async function fetchPreviewConfig(repoFullName, branch) {
const [owner, repo] = repoFullName.split('/')
if (!owner || !repo || !config.gitea.apiToken) {
@@ -305,7 +333,16 @@ export async function syncPreviewTls() {
.map((entry) => entry.name)
.sort()
const publicSet = await getPublicSubdomains()
const hostsFile = path.join(outDir, 'preview-hosts.yml')
let publicSet = await getPublicSubdomains()
let publicSource = 'appwrite'
if (!publicSet) {
// Appwrite antwortet nicht: oeffentliche Seiten aus der letzten Datei
// uebernehmen, statt sie alle hinter den Login zu setzen. Neue
// Subdomains bleiben login-geschuetzt (sicherer Default).
publicSet = await fs.readFile(hostsFile, 'utf8').then(publicSubdomainsFromHostsFile, () => new Set())
publicSource = 'previous-file'
}
const blocks = slugs.map((name) => {
const host = `${name}.${baseHost}`
@@ -347,8 +384,34 @@ export async function syncPreviewTls() {
].join('\n')
await fs.mkdir(outDir, { recursive: true })
await fs.writeFile(path.join(outDir, 'preview-hosts.yml'), content, 'utf8')
return { synced: true, hosts: slugs.length }
await fs.writeFile(hostsFile, content, 'utf8')
return { synced: true, hosts: slugs.length, publicSource }
}
/**
* Nach einem Server-Neustart startet der Kundenbereich oft vor Appwrite. Dann
* kennt syncPreviewTls die oeffentlichen Seiten nur aus der letzten Datei; der
* Abgleich wird wiederholt, bis er mit Appwrite-Daten geklappt hat.
*/
export async function syncPreviewTlsOnStartup({
sync = syncPreviewTls,
retryMs = 15_000,
maxAttempts = 40,
} = {}) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const result = await sync()
console.log('[startup] Preview-TLS-Sync:', JSON.stringify(result))
if (!result.synced || result.publicSource === 'appwrite') return result
} catch (err) {
console.error('[startup] Preview-TLS-Sync fehlgeschlagen:', err.message)
}
if (attempt < maxAttempts) await new Promise((resolve) => setTimeout(resolve, retryMs))
}
console.error(
`[startup] Preview-TLS-Sync: Appwrite nach ${maxAttempts} Versuchen nicht erreichbar, oeffentliche Seiten bleiben wie in der letzten Datei`
)
return null
}
export async function runDeploy(repoFullName, branch, previewConfig) {