Compare commits

..

4 Commits

Author SHA1 Message Date
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
5 changed files with 196 additions and 66 deletions

View File

@@ -1,5 +1,24 @@
# Changelog # Changelog
## 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) ## 2026-02-06 - 1.10.0 - feat(classes.dockerfile)
support using a local base image as a build context in buildx commands support using a local base image as a build context in buildx commands

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tsdocker", "name": "@git.zone/tsdocker",
"version": "1.10.0", "version": "1.12.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.10.0', version: '1.12.0',
description: 'develop npm modules cross platform with docker' description: 'develop npm modules cross platform with docker'
} }

View File

@@ -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
*/ */
@@ -135,6 +139,53 @@ 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(): 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)`);
}
/** 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
*/ */
@@ -144,26 +195,41 @@ export class Dockerfile {
): 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);
for (let i = 0; i < total; i++) { if (useRegistry) {
const dockerfileArg = sortedArrayArg[i]; await Dockerfile.startLocalRegistry();
const progress = `(${i + 1}/${total})`; }
logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
const elapsed = await dockerfileArg.build(options); try {
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`); for (let i = 0; i < total; i++) {
const dockerfileArg = sortedArrayArg[i];
const progress = `(${i + 1}/${total})`;
logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
// Tag the built image with the full base image references used by dependent Dockerfiles, const elapsed = await dockerfileArg.build(options);
// so their FROM lines resolve to the locally-built image instead of pulling from a registry. logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
const dependentBaseImages = new Set<string>();
for (const other of sortedArrayArg) { // Tag in host daemon for standard docker build compatibility
if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) { const dependentBaseImages = new Set<string>();
dependentBaseImages.add(other.baseImage); 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);
} }
} }
for (const fullTag of dependentBaseImages) { } finally {
logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`); if (useRegistry) {
await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`); await Dockerfile.stopLocalRegistry();
} }
} }
@@ -366,6 +432,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;
@@ -412,9 +479,12 @@ export class Dockerfile {
let buildContextFlag = ''; let buildContextFlag = '';
if (this.localBaseImageDependent && this.localBaseDockerfile) { if (this.localBaseImageDependent && this.localBaseDockerfile) {
const fromImage = this.baseImage; const fromImage = this.baseImage;
const localTag = this.localBaseDockerfile.buildTag; if (this.localBaseDockerfile.localRegistryTag) {
buildContextFlag = ` --build-context "${fromImage}=docker-image://${localTag}"`; // BuildKit pulls from the local registry (reachable via host network)
logger.log('info', `Using local build context: ${fromImage} -> docker-image://${localTag}`); 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;
@@ -422,6 +492,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(',');
@@ -429,13 +500,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

@@ -132,12 +132,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) {
@@ -148,46 +166,57 @@ export class TsDockerManager {
const total = toBuild.length; const total = toBuild.length;
const overallStart = Date.now(); const overallStart = Date.now();
const useRegistry = Dockerfile.needsLocalRegistry(toBuild, options);
for (let i = 0; i < total; i++) { if (useRegistry) {
const dockerfileArg = toBuild[i]; await Dockerfile.startLocalRegistry();
const progress = `(${i + 1}/${total})`; }
const skip = await cache.shouldSkipBuild(dockerfileArg.cleanTag, dockerfileArg.content);
if (skip) { try {
logger.log('ok', `${progress} Skipped ${dockerfileArg.cleanTag} (cached)`); for (let i = 0; i < total; i++) {
continue; 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();
} }
// Cache miss — build this Dockerfile
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);
} }
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`); logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
// Perform dependency tagging for all Dockerfiles (even cache hits, since tags may be stale)
for (const dockerfileArg of toBuild) {
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}`);
}
}
cache.save(); cache.save();
} else { } else {
// === STANDARD MODE: build all via static helper === // === STANDARD MODE: build all via static helper ===
@@ -225,22 +254,30 @@ 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 platforms = this.config.platforms?.join(', ') || 'default';
logger.log('info', `Setting up Docker buildx for multi-platform builds [${platforms}]...`);
// Check if a buildx builder exists
const inspectResult = await smartshellInstance.exec('docker buildx inspect tsdocker-builder 2>/dev/null'); const inspectResult = await smartshellInstance.exec('docker buildx inspect tsdocker-builder 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 tsdocker-builder --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 tsdocker-builder 2>/dev/null');
await smartshellInstance.exec(
'docker buildx create --name tsdocker-builder --driver docker-container --driver-opt network=host --use'
);
await smartshellInstance.exec('docker buildx inspect --bootstrap');
} else {
await smartshellInstance.exec('docker buildx use tsdocker-builder');
}
} }
logger.log('ok', `Docker buildx ready (platforms: ${platforms})`);
logger.log('ok', 'Docker buildx ready');
} }
/** /**