Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93cf2ee7bf | |||
| 8cf8e43e59 |
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-03-15 - 2.1.0 - feat(cli)
|
||||||
add global remote builder configuration and native SSH buildx nodes for multi-platform builds
|
add global remote builder configuration and native SSH buildx nodes for multi-platform builds
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tsdocker",
|
"name": "@git.zone/tsdocker",
|
||||||
"version": "2.1.0",
|
"version": "2.2.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",
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tsdocker',
|
name: '@git.zone/tsdocker',
|
||||||
version: '2.1.0',
|
version: '2.2.0',
|
||||||
description: 'develop npm modules cross platform with docker'
|
description: 'develop npm modules cross platform with docker'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ export class Dockerfile {
|
|||||||
public static async buildDockerfiles(
|
public static async buildDockerfiles(
|
||||||
sortedArrayArg: Dockerfile[],
|
sortedArrayArg: Dockerfile[],
|
||||||
session: TsDockerSession,
|
session: TsDockerSession,
|
||||||
options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean; parallel?: boolean; parallelConcurrency?: number; onRegistryStarted?: () => Promise<void>; onBeforeRegistryStop?: () => Promise<void> },
|
options?: { platform?: string; timeout?: number; noCache?: boolean; pull?: boolean; verbose?: boolean; isRootless?: boolean; parallel?: boolean; parallelConcurrency?: number; onRegistryStarted?: () => Promise<void>; onBeforeRegistryStop?: () => Promise<void> },
|
||||||
): Promise<Dockerfile[]> {
|
): Promise<Dockerfile[]> {
|
||||||
const total = sortedArrayArg.length;
|
const total = sortedArrayArg.length;
|
||||||
const overallStart = Date.now();
|
const overallStart = Date.now();
|
||||||
@@ -668,13 +668,14 @@ export class Dockerfile {
|
|||||||
/**
|
/**
|
||||||
* Builds the Dockerfile
|
* Builds the Dockerfile
|
||||||
*/
|
*/
|
||||||
public async build(options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean }): Promise<number> {
|
public async build(options?: { platform?: string; timeout?: number; noCache?: boolean; pull?: boolean; verbose?: boolean }): Promise<number> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
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 platformOverride = options?.platform;
|
||||||
const timeout = options?.timeout;
|
const timeout = options?.timeout;
|
||||||
const noCacheFlag = options?.noCache ? ' --no-cache' : '';
|
const noCacheFlag = options?.noCache ? ' --no-cache' : '';
|
||||||
|
const pullFlag = options?.pull !== false ? ' --pull' : '';
|
||||||
const verbose = options?.verbose ?? false;
|
const verbose = options?.verbose ?? false;
|
||||||
|
|
||||||
let buildContextFlag = '';
|
let buildContextFlag = '';
|
||||||
@@ -689,23 +690,24 @@ export class Dockerfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let buildCommand: string;
|
let buildCommand: string;
|
||||||
|
const builderFlag = this.managerRef.currentBuilderName ? ` --builder ${this.managerRef.currentBuilderName}` : '';
|
||||||
|
|
||||||
if (platformOverride) {
|
if (platformOverride) {
|
||||||
// Single platform override via buildx
|
// 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`);
|
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 — always push to local registry
|
// Multi-platform build using buildx — always push to local registry
|
||||||
const platformString = config.platforms.join(',');
|
const platformString = config.platforms.join(',');
|
||||||
const registryHost = this.session?.config.registryHost || 'localhost:5234';
|
const registryHost = this.session?.config.registryHost || 'localhost:5234';
|
||||||
const localTag = `${registryHost}/${this.buildTag}`;
|
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;
|
this.localRegistryTag = localTag;
|
||||||
logger.log('info', `Build: buildx --platform ${platformString} --push to local registry`);
|
logger.log('info', `Build: buildx --platform ${platformString} --push to local registry`);
|
||||||
} 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 --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)');
|
logger.log('info', 'Build: docker build (standard)');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export class TsDockerManager {
|
|||||||
public projectInfo: any;
|
public projectInfo: any;
|
||||||
public dockerContext: DockerContext;
|
public dockerContext: DockerContext;
|
||||||
public session!: TsDockerSession;
|
public session!: TsDockerSession;
|
||||||
|
public currentBuilderName?: string;
|
||||||
private dockerfiles: Dockerfile[] = [];
|
private dockerfiles: Dockerfile[] = [];
|
||||||
private activeRemoteBuilders: IRemoteBuilder[] = [];
|
private activeRemoteBuilders: IRemoteBuilder[] = [];
|
||||||
private sshTunnelManager?: SshTunnelManager;
|
private sshTunnelManager?: SshTunnelManager;
|
||||||
@@ -266,6 +267,7 @@ export class TsDockerManager {
|
|||||||
platform: options?.platform,
|
platform: options?.platform,
|
||||||
timeout: options?.timeout,
|
timeout: options?.timeout,
|
||||||
noCache: options?.noCache,
|
noCache: options?.noCache,
|
||||||
|
pull: options?.pull,
|
||||||
verbose: options?.verbose,
|
verbose: options?.verbose,
|
||||||
});
|
});
|
||||||
logger.log('ok', `${progress} Built ${df.cleanTag} in ${formatDuration(elapsed)}`);
|
logger.log('ok', `${progress} Built ${df.cleanTag} in ${formatDuration(elapsed)}`);
|
||||||
@@ -311,6 +313,7 @@ export class TsDockerManager {
|
|||||||
platform: options?.platform,
|
platform: options?.platform,
|
||||||
timeout: options?.timeout,
|
timeout: options?.timeout,
|
||||||
noCache: options?.noCache,
|
noCache: options?.noCache,
|
||||||
|
pull: options?.pull,
|
||||||
verbose: options?.verbose,
|
verbose: options?.verbose,
|
||||||
});
|
});
|
||||||
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
|
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
|
||||||
@@ -349,6 +352,7 @@ export class TsDockerManager {
|
|||||||
platform: options?.platform,
|
platform: options?.platform,
|
||||||
timeout: options?.timeout,
|
timeout: options?.timeout,
|
||||||
noCache: options?.noCache,
|
noCache: options?.noCache,
|
||||||
|
pull: options?.pull,
|
||||||
verbose: options?.verbose,
|
verbose: options?.verbose,
|
||||||
isRootless: this.dockerContext.contextInfo?.isRootless,
|
isRootless: this.dockerContext.contextInfo?.isRootless,
|
||||||
parallel: options?.parallel,
|
parallel: options?.parallel,
|
||||||
@@ -401,6 +405,7 @@ export class TsDockerManager {
|
|||||||
await this.ensureBuildxLocal(builderName);
|
await this.ensureBuildxLocal(builderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.currentBuilderName = builderName;
|
||||||
logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`);
|
logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +431,7 @@ export class TsDockerManager {
|
|||||||
// Create the local node
|
// Create the local node
|
||||||
const localPlatformFlag = localPlatforms.length > 0 ? ` --platform ${localPlatforms.join(',')}` : '';
|
const localPlatformFlag = localPlatforms.length > 0 ? ` --platform ${localPlatforms.join(',')}` : '';
|
||||||
await smartshellInstance.exec(
|
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
|
// Append remote nodes
|
||||||
@@ -441,7 +446,7 @@ export class TsDockerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bootstrap all nodes
|
// 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
|
// Store active remote builders for SSH tunnel setup during build
|
||||||
this.activeRemoteBuilders = remoteBuilders;
|
this.activeRemoteBuilders = remoteBuilders;
|
||||||
@@ -456,20 +461,18 @@ export class TsDockerManager {
|
|||||||
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 ${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 {
|
} 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 ${builderName} 2>/dev/null`);
|
await smartshellInstance.exec(`docker buildx rm ${builderName} 2>/dev/null`);
|
||||||
await smartshellInstance.exec(
|
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 {
|
|
||||||
await smartshellInstance.exec(`docker buildx use ${builderName}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.activeRemoteBuilders = [];
|
this.activeRemoteBuilders = [];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { logger } from './tsdocker.logging.js';
|
|||||||
|
|
||||||
export interface ISessionConfig {
|
export interface ISessionConfig {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
projectHash: string;
|
||||||
registryPort: number;
|
registryPort: number;
|
||||||
registryHost: string;
|
registryHost: string;
|
||||||
registryContainerName: string;
|
registryContainerName: string;
|
||||||
@@ -17,8 +18,8 @@ export interface ISessionConfig {
|
|||||||
* Generates unique ports, container names, and builder names so that
|
* Generates unique ports, container names, and builder names so that
|
||||||
* concurrent CI jobs on the same Docker host don't collide.
|
* concurrent CI jobs on the same Docker host don't collide.
|
||||||
*
|
*
|
||||||
* In local (non-CI) dev the builder suffix is empty, preserving the
|
* In local (non-CI) dev the builder suffix contains a project hash so
|
||||||
* persistent builder behavior.
|
* that concurrent runs in different project directories use separate builders.
|
||||||
*/
|
*/
|
||||||
export class TsDockerSession {
|
export class TsDockerSession {
|
||||||
public config: ISessionConfig;
|
public config: ISessionConfig;
|
||||||
@@ -34,16 +35,18 @@ export class TsDockerSession {
|
|||||||
public static async create(): Promise<TsDockerSession> {
|
public static async create(): Promise<TsDockerSession> {
|
||||||
const sessionId =
|
const sessionId =
|
||||||
process.env.TSDOCKER_SESSION_ID || crypto.randomBytes(4).toString('hex');
|
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 registryPort = await TsDockerSession.allocatePort();
|
||||||
const registryHost = `localhost:${registryPort}`;
|
const registryHost = `localhost:${registryPort}`;
|
||||||
const registryContainerName = `tsdocker-registry-${sessionId}`;
|
const registryContainerName = `tsdocker-registry-${sessionId}`;
|
||||||
|
|
||||||
const { isCI, ciSystem } = TsDockerSession.detectCI();
|
const { isCI, ciSystem } = TsDockerSession.detectCI();
|
||||||
const builderSuffix = isCI ? `-${sessionId}` : '';
|
const builderSuffix = isCI ? `-${projectHash}-${sessionId}` : `-${projectHash}`;
|
||||||
|
|
||||||
const config: ISessionConfig = {
|
const config: ISessionConfig = {
|
||||||
sessionId,
|
sessionId,
|
||||||
|
projectHash,
|
||||||
registryPort,
|
registryPort,
|
||||||
registryHost,
|
registryHost,
|
||||||
registryContainerName,
|
registryContainerName,
|
||||||
@@ -99,9 +102,10 @@ export class TsDockerSession {
|
|||||||
logger.log('info', '=== TSDOCKER SESSION ===');
|
logger.log('info', '=== TSDOCKER SESSION ===');
|
||||||
logger.log('info', `Session ID: ${c.sessionId}`);
|
logger.log('info', `Session ID: ${c.sessionId}`);
|
||||||
logger.log('info', `Registry: ${c.registryHost} (container: ${c.registryContainerName})`);
|
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) {
|
if (c.isCI) {
|
||||||
logger.log('info', `CI detected: ${c.ciSystem}`);
|
logger.log('info', `CI detected: ${c.ciSystem}`);
|
||||||
logger.log('info', `Builder suffix: ${c.builderSuffix}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export interface IBuildCommandOptions {
|
|||||||
platform?: string; // Single platform override (e.g., 'linux/arm64')
|
platform?: string; // Single platform override (e.g., 'linux/arm64')
|
||||||
timeout?: number; // Build timeout in seconds
|
timeout?: number; // Build timeout in seconds
|
||||||
noCache?: boolean; // Force rebuild without Docker layer cache (--no-cache)
|
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
|
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)
|
context?: string; // Explicit Docker context name (--context flag)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ BUILD / PUSH OPTIONS
|
|||||||
--platform=<p> Target platform (e.g. linux/arm64)
|
--platform=<p> Target platform (e.g. linux/arm64)
|
||||||
--timeout=<s> Build timeout in seconds
|
--timeout=<s> Build timeout in seconds
|
||||||
--no-cache Rebuild without Docker layer cache
|
--no-cache Rebuild without Docker layer cache
|
||||||
|
--no-pull Skip pulling latest base images (use cached)
|
||||||
--cached Skip builds when Dockerfile is unchanged
|
--cached Skip builds when Dockerfile is unchanged
|
||||||
--verbose Stream raw docker build output
|
--verbose Stream raw docker build output
|
||||||
--parallel[=<n>] Parallel builds (optional concurrency limit)
|
--parallel[=<n>] Parallel builds (optional concurrency limit)
|
||||||
@@ -120,6 +121,8 @@ export let run = () => {
|
|||||||
if (argvArg.cache === false) {
|
if (argvArg.cache === false) {
|
||||||
buildOptions.noCache = true;
|
buildOptions.noCache = true;
|
||||||
}
|
}
|
||||||
|
// --pull is default true; --no-pull sets pull=false
|
||||||
|
buildOptions.pull = argvArg.pull !== false;
|
||||||
if (argvArg.cached) {
|
if (argvArg.cached) {
|
||||||
buildOptions.cached = true;
|
buildOptions.cached = true;
|
||||||
}
|
}
|
||||||
@@ -170,6 +173,7 @@ export let run = () => {
|
|||||||
if (argvArg.cache === false) {
|
if (argvArg.cache === false) {
|
||||||
buildOptions.noCache = true;
|
buildOptions.noCache = true;
|
||||||
}
|
}
|
||||||
|
buildOptions.pull = argvArg.pull !== false;
|
||||||
if (argvArg.verbose) {
|
if (argvArg.verbose) {
|
||||||
buildOptions.verbose = true;
|
buildOptions.verbose = true;
|
||||||
}
|
}
|
||||||
@@ -243,6 +247,7 @@ export let run = () => {
|
|||||||
if (argvArg.cache === false) {
|
if (argvArg.cache === false) {
|
||||||
buildOptions.noCache = true;
|
buildOptions.noCache = true;
|
||||||
}
|
}
|
||||||
|
buildOptions.pull = argvArg.pull !== false;
|
||||||
if (argvArg.cached) {
|
if (argvArg.cached) {
|
||||||
buildOptions.cached = true;
|
buildOptions.cached = true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user