Compare commits

...

4 Commits

Author SHA1 Message Date
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
7 changed files with 182 additions and 29 deletions

View File

@@ -1,5 +1,23 @@
# Changelog # Changelog
## 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.6.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.6.0',
description: 'develop npm modules cross platform with docker' description: 'develop npm modules cross platform with docker'
} }

View File

@@ -2,7 +2,7 @@ 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 } 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';
@@ -136,9 +136,25 @@ export class Dockerfile {
/** /**
* 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(
sortedArrayArg: Dockerfile[],
options?: { platform?: string; timeout?: number; noCache?: boolean },
): Promise<Dockerfile[]> {
for (const dockerfileArg of sortedArrayArg) { for (const dockerfileArg of sortedArrayArg) {
await dockerfileArg.build(); await dockerfileArg.build(options);
// Tag the built image with the full base image references used by dependent Dockerfiles,
// so their FROM lines resolve to the locally-built image instead of pulling from a registry.
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}`);
}
} }
return sortedArrayArg; return sortedArrayArg;
} }
@@ -362,18 +378,23 @@ export class Dockerfile {
/** /**
* Builds the Dockerfile * Builds the Dockerfile
*/ */
public async build(): Promise<void> { public async build(options?: { platform?: string; timeout?: number; noCache?: boolean }): Promise<void> {
logger.log('info', 'now building Dockerfile for ' + this.cleanTag); logger.log('info', 'now building Dockerfile for ' + this.cleanTag);
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' : '';
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} --load -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
} 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} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
if (config.push) { if (config.push) {
buildCommand += ' --push'; buildCommand += ' --push';
@@ -383,14 +404,30 @@ export class Dockerfile {
} 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} .`;
} }
const result = await smartshellInstance.exec(buildCommand); if (timeout) {
if (result.exitCode !== 0) { // Use streaming execution with timeout
logger.log('error', `Build failed for ${this.cleanTag}`); const streaming = await smartshellInstance.execStreaming(buildCommand);
console.log(result.stdout); const timeoutPromise = new Promise<never>((_, reject) => {
throw new Error(`Build failed for ${this.cleanTag}`); 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) {
logger.log('error', `Build failed for ${this.cleanTag}`);
throw new Error(`Build failed for ${this.cleanTag}`);
}
} else {
const result = await smartshellInstance.exec(buildCommand);
if (result.exitCode !== 0) {
logger.log('error', `Build failed for ${this.cleanTag}`);
console.log(result.stdout);
throw new Error(`Build failed for ${this.cleanTag}`);
}
} }
logger.log('ok', `Built ${this.cleanTag}`); logger.log('ok', `Built ${this.cleanTag}`);

View File

@@ -4,7 +4,7 @@ import { logger } 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 type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({ const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash', executor: 'bash',
@@ -90,9 +90,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,16 +103,64 @@ 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) { if (options?.platform || (this.config.platforms && this.config.platforms.length > 1)) {
await this.ensureBuildx(); await this.ensureBuildx();
} }
logger.log('info', `Building ${this.dockerfiles.length} Dockerfiles...`); logger.log('info', `Building ${toBuild.length} Dockerfiles...`);
await Dockerfile.buildDockerfiles(this.dockerfiles); await Dockerfile.buildDockerfiles(toBuild, {
platform: options?.platform,
timeout: options?.timeout,
noCache: options?.noCache,
});
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));
} }
/** /**

View File

@@ -68,3 +68,13 @@ 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)
}

View File

@@ -7,6 +7,7 @@ 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';
const tsdockerCli = new plugins.smartcli.Smartcli(); const tsdockerCli = new plugins.smartcli.Smartcli();
@@ -23,14 +24,31 @@ 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();
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;
}
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,6 +58,7 @@ 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 {
@@ -50,11 +69,27 @@ export let run = () => {
// 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;
}
// 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);
@@ -101,7 +136,11 @@ export let run = () => {
await manager.prepare(); await manager.prepare();
// Build images first // Build images first
await manager.build(); const buildOptions: IBuildCommandOptions = {};
if (argvArg.cache === false) {
buildOptions.noCache = true;
}
await manager.build(buildOptions);
// Run tests // Run tests
await manager.test(); await manager.test();