Preview: Deploy auf fremde Subdomain sperren
Ein kopiertes Repo (git clone + push, Vorlage) bringt die .webklar-preview.json des Originals mit. Sein erster Push ueberschrieb die Preview des Originals, danach scheiterte der Webhook mit 500 (Unique-Index auf subdomain). Fast passiert bei Olzinger -> HSK-Vorlage, 04.10.2026. - foreignSubdomainOwner: Subdomain gehoert einem anderen Repo oder einem Projekt ohne Repo; ein in Gitea umgezogenes Repo (Weiterleitung) zaehlt nicht als fremd - Webhook: fremde Subdomain -> kein Deploy, Projekt ohne Preview (neu: synced, Repo-Name statt fremdem Anzeigenamen); eine schon vergebene eigene Preview bleibt, WOMS zeigt den Grund als Preview-Fehler - runDeploy prueft fuer alle anderen Wege (WOMS, Reaktivieren), vor dem Runner, damit ein gesperrter Aufruf keinen laufenden Bau verwaist; Appwrite-Fehler bei der Pruefung blockieren dort nicht (Webhook streng) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4fc4c70b7d
commit
75794f14d5
4 files changed
+370
-5
No files matched your search
@@ -1,11 +1,12 @@
|
||||
import crypto from 'node:crypto'
|
||||
import { Router } from 'express'
|
||||
import { config } from '../../config.js'
|
||||
import { upsertWebsiteProjectByRepo, getDocument } from '../../services/appwriteAdmin.js'
|
||||
import { upsertWebsiteProjectByRepo, getDocument, listDocuments, Query } from '../../services/appwriteAdmin.js'
|
||||
import {
|
||||
fetchPreviewConfig,
|
||||
defaultPreviewConfig,
|
||||
runDeploy,
|
||||
foreignSubdomainBlock,
|
||||
deployKeepsStatus,
|
||||
buildPreviewUrl,
|
||||
} from '../../services/previewDeploy.js'
|
||||
@@ -107,18 +108,24 @@ router.post('/gitea', async (req, res) => {
|
||||
}
|
||||
}
|
||||
const previewConfig = previewConfigFile || defaultPreviewConfig(name)
|
||||
const hasPreview = Boolean(previewConfigFile?.enabled !== false && previewConfigFile)
|
||||
const wantsPreview = Boolean(previewConfigFile?.enabled !== false && previewConfigFile)
|
||||
// Kopiertes Repo mit der .webklar-preview.json eines anderen Projekts: dessen Subdomain weder
|
||||
// beliefern noch uebernehmen - das Projekt bleibt ohne Preview, bis WOMS ihm eine eigene gibt.
|
||||
const blocked = wantsPreview
|
||||
? await foreignSubdomainBlock(name, previewConfig.subdomain || name.split('/').pop())
|
||||
: null
|
||||
const hasPreview = wantsPreview && !blocked
|
||||
// Nur Projekte mit aktivierter Preview bekommen eine Subdomain zugewiesen -
|
||||
// Repo-only-Projekte (enabled:false, keine Subdomain) bleiben ohne Website.
|
||||
const subdomain = hasPreview ? previewConfig.subdomain || name.split('/').pop() : ''
|
||||
const previewUrl = subdomain ? buildPreviewUrl(subdomain) : ''
|
||||
|
||||
let deployResult = { deployed: false, skipped: true }
|
||||
let deployResult = blocked || { deployed: false, skipped: true }
|
||||
const configBranch = previewConfig.branch || 'main'
|
||||
|
||||
if (hasPreview && previewConfig.enabled !== false && branch === configBranch) {
|
||||
try {
|
||||
deployResult = await runDeploy(name, branch, previewConfig, { sha: commitSha })
|
||||
deployResult = await runDeploy(name, branch, previewConfig, { sha: commitSha, subdomainChecked: true })
|
||||
} catch (deployErr) {
|
||||
console.error('[webhook] deploy failed:', deployErr.message)
|
||||
deployResult = { deployed: false, error: deployErr.message }
|
||||
@@ -141,7 +148,8 @@ router.post('/gitea', async (req, res) => {
|
||||
const payloadNameStillCurrent = payloadName.toLowerCase() === current.toLowerCase()
|
||||
|
||||
const projectPayload = {
|
||||
projectName: previewConfig.displayName || (payloadNameStillCurrent ? repo.name : '') || subdomain,
|
||||
// der Anzeigename einer fremden Config gehoert ebenfalls dem Original
|
||||
projectName: (blocked ? '' : previewConfig.displayName) || (payloadNameStillCurrent ? repo.name : '') || subdomain,
|
||||
templateName: previewConfig.templateName || config.gitea.templateRepo,
|
||||
giteaRepoUrl: payloadNameStillCurrent
|
||||
? repo.html_url || `${config.gitea.baseUrl}/${current}`
|
||||
@@ -161,6 +169,20 @@ router.post('/gitea', async (req, res) => {
|
||||
} else if (hasPreview) {
|
||||
projectPayload.status = 'pending'
|
||||
projectPayload.provisioningStatus = deployResult.error ? 'deploy_failed' : 'pending'
|
||||
} else if (blocked) {
|
||||
// eine schon vergebene eigene Preview bleibt, wie sie ist - sonst Projekt ohne Preview;
|
||||
// ein vorhandener Projektname auch (der Repo-Name waere nur der Ersatz fuer neue Projekte)
|
||||
const existing = (await listDocuments(config.collections.websiteProjects, [
|
||||
Query.equal('repoFullName', current),
|
||||
Query.limit(1),
|
||||
]))[0]
|
||||
if (!existing?.subdomain) projectPayload.status = 'synced'
|
||||
else {
|
||||
// sonst stuende im WOMS weiter der alte Bau als aktuell da (Fehlertext nur bei 'failed')
|
||||
projectPayload.previewState = 'failed'
|
||||
projectPayload.previewError = `Neuer Stand nicht veröffentlicht: In .webklar-preview.json steht die Subdomain „${blocked.subdomain}“, die gehört ${blocked.owner}. Projekt im WOMS bearbeiten und speichern, dann stimmt die Datei wieder.`.slice(0, 500)
|
||||
}
|
||||
if (existing?.projectName) delete projectPayload.projectName
|
||||
} else {
|
||||
projectPayload.status = 'synced'
|
||||
}
|
||||
|
||||
@@ -802,7 +802,75 @@ async function runDeployViaRunner(repoFullName, branch, previewConfig, { project
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gehoert `subdomain` schon einem ANDEREN Projekt? Ein kopiertes Repo (git clone + push, Vorlage)
|
||||
* bringt die .webklar-preview.json des Originals mit - ohne diese Pruefung ueberschrieb sein erster
|
||||
* Push die Preview des Originals (Olzinger -> HSK-Vorlage, 04.10.2026). Ein in Gitea umbenanntes
|
||||
* oder umgezogenes Repo ist nicht fremd: Gitea leitet den alten Namen auf `repoFullName` weiter.
|
||||
* Ein Projekt ohne Repo (z. B. die Relaunch-Vorschau) haelt seine Subdomain ebenfalls.
|
||||
* @returns {Promise<string>} Repo-Name bzw. Projekt des fremden Besitzers, sonst ''
|
||||
*/
|
||||
export async function foreignSubdomainOwner(repoFullName, subdomain) {
|
||||
const own = String(repoFullName || '').toLowerCase()
|
||||
// ohne Repo gibt es nichts zu klonen - und ein Projekt ohne Repo sperrt sich nicht selbst
|
||||
if (!subdomain || !own) return ''
|
||||
const docs = await listDocuments(config.collections.websiteProjects, [
|
||||
Query.equal('subdomain', subdomain),
|
||||
Query.limit(5),
|
||||
])
|
||||
for (const doc of docs) {
|
||||
const other = String(doc.repoFullName || '')
|
||||
if (!other) return `Projekt „${doc.projectName || doc.$id}“ (ohne Repo)`
|
||||
if (other.toLowerCase() === own) continue
|
||||
let resolved = ''
|
||||
try {
|
||||
resolved = String((await giteaFetch(`/repos/${other}`))?.full_name || '')
|
||||
} catch {
|
||||
// geloescht oder Gitea nicht erreichbar: im Zweifel fremd
|
||||
}
|
||||
if (resolved.toLowerCase() !== own) return other
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Ergebnis fuer einen gesperrten Deploy oder null. `failOpen`: ist die Pruefung selbst nicht
|
||||
* moeglich (Appwrite weg), trotzdem deployen - so nutzt es runDeploy; der Webhook prueft vorher
|
||||
* streng, und die WOMS-Wege lesen das Projekt ohnehin zuerst aus Appwrite.
|
||||
*/
|
||||
export async function foreignSubdomainBlock(repoFullName, subdomain, { failOpen = false } = {}) {
|
||||
let owner
|
||||
try {
|
||||
owner = await foreignSubdomainOwner(repoFullName, subdomain)
|
||||
} catch (err) {
|
||||
if (!failOpen) throw err
|
||||
console.error(`[previewDeploy] ${repoFullName}: Besitzer der Subdomain "${subdomain}" nicht pruefbar, deploye trotzdem:`, err.message)
|
||||
return null
|
||||
}
|
||||
if (!owner) return null
|
||||
console.warn(`[previewDeploy] ${repoFullName}: Subdomain "${subdomain}" gehoert ${owner} - kein Deploy`)
|
||||
return { deployed: false, blocked: true, reason: 'subdomain_foreign', subdomain, owner }
|
||||
}
|
||||
|
||||
/**
|
||||
* Muss runDeploy den Besitzer der Subdomain pruefen? Nicht, wenn der Aufrufer es schon getan hat
|
||||
* (`subdomainChecked`, der Webhook) oder wenn es die schon vergebene Subdomain des uebergebenen
|
||||
* Projekts selbst ist.
|
||||
*/
|
||||
function needsOwnerCheck(repoFullName, subdomain, { project, subdomainChecked } = {}) {
|
||||
if (subdomainChecked) return false
|
||||
const same = (a, b) => String(a || '').toLowerCase() === String(b || '').toLowerCase()
|
||||
return !(project?.subdomain && same(project.subdomain, subdomain) && same(project.repoFullName, repoFullName))
|
||||
}
|
||||
|
||||
export async function runDeploy(repoFullName, branch, previewConfig, options = {}) {
|
||||
// Vor dem Runner pruefen: ein gesperrter Aufruf belegt dort keinen Platz und laesst so den
|
||||
// Verfolger eines laufenden Baus desselben Repos in Ruhe.
|
||||
const target = previewConfig.subdomain || repoFullName.split('/').pop()
|
||||
if (needsOwnerCheck(repoFullName, target, options)) {
|
||||
const blocked = await foreignSubdomainBlock(repoFullName, target, { failOpen: true })
|
||||
if (blocked) return blocked
|
||||
}
|
||||
if (config.previewRunner.enabled) {
|
||||
return runDeployViaRunner(repoFullName, branch, previewConfig, options)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user