feat: monorepo migration phase 3
App checks / changes (push) Canceled after 0s
App checks / tournament (push) Canceled after 0s
App checks / shifts (push) Canceled after 0s

This commit is contained in:
2026-10-07 11:38:43 +01:00
parent e92ae5064c
commit 561b6a2da9
79 changed files with 4514 additions and 1541 deletions
+73
View File
@@ -0,0 +1,73 @@
// Migration-only transport adapter: runs the unchanged legacy HTTP assertions on FastAPI.
import { spawn } from 'node:child_process';
import { mkdtempSync, rmSync } from 'node:fs';
import net from 'node:net';
import { join } from 'node:path';
export const SESSION_COOKIE_NAME = 'tournament_admin_session';
export async function createApp(options) {
const temporary = mkdtempSync(join(process.env.TOURNAMENT_WORK, 'api-'));
const socket = net.createServer();
await new Promise(resolve => socket.listen(0, '127.0.0.1', resolve));
const port = socket.address().port;
await new Promise(resolve => socket.close(resolve));
const processHandle = spawn(process.env.TOURNAMENT_PYTHON, [
'-m', 'uvicorn', 'app.main:create_app', '--factory', '--host', '127.0.0.1',
'--port', String(port), '--no-proxy-headers',
], { cwd: process.env.TOURNAMENT_BACKEND, env: {
...process.env, ADMIN_PASSWORD: options.authentication.adminPassword,
SESSION_SECRET: options.authentication.sessionSecret,
DATABASE_PATH: join(temporary, 'tournament.sqlite'), APP_BASE_PATH: options.basePath,
TRUST_PROXY: 'false', PYTHONDONTWRITEBYTECODE: '1',
}, stdio: ['ignore', 'pipe', 'pipe'] });
let logs = '';
processHandle.stdout.on('data', chunk => { logs += chunk; });
processHandle.stderr.on('data', chunk => { logs += chunk; });
const base = `http://127.0.0.1:${port}`;
async function close() {
if (processHandle.exitCode === null) {
processHandle.kill('SIGTERM');
const timer = setTimeout(() => processHandle.kill('SIGKILL'), 3000);
await new Promise(resolve => processHandle.once('exit', resolve));
clearTimeout(timer);
}
rmSync(temporary, { recursive: true, force: true });
}
try {
let ready = false;
for (let count = 0; count < 100; count++) {
try {
const response = await fetch(base + '/api/auth/session');
if (response.ok) { ready = true; break; }
} catch { /* Wait for Uvicorn to bind its socket. */ }
if (processHandle.exitCode !== null) throw new Error(logs);
await new Promise(resolve => setTimeout(resolve, 50));
}
if (!ready) throw new Error('FastAPI startup timed out: ' + logs);
} catch (error) {
await close();
throw error;
}
return {
ready: async () => {}, listen: async () => {}, server: { address: () => ({ port }) }, close,
async inject(input) {
const headers = { ...input.headers };
if (input.payload !== undefined) headers['content-type'] = 'application/json';
const response = await fetch(base + input.url, {
method: input.method, headers,
body: input.payload === undefined ? undefined : JSON.stringify(input.payload),
});
const body = await response.text();
return {
statusCode: response.status, headers: Object.fromEntries(response.headers), body,
json: () => JSON.parse(body),
cookies: response.headers.getSetCookie().map(cookie => {
const [pair] = cookie.split(';');
const separator = pair.indexOf('=');
return { name: pair.slice(0, separator), value: pair.slice(separator + 1) };
}),
};
},
};
}