feat(core): Introduce per-invocation TsDockerSession and session-aware local registry and build orchestration; stream and parse buildx output for improved logging and visibility; detect Docker topology and add CI-safe cleanup; update README with multi-arch, parallel-build, caching, and local registry usage and new CLI flags.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user