Files
tsdocker/ts/classes.tsdockersession.ts

108 lines
3.2 KiB
TypeScript

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<TsDockerSession> {
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<number> {
const envPort = process.env.TSDOCKER_REGISTRY_PORT;
if (envPort) {
const parsed = parseInt(envPort, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
}
return new Promise<number>((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}`);
}
}
}