@git.zone/cli 2.18.1 → 2.19.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.
@@ -793,7 +793,7 @@ async function handleRelease(mode: ICliMode): Promise<void> {
793
793
  choices: [
794
794
  { name: "git - push branch and tags", value: "git" },
795
795
  { name: "npm - publish package registries", value: "npm" },
796
- { name: "docker - build and push images", value: "docker" },
796
+ { name: "docker - build and push through tsdocker", value: "docker" },
797
797
  ],
798
798
  default: getDefaultEnabledTargets(currentTargets),
799
799
  });
@@ -860,21 +860,49 @@ async function handleRelease(mode: ICliMode): Promise<void> {
860
860
  }
861
861
 
862
862
  if (enabledTargets.includes("docker")) {
863
- const images = await askValue<string>(interactInstance, {
863
+ const patterns = await askValue<string>(interactInstance, {
864
864
  type: "input",
865
- name: "dockerImages",
866
- message: "Docker image templates (comma-separated, supports {{version}}):",
867
- default: Array.isArray(currentTargets.docker?.images)
868
- ? currentTargets.docker.images.join(", ")
865
+ name: "dockerPatterns",
866
+ message: "tsdocker Dockerfile patterns (comma-separated, empty means all):",
867
+ default: Array.isArray(currentTargets.docker?.patterns)
868
+ ? currentTargets.docker.patterns.join(", ")
869
869
  : "",
870
870
  });
871
+ const cached = await askValue<boolean>(interactInstance, {
872
+ type: "confirm",
873
+ name: "dockerCached",
874
+ message: "Use tsdocker cached builds?",
875
+ default: currentTargets.docker?.cached ?? false,
876
+ });
877
+ const parallel = await askValue<string>(interactInstance, {
878
+ type: "input",
879
+ name: "dockerParallel",
880
+ message: "tsdocker parallel mode (false, true, or concurrency number):",
881
+ default: formatDockerParallel(currentTargets.docker?.parallel ?? false),
882
+ });
883
+ const context = await askValue<string>(interactInstance, {
884
+ type: "input",
885
+ name: "dockerContext",
886
+ message: "Docker context for tsdocker (empty for default):",
887
+ default: currentTargets.docker?.context || "",
888
+ });
889
+ const noBuild = await askValue<boolean>(interactInstance, {
890
+ type: "confirm",
891
+ name: "dockerNoBuild",
892
+ message: "Skip tsdocker build and only push existing local registry images?",
893
+ default: currentTargets.docker?.noBuild ?? false,
894
+ });
871
895
  releaseTargets.docker = {
872
- ...(currentTargets.docker || {}),
873
896
  enabled: true,
874
- images: parseCsv(images),
897
+ engine: "tsdocker",
898
+ patterns: parseCsv(patterns),
899
+ cached,
900
+ parallel: parseDockerParallel(parallel),
901
+ context: context.trim() || undefined,
902
+ noBuild,
875
903
  };
876
904
  } else {
877
- releaseTargets.docker = { ...(currentTargets.docker || {}), enabled: false };
905
+ releaseTargets.docker = { enabled: false, engine: "tsdocker" };
878
906
  }
879
907
 
880
908
  setCliConfigValueInData(smartconfigData, "schemaVersion", CURRENT_GITZONE_CLI_SCHEMA_VERSION);
@@ -1043,7 +1071,7 @@ async function collectDoctorFindings(): Promise<IDoctorFinding[]> {
1043
1071
  await validateDetectedProjectType(cliConfig, findings);
1044
1072
 
1045
1073
  validateCommitConfig(cliConfig.commit || {}, findings);
1046
- await validateReleaseConfig(cliConfig.release || {}, findings);
1074
+ await validateReleaseConfig(cliConfig.release || {}, smartconfigData, findings);
1047
1075
 
1048
1076
  return findings;
1049
1077
  }
@@ -1291,9 +1319,14 @@ function formatTarget(enabled: unknown, targetConfig: any): string {
1291
1319
  details.push(`registries=${targetConfig.registries.length}`);
1292
1320
  }
1293
1321
  if (targetConfig.accessLevel) details.push(`access=${targetConfig.accessLevel}`);
1294
- if (Array.isArray(targetConfig.images)) {
1295
- details.push(`images=${targetConfig.images.length}`);
1296
- }
1322
+ if (targetConfig.engine) details.push(`engine=${targetConfig.engine}`);
1323
+ if (Array.isArray(targetConfig.patterns)) {
1324
+ details.push(`patterns=${targetConfig.patterns.length}`);
1325
+ }
1326
+ if (targetConfig.cached) details.push("cached=true");
1327
+ if (targetConfig.parallel) details.push(`parallel=${targetConfig.parallel}`);
1328
+ if (targetConfig.context) details.push(`context=${targetConfig.context}`);
1329
+ if (targetConfig.noBuild) details.push("noBuild=true");
1297
1330
  return details.length > 0 ? `${state} (${details.join(", ")})` : state;
1298
1331
  }
1299
1332
 
@@ -1338,6 +1371,29 @@ function parseCsv(value: string): string[] {
1338
1371
  return result;
1339
1372
  }
1340
1373
 
1374
+ function formatDockerParallel(value: unknown): string {
1375
+ if (value === true) return "true";
1376
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
1377
+ return String(Math.floor(value));
1378
+ }
1379
+ return "false";
1380
+ }
1381
+
1382
+ function parseDockerParallel(value: string): boolean | number {
1383
+ const normalizedValue = value.trim().toLowerCase();
1384
+ if (!normalizedValue || ["false", "no", "off", "0"].includes(normalizedValue)) {
1385
+ return false;
1386
+ }
1387
+ if (["true", "yes", "on"].includes(normalizedValue)) {
1388
+ return true;
1389
+ }
1390
+ const numericValue = Number(normalizedValue);
1391
+ if (Number.isFinite(numericValue) && numericValue > 0) {
1392
+ return Math.floor(numericValue);
1393
+ }
1394
+ return false;
1395
+ }
1396
+
1341
1397
  function normalizeRegistryUrl(url: string): string {
1342
1398
  let normalizedUrl = url.trim();
1343
1399
  if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
@@ -1391,6 +1447,7 @@ function buildConfigFixPrompt(
1391
1447
  `- Use schemaVersion ${CURRENT_GITZONE_CLI_SCHEMA_VERSION} for ` +
1392
1448
  "`@git.zone/cli`.",
1393
1449
  "- Use target-based release config: `release.targets.git`, `release.targets.npm`, and `release.targets.docker`.",
1450
+ "- Docker release targets must use `release.targets.docker.engine = \"tsdocker\"`; Docker registries belong under `@git.zone/tsdocker`.",
1394
1451
  "- Keep npm registries only at `@git.zone/cli.release.targets.npm.registries`.",
1395
1452
  "- Do not add runtime legacy compatibility code. If legacy config exists, migrate it explicitly.",
1396
1453
  "- Do not commit, release, install dependencies, or modify unrelated files.",
@@ -1514,6 +1571,7 @@ function validateCommitConfig(
1514
1571
 
1515
1572
  async function validateReleaseConfig(
1516
1573
  releaseConfig: Record<string, any>,
1574
+ smartconfigData: Record<string, any>,
1517
1575
  findings: IDoctorFinding[],
1518
1576
  ): Promise<void> {
1519
1577
  const confirmation = releaseConfig.confirmation;
@@ -1554,7 +1612,7 @@ async function validateReleaseConfig(
1554
1612
  const targets = releaseConfig.targets || {};
1555
1613
  await validateGitTarget(targets.git || {}, findings);
1556
1614
  await validateNpmTarget(targets.npm || {}, findings);
1557
- validateDockerTarget(targets.docker || {}, findings);
1615
+ await validateDockerTarget(targets.docker || {}, smartconfigData, findings);
1558
1616
  }
1559
1617
 
1560
1618
  async function validateGitTarget(
@@ -1718,31 +1776,171 @@ async function validateNpmAuth(
1718
1776
  }
1719
1777
  }
1720
1778
 
1721
- function validateDockerTarget(
1779
+ async function validateDockerTarget(
1722
1780
  dockerTarget: Record<string, any>,
1781
+ smartconfigData: Record<string, any>,
1723
1782
  findings: IDoctorFinding[],
1724
- ): void {
1783
+ ): Promise<void> {
1784
+ if ("images" in dockerTarget) {
1785
+ findings.push({
1786
+ level: "error",
1787
+ message: "Docker release target still uses removed images config",
1788
+ fix: "Remove release.targets.docker.images and configure @git.zone/tsdocker instead.",
1789
+ });
1790
+ }
1791
+
1725
1792
  const enabled = dockerTarget.enabled ?? false;
1726
1793
  if (!enabled) {
1727
1794
  findings.push({ level: "ok", message: "Docker release target is disabled" });
1728
1795
  return;
1729
1796
  }
1730
1797
 
1731
- if (!Array.isArray(dockerTarget.images) || dockerTarget.images.length === 0) {
1798
+ if (dockerTarget.engine !== "tsdocker") {
1732
1799
  findings.push({
1733
1800
  level: "error",
1734
- message: "Docker release target is enabled without images",
1735
- fix: "Set release.targets.docker.images or disable release.targets.docker.enabled.",
1801
+ message: "Docker release target must use tsdocker",
1802
+ fix: "Set release.targets.docker.engine to tsdocker.",
1736
1803
  });
1737
- return;
1738
1804
  }
1739
1805
 
1806
+ if (dockerTarget.patterns !== undefined && !Array.isArray(dockerTarget.patterns)) {
1807
+ findings.push({
1808
+ level: "error",
1809
+ message: "Docker release target patterns must be an array",
1810
+ fix: "Set release.targets.docker.patterns to an array of Dockerfile patterns or remove it.",
1811
+ });
1812
+ }
1813
+
1814
+ if (!isValidDockerParallel(dockerTarget.parallel)) {
1815
+ findings.push({
1816
+ level: "error",
1817
+ message: `Invalid tsdocker parallel setting: ${formatValue(dockerTarget.parallel)}`,
1818
+ fix: "Use false, true, or a positive concurrency number.",
1819
+ });
1820
+ }
1821
+
1822
+ const tsdockerConfig = smartconfigData["@git.zone/tsdocker"];
1823
+ if (!isPlainObject(tsdockerConfig)) {
1824
+ findings.push({
1825
+ level: "error",
1826
+ message: "Docker release target is enabled but @git.zone/tsdocker config is missing",
1827
+ fix: "Add @git.zone/tsdocker.registries and optional registryRepoMap/platforms config.",
1828
+ });
1829
+ } else {
1830
+ validateTsdockerProjectConfig(tsdockerConfig, findings);
1831
+ }
1832
+
1833
+ await validateTsdockerCommand(findings);
1834
+
1740
1835
  findings.push({
1741
1836
  level: "ok",
1742
- message: `Docker release target has ${dockerTarget.images.length} image template(s)`,
1837
+ message: `Docker release target uses tsdocker (${formatDockerPatterns(dockerTarget.patterns)})`,
1743
1838
  });
1744
1839
  }
1745
1840
 
1841
+ function formatDockerPatterns(patterns: unknown): string {
1842
+ return Array.isArray(patterns) && patterns.length > 0
1843
+ ? patterns.map((pattern) => String(pattern)).join(", ")
1844
+ : "all Dockerfiles";
1845
+ }
1846
+
1847
+ function isPlainObject(value: unknown): value is Record<string, any> {
1848
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1849
+ }
1850
+
1851
+ function isValidDockerParallel(value: unknown): boolean {
1852
+ return value === undefined ||
1853
+ value === false ||
1854
+ value === true ||
1855
+ (typeof value === "number" && Number.isFinite(value) && value > 0);
1856
+ }
1857
+
1858
+ function validateTsdockerProjectConfig(
1859
+ tsdockerConfig: Record<string, any>,
1860
+ findings: IDoctorFinding[],
1861
+ ): void {
1862
+ const registries = Array.isArray(tsdockerConfig.registries)
1863
+ ? tsdockerConfig.registries
1864
+ : [];
1865
+ if (registries.length === 0) {
1866
+ findings.push({
1867
+ level: "error",
1868
+ message: "@git.zone/tsdocker.registries is empty",
1869
+ fix: "Set @git.zone/tsdocker.registries to registry hosts such as registry.gitlab.com.",
1870
+ });
1871
+ }
1872
+
1873
+ for (const registry of registries) {
1874
+ if (typeof registry !== "string" || !registry.trim()) {
1875
+ findings.push({
1876
+ level: "error",
1877
+ message: `Invalid tsdocker registry: ${formatValue(registry)}`,
1878
+ fix: "Use registry hosts such as registry.gitlab.com.",
1879
+ });
1880
+ continue;
1881
+ }
1882
+ if (registry.startsWith("http://") || registry.startsWith("https://")) {
1883
+ findings.push({
1884
+ level: "error",
1885
+ message: `tsdocker registry must not include a protocol: ${registry}`,
1886
+ fix: `Use ${registry.replace(/^https?:\/\//, "")}`,
1887
+ });
1888
+ }
1889
+ }
1890
+
1891
+ const registryRepoMap = tsdockerConfig.registryRepoMap;
1892
+ if (registryRepoMap !== undefined && !isPlainObject(registryRepoMap)) {
1893
+ findings.push({
1894
+ level: "error",
1895
+ message: "@git.zone/tsdocker.registryRepoMap must be an object",
1896
+ });
1897
+ } else if (isPlainObject(registryRepoMap)) {
1898
+ for (const registry of Object.keys(registryRepoMap)) {
1899
+ if (registry.startsWith("http://") || registry.startsWith("https://")) {
1900
+ findings.push({
1901
+ level: "error",
1902
+ message: `tsdocker registryRepoMap key must not include a protocol: ${registry}`,
1903
+ fix: `Use ${registry.replace(/^https?:\/\//, "")}`,
1904
+ });
1905
+ }
1906
+ }
1907
+ }
1908
+
1909
+ findings.push({
1910
+ level: "ok",
1911
+ message: `@git.zone/tsdocker has ${registries.length} registries`,
1912
+ });
1913
+ }
1914
+
1915
+ async function validateTsdockerCommand(findings: IDoctorFinding[]): Promise<void> {
1916
+ const smartshellInstance = new plugins.smartshell.Smartshell({
1917
+ executor: "bash",
1918
+ sourceFilePaths: [],
1919
+ });
1920
+ try {
1921
+ const result = await smartshellInstance.execSpawn(
1922
+ "tsdocker",
1923
+ ["--version"],
1924
+ { silent: true, timeout: 8000 },
1925
+ );
1926
+ if (result.exitCode === 0) {
1927
+ findings.push({ level: "ok", message: "tsdocker command is available" });
1928
+ } else {
1929
+ findings.push({
1930
+ level: "error",
1931
+ message: "tsdocker command is not available",
1932
+ fix: "Install @git.zone/tsdocker globally or make it available on PATH.",
1933
+ });
1934
+ }
1935
+ } catch (error) {
1936
+ findings.push({
1937
+ level: "error",
1938
+ message: "Could not execute tsdocker",
1939
+ fix: error instanceof Error ? error.message : String(error),
1940
+ });
1941
+ }
1942
+ }
1943
+
1746
1944
  async function validateDetectedProjectType(
1747
1945
  cliConfig: Record<string, any>,
1748
1946
  findings: IDoctorFinding[],
@@ -107,7 +107,7 @@ export const run = async (argvArg: any) => {
107
107
  npmResults.push(...(await runNpmTarget(smartshellInstance, workflow)));
108
108
  }
109
109
  if (workflow.targets.includes("docker")) {
110
- dockerResults.push(...(await runDockerTarget(smartshellInstance, workflow, newVersion)));
110
+ dockerResults.push(...(await runDockerTarget(smartshellInstance, workflow)));
111
111
  }
112
112
 
113
113
  printReleaseSummary(newVersion, gitResults, npmResults, dockerResults);
@@ -262,31 +262,43 @@ async function runNpmTarget(
262
262
  async function runDockerTarget(
263
263
  smartshellInstance: plugins.smartshell.Smartshell,
264
264
  workflow: IResolvedReleaseWorkflow,
265
- newVersion: string,
266
265
  ): Promise<ITargetResult[]> {
267
266
  if (!workflow.dockerEnabled) {
268
267
  return [{ target: "docker", status: "skipped", message: "disabled" }];
269
268
  }
270
- if (workflow.dockerImages.length === 0) {
271
- return [{ target: "docker", status: "failed", message: "no images configured" }];
272
- }
273
269
 
274
- const results: ITargetResult[] = [];
275
- for (const imageTemplate of workflow.dockerImages) {
276
- const image = imageTemplate.replaceAll("{{version}}", newVersion);
277
- const buildResult = await smartshellInstance.exec(`docker build -t ${shellQuote(image)} .`);
278
- if (buildResult.exitCode !== 0) {
279
- results.push({ target: image, status: "failed", message: "docker build failed" });
280
- continue;
281
- }
282
- const pushResult = await smartshellInstance.exec(`docker push ${shellQuote(image)}`);
283
- results.push({
284
- target: image,
285
- status: pushResult.exitCode === 0 ? "success" : "failed",
286
- message: pushResult.exitCode === 0 ? undefined : "docker push failed",
287
- });
270
+ const command = buildTsdockerPushCommand(workflow);
271
+ const result = await smartshellInstance.exec(command);
272
+ const output = `${result.stdout || ""}\n${(result as any).stderr || ""}\n${(result as any).combinedOutput || ""}`;
273
+ return [{
274
+ target: workflow.dockerPatterns.length > 0
275
+ ? `tsdocker:${workflow.dockerPatterns.join(",")}`
276
+ : "tsdocker",
277
+ status: result.exitCode === 0 ? "success" : "failed",
278
+ message: result.exitCode === 0 ? undefined : firstMeaningfulLine(output),
279
+ }];
280
+ }
281
+
282
+ function buildTsdockerPushCommand(workflow: IResolvedReleaseWorkflow): string {
283
+ const commandParts = ["tsdocker", "push"];
284
+ if (workflow.dockerNoBuild) {
285
+ commandParts.push("--no-build");
288
286
  }
289
- return results;
287
+ if (workflow.dockerCached) {
288
+ commandParts.push("--cached");
289
+ }
290
+ if (workflow.dockerParallel === true) {
291
+ commandParts.push("--parallel");
292
+ } else if (typeof workflow.dockerParallel === "number" && Number.isFinite(workflow.dockerParallel) && workflow.dockerParallel > 0) {
293
+ commandParts.push(`--parallel=${Math.floor(workflow.dockerParallel)}`);
294
+ }
295
+ if (workflow.dockerContext) {
296
+ commandParts.push(`--context=${shellQuote(workflow.dockerContext)}`);
297
+ }
298
+ for (const pattern of workflow.dockerPatterns) {
299
+ commandParts.push(shellQuote(pattern));
300
+ }
301
+ return commandParts.join(" ");
290
302
  }
291
303
 
292
304
  function isAlreadyPublishedOutput(output: string): boolean {
@@ -315,11 +327,22 @@ function printReleasePlan(workflow: IResolvedReleaseWorkflow): void {
315
327
  console.log(`npm registries: ${workflow.npmRegistries.length > 0 ? workflow.npmRegistries.join(", ") : "none"}`);
316
328
  }
317
329
  if (workflow.targets.includes("docker")) {
318
- console.log(`docker images: ${workflow.dockerImages.length > 0 ? workflow.dockerImages.join(", ") : "none"}`);
330
+ console.log(`docker engine: ${workflow.dockerEngine}`);
331
+ console.log(`docker patterns: ${workflow.dockerPatterns.length > 0 ? workflow.dockerPatterns.join(", ") : "all Dockerfiles"}`);
332
+ console.log(`docker options: ${formatDockerOptions(workflow)}`);
319
333
  }
320
334
  console.log("");
321
335
  }
322
336
 
337
+ function formatDockerOptions(workflow: IResolvedReleaseWorkflow): string {
338
+ const options: string[] = [];
339
+ if (workflow.dockerCached) options.push("cached");
340
+ if (workflow.dockerParallel) options.push(`parallel=${workflow.dockerParallel === true ? "true" : workflow.dockerParallel}`);
341
+ if (workflow.dockerNoBuild) options.push("no-build");
342
+ if (workflow.dockerContext) options.push(`context=${workflow.dockerContext}`);
343
+ return options.length > 0 ? options.join(", ") : "default";
344
+ }
345
+
323
346
  function printReleaseSummary(
324
347
  newVersion: string,
325
348
  gitResults: ITargetResult[],
@@ -365,7 +388,7 @@ export function showHelp(mode?: ICliMode): void {
365
388
  { flag: "-p, --push", description: "Enable the git release target" },
366
389
  { flag: "--target <names>", description: "Release only selected targets: git,npm,docker" },
367
390
  { flag: "--npm", description: "Enable the npm release target" },
368
- { flag: "--docker", description: "Enable the Docker release target" },
391
+ { flag: "--docker", description: "Enable the tsdocker release target" },
369
392
  { flag: "--no-publish", description: "Run release core and git target only" },
370
393
  { flag: "--plan", description: "Show resolved workflow without mutating files" },
371
394
  ],
@@ -385,7 +408,7 @@ export function showHelp(mode?: ICliMode): void {
385
408
  console.log(" -p, --push Enable the git release target");
386
409
  console.log(" --target <names> Release only selected targets: git,npm,docker");
387
410
  console.log(" --npm Enable the npm release target");
388
- console.log(" --docker Enable the Docker release target");
411
+ console.log(" --docker Enable the tsdocker release target");
389
412
  console.log(" --no-publish Run release core and git target only");
390
413
  console.log(" --major|--minor|--patch Override inferred semver level");
391
414
  console.log(" --plan Show resolved workflow without mutating files");