Umzug zugewiesener Repos nach WEBklar

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-30 10:21:34 +00:00
1 parent 51c4852488
commit 6e6c9a4c24
4 files changed
+369 -1

No files matched your search

+7
View File
@@ -21,6 +21,10 @@ import { createHash } from 'node:crypto'
* Dateiinhalt (Klartext, keine Kodierung) je `"owner/repo:pfad"`, z. B. `"knso/x:.webklar-preview.json"`.
* @param {string[]} [options.orgs]
* Organisationen, die von Anfang an existieren (mit leerer Owners-Mitgliedschaft).
* @param {Record<string, string>} [options.redirects]
* Vorab gesetzte Weiterleitungen (alter `"owner/repo"` -> neuer `"owner/repo"`), z. B. fuer ein
* bereits von Hand verschobenes Repo, ohne dass dafuer ein Transfer-Aufruf noetig war. Der Ziel-
* Repo-Eintrag muss separat ueber `options.repos` angelegt werden.
* @param {string} [options.user] Login, das `GET /user` meldet. Voreinstellung `'webklar-system'`.
* @returns {Promise<{
* baseUrl: string,
@@ -439,6 +443,9 @@ export async function startFakeGitea(options = {}) {
contents.set(`${keyFrom(fullName)}:${filePath}`, text)
}
for (const name of options.orgs || []) createOrgRecord(name)
for (const [from, to] of Object.entries(options.redirects || {})) {
redirects.set(keyFrom(from), to)
}
/* ------------------------------------------------------ oeffentlich --- */
+148
View File
@@ -0,0 +1,148 @@
import { test, before, after } from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { randomUUID } from 'node:crypto'
import { startFakeGitea } from './helpers/fakeGitea.js'
import { startFakeAppwrite } from './helpers/fakeAppwrite.js'
let gitea
let aw
let tmp
let originalCwd
let previewWorkDir
let moveRepoToOrg
let isOrgRepo
let isInternalRepo
let repoLockKey
let currentRepoFullName
// Schluessel: urspruenglich gesetzter repoFullName -> Appwrite-$id. Wird beim Seeden befuellt
// (siehe Controller-Ruling R2) - eine Live-Suche ueber repoFullName wuerde nach einem Umzug
// nichts mehr finden, weil sich das Feld dann geaendert hat.
const idByRepo = new Map()
/** Liefert das gespeicherte Dokument des Repos, ueber den zur Seed-Zeit gebauten Id-Index. */
function doc(name) {
return aw.get('websiteProjects', idByRepo.get(name))
}
before(async () => {
gitea = await startFakeGitea({
repos: [
{ full_name: 'JUSN/D-ner-Nation' },
{ full_name: 'knso/emmas-schatzkiste' },
{ full_name: 'ANDJ/Steuerungstechnik' },
{ full_name: 'knso/hytail-ark' },
{ full_name: 'WEBklar/schon-da' },
{ full_name: 'JUSN/schon-da' },
{ full_name: 'WEBklar/von-hand' },
{ full_name: 'ANDJ/unberuehrt' },
],
hooks: {
'knso/emmas-schatzkiste': [{ id: 110, url: 'https://project.webklar.com/webhook/gitea' }],
'knso/hytail-ark': [{ id: 7, url: 'https://webklar.com/webhook/hytail-ark/deploy' }],
},
// knso/von-hand wurde von Hand bereits nach WEBklar/von-hand verschoben (kein Transfer-Aufruf noetig).
redirects: {
'knso/von-hand': 'WEBklar/von-hand',
},
})
const repoNames = [
'JUSN/D-ner-Nation',
'knso/emmas-schatzkiste',
'ANDJ/Steuerungstechnik',
'knso/hytail-ark',
'WEBklar/schon-da',
'JUSN/schon-da',
'knso/von-hand', // Appwrite kennt noch den alten Namen (siehe Brief)
'ANDJ/unberuehrt',
]
const websiteProjects = repoNames.map((repoFullName) => {
const $id = randomUUID()
idByRepo.set(repoFullName, $id)
return { $id, repoFullName, projectName: repoFullName.split('/').pop() }
})
aw = await startFakeAppwrite({ seed: { websiteProjects } })
originalCwd = process.cwd()
tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'repo-move-'))
process.chdir(tmp)
Object.assign(process.env, {
GITEA_BASE_URL: gitea.baseUrl,
GITEA_API_TOKEN: 'test-token',
APPWRITE_ENDPOINT: aw.endpoint,
APPWRITE_API_KEY: 'test-key',
PREVIEW_DEPLOY_ROOT: '',
TRAEFIK_DYNAMIC_DIR: '',
})
// config.js liest die Umgebung beim Import
;({ previewWorkDir } = await import('../server/services/previewDeploy.js'))
;({ moveRepoToOrg, isOrgRepo, isInternalRepo, repoLockKey, currentRepoFullName } = await import(
'../server/services/repoMove.js'
))
})
after(async () => {
process.chdir(originalCwd)
await gitea.close()
await aw.close()
await fs.rm(tmp, { recursive: true, force: true })
})
test('verschiebt Repo aus eigenem Konto', async () => {
await fs.mkdir(previewWorkDir('JUSN/D-ner-Nation'), { recursive: true })
const r = await moveRepoToOrg(doc('JUSN/D-ner-Nation'))
assert.deepEqual(r, { moved: { from: 'JUSN/D-ner-Nation', to: 'WEBklar/D-ner-Nation' }, warnings: [] })
assert.deepEqual(gitea.calls.find((c) => c.path.endsWith('/transfer')).body, { new_owner: 'WEBklar' })
const d = aw.get('websiteProjects', doc('JUSN/D-ner-Nation').$id)
assert.equal(d.repoFullName, 'WEBklar/D-ner-Nation')
assert.equal(d.giteaRepoUrl, `${gitea.baseUrl}/WEBklar/D-ner-Nation`)
assert.equal(d.giteaRepoName, 'D-ner-Nation')
await assert.rejects(fs.access(previewWorkDir('JUSN/D-ner-Nation')))
})
test('löscht den alten Portal-Webhook', async () => {
await moveRepoToOrg(doc('knso/emmas-schatzkiste'))
assert.deepEqual(gitea.hooksOf('WEBklar/emmas-schatzkiste'), [])
})
test('schon in WEBklar: nichts passiert', async () => {
assert.deepEqual(await moveRepoToOrg(doc('WEBklar/schon-da')), { moved: null, warnings: [] })
})
test('internes Repo per Liste und per fremdem Webhook', async () => {
for (const name of ['ANDJ/Steuerungstechnik', 'knso/hytail-ark']) {
assert.deepEqual(await moveRepoToOrg(doc(name)), { moved: null, warnings: ['Internes Repo, bleibt wo es ist.'] })
}
assert.equal(gitea.calls.filter((c) => c.path.endsWith('/transfer') && /steuerung|hytail/i.test(c.path)).length, 0)
})
test('Name existiert in WEBklar', async () => {
const r = await moveRepoToOrg(doc('JUSN/schon-da'))
assert.deepEqual(r, { moved: null, warnings: ['In WEBklar gibt es schon ein Repo namens schon-da. Bitte in Gitea umbenennen und dann ‚Pushes nachtragen‘ klicken.'] })
})
test('Weiterleitung korrigiert Projektdaten', async () => {
const r = await moveRepoToOrg(doc('knso/von-hand'))
assert.deepEqual(r.moved, { from: 'knso/von-hand', to: 'WEBklar/von-hand' })
assert.equal(gitea.calls.filter((c) => c.path.endsWith('/transfer') && c.path.includes('von-hand')).length, 0)
assert.equal(aw.get('websiteProjects', doc('knso/von-hand').$id).repoFullName, 'WEBklar/von-hand')
})
test('Erkennung ohne Groß-/Kleinschreibung', async () => {
assert.equal(isOrgRepo('webklar/x'), true)
assert.equal(await isInternalRepo('andj/steuerungstechnik'), true)
})
test('repoLockKey', () => {
assert.equal(repoLockKey('JUSN/D-ner-Nation'), 'repo:jusn/d-ner-nation')
})
test('currentRepoFullName', async () => {
assert.equal(await currentRepoFullName('WEBklar/D-ner-Nation'), 'WEBklar/D-ner-Nation') // Eintrag vorhanden
assert.equal(await currentRepoFullName('JUSN/D-ner-Nation'), 'WEBklar/D-ner-Nation') // kein Eintrag, Weiterleitung
})
test('Gitea nicht erreichbar', async () => {
gitea.setDown(true)
const r = await moveRepoToOrg(doc('ANDJ/unberuehrt'))
gitea.setDown(false)
assert.equal(r.moved, null)
assert.match(r.warnings[0], /^Gitea ist gerade nicht erreichbar \(.+\)\. Bitte später ‚Pushes nachtragen‘ klicken\.$/)
})