@git.zone/tsdocker 1.15.1 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dockercontext.js +13 -3
- package/dist_ts/classes.dockerfile.d.ts +13 -6
- package/dist_ts/classes.dockerfile.js +130 -41
- package/dist_ts/classes.tsdockermanager.d.ts +7 -0
- package/dist_ts/classes.tsdockermanager.js +30 -11
- package/dist_ts/classes.tsdockersession.d.ts +35 -0
- package/dist_ts/classes.tsdockersession.js +92 -0
- package/dist_ts/interfaces/index.d.ts +1 -0
- package/dist_ts/tsdocker.cli.js +4 -1
- package/package.json +1 -1
- package/readme.md +186 -111
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dockercontext.ts +12 -2
- package/ts/classes.dockerfile.ts +145 -41
- package/ts/classes.tsdockermanager.ts +31 -10
- package/ts/classes.tsdockersession.ts +107 -0
- package/ts/interfaces/index.ts +1 -0
- package/ts/tsdocker.cli.ts +3 -0
package/ts/classes.dockerfile.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as paths from './tsdocker.paths.js';
|
|
|
3
3
|
import { logger, formatDuration } from './tsdocker.logging.js';
|
|
4
4
|
import { DockerRegistry } from './classes.dockerregistry.js';
|
|
5
5
|
import { RegistryCopy } from './classes.registrycopy.js';
|
|
6
|
+
import { TsDockerSession } from './classes.tsdockersession.js';
|
|
6
7
|
import type { IDockerfileOptions, ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
|
|
7
8
|
import type { TsDockerManager } from './classes.tsdockermanager.js';
|
|
8
9
|
import * as fs from 'fs';
|
|
@@ -11,9 +12,14 @@ const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
|
11
12
|
executor: 'bash',
|
|
12
13
|
});
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Extracts a platform string (e.g. "linux/amd64") from a buildx bracket prefix.
|
|
17
|
+
* The prefix may be like "linux/amd64 ", "linux/amd64 stage-1 ", "stage-1 ", or "".
|
|
18
|
+
*/
|
|
19
|
+
function extractPlatform(prefix: string): string | null {
|
|
20
|
+
const match = prefix.match(/linux\/\w+/);
|
|
21
|
+
return match ? match[0] : null;
|
|
22
|
+
}
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
25
|
* Class Dockerfile represents a Dockerfile on disk
|
|
@@ -148,40 +154,55 @@ export class Dockerfile {
|
|
|
148
154
|
return true;
|
|
149
155
|
}
|
|
150
156
|
|
|
151
|
-
/** Starts a persistent registry:2 container
|
|
152
|
-
public static async startLocalRegistry(isRootless?: boolean): Promise<void> {
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
/** Starts a persistent registry:2 container with session-unique port and name. */
|
|
158
|
+
public static async startLocalRegistry(session: TsDockerSession, isRootless?: boolean): Promise<void> {
|
|
159
|
+
const { registryPort, registryHost, registryContainerName, isCI, sessionId } = session.config;
|
|
160
|
+
|
|
161
|
+
// Ensure persistent storage directory exists — isolate per session in CI
|
|
162
|
+
const registryDataDir = isCI
|
|
163
|
+
? plugins.path.join(paths.cwd, '.nogit', 'docker-registry', sessionId)
|
|
164
|
+
: plugins.path.join(paths.cwd, '.nogit', 'docker-registry');
|
|
155
165
|
fs.mkdirSync(registryDataDir, { recursive: true });
|
|
156
166
|
|
|
157
167
|
await smartshellInstance.execSilent(
|
|
158
|
-
`docker rm -f ${
|
|
159
|
-
);
|
|
160
|
-
const result = await smartshellInstance.execSilent(
|
|
161
|
-
`docker run -d --name ${LOCAL_REGISTRY_CONTAINER} -p ${LOCAL_REGISTRY_PORT}:5000 -v "${registryDataDir}:/var/lib/registry" registry:2`
|
|
168
|
+
`docker rm -f ${registryContainerName} 2>/dev/null || true`
|
|
162
169
|
);
|
|
170
|
+
|
|
171
|
+
const runCmd = `docker run -d --name ${registryContainerName} -p ${registryPort}:5000 -v "${registryDataDir}:/var/lib/registry" registry:2`;
|
|
172
|
+
let result = await smartshellInstance.execSilent(runCmd);
|
|
173
|
+
|
|
174
|
+
// Port retry: if port was stolen between allocation and docker run, reallocate once
|
|
175
|
+
if (result.exitCode !== 0 && (result.stderr || result.stdout || '').includes('port is already allocated')) {
|
|
176
|
+
const newPort = await TsDockerSession.allocatePort();
|
|
177
|
+
logger.log('warn', `Port ${registryPort} taken, retrying with ${newPort}`);
|
|
178
|
+
session.config.registryPort = newPort;
|
|
179
|
+
session.config.registryHost = `localhost:${newPort}`;
|
|
180
|
+
const retryCmd = `docker run -d --name ${registryContainerName} -p ${newPort}:5000 -v "${registryDataDir}:/var/lib/registry" registry:2`;
|
|
181
|
+
result = await smartshellInstance.execSilent(retryCmd);
|
|
182
|
+
}
|
|
183
|
+
|
|
163
184
|
if (result.exitCode !== 0) {
|
|
164
185
|
throw new Error(`Failed to start local registry: ${result.stderr || result.stdout}`);
|
|
165
186
|
}
|
|
166
187
|
// registry:2 starts near-instantly; brief wait for readiness
|
|
167
188
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
168
|
-
logger.log('info', `Started local registry at ${
|
|
189
|
+
logger.log('info', `Started local registry at ${session.config.registryHost} (container: ${registryContainerName})`);
|
|
169
190
|
if (isRootless) {
|
|
170
|
-
logger.log('warn', `[rootless] Registry on port ${
|
|
191
|
+
logger.log('warn', `[rootless] Registry on port ${session.config.registryPort} — if buildx cannot reach localhost, try 127.0.0.1`);
|
|
171
192
|
}
|
|
172
193
|
}
|
|
173
194
|
|
|
174
|
-
/** Stops and removes the
|
|
175
|
-
public static async stopLocalRegistry(): Promise<void> {
|
|
195
|
+
/** Stops and removes the session-specific local registry container. */
|
|
196
|
+
public static async stopLocalRegistry(session: TsDockerSession): Promise<void> {
|
|
176
197
|
await smartshellInstance.execSilent(
|
|
177
|
-
`docker rm -f ${
|
|
198
|
+
`docker rm -f ${session.config.registryContainerName} 2>/dev/null || true`
|
|
178
199
|
);
|
|
179
|
-
logger.log('info',
|
|
200
|
+
logger.log('info', `Stopped local registry (${session.config.registryContainerName})`);
|
|
180
201
|
}
|
|
181
202
|
|
|
182
203
|
/** Pushes a built image to the local registry for buildx consumption. */
|
|
183
|
-
public static async pushToLocalRegistry(dockerfile: Dockerfile): Promise<void> {
|
|
184
|
-
const registryTag = `${
|
|
204
|
+
public static async pushToLocalRegistry(session: TsDockerSession, dockerfile: Dockerfile): Promise<void> {
|
|
205
|
+
const registryTag = `${session.config.registryHost}/${dockerfile.buildTag}`;
|
|
185
206
|
await smartshellInstance.execSilent(`docker tag ${dockerfile.buildTag} ${registryTag}`);
|
|
186
207
|
const result = await smartshellInstance.execSilent(`docker push ${registryTag}`);
|
|
187
208
|
if (result.exitCode !== 0) {
|
|
@@ -244,12 +265,13 @@ export class Dockerfile {
|
|
|
244
265
|
*/
|
|
245
266
|
public static async buildDockerfiles(
|
|
246
267
|
sortedArrayArg: Dockerfile[],
|
|
268
|
+
session: TsDockerSession,
|
|
247
269
|
options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean; parallel?: boolean; parallelConcurrency?: number },
|
|
248
270
|
): Promise<Dockerfile[]> {
|
|
249
271
|
const total = sortedArrayArg.length;
|
|
250
272
|
const overallStart = Date.now();
|
|
251
273
|
|
|
252
|
-
await Dockerfile.startLocalRegistry(options?.isRootless);
|
|
274
|
+
await Dockerfile.startLocalRegistry(session, options?.isRootless);
|
|
253
275
|
|
|
254
276
|
try {
|
|
255
277
|
if (options?.parallel) {
|
|
@@ -296,7 +318,7 @@ export class Dockerfile {
|
|
|
296
318
|
}
|
|
297
319
|
// Push ALL images to local registry (skip if already pushed via buildx)
|
|
298
320
|
if (!df.localRegistryTag) {
|
|
299
|
-
await Dockerfile.pushToLocalRegistry(df);
|
|
321
|
+
await Dockerfile.pushToLocalRegistry(session, df);
|
|
300
322
|
}
|
|
301
323
|
}
|
|
302
324
|
}
|
|
@@ -324,12 +346,12 @@ export class Dockerfile {
|
|
|
324
346
|
|
|
325
347
|
// Push ALL images to local registry (skip if already pushed via buildx)
|
|
326
348
|
if (!dockerfileArg.localRegistryTag) {
|
|
327
|
-
await Dockerfile.pushToLocalRegistry(dockerfileArg);
|
|
349
|
+
await Dockerfile.pushToLocalRegistry(session, dockerfileArg);
|
|
328
350
|
}
|
|
329
351
|
}
|
|
330
352
|
}
|
|
331
353
|
} finally {
|
|
332
|
-
await Dockerfile.stopLocalRegistry();
|
|
354
|
+
await Dockerfile.stopLocalRegistry(session);
|
|
333
355
|
}
|
|
334
356
|
|
|
335
357
|
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
|
|
@@ -520,6 +542,7 @@ export class Dockerfile {
|
|
|
520
542
|
|
|
521
543
|
// INSTANCE PROPERTIES
|
|
522
544
|
public managerRef: TsDockerManager;
|
|
545
|
+
public session?: TsDockerSession;
|
|
523
546
|
public filePath!: string;
|
|
524
547
|
public repo: string;
|
|
525
548
|
public version: string;
|
|
@@ -563,6 +586,79 @@ export class Dockerfile {
|
|
|
563
586
|
this.localBaseImageDependent = false;
|
|
564
587
|
}
|
|
565
588
|
|
|
589
|
+
/**
|
|
590
|
+
* Creates a line-by-line handler for Docker build output that logs
|
|
591
|
+
* recognized layer/step lines in an emphasized format.
|
|
592
|
+
*/
|
|
593
|
+
private createBuildOutputHandler(verbose: boolean): {
|
|
594
|
+
handleChunk: (chunk: Buffer | string) => void;
|
|
595
|
+
} {
|
|
596
|
+
let buffer = '';
|
|
597
|
+
const tag = this.cleanTag;
|
|
598
|
+
|
|
599
|
+
const handleLine = (line: string) => {
|
|
600
|
+
// In verbose mode, write raw output prefixed with tag for identification
|
|
601
|
+
if (verbose) {
|
|
602
|
+
process.stdout.write(`[${tag}] ${line}\n`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Buildx step: #N [platform step/total] INSTRUCTION
|
|
606
|
+
const bxStep = line.match(/^#\d+ \[([^\]]+?)(\d+\/\d+)\] (.+)/);
|
|
607
|
+
if (bxStep) {
|
|
608
|
+
const prefix = bxStep[1].trim();
|
|
609
|
+
const step = bxStep[2];
|
|
610
|
+
const instruction = bxStep[3];
|
|
611
|
+
const platform = extractPlatform(prefix);
|
|
612
|
+
const platStr = platform ? `${platform} ▸ ` : '';
|
|
613
|
+
logger.log('note', `[${tag}] ${platStr}[${step}] ${instruction}`);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Buildx CACHED: #N CACHED
|
|
618
|
+
const bxCached = line.match(/^#(\d+) CACHED/);
|
|
619
|
+
if (bxCached) {
|
|
620
|
+
logger.log('note', `[${tag}] CACHED`);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Buildx DONE: #N DONE 12.3s
|
|
625
|
+
const bxDone = line.match(/^#\d+ DONE (.+)/);
|
|
626
|
+
if (bxDone) {
|
|
627
|
+
const timing = bxDone[1];
|
|
628
|
+
if (!timing.startsWith('0.0')) {
|
|
629
|
+
logger.log('note', `[${tag}] DONE ${timing}`);
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Buildx export phase: #N exporting ...
|
|
635
|
+
const bxExport = line.match(/^#\d+ exporting (.+)/);
|
|
636
|
+
if (bxExport) {
|
|
637
|
+
logger.log('note', `[${tag}] exporting ${bxExport[1]}`);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Standard docker build: Step N/M : INSTRUCTION
|
|
642
|
+
const stdStep = line.match(/^Step (\d+\/\d+) : (.+)/);
|
|
643
|
+
if (stdStep) {
|
|
644
|
+
logger.log('note', `[${tag}] Step ${stdStep[1]}: ${stdStep[2]}`);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
handleChunk: (chunk: Buffer | string) => {
|
|
651
|
+
buffer += chunk.toString();
|
|
652
|
+
const lines = buffer.split('\n');
|
|
653
|
+
buffer = lines.pop() || '';
|
|
654
|
+
for (const line of lines) {
|
|
655
|
+
const trimmed = line.replace(/\r$/, '').trim();
|
|
656
|
+
if (trimmed) handleLine(trimmed);
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
566
662
|
/**
|
|
567
663
|
* Builds the Dockerfile
|
|
568
664
|
*/
|
|
@@ -590,27 +686,32 @@ export class Dockerfile {
|
|
|
590
686
|
|
|
591
687
|
if (platformOverride) {
|
|
592
688
|
// Single platform override via buildx
|
|
593
|
-
buildCommand = `docker buildx build --platform ${platformOverride}${noCacheFlag}${buildContextFlag} --load -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
|
|
689
|
+
buildCommand = `docker buildx build --progress=plain --platform ${platformOverride}${noCacheFlag}${buildContextFlag} --load -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
|
|
594
690
|
logger.log('info', `Build: buildx --platform ${platformOverride} --load`);
|
|
595
691
|
} else if (config.platforms && config.platforms.length > 1) {
|
|
596
692
|
// Multi-platform build using buildx — always push to local registry
|
|
597
693
|
const platformString = config.platforms.join(',');
|
|
598
|
-
const
|
|
599
|
-
|
|
694
|
+
const registryHost = this.session?.config.registryHost || 'localhost:5234';
|
|
695
|
+
const localTag = `${registryHost}/${this.buildTag}`;
|
|
696
|
+
buildCommand = `docker buildx build --progress=plain --platform ${platformString}${noCacheFlag}${buildContextFlag} -t ${localTag} -f ${this.filePath} ${buildArgsString} --push .`;
|
|
600
697
|
this.localRegistryTag = localTag;
|
|
601
698
|
logger.log('info', `Build: buildx --platform ${platformString} --push to local registry`);
|
|
602
699
|
} else {
|
|
603
700
|
// Standard build
|
|
604
701
|
const versionLabel = this.managerRef.projectInfo?.npm?.version || 'unknown';
|
|
605
|
-
buildCommand = `docker build --label="version=${versionLabel}"${noCacheFlag} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
|
|
702
|
+
buildCommand = `docker build --progress=plain --label="version=${versionLabel}"${noCacheFlag} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
|
|
606
703
|
logger.log('info', 'Build: docker build (standard)');
|
|
607
704
|
}
|
|
608
705
|
|
|
706
|
+
// Execute build with real-time layer logging
|
|
707
|
+
const handler = this.createBuildOutputHandler(verbose);
|
|
708
|
+
const streaming = await smartshellInstance.execStreamingSilent(buildCommand);
|
|
709
|
+
|
|
710
|
+
// Intercept output for layer logging
|
|
711
|
+
streaming.childProcess.stdout?.on('data', handler.handleChunk);
|
|
712
|
+
streaming.childProcess.stderr?.on('data', handler.handleChunk);
|
|
713
|
+
|
|
609
714
|
if (timeout) {
|
|
610
|
-
// Use streaming execution with timeout
|
|
611
|
-
const streaming = verbose
|
|
612
|
-
? await smartshellInstance.execStreaming(buildCommand)
|
|
613
|
-
: await smartshellInstance.execStreamingSilent(buildCommand);
|
|
614
715
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
615
716
|
setTimeout(() => {
|
|
616
717
|
streaming.childProcess.kill();
|
|
@@ -623,9 +724,7 @@ export class Dockerfile {
|
|
|
623
724
|
throw new Error(`Build failed for ${this.cleanTag}`);
|
|
624
725
|
}
|
|
625
726
|
} else {
|
|
626
|
-
const result =
|
|
627
|
-
? await smartshellInstance.exec(buildCommand)
|
|
628
|
-
: await smartshellInstance.execSilent(buildCommand);
|
|
727
|
+
const result = await streaming.finalPromise;
|
|
629
728
|
if (result.exitCode !== 0) {
|
|
630
729
|
logger.log('error', `Build failed for ${this.cleanTag}`);
|
|
631
730
|
if (!verbose && result.stdout) {
|
|
@@ -646,12 +745,13 @@ export class Dockerfile {
|
|
|
646
745
|
const destRepo = this.getDestRepo(dockerRegistryArg.registryUrl);
|
|
647
746
|
const destTag = versionSuffix ? `${this.version}_${versionSuffix}` : this.version;
|
|
648
747
|
const registryCopy = new RegistryCopy();
|
|
748
|
+
const registryHost = this.session?.config.registryHost || 'localhost:5234';
|
|
649
749
|
|
|
650
750
|
this.pushTag = `${dockerRegistryArg.registryUrl}/${destRepo}:${destTag}`;
|
|
651
751
|
logger.log('info', `Pushing ${this.pushTag} via OCI copy from local registry...`);
|
|
652
752
|
|
|
653
753
|
await registryCopy.copyImage(
|
|
654
|
-
|
|
754
|
+
registryHost,
|
|
655
755
|
this.repo,
|
|
656
756
|
this.version,
|
|
657
757
|
dockerRegistryArg.registryUrl,
|
|
@@ -701,23 +801,27 @@ export class Dockerfile {
|
|
|
701
801
|
// Use local registry tag for multi-platform images (not in daemon), otherwise buildTag
|
|
702
802
|
const imageRef = this.localRegistryTag || this.buildTag;
|
|
703
803
|
|
|
804
|
+
const sessionId = this.session?.config.sessionId || 'default';
|
|
805
|
+
const testContainerName = `tsdocker_test_${sessionId}`;
|
|
806
|
+
const testImageName = `tsdocker_test_image_${sessionId}`;
|
|
807
|
+
|
|
704
808
|
const testFileExists = fs.existsSync(testFile);
|
|
705
809
|
|
|
706
810
|
if (testFileExists) {
|
|
707
811
|
// Run tests in container
|
|
708
812
|
await smartshellInstance.exec(
|
|
709
|
-
`docker run --name
|
|
813
|
+
`docker run --name ${testContainerName} --entrypoint="bash" ${imageRef} -c "mkdir /tsdocker_test"`
|
|
710
814
|
);
|
|
711
|
-
await smartshellInstance.exec(`docker cp ${testFile}
|
|
712
|
-
await smartshellInstance.exec(`docker commit
|
|
815
|
+
await smartshellInstance.exec(`docker cp ${testFile} ${testContainerName}:/tsdocker_test/test.sh`);
|
|
816
|
+
await smartshellInstance.exec(`docker commit ${testContainerName} ${testImageName}`);
|
|
713
817
|
|
|
714
818
|
const testResult = await smartshellInstance.exec(
|
|
715
|
-
`docker run --entrypoint="bash"
|
|
819
|
+
`docker run --entrypoint="bash" ${testImageName} -x /tsdocker_test/test.sh`
|
|
716
820
|
);
|
|
717
821
|
|
|
718
822
|
// Cleanup
|
|
719
|
-
await smartshellInstance.exec(`docker rm
|
|
720
|
-
await smartshellInstance.exec(`docker rmi --force
|
|
823
|
+
await smartshellInstance.exec(`docker rm ${testContainerName}`);
|
|
824
|
+
await smartshellInstance.exec(`docker rmi --force ${testImageName}`);
|
|
721
825
|
|
|
722
826
|
if (testResult.exitCode !== 0) {
|
|
723
827
|
throw new Error(`Tests failed for ${this.cleanTag}`);
|
|
@@ -6,6 +6,7 @@ import { DockerRegistry } from './classes.dockerregistry.js';
|
|
|
6
6
|
import { RegistryStorage } from './classes.registrystorage.js';
|
|
7
7
|
import { TsDockerCache } from './classes.tsdockercache.js';
|
|
8
8
|
import { DockerContext } from './classes.dockercontext.js';
|
|
9
|
+
import { TsDockerSession } from './classes.tsdockersession.js';
|
|
9
10
|
import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
|
|
10
11
|
|
|
11
12
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
@@ -20,6 +21,7 @@ export class TsDockerManager {
|
|
|
20
21
|
public config: ITsDockerConfig;
|
|
21
22
|
public projectInfo: any;
|
|
22
23
|
public dockerContext: DockerContext;
|
|
24
|
+
public session!: TsDockerSession;
|
|
23
25
|
private dockerfiles: Dockerfile[] = [];
|
|
24
26
|
|
|
25
27
|
constructor(config: ITsDockerConfig) {
|
|
@@ -77,6 +79,9 @@ export class TsDockerManager {
|
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
// Create session identity (unique ports, names for CI concurrency)
|
|
83
|
+
this.session = await TsDockerSession.create();
|
|
84
|
+
|
|
80
85
|
logger.log('info', `Prepared TsDockerManager with ${this.registryStorage.getAllRegistries().length} registries`);
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -98,6 +103,10 @@ export class TsDockerManager {
|
|
|
98
103
|
this.dockerfiles = await Dockerfile.readDockerfiles(this);
|
|
99
104
|
this.dockerfiles = await Dockerfile.sortDockerfiles(this.dockerfiles);
|
|
100
105
|
this.dockerfiles = await Dockerfile.mapDockerfiles(this.dockerfiles);
|
|
106
|
+
// Inject session into each Dockerfile
|
|
107
|
+
for (const df of this.dockerfiles) {
|
|
108
|
+
df.session = this.session;
|
|
109
|
+
}
|
|
101
110
|
return this.dockerfiles;
|
|
102
111
|
}
|
|
103
112
|
|
|
@@ -187,7 +196,7 @@ export class TsDockerManager {
|
|
|
187
196
|
|
|
188
197
|
const total = toBuild.length;
|
|
189
198
|
const overallStart = Date.now();
|
|
190
|
-
await Dockerfile.startLocalRegistry(this.dockerContext.contextInfo?.isRootless);
|
|
199
|
+
await Dockerfile.startLocalRegistry(this.session, this.dockerContext.contextInfo?.isRootless);
|
|
191
200
|
|
|
192
201
|
try {
|
|
193
202
|
if (options?.parallel) {
|
|
@@ -240,7 +249,7 @@ export class TsDockerManager {
|
|
|
240
249
|
}
|
|
241
250
|
// Push ALL images to local registry (skip if already pushed via buildx)
|
|
242
251
|
if (!df.localRegistryTag) {
|
|
243
|
-
await Dockerfile.pushToLocalRegistry(df);
|
|
252
|
+
await Dockerfile.pushToLocalRegistry(this.session, df);
|
|
244
253
|
}
|
|
245
254
|
}
|
|
246
255
|
}
|
|
@@ -280,19 +289,19 @@ export class TsDockerManager {
|
|
|
280
289
|
|
|
281
290
|
// Push ALL images to local registry (skip if already pushed via buildx)
|
|
282
291
|
if (!dockerfileArg.localRegistryTag) {
|
|
283
|
-
await Dockerfile.pushToLocalRegistry(dockerfileArg);
|
|
292
|
+
await Dockerfile.pushToLocalRegistry(this.session, dockerfileArg);
|
|
284
293
|
}
|
|
285
294
|
}
|
|
286
295
|
}
|
|
287
296
|
} finally {
|
|
288
|
-
await Dockerfile.stopLocalRegistry();
|
|
297
|
+
await Dockerfile.stopLocalRegistry(this.session);
|
|
289
298
|
}
|
|
290
299
|
|
|
291
300
|
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
|
|
292
301
|
cache.save();
|
|
293
302
|
} else {
|
|
294
303
|
// === STANDARD MODE: build all via static helper ===
|
|
295
|
-
await Dockerfile.buildDockerfiles(toBuild, {
|
|
304
|
+
await Dockerfile.buildDockerfiles(toBuild, this.session, {
|
|
296
305
|
platform: options?.platform,
|
|
297
306
|
timeout: options?.timeout,
|
|
298
307
|
noCache: options?.noCache,
|
|
@@ -329,7 +338,7 @@ export class TsDockerManager {
|
|
|
329
338
|
* Ensures Docker buildx is set up for multi-architecture builds
|
|
330
339
|
*/
|
|
331
340
|
private async ensureBuildx(): Promise<void> {
|
|
332
|
-
const builderName = this.dockerContext.getBuilderName();
|
|
341
|
+
const builderName = this.dockerContext.getBuilderName() + (this.session?.config.builderSuffix || '');
|
|
333
342
|
const platforms = this.config.platforms?.join(', ') || 'default';
|
|
334
343
|
logger.log('info', `Setting up Docker buildx [${platforms}]...`);
|
|
335
344
|
logger.log('info', `Builder: ${builderName}`);
|
|
@@ -394,7 +403,7 @@ export class TsDockerManager {
|
|
|
394
403
|
}
|
|
395
404
|
|
|
396
405
|
// Start local registry (reads from persistent .nogit/docker-registry/)
|
|
397
|
-
await Dockerfile.startLocalRegistry(this.dockerContext.contextInfo?.isRootless);
|
|
406
|
+
await Dockerfile.startLocalRegistry(this.session, this.dockerContext.contextInfo?.isRootless);
|
|
398
407
|
try {
|
|
399
408
|
// Push each Dockerfile to each registry via OCI copy
|
|
400
409
|
for (const dockerfile of this.dockerfiles) {
|
|
@@ -403,7 +412,7 @@ export class TsDockerManager {
|
|
|
403
412
|
}
|
|
404
413
|
}
|
|
405
414
|
} finally {
|
|
406
|
-
await Dockerfile.stopLocalRegistry();
|
|
415
|
+
await Dockerfile.stopLocalRegistry(this.session);
|
|
407
416
|
}
|
|
408
417
|
|
|
409
418
|
logger.log('success', 'All images pushed successfully');
|
|
@@ -446,11 +455,11 @@ export class TsDockerManager {
|
|
|
446
455
|
logger.log('info', '');
|
|
447
456
|
logger.log('info', '=== TEST PHASE ===');
|
|
448
457
|
|
|
449
|
-
await Dockerfile.startLocalRegistry(this.dockerContext.contextInfo?.isRootless);
|
|
458
|
+
await Dockerfile.startLocalRegistry(this.session, this.dockerContext.contextInfo?.isRootless);
|
|
450
459
|
try {
|
|
451
460
|
await Dockerfile.testDockerfiles(this.dockerfiles);
|
|
452
461
|
} finally {
|
|
453
|
-
await Dockerfile.stopLocalRegistry();
|
|
462
|
+
await Dockerfile.stopLocalRegistry(this.session);
|
|
454
463
|
}
|
|
455
464
|
|
|
456
465
|
logger.log('success', 'All tests completed');
|
|
@@ -490,4 +499,16 @@ export class TsDockerManager {
|
|
|
490
499
|
public getDockerfiles(): Dockerfile[] {
|
|
491
500
|
return this.dockerfiles;
|
|
492
501
|
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Cleans up session-specific resources.
|
|
505
|
+
* In CI, removes the session-specific buildx builder to avoid accumulation.
|
|
506
|
+
*/
|
|
507
|
+
public async cleanup(): Promise<void> {
|
|
508
|
+
if (this.session?.config.isCI && this.session.config.builderSuffix) {
|
|
509
|
+
const builderName = this.dockerContext.getBuilderName() + this.session.config.builderSuffix;
|
|
510
|
+
logger.log('info', `CI cleanup: removing buildx builder ${builderName}`);
|
|
511
|
+
await smartshellInstance.execSilent(`docker buildx rm ${builderName} 2>/dev/null || true`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
493
514
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import * as net from 'net';
|
|
3
|
+
import { logger } from './tsdocker.logging.js';
|
|
4
|
+
|
|
5
|
+
export interface ISessionConfig {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
registryPort: number;
|
|
8
|
+
registryHost: string;
|
|
9
|
+
registryContainerName: string;
|
|
10
|
+
isCI: boolean;
|
|
11
|
+
ciSystem: string | null;
|
|
12
|
+
builderSuffix: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Per-invocation session identity for tsdocker.
|
|
17
|
+
* Generates unique ports, container names, and builder names so that
|
|
18
|
+
* concurrent CI jobs on the same Docker host don't collide.
|
|
19
|
+
*
|
|
20
|
+
* In local (non-CI) dev the builder suffix is empty, preserving the
|
|
21
|
+
* persistent builder behavior.
|
|
22
|
+
*/
|
|
23
|
+
export class TsDockerSession {
|
|
24
|
+
public config: ISessionConfig;
|
|
25
|
+
|
|
26
|
+
private constructor(config: ISessionConfig) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a new session. Allocates a dynamic port unless overridden
|
|
32
|
+
* via `TSDOCKER_REGISTRY_PORT`.
|
|
33
|
+
*/
|
|
34
|
+
public static async create(): Promise<TsDockerSession> {
|
|
35
|
+
const sessionId =
|
|
36
|
+
process.env.TSDOCKER_SESSION_ID || crypto.randomBytes(4).toString('hex');
|
|
37
|
+
|
|
38
|
+
const registryPort = await TsDockerSession.allocatePort();
|
|
39
|
+
const registryHost = `localhost:${registryPort}`;
|
|
40
|
+
const registryContainerName = `tsdocker-registry-${sessionId}`;
|
|
41
|
+
|
|
42
|
+
const { isCI, ciSystem } = TsDockerSession.detectCI();
|
|
43
|
+
const builderSuffix = isCI ? `-${sessionId}` : '';
|
|
44
|
+
|
|
45
|
+
const config: ISessionConfig = {
|
|
46
|
+
sessionId,
|
|
47
|
+
registryPort,
|
|
48
|
+
registryHost,
|
|
49
|
+
registryContainerName,
|
|
50
|
+
isCI,
|
|
51
|
+
ciSystem,
|
|
52
|
+
builderSuffix,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const session = new TsDockerSession(config);
|
|
56
|
+
session.logInfo();
|
|
57
|
+
return session;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Allocates a free TCP port. Respects `TSDOCKER_REGISTRY_PORT` override.
|
|
62
|
+
*/
|
|
63
|
+
public static async allocatePort(): Promise<number> {
|
|
64
|
+
const envPort = process.env.TSDOCKER_REGISTRY_PORT;
|
|
65
|
+
if (envPort) {
|
|
66
|
+
const parsed = parseInt(envPort, 10);
|
|
67
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return new Promise<number>((resolve, reject) => {
|
|
73
|
+
const srv = net.createServer();
|
|
74
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
75
|
+
const addr = srv.address() as net.AddressInfo;
|
|
76
|
+
const port = addr.port;
|
|
77
|
+
srv.close((err) => {
|
|
78
|
+
if (err) reject(err);
|
|
79
|
+
else resolve(port);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
srv.on('error', reject);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Detects whether we're running inside a CI system.
|
|
88
|
+
*/
|
|
89
|
+
private static detectCI(): { isCI: boolean; ciSystem: string | null } {
|
|
90
|
+
if (process.env.GITEA_ACTIONS) return { isCI: true, ciSystem: 'gitea-actions' };
|
|
91
|
+
if (process.env.GITHUB_ACTIONS) return { isCI: true, ciSystem: 'github-actions' };
|
|
92
|
+
if (process.env.GITLAB_CI) return { isCI: true, ciSystem: 'gitlab-ci' };
|
|
93
|
+
if (process.env.CI) return { isCI: true, ciSystem: 'generic' };
|
|
94
|
+
return { isCI: false, ciSystem: null };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private logInfo(): void {
|
|
98
|
+
const c = this.config;
|
|
99
|
+
logger.log('info', '=== TSDOCKER SESSION ===');
|
|
100
|
+
logger.log('info', `Session ID: ${c.sessionId}`);
|
|
101
|
+
logger.log('info', `Registry: ${c.registryHost} (container: ${c.registryContainerName})`);
|
|
102
|
+
if (c.isCI) {
|
|
103
|
+
logger.log('info', `CI detected: ${c.ciSystem}`);
|
|
104
|
+
logger.log('info', `Builder suffix: ${c.builderSuffix}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
package/ts/interfaces/index.ts
CHANGED
package/ts/tsdocker.cli.ts
CHANGED
|
@@ -64,6 +64,7 @@ export let run = () => {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
await manager.build(buildOptions);
|
|
67
|
+
await manager.cleanup();
|
|
67
68
|
logger.log('success', 'Build completed successfully');
|
|
68
69
|
} catch (err) {
|
|
69
70
|
logger.log('error', `Build failed: ${(err as Error).message}`);
|
|
@@ -117,6 +118,7 @@ export let run = () => {
|
|
|
117
118
|
const registries = registryArg ? [registryArg] : undefined;
|
|
118
119
|
|
|
119
120
|
await manager.push(registries);
|
|
121
|
+
await manager.cleanup();
|
|
120
122
|
logger.log('success', 'Push completed successfully');
|
|
121
123
|
} catch (err) {
|
|
122
124
|
logger.log('error', `Push failed: ${(err as Error).message}`);
|
|
@@ -180,6 +182,7 @@ export let run = () => {
|
|
|
180
182
|
|
|
181
183
|
// Run tests
|
|
182
184
|
await manager.test();
|
|
185
|
+
await manager.cleanup();
|
|
183
186
|
logger.log('success', 'Tests completed successfully');
|
|
184
187
|
} catch (err) {
|
|
185
188
|
logger.log('error', `Tests failed: ${(err as Error).message}`);
|