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:
2026-02-07 10:30:52 +00:00
parent 63078139ec
commit 101c4286c1
9 changed files with 500 additions and 167 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tsdocker',
version: '1.15.1',
version: '1.16.0',
description: 'develop npm modules cross platform with docker'
}

View File

@@ -1,4 +1,5 @@
import * as plugins from './tsdocker.plugins.js';
import * as fs from 'fs';
import { logger } from './tsdocker.logging.js';
import type { IDockerContextInfo } from './interfaces/index.js';
@@ -38,19 +39,28 @@ export class DockerContext {
isRootless = infoResult.stdout.includes('name=rootless');
}
this.contextInfo = { name, endpoint, isRootless, dockerHost: process.env.DOCKER_HOST };
// Detect topology
let topology: 'socket-mount' | 'dind' | 'local' = 'local';
if (process.env.DOCKER_HOST && process.env.DOCKER_HOST.startsWith('tcp://')) {
topology = 'dind';
} else if (fs.existsSync('/.dockerenv')) {
topology = 'socket-mount';
}
this.contextInfo = { name, endpoint, isRootless, dockerHost: process.env.DOCKER_HOST, topology };
return this.contextInfo;
}
/** Logs context info prominently. */
public logContextInfo(): void {
if (!this.contextInfo) return;
const { name, endpoint, isRootless, dockerHost } = this.contextInfo;
const { name, endpoint, isRootless, dockerHost, topology } = this.contextInfo;
logger.log('info', '=== DOCKER CONTEXT ===');
logger.log('info', `Context: ${name}`);
logger.log('info', `Endpoint: ${endpoint}`);
if (dockerHost) logger.log('info', `DOCKER_HOST: ${dockerHost}`);
logger.log('info', `Rootless: ${isRootless ? 'yes' : 'no'}`);
logger.log('info', `Topology: ${topology || 'local'}`);
}
/** Emits rootless-specific warnings. */

View File

@@ -3,6 +3,7 @@ import * as paths from './tsdocker.paths.js';
import { logger, formatDuration } from './tsdocker.logging.js';
import { DockerRegistry } from './classes.dockerregistry.js';
import { RegistryCopy } from './classes.registrycopy.js';
import { TsDockerSession } from './classes.tsdockersession.js';
import type { IDockerfileOptions, ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
import type { TsDockerManager } from './classes.tsdockermanager.js';
import * as fs from 'fs';
@@ -11,9 +12,14 @@ const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
});
const LOCAL_REGISTRY_PORT = 5234;
const LOCAL_REGISTRY_HOST = `localhost:${LOCAL_REGISTRY_PORT}`;
const LOCAL_REGISTRY_CONTAINER = 'tsdocker-local-registry';
/**
* Extracts a platform string (e.g. "linux/amd64") from a buildx bracket prefix.
* The prefix may be like "linux/amd64 ", "linux/amd64 stage-1 ", "stage-1 ", or "".
*/
function extractPlatform(prefix: string): string | null {
const match = prefix.match(/linux\/\w+/);
return match ? match[0] : null;
}
/**
* Class Dockerfile represents a Dockerfile on disk
@@ -148,40 +154,55 @@ export class Dockerfile {
return true;
}
/** Starts a persistent registry:2 container on port 5234 with volume storage. */
public static async startLocalRegistry(isRootless?: boolean): Promise<void> {
// Ensure persistent storage directory exists
const registryDataDir = plugins.path.join(paths.cwd, '.nogit', 'docker-registry');
/** Starts a persistent registry:2 container with session-unique port and name. */
public static async startLocalRegistry(session: TsDockerSession, isRootless?: boolean): Promise<void> {
const { registryPort, registryHost, registryContainerName, isCI, sessionId } = session.config;
// Ensure persistent storage directory exists — isolate per session in CI
const registryDataDir = isCI
? plugins.path.join(paths.cwd, '.nogit', 'docker-registry', sessionId)
: plugins.path.join(paths.cwd, '.nogit', 'docker-registry');
fs.mkdirSync(registryDataDir, { recursive: true });
await smartshellInstance.execSilent(
`docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true`
);
const result = await smartshellInstance.execSilent(
`docker run -d --name ${LOCAL_REGISTRY_CONTAINER} -p ${LOCAL_REGISTRY_PORT}:5000 -v "${registryDataDir}:/var/lib/registry" registry:2`
`docker rm -f ${registryContainerName} 2>/dev/null || true`
);
const runCmd = `docker run -d --name ${registryContainerName} -p ${registryPort}:5000 -v "${registryDataDir}:/var/lib/registry" registry:2`;
let result = await smartshellInstance.execSilent(runCmd);
// Port retry: if port was stolen between allocation and docker run, reallocate once
if (result.exitCode !== 0 && (result.stderr || result.stdout || '').includes('port is already allocated')) {
const newPort = await TsDockerSession.allocatePort();
logger.log('warn', `Port ${registryPort} taken, retrying with ${newPort}`);
session.config.registryPort = newPort;
session.config.registryHost = `localhost:${newPort}`;
const retryCmd = `docker run -d --name ${registryContainerName} -p ${newPort}:5000 -v "${registryDataDir}:/var/lib/registry" registry:2`;
result = await smartshellInstance.execSilent(retryCmd);
}
if (result.exitCode !== 0) {
throw new Error(`Failed to start local registry: ${result.stderr || result.stdout}`);
}
// registry:2 starts near-instantly; brief wait for readiness
await new Promise(resolve => setTimeout(resolve, 1000));
logger.log('info', `Started local registry at ${LOCAL_REGISTRY_HOST} (persistent storage at .nogit/docker-registry/)`);
logger.log('info', `Started local registry at ${session.config.registryHost} (container: ${registryContainerName})`);
if (isRootless) {
logger.log('warn', `[rootless] Registry on port ${LOCAL_REGISTRY_PORT} — if buildx cannot reach localhost:${LOCAL_REGISTRY_PORT}, try 127.0.0.1:${LOCAL_REGISTRY_PORT}`);
logger.log('warn', `[rootless] Registry on port ${session.config.registryPort} — if buildx cannot reach localhost, try 127.0.0.1`);
}
}
/** Stops and removes the temporary local registry container. */
public static async stopLocalRegistry(): Promise<void> {
/** Stops and removes the session-specific local registry container. */
public static async stopLocalRegistry(session: TsDockerSession): Promise<void> {
await smartshellInstance.execSilent(
`docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true`
`docker rm -f ${session.config.registryContainerName} 2>/dev/null || true`
);
logger.log('info', 'Stopped local registry');
logger.log('info', `Stopped local registry (${session.config.registryContainerName})`);
}
/** Pushes a built image to the local registry for buildx consumption. */
public static async pushToLocalRegistry(dockerfile: Dockerfile): Promise<void> {
const registryTag = `${LOCAL_REGISTRY_HOST}/${dockerfile.buildTag}`;
public static async pushToLocalRegistry(session: TsDockerSession, dockerfile: Dockerfile): Promise<void> {
const registryTag = `${session.config.registryHost}/${dockerfile.buildTag}`;
await smartshellInstance.execSilent(`docker tag ${dockerfile.buildTag} ${registryTag}`);
const result = await smartshellInstance.execSilent(`docker push ${registryTag}`);
if (result.exitCode !== 0) {
@@ -244,12 +265,13 @@ export class Dockerfile {
*/
public static async buildDockerfiles(
sortedArrayArg: Dockerfile[],
session: TsDockerSession,
options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean; parallel?: boolean; parallelConcurrency?: number },
): Promise<Dockerfile[]> {
const total = sortedArrayArg.length;
const overallStart = Date.now();
await Dockerfile.startLocalRegistry(options?.isRootless);
await Dockerfile.startLocalRegistry(session, options?.isRootless);
try {
if (options?.parallel) {
@@ -296,7 +318,7 @@ export class Dockerfile {
}
// Push ALL images to local registry (skip if already pushed via buildx)
if (!df.localRegistryTag) {
await Dockerfile.pushToLocalRegistry(df);
await Dockerfile.pushToLocalRegistry(session, df);
}
}
}
@@ -324,12 +346,12 @@ export class Dockerfile {
// Push ALL images to local registry (skip if already pushed via buildx)
if (!dockerfileArg.localRegistryTag) {
await Dockerfile.pushToLocalRegistry(dockerfileArg);
await Dockerfile.pushToLocalRegistry(session, dockerfileArg);
}
}
}
} finally {
await Dockerfile.stopLocalRegistry();
await Dockerfile.stopLocalRegistry(session);
}
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
@@ -520,6 +542,7 @@ export class Dockerfile {
// INSTANCE PROPERTIES
public managerRef: TsDockerManager;
public session?: TsDockerSession;
public filePath!: string;
public repo: string;
public version: string;
@@ -563,6 +586,79 @@ export class Dockerfile {
this.localBaseImageDependent = false;
}
/**
* Creates a line-by-line handler for Docker build output that logs
* recognized layer/step lines in an emphasized format.
*/
private createBuildOutputHandler(verbose: boolean): {
handleChunk: (chunk: Buffer | string) => void;
} {
let buffer = '';
const tag = this.cleanTag;
const handleLine = (line: string) => {
// In verbose mode, write raw output prefixed with tag for identification
if (verbose) {
process.stdout.write(`[${tag}] ${line}\n`);
}
// Buildx step: #N [platform step/total] INSTRUCTION
const bxStep = line.match(/^#\d+ \[([^\]]+?)(\d+\/\d+)\] (.+)/);
if (bxStep) {
const prefix = bxStep[1].trim();
const step = bxStep[2];
const instruction = bxStep[3];
const platform = extractPlatform(prefix);
const platStr = platform ? `${platform}` : '';
logger.log('note', `[${tag}] ${platStr}[${step}] ${instruction}`);
return;
}
// Buildx CACHED: #N CACHED
const bxCached = line.match(/^#(\d+) CACHED/);
if (bxCached) {
logger.log('note', `[${tag}] CACHED`);
return;
}
// Buildx DONE: #N DONE 12.3s
const bxDone = line.match(/^#\d+ DONE (.+)/);
if (bxDone) {
const timing = bxDone[1];
if (!timing.startsWith('0.0')) {
logger.log('note', `[${tag}] DONE ${timing}`);
}
return;
}
// Buildx export phase: #N exporting ...
const bxExport = line.match(/^#\d+ exporting (.+)/);
if (bxExport) {
logger.log('note', `[${tag}] exporting ${bxExport[1]}`);
return;
}
// Standard docker build: Step N/M : INSTRUCTION
const stdStep = line.match(/^Step (\d+\/\d+) : (.+)/);
if (stdStep) {
logger.log('note', `[${tag}] Step ${stdStep[1]}: ${stdStep[2]}`);
return;
}
};
return {
handleChunk: (chunk: Buffer | string) => {
buffer += chunk.toString();
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmed = line.replace(/\r$/, '').trim();
if (trimmed) handleLine(trimmed);
}
},
};
}
/**
* Builds the Dockerfile
*/
@@ -590,27 +686,32 @@ export class Dockerfile {
if (platformOverride) {
// Single platform override via buildx
buildCommand = `docker buildx build --platform ${platformOverride}${noCacheFlag}${buildContextFlag} --load -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
buildCommand = `docker buildx build --progress=plain --platform ${platformOverride}${noCacheFlag}${buildContextFlag} --load -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
logger.log('info', `Build: buildx --platform ${platformOverride} --load`);
} else if (config.platforms && config.platforms.length > 1) {
// Multi-platform build using buildx — always push to local registry
const platformString = config.platforms.join(',');
const localTag = `${LOCAL_REGISTRY_HOST}/${this.buildTag}`;
buildCommand = `docker buildx build --platform ${platformString}${noCacheFlag}${buildContextFlag} -t ${localTag} -f ${this.filePath} ${buildArgsString} --push .`;
const registryHost = this.session?.config.registryHost || 'localhost:5234';
const localTag = `${registryHost}/${this.buildTag}`;
buildCommand = `docker buildx build --progress=plain --platform ${platformString}${noCacheFlag}${buildContextFlag} -t ${localTag} -f ${this.filePath} ${buildArgsString} --push .`;
this.localRegistryTag = localTag;
logger.log('info', `Build: buildx --platform ${platformString} --push to local registry`);
} else {
// Standard build
const versionLabel = this.managerRef.projectInfo?.npm?.version || 'unknown';
buildCommand = `docker build --label="version=${versionLabel}"${noCacheFlag} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
buildCommand = `docker build --progress=plain --label="version=${versionLabel}"${noCacheFlag} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
logger.log('info', 'Build: docker build (standard)');
}
// Execute build with real-time layer logging
const handler = this.createBuildOutputHandler(verbose);
const streaming = await smartshellInstance.execStreamingSilent(buildCommand);
// Intercept output for layer logging
streaming.childProcess.stdout?.on('data', handler.handleChunk);
streaming.childProcess.stderr?.on('data', handler.handleChunk);
if (timeout) {
// Use streaming execution with timeout
const streaming = verbose
? await smartshellInstance.execStreaming(buildCommand)
: await smartshellInstance.execStreamingSilent(buildCommand);
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
streaming.childProcess.kill();
@@ -623,9 +724,7 @@ export class Dockerfile {
throw new Error(`Build failed for ${this.cleanTag}`);
}
} else {
const result = verbose
? await smartshellInstance.exec(buildCommand)
: await smartshellInstance.execSilent(buildCommand);
const result = await streaming.finalPromise;
if (result.exitCode !== 0) {
logger.log('error', `Build failed for ${this.cleanTag}`);
if (!verbose && result.stdout) {
@@ -646,12 +745,13 @@ export class Dockerfile {
const destRepo = this.getDestRepo(dockerRegistryArg.registryUrl);
const destTag = versionSuffix ? `${this.version}_${versionSuffix}` : this.version;
const registryCopy = new RegistryCopy();
const registryHost = this.session?.config.registryHost || 'localhost:5234';
this.pushTag = `${dockerRegistryArg.registryUrl}/${destRepo}:${destTag}`;
logger.log('info', `Pushing ${this.pushTag} via OCI copy from local registry...`);
await registryCopy.copyImage(
LOCAL_REGISTRY_HOST,
registryHost,
this.repo,
this.version,
dockerRegistryArg.registryUrl,
@@ -701,23 +801,27 @@ export class Dockerfile {
// Use local registry tag for multi-platform images (not in daemon), otherwise buildTag
const imageRef = this.localRegistryTag || this.buildTag;
const sessionId = this.session?.config.sessionId || 'default';
const testContainerName = `tsdocker_test_${sessionId}`;
const testImageName = `tsdocker_test_image_${sessionId}`;
const testFileExists = fs.existsSync(testFile);
if (testFileExists) {
// Run tests in container
await smartshellInstance.exec(
`docker run --name tsdocker_test_container --entrypoint="bash" ${imageRef} -c "mkdir /tsdocker_test"`
`docker run --name ${testContainerName} --entrypoint="bash" ${imageRef} -c "mkdir /tsdocker_test"`
);
await smartshellInstance.exec(`docker cp ${testFile} tsdocker_test_container:/tsdocker_test/test.sh`);
await smartshellInstance.exec(`docker commit tsdocker_test_container tsdocker_test_image`);
await smartshellInstance.exec(`docker cp ${testFile} ${testContainerName}:/tsdocker_test/test.sh`);
await smartshellInstance.exec(`docker commit ${testContainerName} ${testImageName}`);
const testResult = await smartshellInstance.exec(
`docker run --entrypoint="bash" tsdocker_test_image -x /tsdocker_test/test.sh`
`docker run --entrypoint="bash" ${testImageName} -x /tsdocker_test/test.sh`
);
// Cleanup
await smartshellInstance.exec(`docker rm tsdocker_test_container`);
await smartshellInstance.exec(`docker rmi --force tsdocker_test_image`);
await smartshellInstance.exec(`docker rm ${testContainerName}`);
await smartshellInstance.exec(`docker rmi --force ${testImageName}`);
if (testResult.exitCode !== 0) {
throw new Error(`Tests failed for ${this.cleanTag}`);

View File

@@ -6,6 +6,7 @@ import { DockerRegistry } from './classes.dockerregistry.js';
import { RegistryStorage } from './classes.registrystorage.js';
import { TsDockerCache } from './classes.tsdockercache.js';
import { DockerContext } from './classes.dockercontext.js';
import { TsDockerSession } from './classes.tsdockersession.js';
import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({
@@ -20,6 +21,7 @@ export class TsDockerManager {
public config: ITsDockerConfig;
public projectInfo: any;
public dockerContext: DockerContext;
public session!: TsDockerSession;
private dockerfiles: Dockerfile[] = [];
constructor(config: ITsDockerConfig) {
@@ -77,6 +79,9 @@ export class TsDockerManager {
}
}
// Create session identity (unique ports, names for CI concurrency)
this.session = await TsDockerSession.create();
logger.log('info', `Prepared TsDockerManager with ${this.registryStorage.getAllRegistries().length} registries`);
}
@@ -98,6 +103,10 @@ export class TsDockerManager {
this.dockerfiles = await Dockerfile.readDockerfiles(this);
this.dockerfiles = await Dockerfile.sortDockerfiles(this.dockerfiles);
this.dockerfiles = await Dockerfile.mapDockerfiles(this.dockerfiles);
// Inject session into each Dockerfile
for (const df of this.dockerfiles) {
df.session = this.session;
}
return this.dockerfiles;
}
@@ -187,7 +196,7 @@ export class TsDockerManager {
const total = toBuild.length;
const overallStart = Date.now();
await Dockerfile.startLocalRegistry(this.dockerContext.contextInfo?.isRootless);
await Dockerfile.startLocalRegistry(this.session, this.dockerContext.contextInfo?.isRootless);
try {
if (options?.parallel) {
@@ -240,7 +249,7 @@ export class TsDockerManager {
}
// Push ALL images to local registry (skip if already pushed via buildx)
if (!df.localRegistryTag) {
await Dockerfile.pushToLocalRegistry(df);
await Dockerfile.pushToLocalRegistry(this.session, df);
}
}
}
@@ -280,19 +289,19 @@ export class TsDockerManager {
// Push ALL images to local registry (skip if already pushed via buildx)
if (!dockerfileArg.localRegistryTag) {
await Dockerfile.pushToLocalRegistry(dockerfileArg);
await Dockerfile.pushToLocalRegistry(this.session, dockerfileArg);
}
}
}
} finally {
await Dockerfile.stopLocalRegistry();
await Dockerfile.stopLocalRegistry(this.session);
}
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
cache.save();
} else {
// === STANDARD MODE: build all via static helper ===
await Dockerfile.buildDockerfiles(toBuild, {
await Dockerfile.buildDockerfiles(toBuild, this.session, {
platform: options?.platform,
timeout: options?.timeout,
noCache: options?.noCache,
@@ -329,7 +338,7 @@ export class TsDockerManager {
* Ensures Docker buildx is set up for multi-architecture builds
*/
private async ensureBuildx(): Promise<void> {
const builderName = this.dockerContext.getBuilderName();
const builderName = this.dockerContext.getBuilderName() + (this.session?.config.builderSuffix || '');
const platforms = this.config.platforms?.join(', ') || 'default';
logger.log('info', `Setting up Docker buildx [${platforms}]...`);
logger.log('info', `Builder: ${builderName}`);
@@ -394,7 +403,7 @@ export class TsDockerManager {
}
// Start local registry (reads from persistent .nogit/docker-registry/)
await Dockerfile.startLocalRegistry(this.dockerContext.contextInfo?.isRootless);
await Dockerfile.startLocalRegistry(this.session, this.dockerContext.contextInfo?.isRootless);
try {
// Push each Dockerfile to each registry via OCI copy
for (const dockerfile of this.dockerfiles) {
@@ -403,7 +412,7 @@ export class TsDockerManager {
}
}
} finally {
await Dockerfile.stopLocalRegistry();
await Dockerfile.stopLocalRegistry(this.session);
}
logger.log('success', 'All images pushed successfully');
@@ -446,11 +455,11 @@ export class TsDockerManager {
logger.log('info', '');
logger.log('info', '=== TEST PHASE ===');
await Dockerfile.startLocalRegistry(this.dockerContext.contextInfo?.isRootless);
await Dockerfile.startLocalRegistry(this.session, this.dockerContext.contextInfo?.isRootless);
try {
await Dockerfile.testDockerfiles(this.dockerfiles);
} finally {
await Dockerfile.stopLocalRegistry();
await Dockerfile.stopLocalRegistry(this.session);
}
logger.log('success', 'All tests completed');
@@ -490,4 +499,16 @@ export class TsDockerManager {
public getDockerfiles(): Dockerfile[] {
return this.dockerfiles;
}
/**
* Cleans up session-specific resources.
* In CI, removes the session-specific buildx builder to avoid accumulation.
*/
public async cleanup(): Promise<void> {
if (this.session?.config.isCI && this.session.config.builderSuffix) {
const builderName = this.dockerContext.getBuilderName() + this.session.config.builderSuffix;
logger.log('info', `CI cleanup: removing buildx builder ${builderName}`);
await smartshellInstance.execSilent(`docker buildx rm ${builderName} 2>/dev/null || true`);
}
}
}

View File

@@ -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}`);
}
}
}

View File

@@ -101,4 +101,5 @@ export interface IDockerContextInfo {
endpoint: string; // 'unix:///var/run/docker.sock'
isRootless: boolean;
dockerHost?: string; // value of DOCKER_HOST env var, if set
topology?: 'socket-mount' | 'dind' | 'local';
}

View File

@@ -64,6 +64,7 @@ export let run = () => {
}
await manager.build(buildOptions);
await manager.cleanup();
logger.log('success', 'Build completed successfully');
} catch (err) {
logger.log('error', `Build failed: ${(err as Error).message}`);
@@ -117,6 +118,7 @@ export let run = () => {
const registries = registryArg ? [registryArg] : undefined;
await manager.push(registries);
await manager.cleanup();
logger.log('success', 'Push completed successfully');
} catch (err) {
logger.log('error', `Push failed: ${(err as Error).message}`);
@@ -180,6 +182,7 @@ export let run = () => {
// Run tests
await manager.test();
await manager.cleanup();
logger.log('success', 'Tests completed successfully');
} catch (err) {
logger.log('error', `Tests failed: ${(err as Error).message}`);