import * as crypto from 'crypto'; import * as net from 'net'; import { logger } from './tsdocker.logging.js'; export interface ISessionConfig { sessionId: string; registryPort: number; registryHost: string; registryContainerName: string; isCI: boolean; ciSystem: string | null; builderSuffix: string; } /** * Per-invocation session identity for tsdocker. * Generates unique ports, container names, and builder names so that * concurrent CI jobs on the same Docker host don't collide. * * In local (non-CI) dev the builder suffix is empty, preserving the * persistent builder behavior. */ export class TsDockerSession { public config: ISessionConfig; private constructor(config: ISessionConfig) { this.config = config; } /** * Creates a new session. Allocates a dynamic port unless overridden * via `TSDOCKER_REGISTRY_PORT`. */ public static async create(): Promise { const sessionId = process.env.TSDOCKER_SESSION_ID || crypto.randomBytes(4).toString('hex'); const registryPort = await TsDockerSession.allocatePort(); const registryHost = `localhost:${registryPort}`; const registryContainerName = `tsdocker-registry-${sessionId}`; const { isCI, ciSystem } = TsDockerSession.detectCI(); const builderSuffix = isCI ? `-${sessionId}` : ''; const config: ISessionConfig = { sessionId, registryPort, registryHost, registryContainerName, isCI, ciSystem, builderSuffix, }; const session = new TsDockerSession(config); session.logInfo(); return session; } /** * Allocates a free TCP port. Respects `TSDOCKER_REGISTRY_PORT` override. */ public static async allocatePort(): Promise { const envPort = process.env.TSDOCKER_REGISTRY_PORT; if (envPort) { const parsed = parseInt(envPort, 10); if (!isNaN(parsed) && parsed > 0) { return parsed; } } return new Promise((resolve, reject) => { const srv = net.createServer(); srv.listen(0, '127.0.0.1', () => { const addr = srv.address() as net.AddressInfo; const port = addr.port; srv.close((err) => { if (err) reject(err); else resolve(port); }); }); srv.on('error', reject); }); } /** * Detects whether we're running inside a CI system. */ private static detectCI(): { isCI: boolean; ciSystem: string | null } { if (process.env.GITEA_ACTIONS) return { isCI: true, ciSystem: 'gitea-actions' }; if (process.env.GITHUB_ACTIONS) return { isCI: true, ciSystem: 'github-actions' }; if (process.env.GITLAB_CI) return { isCI: true, ciSystem: 'gitlab-ci' }; if (process.env.CI) return { isCI: true, ciSystem: 'generic' }; return { isCI: false, ciSystem: null }; } private logInfo(): void { const c = this.config; logger.log('info', '=== TSDOCKER SESSION ==='); logger.log('info', `Session ID: ${c.sessionId}`); logger.log('info', `Registry: ${c.registryHost} (container: ${c.registryContainerName})`); if (c.isCI) { logger.log('info', `CI detected: ${c.ciSystem}`); logger.log('info', `Builder suffix: ${c.builderSuffix}`); } } }