Neue Repos importieren, Abgleich ohne Subdomains, Selbstheilung nur für gehostete Projekte
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5ce8aa6fbb
commit
5c39640224
2 files changed
+183
-32
No files matched your search
@@ -15,6 +15,13 @@ import {
|
||||
} from './previewDeploy.js'
|
||||
import { giteaFetch } from './giteaClient.js'
|
||||
import { slugify } from './slug.js'
|
||||
import { withLocks } from './keyedLock.js'
|
||||
import { repoLockKey } from './repoMove.js'
|
||||
|
||||
// Sicherheitsnetz wie bei listRecentPushes (siehe pushHistory.js, Controller-Ruling R8): sollte
|
||||
// Gitea "page" ignorieren und immer dieselbe volle Seite liefern, blaettert die Schleife nicht
|
||||
// endlos weiter (40 x 50 = 2000 Repos).
|
||||
const MAX_REPO_PAGES = 40
|
||||
|
||||
export async function listAllAccessibleRepos() {
|
||||
const repos = []
|
||||
@@ -27,40 +34,67 @@ export async function listAllAccessibleRepos() {
|
||||
if (data.length === 0) break
|
||||
repos.push(...data)
|
||||
if (data.length < limit) break
|
||||
if (page >= MAX_REPO_PAGES) {
|
||||
console.warn(
|
||||
`[giteaAdmin] mehr als ${MAX_REPO_PAGES} Seiten Repos – weitere Repos werden nicht gelistet`
|
||||
)
|
||||
break
|
||||
}
|
||||
page += 1
|
||||
}
|
||||
|
||||
return repos
|
||||
}
|
||||
|
||||
async function resolveSubdomainForSync(repo, existingDoc) {
|
||||
if (existingDoc?.subdomain) return existingDoc.subdomain
|
||||
/**
|
||||
* Findet Repos, die in Gitea liegen, aber noch kein Appwrite-Projektdokument haben, und legt fuer
|
||||
* jedes nicht archivierte ein minimales Dokument an (status "synced", ohne Hosting/Subdomain) -
|
||||
* ohne dass dafuer manuell "Abgleich" gedrueckt werden muesste. Wird vom Projekt-Dialog bei jedem
|
||||
* Oeffnen aufgerufen, laeuft also oft: die aeussere `withLocks(['import'])`-Sperre serialisiert
|
||||
* parallele Laeufe komplett (ein zweiter Aufruf sieht danach garantiert den Stand des ersten), die
|
||||
* innere `withLocks([repoLockKey(...)])` je neuem Repo schuetzt zusaetzlich gegen andere
|
||||
* gleichzeitige Schreiber desselben Projektdokuments (z. B. Umzug/Zuweisung).
|
||||
*/
|
||||
export async function importNewRepos() {
|
||||
return withLocks(['import'], async () => {
|
||||
const [repos, docs] = await Promise.all([
|
||||
listAllAccessibleRepos(),
|
||||
listDocuments(config.collections.websiteProjects, [Query.limit(5000)]),
|
||||
])
|
||||
|
||||
const candidate = slugify(repo.name) || slugify(repo.full_name?.split('/').pop())
|
||||
if (!candidate) return ''
|
||||
// Ohne Ruecksicht auf Gross-/Kleinschreibung vergleichen (Controller-Ruling).
|
||||
const existingNames = new Set(
|
||||
docs.map((d) => String(d.repoFullName || '').toLowerCase()).filter(Boolean)
|
||||
)
|
||||
|
||||
const conflict = await listDocuments(config.collections.websiteProjects, [
|
||||
Query.equal('subdomain', candidate),
|
||||
Query.limit(1),
|
||||
])
|
||||
let created = 0
|
||||
for (const repo of repos) {
|
||||
const fullName = repo.full_name
|
||||
if (!fullName || repo.archived) continue
|
||||
if (existingNames.has(fullName.toLowerCase())) continue
|
||||
|
||||
if (!conflict[0] || conflict[0].repoFullName === repo.full_name) {
|
||||
return candidate
|
||||
}
|
||||
await withLocks([repoLockKey(fullName)], async () => {
|
||||
const recheck = await listDocuments(config.collections.websiteProjects, [
|
||||
Query.equal('repoFullName', fullName),
|
||||
Query.limit(1),
|
||||
])
|
||||
if (recheck[0]) return
|
||||
|
||||
const ownerSlug = slugify(repo.full_name?.replace('/', '-'))
|
||||
if (!ownerSlug) return ''
|
||||
await upsertWebsiteProjectByRepo(fullName, {
|
||||
projectName: repo.description || repo.name || fullName.split('/').pop(),
|
||||
giteaRepoUrl: repo.html_url,
|
||||
giteaRepoName: repo.name,
|
||||
repoFullName: fullName,
|
||||
archived: false,
|
||||
status: 'synced',
|
||||
giteaSyncedAt: new Date().toISOString(),
|
||||
})
|
||||
created += 1
|
||||
})
|
||||
}
|
||||
|
||||
const ownerConflict = await listDocuments(config.collections.websiteProjects, [
|
||||
Query.equal('subdomain', ownerSlug),
|
||||
Query.limit(1),
|
||||
])
|
||||
|
||||
if (!ownerConflict[0] || ownerConflict[0].repoFullName === repo.full_name) {
|
||||
return ownerSlug
|
||||
}
|
||||
|
||||
return ''
|
||||
return { created }
|
||||
})
|
||||
}
|
||||
|
||||
export async function syncReposToAppwrite() {
|
||||
@@ -77,10 +111,6 @@ export async function syncReposToAppwrite() {
|
||||
Query.limit(1),
|
||||
])
|
||||
|
||||
const branch = repo.default_branch || 'main'
|
||||
const subdomain = await resolveSubdomainForSync(repo, existing[0])
|
||||
const previewUrl = subdomain ? buildPreviewUrl(subdomain) : ''
|
||||
|
||||
const payload = {
|
||||
projectName: repo.description || repo.name || repoFullName.split('/').pop(),
|
||||
giteaRepoUrl: repo.html_url || `${config.gitea.baseUrl}/${repoFullName}`,
|
||||
@@ -90,9 +120,6 @@ export async function syncReposToAppwrite() {
|
||||
archived: Boolean(repo.archived),
|
||||
}
|
||||
|
||||
if (subdomain) payload.subdomain = subdomain
|
||||
if (previewUrl) payload.previewUrl = previewUrl
|
||||
|
||||
if (!existing[0]) {
|
||||
payload.status = 'synced'
|
||||
created += 1
|
||||
@@ -186,6 +213,20 @@ async function updatePreviewConfigFile(repoFullName, previewConfig, branch = 'ma
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* true, wenn zu diesem Projekt wirklich ein Hosting gehoert (Preview/Website/Vorlage, oder ein
|
||||
* bereits deployter bzw. anstehender Stand) - im Unterschied zu einem nur importierten/
|
||||
* abgeglichenen Repo ohne eigenes Hosting (z. B. status "synced" ohne Website). Bestimmt u. a.,
|
||||
* ob die Selbstheilung (ensureRepoPreviewConfig) fuer dieses Projekt ueberhaupt aktiv werden darf -
|
||||
* sonst wuerde mit dem organisationsweiten Webhook jeder Push in ein bloss abgeglichenes Repo eine
|
||||
* Preview anlegen.
|
||||
*/
|
||||
export function isHostedProject(doc) {
|
||||
if (['preview', 'website', 'template'].includes(doc?.projectType)) return true
|
||||
if (['deployed', 'pending'].includes(doc?.status)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Selbstheilung fuer eine geloeschte/fehlende .webklar-preview.json:
|
||||
* Rekonstruiert die Hosting-Config aus dem in Appwrite gespeicherten
|
||||
@@ -201,11 +242,11 @@ export async function ensureRepoPreviewConfig(repoFullName, branch = 'main') {
|
||||
])
|
||||
const doc = docs[0]
|
||||
if (!doc || doc.archived) return null
|
||||
if (!isHostedProject(doc)) return null
|
||||
|
||||
const type = deriveProjectType(doc)
|
||||
const hasWebsite = type === 'preview' || type === 'website' || type === 'template'
|
||||
const subdomain = doc.subdomain || ''
|
||||
if (!hasWebsite || !subdomain) return null
|
||||
if (!subdomain) return null
|
||||
|
||||
const previewConfig = {
|
||||
enabled: true,
|
||||
|
||||
Reference in new issue
Block a user