diff --git a/changelog.md b/changelog.md index bfeb8b7..20ea7b3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-03-19 - 2.2.0 - feat(cli/buildx) +add pull control for builds and isolate buildx builders per project + +- adds a new pull build option with --no-pull CLI support and defaults builds to refreshing base images with --pull +- passes the selected buildx builder explicitly into build commands instead of relying on global docker buildx use state +- generates project-hashed builder suffixes so concurrent runs from different project directories do not share the same local builder +- updates session logging to include project hash and builder suffix for easier build diagnostics + ## 2026-03-15 - 2.1.0 - feat(cli) add global remote builder configuration and native SSH buildx nodes for multi-platform builds diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 523600e..e173095 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/tsdocker', - version: '2.1.0', + version: '2.2.0', description: 'develop npm modules cross platform with docker' } diff --git a/ts/classes.dockerfile.ts b/ts/classes.dockerfile.ts index d92895d..3108bed 100644 --- a/ts/classes.dockerfile.ts +++ b/ts/classes.dockerfile.ts @@ -266,7 +266,7 @@ export class Dockerfile { public static async buildDockerfiles( sortedArrayArg: Dockerfile[], session: TsDockerSession, - options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean; parallel?: boolean; parallelConcurrency?: number; onRegistryStarted?: () => Promise; onBeforeRegistryStop?: () => Promise }, + options?: { platform?: string; timeout?: number; noCache?: boolean; pull?: boolean; verbose?: boolean; isRootless?: boolean; parallel?: boolean; parallelConcurrency?: number; onRegistryStarted?: () => Promise; onBeforeRegistryStop?: () => Promise }, ): Promise { const total = sortedArrayArg.length; const overallStart = Date.now(); @@ -668,13 +668,14 @@ export class Dockerfile { /** * Builds the Dockerfile */ - public async build(options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean }): Promise { + public async build(options?: { platform?: string; timeout?: number; noCache?: boolean; pull?: boolean; verbose?: boolean }): Promise { const startTime = Date.now(); const buildArgsString = await Dockerfile.getDockerBuildArgs(this.managerRef); const config = this.managerRef.config; const platformOverride = options?.platform; const timeout = options?.timeout; const noCacheFlag = options?.noCache ? ' --no-cache' : ''; + const pullFlag = options?.pull !== false ? ' --pull' : ''; const verbose = options?.verbose ?? false; let buildContextFlag = ''; @@ -689,23 +690,24 @@ export class Dockerfile { } let buildCommand: string; + const builderFlag = this.managerRef.currentBuilderName ? ` --builder ${this.managerRef.currentBuilderName}` : ''; if (platformOverride) { // Single platform override via buildx - buildCommand = `docker buildx build --progress=plain --platform ${platformOverride}${noCacheFlag}${buildContextFlag} --load -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`; + buildCommand = `docker buildx build${builderFlag} --progress=plain --platform ${platformOverride}${noCacheFlag}${pullFlag}${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 — always push to local registry const platformString = config.platforms.join(','); const registryHost = this.session?.config.registryHost || 'localhost:5234'; const localTag = `${registryHost}/${this.buildTag}`; - buildCommand = `docker buildx build --progress=plain --platform ${platformString}${noCacheFlag}${buildContextFlag} -t ${localTag} -f ${this.filePath} ${buildArgsString} --push .`; + buildCommand = `docker buildx build${builderFlag} --progress=plain --platform ${platformString}${noCacheFlag}${pullFlag}${buildContextFlag} -t ${localTag} -f ${this.filePath} ${buildArgsString} --push .`; this.localRegistryTag = localTag; logger.log('info', `Build: buildx --platform ${platformString} --push to local registry`); } else { // Standard build const versionLabel = this.managerRef.projectInfo?.npm?.version || 'unknown'; - buildCommand = `docker build --progress=plain --label="version=${versionLabel}"${noCacheFlag} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`; + buildCommand = `docker build --progress=plain --label="version=${versionLabel}"${noCacheFlag}${pullFlag} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`; logger.log('info', 'Build: docker build (standard)'); } diff --git a/ts/classes.tsdockermanager.ts b/ts/classes.tsdockermanager.ts index f21c7f3..ca4a3e8 100644 --- a/ts/classes.tsdockermanager.ts +++ b/ts/classes.tsdockermanager.ts @@ -25,6 +25,7 @@ export class TsDockerManager { public projectInfo: any; public dockerContext: DockerContext; public session!: TsDockerSession; + public currentBuilderName?: string; private dockerfiles: Dockerfile[] = []; private activeRemoteBuilders: IRemoteBuilder[] = []; private sshTunnelManager?: SshTunnelManager; @@ -266,6 +267,7 @@ export class TsDockerManager { platform: options?.platform, timeout: options?.timeout, noCache: options?.noCache, + pull: options?.pull, verbose: options?.verbose, }); logger.log('ok', `${progress} Built ${df.cleanTag} in ${formatDuration(elapsed)}`); @@ -311,6 +313,7 @@ export class TsDockerManager { platform: options?.platform, timeout: options?.timeout, noCache: options?.noCache, + pull: options?.pull, verbose: options?.verbose, }); logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`); @@ -349,6 +352,7 @@ export class TsDockerManager { platform: options?.platform, timeout: options?.timeout, noCache: options?.noCache, + pull: options?.pull, verbose: options?.verbose, isRootless: this.dockerContext.contextInfo?.isRootless, parallel: options?.parallel, @@ -401,6 +405,7 @@ export class TsDockerManager { await this.ensureBuildxLocal(builderName); } + this.currentBuilderName = builderName; logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`); } @@ -426,7 +431,7 @@ export class TsDockerManager { // Create the local node const localPlatformFlag = localPlatforms.length > 0 ? ` --platform ${localPlatforms.join(',')}` : ''; await smartshellInstance.exec( - `docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host${localPlatformFlag} --use` + `docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host${localPlatformFlag}` ); // Append remote nodes @@ -441,7 +446,7 @@ export class TsDockerManager { } // Bootstrap all nodes - await smartshellInstance.exec('docker buildx inspect --bootstrap'); + await smartshellInstance.exec(`docker buildx inspect --builder ${builderName} --bootstrap`); // Store active remote builders for SSH tunnel setup during build this.activeRemoteBuilders = remoteBuilders; @@ -456,20 +461,18 @@ export class TsDockerManager { if (inspectResult.exitCode !== 0) { logger.log('info', 'Creating new buildx builder with host network...'); await smartshellInstance.exec( - `docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host --use` + `docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host` ); - await smartshellInstance.exec('docker buildx inspect --bootstrap'); + await smartshellInstance.exec(`docker buildx inspect --builder ${builderName} --bootstrap`); } else { const inspectOutput = inspectResult.stdout || ''; 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` + `docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host` ); - await smartshellInstance.exec('docker buildx inspect --bootstrap'); - } else { - await smartshellInstance.exec(`docker buildx use ${builderName}`); + await smartshellInstance.exec(`docker buildx inspect --builder ${builderName} --bootstrap`); } } this.activeRemoteBuilders = []; diff --git a/ts/classes.tsdockersession.ts b/ts/classes.tsdockersession.ts index 69e1fbc..a103d9c 100644 --- a/ts/classes.tsdockersession.ts +++ b/ts/classes.tsdockersession.ts @@ -4,6 +4,7 @@ import { logger } from './tsdocker.logging.js'; export interface ISessionConfig { sessionId: string; + projectHash: string; registryPort: number; registryHost: string; registryContainerName: string; @@ -17,8 +18,8 @@ export interface ISessionConfig { * Generates unique ports, container names, and builder names so that * concurrent CI jobs on the same Docker host don't collide. * - * In local (non-CI) dev the builder suffix is empty, preserving the - * persistent builder behavior. + * In local (non-CI) dev the builder suffix contains a project hash so + * that concurrent runs in different project directories use separate builders. */ export class TsDockerSession { public config: ISessionConfig; @@ -34,16 +35,18 @@ export class TsDockerSession { public static async create(): Promise { const sessionId = process.env.TSDOCKER_SESSION_ID || crypto.randomBytes(4).toString('hex'); + const projectHash = crypto.createHash('sha256').update(process.cwd()).digest('hex').substring(0, 8); const registryPort = await TsDockerSession.allocatePort(); const registryHost = `localhost:${registryPort}`; const registryContainerName = `tsdocker-registry-${sessionId}`; const { isCI, ciSystem } = TsDockerSession.detectCI(); - const builderSuffix = isCI ? `-${sessionId}` : ''; + const builderSuffix = isCI ? `-${projectHash}-${sessionId}` : `-${projectHash}`; const config: ISessionConfig = { sessionId, + projectHash, registryPort, registryHost, registryContainerName, @@ -99,9 +102,10 @@ export class TsDockerSession { logger.log('info', '=== TSDOCKER SESSION ==='); logger.log('info', `Session ID: ${c.sessionId}`); logger.log('info', `Registry: ${c.registryHost} (container: ${c.registryContainerName})`); + logger.log('info', `Project hash: ${c.projectHash}`); + logger.log('info', `Builder suffix: ${c.builderSuffix}`); if (c.isCI) { logger.log('info', `CI detected: ${c.ciSystem}`); - logger.log('info', `Builder suffix: ${c.builderSuffix}`); } } } diff --git a/ts/interfaces/index.ts b/ts/interfaces/index.ts index 668d80f..469838a 100644 --- a/ts/interfaces/index.ts +++ b/ts/interfaces/index.ts @@ -69,6 +69,7 @@ export interface IBuildCommandOptions { 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) + pull?: boolean; // Pull fresh base images before building (default: true) 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) diff --git a/ts/tsdocker.cli.ts b/ts/tsdocker.cli.ts index 0cb501e..b47f300 100644 --- a/ts/tsdocker.cli.ts +++ b/ts/tsdocker.cli.ts @@ -41,6 +41,7 @@ BUILD / PUSH OPTIONS --platform=

Target platform (e.g. linux/arm64) --timeout= Build timeout in seconds --no-cache Rebuild without Docker layer cache + --no-pull Skip pulling latest base images (use cached) --cached Skip builds when Dockerfile is unchanged --verbose Stream raw docker build output --parallel[=] Parallel builds (optional concurrency limit) @@ -120,6 +121,8 @@ export let run = () => { if (argvArg.cache === false) { buildOptions.noCache = true; } + // --pull is default true; --no-pull sets pull=false + buildOptions.pull = argvArg.pull !== false; if (argvArg.cached) { buildOptions.cached = true; } @@ -170,6 +173,7 @@ export let run = () => { if (argvArg.cache === false) { buildOptions.noCache = true; } + buildOptions.pull = argvArg.pull !== false; if (argvArg.verbose) { buildOptions.verbose = true; } @@ -243,6 +247,7 @@ export let run = () => { if (argvArg.cache === false) { buildOptions.noCache = true; } + buildOptions.pull = argvArg.pull !== false; if (argvArg.cached) { buildOptions.cached = true; }