108 lines
3.2 KiB
TypeScript
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}`);
|
|
}
|
|
}
|
|
}
|