Compare commits

...

4 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
8 changed files with 157 additions and 22 deletions

View File

@@ -1,5 +1,23 @@
# 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) ## 2026-02-06 - 1.11.0 - feat(docker)
start temporary local registry for buildx dependency resolution and ensure buildx builder uses host network start temporary local registry for buildx dependency resolution and ensure buildx builder uses host network

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tsdocker", "name": "@git.zone/tsdocker",
"version": "1.11.0", "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.11.0', 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

@@ -151,7 +151,7 @@ export class Dockerfile {
} }
/** Starts a temporary registry:2 container on port 5234. */ /** Starts a temporary registry:2 container on port 5234. */
public static async startLocalRegistry(): Promise<void> { public static async startLocalRegistry(isRootless?: boolean): Promise<void> {
await smartshellInstance.execSilent( await smartshellInstance.execSilent(
`docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true` `docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true`
); );
@@ -163,7 +163,10 @@ export class Dockerfile {
} }
// registry:2 starts near-instantly; brief wait for readiness // registry:2 starts near-instantly; brief wait for readiness
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
logger.log('info', `Started local registry at ${LOCAL_REGISTRY_HOST}`); 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. */ /** Stops and removes the temporary local registry container. */
@@ -191,14 +194,14 @@ export class Dockerfile {
*/ */
public static async buildDockerfiles( public static async buildDockerfiles(
sortedArrayArg: Dockerfile[], sortedArrayArg: Dockerfile[],
options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean }, options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean },
): Promise<Dockerfile[]> { ): Promise<Dockerfile[]> {
const total = sortedArrayArg.length; const total = sortedArrayArg.length;
const overallStart = Date.now(); const overallStart = Date.now();
const useRegistry = Dockerfile.needsLocalRegistry(sortedArrayArg, options); const useRegistry = Dockerfile.needsLocalRegistry(sortedArrayArg, options);
if (useRegistry) { if (useRegistry) {
await Dockerfile.startLocalRegistry(); await Dockerfile.startLocalRegistry(options?.isRootless);
} }
try { try {
@@ -492,6 +495,7 @@ export class Dockerfile {
if (platformOverride) { if (platformOverride) {
// Single platform override via buildx // 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 --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) { } 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(',');
@@ -499,13 +503,16 @@ export class Dockerfile {
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}"${noCacheFlag} -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)');
} }
if (timeout) { if (timeout) {

View File

@@ -5,6 +5,7 @@ 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 { TsDockerCache } from './classes.tsdockercache.js'; import { TsDockerCache } from './classes.tsdockercache.js';
import { DockerContext } from './classes.dockercontext.js';
import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js'; import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({ const smartshellInstance = new plugins.smartshell.Smartshell({
@@ -18,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);
@@ -132,12 +143,30 @@ export class TsDockerManager {
} }
// Check if buildx is needed // Check if buildx is needed
if (options?.platform || (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', ''); logger.log('info', '');
logger.log('info', '=== BUILD PHASE ==='); 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)...`); logger.log('info', `Building ${toBuild.length} Dockerfile(s)...`);
if (options?.cached) { if (options?.cached) {
@@ -151,7 +180,7 @@ export class TsDockerManager {
const useRegistry = Dockerfile.needsLocalRegistry(toBuild, options); const useRegistry = Dockerfile.needsLocalRegistry(toBuild, options);
if (useRegistry) { if (useRegistry) {
await Dockerfile.startLocalRegistry(); await Dockerfile.startLocalRegistry(this.dockerContext.contextInfo?.isRootless);
} }
try { try {
@@ -207,6 +236,7 @@ export class TsDockerManager {
timeout: options?.timeout, timeout: options?.timeout,
noCache: options?.noCache, noCache: options?.noCache,
verbose: options?.verbose, verbose: options?.verbose,
isRootless: this.dockerContext.contextInfo?.isRootless,
}); });
} }
@@ -236,29 +266,32 @@ export class TsDockerManager {
* 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 inspectResult = await smartshellInstance.exec('docker buildx inspect tsdocker-builder 2>/dev/null'); const platforms = this.config.platforms?.join(', ') || 'default';
logger.log('info', `Setting up Docker buildx [${platforms}]...`);
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) {
logger.log('info', 'Creating new buildx builder with host network...'); logger.log('info', 'Creating new buildx builder with host network...');
await smartshellInstance.exec( await smartshellInstance.exec(
'docker buildx create --name tsdocker-builder --driver docker-container --driver-opt network=host --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 {
const inspectOutput = inspectResult.stdout || ''; const inspectOutput = inspectResult.stdout || '';
if (!inspectOutput.includes('network=host')) { if (!inspectOutput.includes('network=host')) {
logger.log('info', 'Recreating buildx builder with host network (migration)...'); logger.log('info', 'Recreating buildx builder with host network (migration)...');
await smartshellInstance.exec('docker buildx rm tsdocker-builder 2>/dev/null'); await smartshellInstance.exec(`docker buildx rm ${builderName} 2>/dev/null`);
await smartshellInstance.exec( await smartshellInstance.exec(
'docker buildx create --name tsdocker-builder --driver docker-container --driver-opt network=host --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 {
await smartshellInstance.exec('docker buildx use tsdocker-builder'); await smartshellInstance.exec(`docker buildx use ${builderName}`);
} }
} }
logger.log('ok', 'Docker buildx ready'); logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`);
} }
/** /**

View File

@@ -79,6 +79,7 @@ export interface IBuildCommandOptions {
noCache?: boolean; // Force rebuild without Docker layer cache (--no-cache) noCache?: boolean; // Force rebuild without Docker layer cache (--no-cache)
cached?: boolean; // Skip builds when Dockerfile content hasn't changed cached?: boolean; // Skip builds when Dockerfile content hasn't changed
verbose?: boolean; // Stream raw docker build output (default: silent) verbose?: boolean; // Stream raw docker build output (default: silent)
context?: string; // Explicit Docker context name (--context flag)
} }
export interface ICacheEntry { export interface ICacheEntry {
@@ -92,3 +93,10 @@ export interface ICacheData {
version: 1; version: 1;
entries: { [cleanTag: string]: ICacheEntry }; 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

@@ -33,7 +33,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);
const buildOptions: IBuildCommandOptions = {}; const buildOptions: IBuildCommandOptions = {};
const patterns = argvArg._.slice(1) as string[]; const patterns = argvArg._.slice(1) as string[];
@@ -72,7 +72,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);
// Login first // Login first
await manager.login(); await manager.login();
@@ -124,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();
@@ -144,7 +144,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);
// Build images first // Build images first
const buildOptions: IBuildCommandOptions = {}; const buildOptions: IBuildCommandOptions = {};
@@ -175,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) {
@@ -191,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}`);