Compare commits

..

2 Commits

Author SHA1 Message Date
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 144 additions and 26 deletions

View File

@@ -1,5 +1,14 @@
# Changelog # Changelog
## 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.5.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.5.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,12 @@ 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 },
): Promise<Dockerfile[]> {
for (const dockerfileArg of sortedArrayArg) { for (const dockerfileArg of sortedArrayArg) {
await dockerfileArg.build(); await dockerfileArg.build(options);
} }
return sortedArrayArg; return sortedArrayArg;
} }
@@ -362,15 +365,19 @@ export class Dockerfile {
/** /**
* Builds the Dockerfile * Builds the Dockerfile
*/ */
public async build(): Promise<void> { public async build(options?: { platform?: string; timeout?: number }): 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;
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} --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} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
@@ -386,11 +393,27 @@ export class Dockerfile {
buildCommand = `docker build --label="version=${versionLabel}" -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`; buildCommand = `docker build --label="version=${versionLabel}" -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,63 @@ 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,
});
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,12 @@ 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
}

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,28 @@ 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);
}
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 +55,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 +66,24 @@ 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);
}
// 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);