#!/usr/bin/env node /** * Richtet die Gitea-Organisation "WEBklar" fuer den Umzug der Kundenprojekte ein: legt die * Organisation an (falls sie fehlt), fuegt die Mitglieder knso/jusn/andj zum Owners-Team hinzu * und legt den organisationsweiten Webhook auf das Portal an. Idempotent - ein zweiter Aufruf * aendert nichts mehr, wenn Organisation/Mitglieder/Hook bereits vorhanden sind. * * Haengt bewusst NICHT an server/config.js - laeuft spaeter auf dem HOST (nicht im Container), * wo dessen Appwrite-/Preview-Umgebungsvariablen nicht gesetzt sind. baseUrl/token/secret kommen * stattdessen als Parameter bzw. (im CLI-Teil) direkt aus der uebergebenen .env-Datei. * * Usage (auf dem Host): node scripts/gitea-organisation-setup.mjs --env /home/webklar/apps/.env */ import fs from 'node:fs' import { pathToFileURL } from 'node:url' /** * Liest eine simple .env-Datei (KEY=VALUE je Zeile, `#`-Kommentare und Leerzeilen werden * ignoriert, umschliessende `"`/`'` werden entfernt) ein. Gibt NIE etwas auf der Konsole aus - * die Datei kann Secrets (GITEA_SYSTEM_TOKEN, GITEA_WEBHOOK_TOKEN) enthalten. */ export function readEnvFile(filePath) { const content = fs.readFileSync(filePath, 'utf8') const result = {} for (const rawLine of content.split('\n')) { const line = rawLine.trim() if (!line || line.startsWith('#')) continue const eq = line.indexOf('=') if (eq === -1) continue const key = line.slice(0, eq).trim() let value = line.slice(eq + 1).trim() if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1) } result[key] = value } return result } /** Ruft `${baseUrl}/api/v1` mit `Authorization: token ` auf. Ein 404 wird NICHT geworfen (Aufrufer prueft `status`), alles andere Nicht-2xx schon. */ async function giteaJson(baseUrl, token, path, { method = 'GET', body } = {}) { const response = await fetch(`${baseUrl}/api/v1${path}`, { method, headers: { Authorization: `token ${token}`, 'Content-Type': 'application/json', }, body: body !== undefined ? JSON.stringify(body) : undefined, }) const text = await response.text() let data = null if (text) { try { data = JSON.parse(text) } catch { data = { message: text } } } if (!response.ok && response.status !== 404) { const error = new Error(data?.message || `Gitea ${response.status}`) error.status = response.status throw error } return { status: response.status, data } } export async function setupOrganisation({ baseUrl, token, org = 'WEBklar', members = ['knso', 'jusn', 'andj'], hookUrl = 'https://project.webklar.com/webhook/gitea', secret, } = {}) { // 1. Organisation anlegen, falls sie noch nicht existiert. let orgCreated = false const orgGet = await giteaJson(baseUrl, token, `/orgs/${org}`) if (orgGet.status === 404) { await giteaJson(baseUrl, token, '/orgs', { method: 'POST', body: { username: org, full_name: org, visibility: 'public' }, }) orgCreated = true } // 2. Fehlende Mitglieder im Owners-Team ergaenzen. const membersAdded = [] const teamsResult = await giteaJson(baseUrl, token, `/orgs/${org}/teams`) const owners = (teamsResult.data || []).find((t) => t.name === 'Owners') if (owners) { const existingResult = await giteaJson(baseUrl, token, `/teams/${owners.id}/members`) const existingLogins = new Set( (existingResult.data || []).map((m) => String(m.login || '').toLowerCase()) ) for (const member of members) { if (existingLogins.has(member.toLowerCase())) continue await giteaJson(baseUrl, token, `/teams/${owners.id}/members/${member}`, { method: 'PUT' }) membersAdded.push(member) } } // 3. Organisationsweiten Webhook aufs Portal anlegen, falls er noch fehlt. let hookCreated = false const hooksResult = await giteaJson(baseUrl, token, `/orgs/${org}/hooks`) const hasHook = (hooksResult.data || []).some((h) => h.config?.url === hookUrl) if (!hasHook) { await giteaJson(baseUrl, token, `/orgs/${org}/hooks`, { method: 'POST', body: { type: 'gitea', active: true, events: ['push'], branch_filter: '*', config: { url: hookUrl, content_type: 'json', secret }, }, }) hookCreated = true } return { orgCreated, membersAdded, hookCreated } } async function main() { const args = process.argv.slice(2) const envIdx = args.indexOf('--env') const envPath = envIdx !== -1 ? args[envIdx + 1] : undefined if (!envPath) { throw new Error('--env erforderlich') } const env = readEnvFile(envPath) const result = await setupOrganisation({ baseUrl: 'https://git.webklar.com', token: env.GITEA_SYSTEM_TOKEN, secret: env.GITEA_WEBHOOK_TOKEN, }) // Nur das Ergebnis - nie Token/Secret ausgeben. console.log(JSON.stringify(result)) } if (import.meta.url === pathToFileURL(process.argv[1]).href) { main().catch((err) => { console.error(err.message) process.exit(1) }) }