@git.zone/tsdocker 1.15.0 → 1.15.1

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.
@@ -2,6 +2,7 @@ import * as plugins from './tsdocker.plugins.js';
2
2
  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
+ import { RegistryCopy } from './classes.registrycopy.js';
5
6
  import type { IDockerfileOptions, ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
6
7
  import type { TsDockerManager } from './classes.tsdockermanager.js';
7
8
  import * as fs from 'fs';
@@ -139,31 +140,32 @@ export class Dockerfile {
139
140
  return sortedDockerfileArray;
140
141
  }
141
142
 
142
- /** Determines if a local registry is needed for buildx dependency resolution. */
143
+ /** Local registry is always needed — it's the canonical store for all built images. */
143
144
  public static needsLocalRegistry(
144
- dockerfiles: Dockerfile[],
145
- options?: { platform?: string },
145
+ _dockerfiles?: Dockerfile[],
146
+ _options?: { platform?: string },
146
147
  ): boolean {
147
- const hasLocalDeps = dockerfiles.some(df => df.localBaseImageDependent);
148
- if (!hasLocalDeps) return false;
149
- const config = dockerfiles[0]?.managerRef?.config;
150
- return !!options?.platform || !!(config?.platforms && config.platforms.length > 1);
148
+ return true;
151
149
  }
152
150
 
153
- /** Starts a temporary registry:2 container on port 5234. */
151
+ /** Starts a persistent registry:2 container on port 5234 with volume storage. */
154
152
  public static async startLocalRegistry(isRootless?: boolean): Promise<void> {
153
+ // Ensure persistent storage directory exists
154
+ const registryDataDir = plugins.path.join(paths.cwd, '.nogit', 'docker-registry');
155
+ fs.mkdirSync(registryDataDir, { recursive: true });
156
+
155
157
  await smartshellInstance.execSilent(
156
158
  `docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true`
157
159
  );
158
160
  const result = await smartshellInstance.execSilent(
159
- `docker run -d --name ${LOCAL_REGISTRY_CONTAINER} -p ${LOCAL_REGISTRY_PORT}:5000 registry:2`
161
+ `docker run -d --name ${LOCAL_REGISTRY_CONTAINER} -p ${LOCAL_REGISTRY_PORT}:5000 -v "${registryDataDir}:/var/lib/registry" registry:2`
160
162
  );
161
163
  if (result.exitCode !== 0) {
162
164
  throw new Error(`Failed to start local registry: ${result.stderr || result.stdout}`);
163
165
  }
164
166
  // registry:2 starts near-instantly; brief wait for readiness
165
167
  await new Promise(resolve => setTimeout(resolve, 1000));
166
- logger.log('info', `Started local registry at ${LOCAL_REGISTRY_HOST} (buildx dependency bridge)`);
168
+ logger.log('info', `Started local registry at ${LOCAL_REGISTRY_HOST} (persistent storage at .nogit/docker-registry/)`);
167
169
  if (isRootless) {
168
170
  logger.log('warn', `[rootless] Registry on port ${LOCAL_REGISTRY_PORT} — if buildx cannot reach localhost:${LOCAL_REGISTRY_PORT}, try 127.0.0.1:${LOCAL_REGISTRY_PORT}`);
169
171
  }
@@ -246,11 +248,8 @@ export class Dockerfile {
246
248
  ): Promise<Dockerfile[]> {
247
249
  const total = sortedArrayArg.length;
248
250
  const overallStart = Date.now();
249
- const useRegistry = Dockerfile.needsLocalRegistry(sortedArrayArg, options);
250
251
 
251
- if (useRegistry) {
252
- await Dockerfile.startLocalRegistry(options?.isRootless);
253
- }
252
+ await Dockerfile.startLocalRegistry(options?.isRootless);
254
253
 
255
254
  try {
256
255
  if (options?.parallel) {
@@ -282,8 +281,9 @@ export class Dockerfile {
282
281
 
283
282
  await Dockerfile.runWithConcurrency(tasks, concurrency);
284
283
 
285
- // After the entire level completes, tag + push for dependency resolution
284
+ // After the entire level completes, push all to local registry + tag for deps
286
285
  for (const df of level) {
286
+ // Tag in host daemon for dependency resolution
287
287
  const dependentBaseImages = new Set<string>();
288
288
  for (const other of sortedArrayArg) {
289
289
  if (other.localBaseDockerfile === df && other.baseImage !== df.buildTag) {
@@ -294,7 +294,8 @@ export class Dockerfile {
294
294
  logger.log('info', `Tagging ${df.buildTag} as ${fullTag} for local dependency resolution`);
295
295
  await smartshellInstance.exec(`docker tag ${df.buildTag} ${fullTag}`);
296
296
  }
297
- if (useRegistry && sortedArrayArg.some(other => other.localBaseDockerfile === df)) {
297
+ // Push ALL images to local registry (skip if already pushed via buildx)
298
+ if (!df.localRegistryTag) {
298
299
  await Dockerfile.pushToLocalRegistry(df);
299
300
  }
300
301
  }
@@ -321,16 +322,14 @@ export class Dockerfile {
321
322
  await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
322
323
  }
323
324
 
324
- // Push to local registry for buildx dependency resolution
325
- if (useRegistry && sortedArrayArg.some(other => other.localBaseDockerfile === dockerfileArg)) {
325
+ // Push ALL images to local registry (skip if already pushed via buildx)
326
+ if (!dockerfileArg.localRegistryTag) {
326
327
  await Dockerfile.pushToLocalRegistry(dockerfileArg);
327
328
  }
328
329
  }
329
330
  }
330
331
  } finally {
331
- if (useRegistry) {
332
- await Dockerfile.stopLocalRegistry();
333
- }
332
+ await Dockerfile.stopLocalRegistry();
334
333
  }
335
334
 
336
335
  logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
@@ -594,17 +593,12 @@ export class Dockerfile {
594
593
  buildCommand = `docker buildx build --platform ${platformOverride}${noCacheFlag}${buildContextFlag} --load -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
595
594
  logger.log('info', `Build: buildx --platform ${platformOverride} --load`);
596
595
  } else if (config.platforms && config.platforms.length > 1) {
597
- // Multi-platform build using buildx
596
+ // Multi-platform build using buildx — always push to local registry
598
597
  const platformString = config.platforms.join(',');
599
- buildCommand = `docker buildx build --platform ${platformString}${noCacheFlag}${buildContextFlag} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
600
-
601
- if (config.push) {
602
- buildCommand += ' --push';
603
- logger.log('info', `Build: buildx --platform ${platformString} --push`);
604
- } else {
605
- buildCommand += ' --load';
606
- logger.log('info', `Build: buildx --platform ${platformString} --load`);
607
- }
598
+ const localTag = `${LOCAL_REGISTRY_HOST}/${this.buildTag}`;
599
+ buildCommand = `docker buildx build --platform ${platformString}${noCacheFlag}${buildContextFlag} -t ${localTag} -f ${this.filePath} ${buildArgsString} --push .`;
600
+ this.localRegistryTag = localTag;
601
+ logger.log('info', `Build: buildx --platform ${platformString} --push to local registry`);
608
602
  } else {
609
603
  // Standard build
610
604
  const versionLabel = this.managerRef.projectInfo?.npm?.version || 'unknown';
@@ -645,38 +639,39 @@ export class Dockerfile {
645
639
  }
646
640
 
647
641
  /**
648
- * Pushes the Dockerfile to a registry
642
+ * Pushes the Dockerfile to a registry using OCI Distribution API copy
643
+ * from the local registry to the remote registry.
649
644
  */
650
645
  public async push(dockerRegistryArg: DockerRegistry, versionSuffix?: string): Promise<void> {
651
- this.pushTag = Dockerfile.getDockerTagString(
652
- this.managerRef,
653
- dockerRegistryArg.registryUrl,
654
- this.repo,
655
- this.version,
656
- versionSuffix
657
- );
646
+ const destRepo = this.getDestRepo(dockerRegistryArg.registryUrl);
647
+ const destTag = versionSuffix ? `${this.version}_${versionSuffix}` : this.version;
648
+ const registryCopy = new RegistryCopy();
658
649
 
659
- await smartshellInstance.exec(`docker tag ${this.buildTag} ${this.pushTag}`);
660
- const pushResult = await smartshellInstance.exec(`docker push ${this.pushTag}`);
650
+ this.pushTag = `${dockerRegistryArg.registryUrl}/${destRepo}:${destTag}`;
651
+ logger.log('info', `Pushing ${this.pushTag} via OCI copy from local registry...`);
661
652
 
662
- if (pushResult.exitCode !== 0) {
663
- logger.log('error', `Push failed for ${this.pushTag}`);
664
- throw new Error(`Push failed for ${this.pushTag}`);
665
- }
666
-
667
- // Get image digest
668
- const inspectResult = await smartshellInstance.exec(
669
- `docker inspect --format="{{index .RepoDigests 0}}" ${this.pushTag}`
653
+ await registryCopy.copyImage(
654
+ LOCAL_REGISTRY_HOST,
655
+ this.repo,
656
+ this.version,
657
+ dockerRegistryArg.registryUrl,
658
+ destRepo,
659
+ destTag,
660
+ { username: dockerRegistryArg.username, password: dockerRegistryArg.password },
670
661
  );
671
662
 
672
- if (inspectResult.exitCode === 0 && inspectResult.stdout.includes('@')) {
673
- const imageDigest = inspectResult.stdout.split('@')[1]?.trim();
674
- logger.log('info', `The image ${this.pushTag} has digest ${imageDigest}`);
675
- }
676
-
677
663
  logger.log('ok', `Pushed ${this.pushTag}`);
678
664
  }
679
665
 
666
+ /**
667
+ * Returns the destination repository for a given registry URL,
668
+ * using registryRepoMap if configured, otherwise the default repo.
669
+ */
670
+ private getDestRepo(registryUrl: string): string {
671
+ const config = this.managerRef.config;
672
+ return config.registryRepoMap?.[registryUrl] || this.repo;
673
+ }
674
+
680
675
  /**
681
676
  * Pulls the Dockerfile from a registry
682
677
  */
@@ -696,19 +691,22 @@ export class Dockerfile {
696
691
  }
697
692
 
698
693
  /**
699
- * Tests the Dockerfile by running a test script if it exists
694
+ * Tests the Dockerfile by running a test script if it exists.
695
+ * For multi-platform builds, uses the local registry tag so Docker can auto-pull.
700
696
  */
701
697
  public async test(): Promise<number> {
702
698
  const startTime = Date.now();
703
699
  const testDir = this.managerRef.config.testDir || plugins.path.join(paths.cwd, 'test');
704
700
  const testFile = plugins.path.join(testDir, 'test_' + this.version + '.sh');
701
+ // Use local registry tag for multi-platform images (not in daemon), otherwise buildTag
702
+ const imageRef = this.localRegistryTag || this.buildTag;
705
703
 
706
704
  const testFileExists = fs.existsSync(testFile);
707
705
 
708
706
  if (testFileExists) {
709
707
  // Run tests in container
710
708
  await smartshellInstance.exec(
711
- `docker run --name tsdocker_test_container --entrypoint="bash" ${this.buildTag} -c "mkdir /tsdocker_test"`
709
+ `docker run --name tsdocker_test_container --entrypoint="bash" ${imageRef} -c "mkdir /tsdocker_test"`
712
710
  );
713
711
  await smartshellInstance.exec(`docker cp ${testFile} tsdocker_test_container:/tsdocker_test/test.sh`);
714
712
  await smartshellInstance.exec(`docker commit tsdocker_test_container tsdocker_test_image`);