Compare commits

...

18 Commits

Author SHA1 Message Date
b04b8c9033 v1.13.0
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 4m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-07 04:33:07 +00:00
2130a8a879 feat(docker): add Docker context detection, rootless support, and context-aware buildx registry handling 2026-02-07 04:33:07 +00:00
17de78aed3 v1.12.0
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 4m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-06 16:35:49 +00:00
eddb8cd156 feat(docker): add detailed logging for buildx, build commands, local registry, and local dependency info 2026-02-06 16:35:49 +00:00
cfc7798d49 v1.11.0
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 3m59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-06 15:53:32 +00:00
37dfde005e feat(docker): start temporary local registry for buildx dependency resolution and ensure buildx builder uses host network 2026-02-06 15:53:32 +00:00
d1785aab86 v1.10.0
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 4m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-06 15:05:46 +00:00
31fb4aea3c feat(classes.dockerfile): support using a local base image as a build context in buildx commands 2026-02-06 15:05:46 +00:00
907048fa87 v1.9.0
Some checks failed
Default (tags) / security (push) Successful in 50s
Default (tags) / test (push) Failing after 4m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-06 14:52:16 +00:00
02b267ee10 feat(build): add verbose build output, progress logging, and timing for builds/tests 2026-02-06 14:52:16 +00:00
16cd0bbd87 v1.8.0
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 4m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-06 14:18:06 +00:00
cc83743f9a feat(build): add optional content-hash based build cache to skip rebuilding unchanged Dockerfiles 2026-02-06 14:18:06 +00:00
7131c16f80 v1.7.0
Some checks failed
Default (tags) / security (push) Successful in 31s
Default (tags) / test (push) Failing after 3m59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-06 13:39:24 +00:00
02688861f4 feat(cli): add CLI version display using commitinfo 2026-02-06 13:39:24 +00:00
3a8b301b3e v1.6.0
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 4m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-06 13:25:22 +00:00
c09bef33c3 feat(docker): add support for no-cache builds and tag built images for local dependency resolution 2026-02-06 13:25:21 +00:00
32eb0d1d77 v1.5.0
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 4m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-06 11:58:32 +00:00
7cac628975 feat(build): add support for selective builds, platform override and build timeout 2026-02-06 11:58:32 +00:00
10 changed files with 735 additions and 71 deletions

View File

@@ -1,5 +1,83 @@
# Changelog # Changelog
## 2026-02-07 - 1.13.0 - feat(docker)
add Docker context detection, rootless support, and context-aware buildx registry handling
- Introduce DockerContext class to detect current Docker context and rootless mode and to log warnings and context info
- Add IDockerContextInfo interface and a new context option on build/config to pass explicit Docker context
- Propagate --context CLI flag into TsDockerManager.prepare so CLI commands can set an explicit Docker context
- Make buildx builder name context-aware (tsdocker-builder-<sanitized-context>) and log builder name/platforms
- Pass isRootless into local registry startup and build pipeline; emit rootless-specific warnings and registry reachability hint
## 2026-02-06 - 1.12.0 - feat(docker)
add detailed logging for buildx, build commands, local registry, and local dependency info
- Log startup of local registry including a note about buildx dependency bridging
- Log constructed build commands and indicate whether buildx or standard docker build is used (including platforms and --push/--load distinctions)
- Emit build mode summary at start of build phase and report local base-image dependency mappings
- Report when --no-cache is enabled and surface buildx setup readiness with configured platforms
- Non-functional change: purely adds informational logging to improve observability during builds
## 2026-02-06 - 1.11.0 - feat(docker)
start temporary local registry for buildx dependency resolution and ensure buildx builder uses host network
- Introduce a temporary local registry (localhost:5234) with start/stop helpers and push support to expose local images for buildx
- Add Dockerfile.needsLocalRegistry to decide when a local registry is required (local base dependencies + multi-platform or platform option)
- Push built images to the local registry and set localRegistryTag on Dockerfile instances for BuildKit build-context usage
- Tag built images in the host daemon for dependent Dockerfiles to resolve local FROM references
- Integrate registry lifecycle into Dockerfile.buildDockerfiles and TsDockerManager build flows (start before builds, stop after)
- Ensure buildx builder is created with --driver-opt network=host and recreate existing builder if it lacks host network to allow registry access from build containers
## 2026-02-06 - 1.10.0 - feat(classes.dockerfile)
support using a local base image as a build context in buildx commands
- Adds --build-context flag mapping base image to docker-image://<localTag> when localBaseImageDependent && localBaseDockerfile are set
- Appends the build context flag to both single-platform and multi-platform docker buildx commands
- Logs an info message indicating the local build context mapping
## 2026-02-06 - 1.9.0 - feat(build)
add verbose build output, progress logging, and timing for builds/tests
- Add 'verbose' option to build/test flows (interfaces, CLI, and method signatures) to allow streaming raw docker build output or run silently
- Log per-item progress for build and test phases (e.g. (1/N) Building/Testing <tag>) and report individual durations
- Return elapsed time from Dockerfile.build() and Dockerfile.test() and aggregate total build/test times in manager
- Introduce formatDuration(ms) helper in logging module to format timings
- Switch from console.log to structured logger calls across cache, manager, dockerfile and push paths
- Use silent exec variants when verbose is false and stream exec when verbose is true
## 2026-02-06 - 1.8.0 - feat(build)
add optional content-hash based build cache to skip rebuilding unchanged Dockerfiles
- Introduce TsDockerCache to compute SHA-256 of Dockerfile content and persist cache to .nogit/tsdocker_support.json
- Add ICacheEntry and ICacheData interfaces and a cached flag to IBuildCommandOptions
- Integrate cached mode in TsDockerManager: skip builds on cache hits, verify image presence, record builds on misses, and still perform dependency tagging
- Expose --cached option in CLI to enable the cached build flow
- Cache records store contentHash, imageId, buildTag and timestamp
## 2026-02-06 - 1.7.0 - feat(cli)
add CLI version display using commitinfo
- Imported commitinfo from './00_commitinfo_data.js' and called tsdockerCli.addVersion(commitinfo.version) to surface package/commit version in the Smartcli instance
- Change made in ts/tsdocker.cli.ts — small user-facing CLI enhancement; no breaking changes
## 2026-02-06 - 1.6.0 - feat(docker)
add support for no-cache builds and tag built images for local dependency resolution
- Introduce IBuildCommandOptions.noCache to control --no-cache behavior
- Propagate noCache from CLI (via cache flag) through TsDockerManager to Dockerfile.build
- Append --no-cache to docker build/buildx commands when noCache is true
- After building an image, tag it with full base image references used by dependent Dockerfiles so their FROM lines resolve to the locally-built image
- Log tagging actions and execute docker tag via smartshellInstance
## 2026-02-06 - 1.5.0 - feat(build)
add support for selective builds, platform override and build timeout
- Introduce IBuildCommandOptions with patterns, platform and timeout to control build behavior
- Allow manager.build() to accept options and build only matching Dockerfiles (including dependencies) preserving topological order
- Add CLI parsing for build/push to accept positional Dockerfile patterns and --platform/--timeout flags
- Support single-platform override via docker buildx and multi-platform buildx detection
- Implement streaming exec with timeout to kill long-running builds and surface timeout errors
## 2026-02-04 - 1.4.3 - fix(dockerfile) ## 2026-02-04 - 1.4.3 - fix(dockerfile)
fix matching of base images to local Dockerfiles by stripping registry prefixes when comparing image references fix matching of base images to local Dockerfiles by stripping registry prefixes when comparing image references

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tsdocker", "name": "@git.zone/tsdocker",
"version": "1.4.3", "version": "1.13.0",
"private": false, "private": false,
"description": "develop npm modules cross platform with docker", "description": "develop npm modules cross platform with docker",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

View File

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

View File

@@ -0,0 +1,69 @@
import * as plugins from './tsdocker.plugins.js';
import { logger } from './tsdocker.logging.js';
import type { IDockerContextInfo } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash' });
export class DockerContext {
public contextInfo: IDockerContextInfo | null = null;
/** Sets DOCKER_CONTEXT env var for explicit context selection. */
public setContext(contextName: string): void {
process.env.DOCKER_CONTEXT = contextName;
logger.log('info', `Docker context explicitly set to: ${contextName}`);
}
/** Detects current Docker context via `docker context inspect` and rootless via `docker info`. */
public async detect(): Promise<IDockerContextInfo> {
let name = 'default';
let endpoint = 'unknown';
const contextResult = await smartshellInstance.execSilent(
`docker context inspect --format '{{json .}}'`
);
if (contextResult.exitCode === 0 && contextResult.stdout) {
try {
const parsed = JSON.parse(contextResult.stdout.trim());
const data = Array.isArray(parsed) ? parsed[0] : parsed;
name = data.Name || 'default';
endpoint = data.Endpoints?.docker?.Host || 'unknown';
} catch { /* fallback to defaults */ }
}
let isRootless = false;
const infoResult = await smartshellInstance.execSilent(
`docker info --format '{{json .SecurityOptions}}'`
);
if (infoResult.exitCode === 0 && infoResult.stdout) {
isRootless = infoResult.stdout.includes('name=rootless');
}
this.contextInfo = { name, endpoint, isRootless, dockerHost: process.env.DOCKER_HOST };
return this.contextInfo;
}
/** Logs context info prominently. */
public logContextInfo(): void {
if (!this.contextInfo) return;
const { name, endpoint, isRootless, dockerHost } = 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'}`);
}
/** Emits rootless-specific warnings. */
public logRootlessWarnings(): void {
if (!this.contextInfo?.isRootless) return;
logger.log('warn', '[rootless] network=host in buildx is namespaced by rootlesskit');
logger.log('warn', '[rootless] Local registry may have localhost vs 127.0.0.1 resolution quirks');
}
/** Returns context-aware builder name: tsdocker-builder-<context> */
public getBuilderName(): string {
const contextName = this.contextInfo?.name || 'default';
const sanitized = contextName.replace(/[^a-zA-Z0-9_-]/g, '-');
return `tsdocker-builder-${sanitized}`;
}
}

View File

@@ -1,8 +1,8 @@
import * as plugins from './tsdocker.plugins.js'; import * as plugins from './tsdocker.plugins.js';
import * as paths from './tsdocker.paths.js'; import * as paths from './tsdocker.paths.js';
import { logger } from './tsdocker.logging.js'; import { logger, formatDuration } from './tsdocker.logging.js';
import { DockerRegistry } from './classes.dockerregistry.js'; import { DockerRegistry } from './classes.dockerregistry.js';
import type { IDockerfileOptions, ITsDockerConfig } from './interfaces/index.js'; import type { IDockerfileOptions, ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
import type { TsDockerManager } from './classes.tsdockermanager.js'; import type { TsDockerManager } from './classes.tsdockermanager.js';
import * as fs from 'fs'; import * as fs from 'fs';
@@ -10,6 +10,10 @@ const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash', executor: 'bash',
}); });
const LOCAL_REGISTRY_PORT = 5234;
const LOCAL_REGISTRY_HOST = `localhost:${LOCAL_REGISTRY_PORT}`;
const LOCAL_REGISTRY_CONTAINER = 'tsdocker-local-registry';
/** /**
* Class Dockerfile represents a Dockerfile on disk * Class Dockerfile represents a Dockerfile on disk
*/ */
@@ -26,8 +30,10 @@ export class Dockerfile {
.map(entry => plugins.path.join(paths.cwd, entry.name)); .map(entry => plugins.path.join(paths.cwd, entry.name));
const readDockerfilesArray: Dockerfile[] = []; const readDockerfilesArray: Dockerfile[] = [];
logger.log('info', `found ${fileTree.length} Dockerfiles:`); logger.log('info', `found ${fileTree.length} Dockerfile(s):`);
console.log(fileTree); for (const filePath of fileTree) {
logger.log('info', ` ${plugins.path.basename(filePath)}`);
}
for (const dockerfilePath of fileTree) { for (const dockerfilePath of fileTree) {
const myDockerfile = new Dockerfile(managerRef, { const myDockerfile = new Dockerfile(managerRef, {
@@ -133,13 +139,104 @@ export class Dockerfile {
return sortedDockerfileArray; return sortedDockerfileArray;
} }
/** Determines if a local registry is needed for buildx dependency resolution. */
public static needsLocalRegistry(
dockerfiles: Dockerfile[],
options?: { platform?: string },
): boolean {
const hasLocalDeps = dockerfiles.some(df => df.localBaseImageDependent);
if (!hasLocalDeps) return false;
const config = dockerfiles[0]?.managerRef?.config;
return !!options?.platform || !!(config?.platforms && config.platforms.length > 1);
}
/** Starts a temporary registry:2 container on port 5234. */
public static async startLocalRegistry(isRootless?: boolean): Promise<void> {
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 registry:2`
);
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} (buildx dependency bridge)`);
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}`);
}
}
/** Stops and removes the temporary local registry container. */
public static async stopLocalRegistry(): Promise<void> {
await smartshellInstance.execSilent(
`docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true`
);
logger.log('info', 'Stopped local registry');
}
/** 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}`;
await smartshellInstance.execSilent(`docker tag ${dockerfile.buildTag} ${registryTag}`);
const result = await smartshellInstance.execSilent(`docker push ${registryTag}`);
if (result.exitCode !== 0) {
throw new Error(`Failed to push to local registry: ${result.stderr || result.stdout}`);
}
dockerfile.localRegistryTag = registryTag;
logger.log('info', `Pushed ${dockerfile.buildTag} to local registry as ${registryTag}`);
}
/** /**
* Builds the corresponding real docker image for each Dockerfile class instance * Builds the corresponding real docker image for each Dockerfile class instance
*/ */
public static async buildDockerfiles(sortedArrayArg: Dockerfile[]): Promise<Dockerfile[]> { public static async buildDockerfiles(
for (const dockerfileArg of sortedArrayArg) { sortedArrayArg: Dockerfile[],
await dockerfileArg.build(); options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean },
): Promise<Dockerfile[]> {
const total = sortedArrayArg.length;
const overallStart = Date.now();
const useRegistry = Dockerfile.needsLocalRegistry(sortedArrayArg, options);
if (useRegistry) {
await Dockerfile.startLocalRegistry(options?.isRootless);
} }
try {
for (let i = 0; i < total; i++) {
const dockerfileArg = sortedArrayArg[i];
const progress = `(${i + 1}/${total})`;
logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
const elapsed = await dockerfileArg.build(options);
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
// Tag in host daemon for standard docker build compatibility
const dependentBaseImages = new Set<string>();
for (const other of sortedArrayArg) {
if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
dependentBaseImages.add(other.baseImage);
}
}
for (const fullTag of dependentBaseImages) {
logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
}
// Push to local registry for buildx dependency resolution
if (useRegistry && sortedArrayArg.some(other => other.localBaseDockerfile === dockerfileArg)) {
await Dockerfile.pushToLocalRegistry(dockerfileArg);
}
}
} finally {
if (useRegistry) {
await Dockerfile.stopLocalRegistry();
}
}
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
return sortedArrayArg; return sortedArrayArg;
} }
@@ -147,9 +244,19 @@ export class Dockerfile {
* Tests all Dockerfiles by calling Dockerfile.test() * Tests all Dockerfiles by calling Dockerfile.test()
*/ */
public static async testDockerfiles(sortedArrayArg: Dockerfile[]): Promise<Dockerfile[]> { public static async testDockerfiles(sortedArrayArg: Dockerfile[]): Promise<Dockerfile[]> {
for (const dockerfileArg of sortedArrayArg) { const total = sortedArrayArg.length;
await dockerfileArg.test(); const overallStart = Date.now();
for (let i = 0; i < total; i++) {
const dockerfileArg = sortedArrayArg[i];
const progress = `(${i + 1}/${total})`;
logger.log('info', `${progress} Testing ${dockerfileArg.cleanTag}...`);
const elapsed = await dockerfileArg.test();
logger.log('ok', `${progress} Tested ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
} }
logger.log('info', `Total test time: ${formatDuration(Date.now() - overallStart)}`);
return sortedArrayArg; return sortedArrayArg;
} }
@@ -328,6 +435,7 @@ export class Dockerfile {
public baseImage: string; public baseImage: string;
public localBaseImageDependent: boolean; public localBaseImageDependent: boolean;
public localBaseDockerfile!: Dockerfile; public localBaseDockerfile!: Dockerfile;
public localRegistryTag?: string;
constructor(managerRefArg: TsDockerManager, options: IDockerfileOptions) { constructor(managerRefArg: TsDockerManager, options: IDockerfileOptions) {
this.managerRef = managerRefArg; this.managerRef = managerRefArg;
@@ -362,38 +470,81 @@ export class Dockerfile {
/** /**
* Builds the Dockerfile * Builds the Dockerfile
*/ */
public async build(): Promise<void> { public async build(options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean }): Promise<number> {
logger.log('info', 'now building Dockerfile for ' + this.cleanTag); const startTime = Date.now();
const buildArgsString = await Dockerfile.getDockerBuildArgs(this.managerRef); const buildArgsString = await Dockerfile.getDockerBuildArgs(this.managerRef);
const config = this.managerRef.config; const config = this.managerRef.config;
const platformOverride = options?.platform;
const timeout = options?.timeout;
const noCacheFlag = options?.noCache ? ' --no-cache' : '';
const verbose = options?.verbose ?? false;
let buildContextFlag = '';
if (this.localBaseImageDependent && this.localBaseDockerfile) {
const fromImage = this.baseImage;
if (this.localBaseDockerfile.localRegistryTag) {
// BuildKit pulls from the local registry (reachable via host network)
const registryTag = this.localBaseDockerfile.localRegistryTag;
buildContextFlag = ` --build-context "${fromImage}=docker-image://${registryTag}"`;
logger.log('info', `Using local registry build context: ${fromImage} -> docker-image://${registryTag}`);
}
}
let buildCommand: string; let buildCommand: string;
// Check if multi-platform build is needed if (platformOverride) {
if (config.platforms && config.platforms.length > 1) { // Single platform override via buildx
buildCommand = `docker buildx build --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 // Multi-platform build using buildx
const platformString = config.platforms.join(','); const platformString = config.platforms.join(',');
buildCommand = `docker buildx build --platform ${platformString} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`; buildCommand = `docker buildx build --platform ${platformString}${noCacheFlag}${buildContextFlag} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
if (config.push) { if (config.push) {
buildCommand += ' --push'; buildCommand += ' --push';
logger.log('info', `Build: buildx --platform ${platformString} --push`);
} else { } else {
buildCommand += ' --load'; buildCommand += ' --load';
logger.log('info', `Build: buildx --platform ${platformString} --load`);
} }
} else { } else {
// Standard build // Standard build
const versionLabel = this.managerRef.projectInfo?.npm?.version || 'unknown'; const versionLabel = this.managerRef.projectInfo?.npm?.version || 'unknown';
buildCommand = `docker build --label="version=${versionLabel}" -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`; buildCommand = `docker build --label="version=${versionLabel}"${noCacheFlag} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
logger.log('info', 'Build: docker build (standard)');
} }
const result = await smartshellInstance.exec(buildCommand); 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();
reject(new Error(`Build timed out after ${timeout}s for ${this.cleanTag}`));
}, timeout * 1000);
});
const result = await Promise.race([streaming.finalPromise, timeoutPromise]);
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
logger.log('error', `Build failed for ${this.cleanTag}`); logger.log('error', `Build failed for ${this.cleanTag}`);
console.log(result.stdout);
throw new Error(`Build failed for ${this.cleanTag}`); throw new Error(`Build failed for ${this.cleanTag}`);
} }
} else {
const result = verbose
? await smartshellInstance.exec(buildCommand)
: await smartshellInstance.execSilent(buildCommand);
if (result.exitCode !== 0) {
logger.log('error', `Build failed for ${this.cleanTag}`);
if (!verbose && result.stdout) {
logger.log('error', `Build output:\n${result.stdout}`);
}
throw new Error(`Build failed for ${this.cleanTag}`);
}
}
logger.log('ok', `Built ${this.cleanTag}`); return Date.now() - startTime;
} }
/** /**
@@ -423,7 +574,7 @@ export class Dockerfile {
if (inspectResult.exitCode === 0 && inspectResult.stdout.includes('@')) { if (inspectResult.exitCode === 0 && inspectResult.stdout.includes('@')) {
const imageDigest = inspectResult.stdout.split('@')[1]?.trim(); const imageDigest = inspectResult.stdout.split('@')[1]?.trim();
console.log(`The image ${this.pushTag} has digest ${imageDigest}`); logger.log('info', `The image ${this.pushTag} has digest ${imageDigest}`);
} }
logger.log('ok', `Pushed ${this.pushTag}`); logger.log('ok', `Pushed ${this.pushTag}`);
@@ -450,15 +601,14 @@ export class Dockerfile {
/** /**
* Tests the Dockerfile by running a test script if it exists * Tests the Dockerfile by running a test script if it exists
*/ */
public async test(): Promise<void> { public async test(): Promise<number> {
const startTime = Date.now();
const testDir = this.managerRef.config.testDir || plugins.path.join(paths.cwd, 'test'); const testDir = this.managerRef.config.testDir || plugins.path.join(paths.cwd, 'test');
const testFile = plugins.path.join(testDir, 'test_' + this.version + '.sh'); const testFile = plugins.path.join(testDir, 'test_' + this.version + '.sh');
const testFileExists = fs.existsSync(testFile); const testFileExists = fs.existsSync(testFile);
if (testFileExists) { if (testFileExists) {
logger.log('info', `Running tests for ${this.cleanTag}`);
// Run tests in container // Run tests in container
await smartshellInstance.exec( await smartshellInstance.exec(
`docker run --name tsdocker_test_container --entrypoint="bash" ${this.buildTag} -c "mkdir /tsdocker_test"` `docker run --name tsdocker_test_container --entrypoint="bash" ${this.buildTag} -c "mkdir /tsdocker_test"`
@@ -477,11 +627,11 @@ export class Dockerfile {
if (testResult.exitCode !== 0) { if (testResult.exitCode !== 0) {
throw new Error(`Tests failed for ${this.cleanTag}`); throw new Error(`Tests failed for ${this.cleanTag}`);
} }
logger.log('ok', `Tests passed for ${this.cleanTag}`);
} else { } else {
logger.log('warn', `Skipping tests for ${this.cleanTag} because no test file was found at ${testFile}`); logger.log('warn', `Skipping tests for ${this.cleanTag} no test file at ${testFile}`);
} }
return Date.now() - startTime;
} }
/** /**

108
ts/classes.tsdockercache.ts Normal file
View File

@@ -0,0 +1,108 @@
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import * as plugins from './tsdocker.plugins.js';
import * as paths from './tsdocker.paths.js';
import { logger } from './tsdocker.logging.js';
import type { ICacheData, ICacheEntry } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
});
/**
* Manages content-hash-based build caching for Dockerfiles.
* Cache is stored in .nogit/tsdocker_support.json.
*/
export class TsDockerCache {
private cacheFilePath: string;
private data: ICacheData;
constructor() {
this.cacheFilePath = path.join(paths.cwd, '.nogit', 'tsdocker_support.json');
this.data = { version: 1, entries: {} };
}
/**
* Loads cache data from disk. Falls back to empty cache on missing/corrupt file.
*/
public load(): void {
try {
const raw = fs.readFileSync(this.cacheFilePath, 'utf-8');
const parsed = JSON.parse(raw);
if (parsed && parsed.version === 1 && parsed.entries) {
this.data = parsed;
} else {
logger.log('warn', '[cache] Cache file has unexpected format, starting fresh');
this.data = { version: 1, entries: {} };
}
} catch {
// Missing or corrupt file — start fresh
this.data = { version: 1, entries: {} };
}
}
/**
* Saves cache data to disk. Creates .nogit directory if needed.
*/
public save(): void {
const dir = path.dirname(this.cacheFilePath);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(this.cacheFilePath, JSON.stringify(this.data, null, 2), 'utf-8');
}
/**
* Computes SHA-256 hash of Dockerfile content.
*/
public computeContentHash(content: string): string {
return crypto.createHash('sha256').update(content).digest('hex');
}
/**
* Checks whether a build can be skipped for the given Dockerfile.
* Logs detailed diagnostics and returns true if the build should be skipped.
*/
public async shouldSkipBuild(cleanTag: string, content: string): Promise<boolean> {
const contentHash = this.computeContentHash(content);
const entry = this.data.entries[cleanTag];
if (!entry) {
logger.log('info', `[cache] ${cleanTag}: no cached entry, will build`);
return false;
}
const hashMatch = entry.contentHash === contentHash;
logger.log('info', `[cache] ${cleanTag}: hash ${hashMatch ? 'matches' : 'changed'}`);
if (!hashMatch) {
logger.log('info', `[cache] ${cleanTag}: content changed, will build`);
return false;
}
// Hash matches — verify the image still exists locally
const inspectResult = await smartshellInstance.exec(
`docker image inspect ${entry.imageId} > /dev/null 2>&1`
);
const available = inspectResult.exitCode === 0;
if (available) {
logger.log('info', `[cache] ${cleanTag}: cache hit, skipping build`);
return true;
}
logger.log('info', `[cache] ${cleanTag}: image no longer available, will build`);
return false;
}
/**
* Records a successful build in the cache.
*/
public recordBuild(cleanTag: string, content: string, imageId: string, buildTag: string): void {
this.data.entries[cleanTag] = {
contentHash: this.computeContentHash(content),
imageId,
buildTag,
timestamp: Date.now(),
};
}
}

View File

@@ -1,10 +1,12 @@
import * as plugins from './tsdocker.plugins.js'; import * as plugins from './tsdocker.plugins.js';
import * as paths from './tsdocker.paths.js'; import * as paths from './tsdocker.paths.js';
import { logger } from './tsdocker.logging.js'; import { logger, formatDuration } from './tsdocker.logging.js';
import { Dockerfile } from './classes.dockerfile.js'; import { Dockerfile } from './classes.dockerfile.js';
import { DockerRegistry } from './classes.dockerregistry.js'; import { DockerRegistry } from './classes.dockerregistry.js';
import { RegistryStorage } from './classes.registrystorage.js'; import { RegistryStorage } from './classes.registrystorage.js';
import type { ITsDockerConfig } from './interfaces/index.js'; import { TsDockerCache } from './classes.tsdockercache.js';
import { DockerContext } from './classes.dockercontext.js';
import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({ const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash', executor: 'bash',
@@ -17,17 +19,27 @@ export class TsDockerManager {
public registryStorage: RegistryStorage; public registryStorage: RegistryStorage;
public config: ITsDockerConfig; public config: ITsDockerConfig;
public projectInfo: any; public projectInfo: any;
public dockerContext: DockerContext;
private dockerfiles: Dockerfile[] = []; private dockerfiles: Dockerfile[] = [];
constructor(config: ITsDockerConfig) { constructor(config: ITsDockerConfig) {
this.config = config; this.config = config;
this.registryStorage = new RegistryStorage(); this.registryStorage = new RegistryStorage();
this.dockerContext = new DockerContext();
} }
/** /**
* Prepares the manager by loading project info and registries * Prepares the manager by loading project info and registries
*/ */
public async prepare(): Promise<void> { public async prepare(contextArg?: string): Promise<void> {
// Detect Docker context
if (contextArg) {
this.dockerContext.setContext(contextArg);
}
await this.dockerContext.detect();
this.dockerContext.logContextInfo();
this.dockerContext.logRootlessWarnings();
// Load project info // Load project info
try { try {
const projectinfoInstance = new plugins.projectinfo.ProjectInfo(paths.cwd); const projectinfoInstance = new plugins.projectinfo.ProjectInfo(paths.cwd);
@@ -90,9 +102,10 @@ export class TsDockerManager {
} }
/** /**
* Builds all discovered Dockerfiles in dependency order * Builds discovered Dockerfiles in dependency order.
* When options.patterns is provided, only matching Dockerfiles (and their dependencies) are built.
*/ */
public async build(): Promise<Dockerfile[]> { public async build(options?: IBuildCommandOptions): Promise<Dockerfile[]> {
if (this.dockerfiles.length === 0) { if (this.dockerfiles.length === 0) {
await this.discoverDockerfiles(); await this.discoverDockerfiles();
} }
@@ -102,38 +115,183 @@ export class TsDockerManager {
return []; return [];
} }
// Determine which Dockerfiles to build
let toBuild = this.dockerfiles;
if (options?.patterns && options.patterns.length > 0) {
// Filter to matching Dockerfiles
const matched = this.dockerfiles.filter((df) => {
const basename = plugins.path.basename(df.filePath);
return options.patterns!.some((pattern) => {
if (pattern.includes('*') || pattern.includes('?')) {
// Convert glob pattern to regex
const regexStr = '^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$';
return new RegExp(regexStr).test(basename);
}
return basename === pattern;
});
});
if (matched.length === 0) {
logger.log('warn', `No Dockerfiles matched patterns: ${options.patterns.join(', ')}`);
return [];
}
// Resolve dependency chain and preserve topological order
toBuild = this.resolveWithDependencies(matched, this.dockerfiles);
logger.log('info', `Matched ${matched.length} Dockerfile(s), building ${toBuild.length} (including dependencies)`);
}
// Check if buildx is needed // Check if buildx is needed
if (this.config.platforms && this.config.platforms.length > 1) { const useBuildx = !!(options?.platform || (this.config.platforms && this.config.platforms.length > 1));
if (useBuildx) {
await this.ensureBuildx(); await this.ensureBuildx();
} }
logger.log('info', `Building ${this.dockerfiles.length} Dockerfiles...`); logger.log('info', '');
await Dockerfile.buildDockerfiles(this.dockerfiles); logger.log('info', '=== BUILD PHASE ===');
if (useBuildx) {
const platforms = options?.platform || this.config.platforms!.join(', ');
logger.log('info', `Build mode: buildx multi-platform [${platforms}]`);
} else {
logger.log('info', 'Build mode: standard docker build');
}
const localDeps = toBuild.filter(df => df.localBaseImageDependent);
if (localDeps.length > 0) {
logger.log('info', `Local dependencies: ${localDeps.map(df => `${df.cleanTag} -> ${df.localBaseDockerfile?.cleanTag}`).join(', ')}`);
}
if (options?.noCache) {
logger.log('info', 'Cache: disabled (--no-cache)');
}
logger.log('info', `Building ${toBuild.length} Dockerfile(s)...`);
if (options?.cached) {
// === CACHED MODE: skip builds for unchanged Dockerfiles ===
logger.log('info', '(cached mode active)');
const cache = new TsDockerCache();
cache.load();
const total = toBuild.length;
const overallStart = Date.now();
const useRegistry = Dockerfile.needsLocalRegistry(toBuild, options);
if (useRegistry) {
await Dockerfile.startLocalRegistry(this.dockerContext.contextInfo?.isRootless);
}
try {
for (let i = 0; i < total; i++) {
const dockerfileArg = toBuild[i];
const progress = `(${i + 1}/${total})`;
const skip = await cache.shouldSkipBuild(dockerfileArg.cleanTag, dockerfileArg.content);
if (skip) {
logger.log('ok', `${progress} Skipped ${dockerfileArg.cleanTag} (cached)`);
} else {
logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
const elapsed = await dockerfileArg.build({
platform: options?.platform,
timeout: options?.timeout,
noCache: options?.noCache,
verbose: options?.verbose,
});
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
const imageId = await dockerfileArg.getId();
cache.recordBuild(dockerfileArg.cleanTag, dockerfileArg.content, imageId, dockerfileArg.buildTag);
}
// Tag for dependents IMMEDIATELY (not after all builds)
const dependentBaseImages = new Set<string>();
for (const other of toBuild) {
if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
dependentBaseImages.add(other.baseImage);
}
}
for (const fullTag of dependentBaseImages) {
logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
}
// Push to local registry for buildx (even for cache hits — image exists but registry doesn't)
if (useRegistry && toBuild.some(other => other.localBaseDockerfile === dockerfileArg)) {
await Dockerfile.pushToLocalRegistry(dockerfileArg);
}
}
} finally {
if (useRegistry) {
await Dockerfile.stopLocalRegistry();
}
}
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
cache.save();
} else {
// === STANDARD MODE: build all via static helper ===
await Dockerfile.buildDockerfiles(toBuild, {
platform: options?.platform,
timeout: options?.timeout,
noCache: options?.noCache,
verbose: options?.verbose,
isRootless: this.dockerContext.contextInfo?.isRootless,
});
}
logger.log('success', 'All Dockerfiles built successfully'); logger.log('success', 'All Dockerfiles built successfully');
return this.dockerfiles; return toBuild;
}
/**
* Resolves a set of target Dockerfiles to include all their local base image dependencies,
* preserving the original topological build order.
*/
private resolveWithDependencies(targets: Dockerfile[], allSorted: Dockerfile[]): Dockerfile[] {
const needed = new Set<Dockerfile>();
const addWithDeps = (df: Dockerfile) => {
if (needed.has(df)) return;
needed.add(df);
if (df.localBaseImageDependent && df.localBaseDockerfile) {
addWithDeps(df.localBaseDockerfile);
}
};
for (const df of targets) addWithDeps(df);
return allSorted.filter((df) => needed.has(df));
} }
/** /**
* Ensures Docker buildx is set up for multi-architecture builds * Ensures Docker buildx is set up for multi-architecture builds
*/ */
private async ensureBuildx(): Promise<void> { private async ensureBuildx(): Promise<void> {
logger.log('info', 'Setting up Docker buildx for multi-platform builds...'); const builderName = this.dockerContext.getBuilderName();
const platforms = this.config.platforms?.join(', ') || 'default';
// Check if a buildx builder exists logger.log('info', `Setting up Docker buildx [${platforms}]...`);
const inspectResult = await smartshellInstance.exec('docker buildx inspect tsdocker-builder 2>/dev/null'); logger.log('info', `Builder: ${builderName}`);
const inspectResult = await smartshellInstance.exec(`docker buildx inspect ${builderName} 2>/dev/null`);
if (inspectResult.exitCode !== 0) { if (inspectResult.exitCode !== 0) {
// Create a new buildx builder logger.log('info', 'Creating new buildx builder with host network...');
logger.log('info', 'Creating new buildx builder...'); await smartshellInstance.exec(
await smartshellInstance.exec('docker buildx create --name tsdocker-builder --use'); `docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host --use`
);
await smartshellInstance.exec('docker buildx inspect --bootstrap'); await smartshellInstance.exec('docker buildx inspect --bootstrap');
} else { } else {
// Use existing builder const inspectOutput = inspectResult.stdout || '';
await smartshellInstance.exec('docker buildx use tsdocker-builder'); if (!inspectOutput.includes('network=host')) {
logger.log('info', 'Recreating buildx builder with host network (migration)...');
await smartshellInstance.exec(`docker buildx rm ${builderName} 2>/dev/null`);
await smartshellInstance.exec(
`docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host --use`
);
await smartshellInstance.exec('docker buildx inspect --bootstrap');
} else {
await smartshellInstance.exec(`docker buildx use ${builderName}`);
} }
}
logger.log('ok', 'Docker buildx ready'); logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`);
} }
/** /**
@@ -215,6 +373,8 @@ export class TsDockerManager {
return; return;
} }
logger.log('info', '');
logger.log('info', '=== TEST PHASE ===');
await Dockerfile.testDockerfiles(this.dockerfiles); await Dockerfile.testDockerfiles(this.dockerfiles);
logger.log('success', 'All tests completed'); logger.log('success', 'All tests completed');
} }
@@ -227,19 +387,21 @@ export class TsDockerManager {
await this.discoverDockerfiles(); await this.discoverDockerfiles();
} }
console.log('\nDiscovered Dockerfiles:'); logger.log('info', '');
console.log('========================\n'); logger.log('info', 'Discovered Dockerfiles:');
logger.log('info', '========================');
logger.log('info', '');
for (let i = 0; i < this.dockerfiles.length; i++) { for (let i = 0; i < this.dockerfiles.length; i++) {
const df = this.dockerfiles[i]; const df = this.dockerfiles[i];
console.log(`${i + 1}. ${df.filePath}`); logger.log('info', `${i + 1}. ${df.filePath}`);
console.log(` Tag: ${df.cleanTag}`); logger.log('info', ` Tag: ${df.cleanTag}`);
console.log(` Base Image: ${df.baseImage}`); logger.log('info', ` Base Image: ${df.baseImage}`);
console.log(` Version: ${df.version}`); logger.log('info', ` Version: ${df.version}`);
if (df.localBaseImageDependent) { if (df.localBaseImageDependent) {
console.log(` Depends on: ${df.localBaseDockerfile?.cleanTag}`); logger.log('info', ` Depends on: ${df.localBaseDockerfile?.cleanTag}`);
} }
console.log(''); logger.log('info', '');
} }
return this.dockerfiles; return this.dockerfiles;

View File

@@ -68,3 +68,35 @@ export interface IPushResult {
digest?: string; digest?: string;
error?: string; error?: string;
} }
/**
* Options for the build command
*/
export interface IBuildCommandOptions {
patterns?: string[]; // Dockerfile name patterns (e.g., ['Dockerfile_base', 'Dockerfile_*'])
platform?: string; // Single platform override (e.g., 'linux/arm64')
timeout?: number; // Build timeout in seconds
noCache?: boolean; // Force rebuild without Docker layer cache (--no-cache)
cached?: boolean; // Skip builds when Dockerfile content hasn't changed
verbose?: boolean; // Stream raw docker build output (default: silent)
context?: string; // Explicit Docker context name (--context flag)
}
export interface ICacheEntry {
contentHash: string; // SHA-256 hex of Dockerfile content
imageId: string; // Docker image ID (sha256:...)
buildTag: string;
timestamp: number; // Unix ms
}
export interface ICacheData {
version: 1;
entries: { [cleanTag: string]: ICacheEntry };
}
export interface IDockerContextInfo {
name: string; // 'default', 'rootless', 'colima', etc.
endpoint: string; // 'unix:///var/run/docker.sock'
isRootless: boolean;
dockerHost?: string; // value of DOCKER_HOST env var, if set
}

View File

@@ -7,8 +7,11 @@ import * as DockerModule from './tsdocker.docker.js';
import { logger, ora } from './tsdocker.logging.js'; import { logger, ora } from './tsdocker.logging.js';
import { TsDockerManager } from './classes.tsdockermanager.js'; import { TsDockerManager } from './classes.tsdockermanager.js';
import type { IBuildCommandOptions } from './interfaces/index.js';
import { commitinfo } from './00_commitinfo_data.js';
const tsdockerCli = new plugins.smartcli.Smartcli(); const tsdockerCli = new plugins.smartcli.Smartcli();
tsdockerCli.addVersion(commitinfo.version);
export let run = () => { export let run = () => {
// Default command: run tests in container (legacy behavior) // Default command: run tests in container (legacy behavior)
@@ -23,14 +26,37 @@ export let run = () => {
}); });
/** /**
* Build all Dockerfiles in dependency order * Build Dockerfiles in dependency order
* Usage: tsdocker build [Dockerfile_patterns...] [--platform=linux/arm64] [--timeout=600]
*/ */
tsdockerCli.addCommand('build').subscribe(async argvArg => { tsdockerCli.addCommand('build').subscribe(async argvArg => {
try { try {
const config = await ConfigModule.run(); const config = await ConfigModule.run();
const manager = new TsDockerManager(config); const manager = new TsDockerManager(config);
await manager.prepare(); await manager.prepare(argvArg.context as string | undefined);
await manager.build();
const buildOptions: IBuildCommandOptions = {};
const patterns = argvArg._.slice(1) as string[];
if (patterns.length > 0) {
buildOptions.patterns = patterns;
}
if (argvArg.platform) {
buildOptions.platform = argvArg.platform as string;
}
if (argvArg.timeout) {
buildOptions.timeout = Number(argvArg.timeout);
}
if (argvArg.cache === false) {
buildOptions.noCache = true;
}
if (argvArg.cached) {
buildOptions.cached = true;
}
if (argvArg.verbose) {
buildOptions.verbose = true;
}
await manager.build(buildOptions);
logger.log('success', 'Build completed successfully'); logger.log('success', 'Build completed successfully');
} catch (err) { } catch (err) {
logger.log('error', `Build failed: ${(err as Error).message}`); logger.log('error', `Build failed: ${(err as Error).message}`);
@@ -40,21 +66,41 @@ export let run = () => {
/** /**
* Push built images to configured registries * Push built images to configured registries
* Usage: tsdocker push [Dockerfile_patterns...] [--platform=linux/arm64] [--timeout=600] [--registry=url]
*/ */
tsdockerCli.addCommand('push').subscribe(async argvArg => { tsdockerCli.addCommand('push').subscribe(async argvArg => {
try { try {
const config = await ConfigModule.run(); const config = await ConfigModule.run();
const manager = new TsDockerManager(config); const manager = new TsDockerManager(config);
await manager.prepare(); await manager.prepare(argvArg.context as string | undefined);
// Login first // Login first
await manager.login(); await manager.login();
// Build images first (if not already built) // Parse build options from positional args and flags
await manager.build(); const buildOptions: IBuildCommandOptions = {};
const patterns = argvArg._.slice(1) as string[];
if (patterns.length > 0) {
buildOptions.patterns = patterns;
}
if (argvArg.platform) {
buildOptions.platform = argvArg.platform as string;
}
if (argvArg.timeout) {
buildOptions.timeout = Number(argvArg.timeout);
}
if (argvArg.cache === false) {
buildOptions.noCache = true;
}
if (argvArg.verbose) {
buildOptions.verbose = true;
}
// Get registry from arguments if specified // Build images first (if not already built)
const registryArg = argvArg._[1]; // e.g., tsdocker push registry.gitlab.com await manager.build(buildOptions);
// Get registry from --registry flag
const registryArg = argvArg.registry as string | undefined;
const registries = registryArg ? [registryArg] : undefined; const registries = registryArg ? [registryArg] : undefined;
await manager.push(registries); await manager.push(registries);
@@ -78,7 +124,7 @@ export let run = () => {
const config = await ConfigModule.run(); const config = await ConfigModule.run();
const manager = new TsDockerManager(config); const manager = new TsDockerManager(config);
await manager.prepare(); await manager.prepare(argvArg.context as string | undefined);
// Login first // Login first
await manager.login(); await manager.login();
@@ -98,10 +144,20 @@ export let run = () => {
try { try {
const config = await ConfigModule.run(); const config = await ConfigModule.run();
const manager = new TsDockerManager(config); const manager = new TsDockerManager(config);
await manager.prepare(); await manager.prepare(argvArg.context as string | undefined);
// Build images first // Build images first
await manager.build(); const buildOptions: IBuildCommandOptions = {};
if (argvArg.cache === false) {
buildOptions.noCache = true;
}
if (argvArg.cached) {
buildOptions.cached = true;
}
if (argvArg.verbose) {
buildOptions.verbose = true;
}
await manager.build(buildOptions);
// Run tests // Run tests
await manager.test(); await manager.test();
@@ -119,7 +175,7 @@ export let run = () => {
try { try {
const config = await ConfigModule.run(); const config = await ConfigModule.run();
const manager = new TsDockerManager(config); const manager = new TsDockerManager(config);
await manager.prepare(); await manager.prepare(argvArg.context as string | undefined);
await manager.login(); await manager.login();
logger.log('success', 'Login completed successfully'); logger.log('success', 'Login completed successfully');
} catch (err) { } catch (err) {
@@ -135,7 +191,7 @@ export let run = () => {
try { try {
const config = await ConfigModule.run(); const config = await ConfigModule.run();
const manager = new TsDockerManager(config); const manager = new TsDockerManager(config);
await manager.prepare(); await manager.prepare(argvArg.context as string | undefined);
await manager.list(); await manager.list();
} catch (err) { } catch (err) {
logger.log('error', `List failed: ${(err as Error).message}`); logger.log('error', `List failed: ${(err as Error).message}`);

View File

@@ -15,3 +15,12 @@ export const logger = new plugins.smartlog.Smartlog({
logger.addLogDestination(new plugins.smartlogDestinationLocal.DestinationLocal()); logger.addLogDestination(new plugins.smartlogDestinationLocal.DestinationLocal());
export const ora = new plugins.smartlogSouceOra.SmartlogSourceOra(); export const ora = new plugins.smartlogSouceOra.SmartlogSourceOra();
export function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`;
const totalSeconds = ms / 1000;
if (totalSeconds < 60) return `${totalSeconds.toFixed(1)}s`;
const minutes = Math.floor(totalSeconds / 60);
const seconds = Math.round(totalSeconds % 60);
return `${minutes}m ${seconds}s`;
}