@git.zone/cli 3.2.1 → 5.0.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.
Files changed (135) hide show
  1. package/.smartconfig.json +0 -1
  2. package/dist_ts/00_commitinfo_data.js +2 -2
  3. package/dist_ts/classes.gitzoneconfig.js +1 -1
  4. package/dist_ts/classes.project.js +1 -1
  5. package/dist_ts/gitzone.cli.js +1 -1
  6. package/dist_ts/gitzone.logging.js +1 -1
  7. package/dist_ts/helpers.changelog.js +1 -1
  8. package/dist_ts/helpers.climode.js +1 -1
  9. package/dist_ts/helpers.smartconfig.js +61 -11
  10. package/dist_ts/helpers.smartconfigmigrations.js +1 -1
  11. package/dist_ts/helpers.tsdocker.js +1 -1
  12. package/dist_ts/helpers.workflow.d.ts +0 -2
  13. package/dist_ts/helpers.workflow.js +23 -12
  14. package/dist_ts/index.js +1 -1
  15. package/dist_ts/mod_audit/index.js +1 -1
  16. package/dist_ts/mod_commit/index.js +1 -1
  17. package/dist_ts/mod_commit/mod.helpers.d.ts +3 -3
  18. package/dist_ts/mod_commit/mod.helpers.js +10 -10
  19. package/dist_ts/mod_commit/mod.plugins.js +1 -1
  20. package/dist_ts/mod_commit/mod.ui.js +1 -1
  21. package/dist_ts/mod_config/classes.commitconfig.js +1 -1
  22. package/dist_ts/mod_config/classes.releaseconfig.js +1 -1
  23. package/dist_ts/mod_config/index.js +23 -24
  24. package/dist_ts/mod_config/mod.plugins.js +1 -1
  25. package/dist_ts/mod_deprecate/index.js +1 -1
  26. package/dist_ts/mod_deprecate/mod.plugins.js +1 -1
  27. package/dist_ts/mod_docker/classes.dockerpruner.js +7 -1
  28. package/dist_ts/mod_docker/index.js +1 -1
  29. package/dist_ts/mod_docker/mod.plugins.js +1 -1
  30. package/dist_ts/mod_format/classes.baseformatter.js +1 -1
  31. package/dist_ts/mod_format/classes.diffreporter.js +1 -1
  32. package/dist_ts/mod_format/classes.formatcontext.js +1 -1
  33. package/dist_ts/mod_format/classes.formatplanner.js +1 -1
  34. package/dist_ts/mod_format/classes.formatstats.js +1 -1
  35. package/dist_ts/mod_format/formatters/assets.formatter.js +1 -1
  36. package/dist_ts/mod_format/formatters/cleanup.formatter.js +1 -1
  37. package/dist_ts/mod_format/formatters/copy.formatter.js +1 -1
  38. package/dist_ts/mod_format/formatters/gitignore.formatter.js +1 -1
  39. package/dist_ts/mod_format/formatters/license.formatter.js +1 -1
  40. package/dist_ts/mod_format/formatters/packagejson.formatter.js +1 -1
  41. package/dist_ts/mod_format/formatters/prettier.formatter.js +1 -1
  42. package/dist_ts/mod_format/formatters/readme.formatter.js +1 -1
  43. package/dist_ts/mod_format/formatters/smartconfig.formatter.js +1 -1
  44. package/dist_ts/mod_format/formatters/templates.formatter.js +1 -1
  45. package/dist_ts/mod_format/formatters/tsconfig.formatter.js +1 -1
  46. package/dist_ts/mod_format/index.js +1 -1
  47. package/dist_ts/mod_format/interfaces.format.js +1 -1
  48. package/dist_ts/mod_format/mod.plugins.js +1 -1
  49. package/dist_ts/mod_helpers/index.js +1 -1
  50. package/dist_ts/mod_helpers/mod.plugins.js +1 -1
  51. package/dist_ts/mod_meta/index.js +1 -1
  52. package/dist_ts/mod_meta/meta.classes.meta.js +1 -1
  53. package/dist_ts/mod_meta/meta.interfaces.js +1 -1
  54. package/dist_ts/mod_meta/meta.plugins.js +1 -1
  55. package/dist_ts/mod_open/index.js +1 -1
  56. package/dist_ts/mod_open/mod.plugins.js +1 -1
  57. package/dist_ts/mod_release/helpers.releasebranch.d.ts +46 -0
  58. package/dist_ts/mod_release/helpers.releasebranch.js +579 -0
  59. package/dist_ts/mod_release/index.d.ts +10 -0
  60. package/dist_ts/mod_release/index.js +300 -92
  61. package/dist_ts/mod_release/mod.plugins.js +1 -1
  62. package/dist_ts/mod_services/classes.dockercontainer.d.ts +50 -3
  63. package/dist_ts/mod_services/classes.dockercontainer.js +646 -84
  64. package/dist_ts/mod_services/classes.globalregistry.d.ts +10 -24
  65. package/dist_ts/mod_services/classes.globalregistry.js +210 -62
  66. package/dist_ts/mod_services/classes.globalregistrystore.d.ts +48 -0
  67. package/dist_ts/mod_services/classes.globalregistrystore.js +156 -0
  68. package/dist_ts/mod_services/classes.interprocesslock.d.ts +21 -0
  69. package/dist_ts/mod_services/classes.interprocesslock.js +141 -0
  70. package/dist_ts/mod_services/classes.serviceconfiguration.d.ts +15 -12
  71. package/dist_ts/mod_services/classes.serviceconfiguration.js +188 -91
  72. package/dist_ts/mod_services/classes.servicedatamarker.d.ts +32 -7
  73. package/dist_ts/mod_services/classes.servicedatamarker.js +145 -49
  74. package/dist_ts/mod_services/classes.servicemanager.d.ts +70 -28
  75. package/dist_ts/mod_services/classes.servicemanager.js +995 -421
  76. package/dist_ts/mod_services/classes.servicenames.d.ts +8 -0
  77. package/dist_ts/mod_services/classes.servicenames.js +54 -0
  78. package/dist_ts/mod_services/classes.serviceoptions.js +1 -1
  79. package/dist_ts/mod_services/classes.servicepruner.d.ts +3 -0
  80. package/dist_ts/mod_services/classes.servicepruner.js +208 -46
  81. package/dist_ts/mod_services/helpers.js +1 -1
  82. package/dist_ts/mod_services/index.d.ts +2 -0
  83. package/dist_ts/mod_services/index.js +81 -58
  84. package/dist_ts/mod_services/mod.plugins.js +1 -1
  85. package/dist_ts/mod_standard/index.js +2 -2
  86. package/dist_ts/mod_standard/mod.plugins.js +1 -1
  87. package/dist_ts/mod_start/index.js +1 -1
  88. package/dist_ts/mod_start/mod.plugins.js +1 -1
  89. package/dist_ts/mod_template/index.js +1 -1
  90. package/dist_ts/mod_template/mod.plugins.js +1 -1
  91. package/dist_ts/mod_tools/classes.packagemanager.js +1 -1
  92. package/dist_ts/mod_tools/index.js +1 -1
  93. package/dist_ts/mod_tools/mod.plugins.js +1 -1
  94. package/dist_ts/paths.js +1 -1
  95. package/dist_ts/plugins.d.ts +3 -1
  96. package/dist_ts/plugins.js +4 -2
  97. package/dist_ts_migration/001.service-selection.d.ts +9 -0
  98. package/dist_ts_migration/001.service-selection.js +51 -0
  99. package/dist_ts_migration/002.runtime-config.d.ts +19 -0
  100. package/dist_ts_migration/002.runtime-config.js +170 -0
  101. package/dist_ts_migration/003.service-data-marker.d.ts +9 -0
  102. package/dist_ts_migration/003.service-data-marker.js +156 -0
  103. package/dist_ts_migration/004.global-registry.d.ts +13 -0
  104. package/dist_ts_migration/004.global-registry.js +165 -0
  105. package/dist_ts_migration/index.d.ts +11 -0
  106. package/dist_ts_migration/index.js +59 -0
  107. package/package.json +6 -3
  108. package/readme.hints.md +102 -25
  109. package/readme.md +144 -41
  110. package/readme.plan.md +29 -16
  111. package/ts/00_commitinfo_data.ts +1 -1
  112. package/ts/helpers.smartconfig.ts +63 -10
  113. package/ts/helpers.workflow.ts +41 -12
  114. package/ts/mod_commit/mod.helpers.ts +12 -8
  115. package/ts/mod_config/index.ts +22 -23
  116. package/ts/mod_docker/classes.dockerpruner.ts +6 -0
  117. package/ts/mod_release/helpers.releasebranch.ts +1282 -0
  118. package/ts/mod_release/index.ts +608 -129
  119. package/ts/mod_services/classes.dockercontainer.ts +912 -96
  120. package/ts/mod_services/classes.globalregistry.ts +290 -89
  121. package/ts/mod_services/classes.globalregistrystore.ts +279 -0
  122. package/ts/mod_services/classes.interprocesslock.ts +189 -0
  123. package/ts/mod_services/classes.serviceconfiguration.ts +215 -108
  124. package/ts/mod_services/classes.servicedatamarker.ts +210 -52
  125. package/ts/mod_services/classes.servicemanager.ts +1332 -512
  126. package/ts/mod_services/classes.servicenames.ts +76 -0
  127. package/ts/mod_services/classes.servicepruner.ts +310 -52
  128. package/ts/mod_services/index.ts +89 -62
  129. package/ts/mod_standard/index.ts +1 -1
  130. package/ts/plugins.ts +4 -0
  131. package/ts_migration/001.service-selection.ts +77 -0
  132. package/ts_migration/002.runtime-config.ts +218 -0
  133. package/ts_migration/003.service-data-marker.ts +216 -0
  134. package/ts_migration/004.global-registry.ts +272 -0
  135. package/ts_migration/index.ts +83 -0
@@ -18,6 +18,16 @@ import {
18
18
  } from "../helpers.workflow.js";
19
19
  import * as commitHelpers from "../mod_commit/mod.helpers.js";
20
20
  import { hasInstalledProjectTsdockerBinary } from "../helpers.tsdocker.js";
21
+ import {
22
+ inspectReleaseBranch,
23
+ integrateReleaseBranch,
24
+ prepareReleaseIntegration,
25
+ revalidatePreparedReleaseBranch,
26
+ revalidateReleasePublicationState,
27
+ releaseGitEnv,
28
+ resolveReleaseMergeFlag,
29
+ type IReleaseBranchContext,
30
+ } from "./helpers.releasebranch.js";
21
31
 
22
32
  type TTargetStatus = "success" | "already-published" | "skipped" | "failed";
23
33
 
@@ -39,24 +49,49 @@ export const run = async (argvArg: any) => {
39
49
  if (mode.json) {
40
50
  printJson({
41
51
  ok: false,
42
- error: "JSON output is not supported for mutating release workflows yet. Use `gitzone release --plan` for a human-readable plan.",
52
+ error:
53
+ "JSON output is not supported for mutating release workflows yet. Use `gitzone release --plan` for a human-readable plan.",
43
54
  });
44
55
  return;
45
56
  }
46
57
 
47
- const workflow = await resolveReleaseWorkflow(argvArg);
48
- printReleasePlan(workflow);
49
- if (workflow.confirmation === "plan") {
50
- return;
51
- }
52
-
53
58
  const smartshellInstance = new plugins.smartshell.Smartshell({
54
59
  executor: "bash",
55
60
  sourceFilePaths: [],
56
61
  });
62
+ let mergeRequested: boolean;
63
+ try {
64
+ mergeRequested = resolveReleaseMergeFlag(argvArg);
65
+ } catch (error) {
66
+ logger.log("error", error instanceof Error ? error.message : String(error));
67
+ process.exit(1);
68
+ return;
69
+ }
70
+ const workflow = await resolveReleaseWorkflow(argvArg);
71
+ let branchContext: IReleaseBranchContext;
72
+ try {
73
+ branchContext = await inspectReleaseBranch({
74
+ smartshell: smartshellInstance,
75
+ cwd: paths.cwd,
76
+ gitRemote: workflow.gitRemote,
77
+ gitTargetActive: workflow.targets.includes("git"),
78
+ pushBranch: workflow.pushBranch,
79
+ pushTags: workflow.pushTags,
80
+ mergeRequested,
81
+ planMode: workflow.confirmation === "plan",
82
+ });
83
+ } catch (error) {
84
+ logger.log("error", error instanceof Error ? error.message : String(error));
85
+ process.exit(1);
86
+ return;
87
+ }
88
+ printReleasePlan(workflow, branchContext);
89
+ if (workflow.confirmation === "plan") {
90
+ return;
91
+ }
57
92
 
58
93
  const pending = await readPendingChangelog(
59
- plugins.path.join(paths.cwd, workflow.changelogFile),
94
+ plugins.path.join(branchContext.sourceCwd, workflow.changelogFile),
60
95
  workflow.changelogPendingSection,
61
96
  );
62
97
  if (pending.isEmpty && !argvArg["allow-empty"] && !argvArg.allowEmpty) {
@@ -65,18 +100,29 @@ export const run = async (argvArg: any) => {
65
100
  }
66
101
 
67
102
  const versionType = resolveVersionType(argvArg, pending.block);
68
- const projectType = await commitHelpers.detectProjectType();
69
- const currentVersion = await commitHelpers.readCurrentVersion(projectType);
70
- const plannedVersion = commitHelpers.calculateNewVersion(currentVersion, versionType);
103
+ const projectType = await commitHelpers.detectProjectType(
104
+ branchContext.sourceCwd,
105
+ );
106
+ const currentVersion = await commitHelpers.readCurrentVersion(
107
+ projectType,
108
+ branchContext.sourceCwd,
109
+ );
110
+ const plannedVersion = commitHelpers.calculateNewVersion(
111
+ currentVersion,
112
+ versionType,
113
+ );
71
114
 
72
115
  if (workflow.confirmation === "prompt") {
73
116
  if (!mode.interactive) {
74
- throw new Error("Release confirmation requires an interactive terminal. Use `-y` or set release.confirmation to `auto`.");
117
+ throw new Error(
118
+ "Release confirmation requires an interactive terminal. Use `-y` or set release.confirmation to `auto`.",
119
+ );
75
120
  }
76
- const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
77
- `Release v${plannedVersion} (${versionType}) now?`,
78
- true,
79
- );
121
+ const confirmed =
122
+ await plugins.smartinteract.SmartInteract.getCliConfirmation(
123
+ `Release v${plannedVersion} (${versionType}) now?`,
124
+ true,
125
+ );
80
126
  if (!confirmed) {
81
127
  logger.log("info", "Release cancelled.");
82
128
  return;
@@ -88,40 +134,171 @@ export const run = async (argvArg: any) => {
88
134
  const npmResults: ITargetResult[] = [];
89
135
  const dockerResults: ITargetResult[] = [];
90
136
 
91
- if (workflow.requireCleanTree) {
92
- await verifyCleanTree(smartshellInstance, "Working tree is not clean. Commit or stash changes before releasing.");
137
+ try {
138
+ branchContext = await prepareReleaseIntegration(
139
+ smartshellInstance,
140
+ branchContext,
141
+ );
142
+ } catch (error) {
143
+ logger.log("error", error instanceof Error ? error.message : String(error));
144
+ process.exit(1);
145
+ return;
93
146
  }
94
147
  if (workflow.runTests) {
95
- await runCommandStep(smartshellInstance, "Running tests", workflow.testCommand);
148
+ await runCommandStep(
149
+ smartshellInstance,
150
+ branchContext.sourceCwd,
151
+ "Running tests",
152
+ workflow.testCommand,
153
+ );
96
154
  }
97
155
 
98
- newVersion = await runVersionStep(projectType, versionType);
99
- await runChangelogStep(workflow, newVersion);
100
- await runReleaseCommitStep(smartshellInstance, newVersion);
101
- await runTagStep(smartshellInstance, newVersion);
156
+ let releaseCwd = branchContext.sourceCwd;
157
+ let releasePushUrl = branchContext.pushUrl;
158
+ let expectedRemoteMainOid = branchContext.remoteMainOid;
159
+ if (branchContext.sourceBranch === "main") {
160
+ try {
161
+ await revalidatePreparedReleaseBranch(smartshellInstance, branchContext);
162
+ } catch (error) {
163
+ logger.log(
164
+ "error",
165
+ error instanceof Error ? error.message : String(error),
166
+ );
167
+ process.exit(1);
168
+ return;
169
+ }
170
+ } else {
171
+ try {
172
+ const integration = await integrateReleaseBranch(
173
+ smartshellInstance,
174
+ branchContext,
175
+ );
176
+ releaseCwd = integration.releaseCwd;
177
+ releasePushUrl = integration.pushUrl;
178
+ expectedRemoteMainOid = integration.expectedRemoteMainOid;
179
+ } catch (error) {
180
+ logger.log(
181
+ "error",
182
+ error instanceof Error ? error.message : String(error),
183
+ );
184
+ process.exit(1);
185
+ return;
186
+ }
187
+ }
188
+
189
+ newVersion = await runVersionStep(projectType, versionType, releaseCwd);
190
+ await runChangelogStep(workflow, newVersion, releaseCwd);
191
+ const releaseCommitOid = await runReleaseCommitStep(
192
+ smartshellInstance,
193
+ newVersion,
194
+ releaseCwd,
195
+ );
196
+ await runTagStep(
197
+ smartshellInstance,
198
+ newVersion,
199
+ releaseCommitOid,
200
+ releaseCwd,
201
+ );
202
+ const releaseRefs = await captureReleaseRefs(
203
+ smartshellInstance,
204
+ releaseCwd,
205
+ newVersion,
206
+ releaseCommitOid,
207
+ );
102
208
 
103
209
  if (workflow.runBuild) {
104
- await runCommandStep(smartshellInstance, "Running release build", workflow.buildCommand);
105
- await verifyCleanTree(smartshellInstance, "Build produced uncommitted changes. Aborting release.");
210
+ await runCommandStep(
211
+ smartshellInstance,
212
+ releaseCwd,
213
+ "Running release build",
214
+ workflow.buildCommand,
215
+ );
216
+ await verifyCleanTree(
217
+ smartshellInstance,
218
+ releaseCwd,
219
+ "Build produced uncommitted changes. Aborting release.",
220
+ );
221
+ }
222
+
223
+ const ensurePublicationState = async (): Promise<boolean> => {
224
+ try {
225
+ await verifyReleaseRefs(
226
+ smartshellInstance,
227
+ releaseCwd,
228
+ newVersion,
229
+ releaseRefs,
230
+ );
231
+ await revalidateReleasePublicationState(
232
+ smartshellInstance,
233
+ branchContext,
234
+ releaseRefs.mainOid,
235
+ expectedRemoteMainOid,
236
+ );
237
+ return true;
238
+ } catch (error) {
239
+ logger.log(
240
+ "error",
241
+ error instanceof Error ? error.message : String(error),
242
+ );
243
+ process.exit(1);
244
+ return false;
245
+ }
246
+ };
247
+
248
+ if (!(await ensurePublicationState())) {
249
+ return;
106
250
  }
107
251
 
108
252
  if (workflow.targets.includes("git")) {
109
- gitResults.push(...(await runGitTarget(smartshellInstance, workflow)));
253
+ gitResults.push(
254
+ ...(await runGitTarget(
255
+ smartshellInstance,
256
+ workflow,
257
+ releaseCwd,
258
+ newVersion,
259
+ releaseRefs,
260
+ releasePushUrl,
261
+ expectedRemoteMainOid,
262
+ )),
263
+ );
264
+ if (gitResults.some((result) => result.status === "failed")) {
265
+ printReleaseSummary(newVersion, gitResults, npmResults, dockerResults);
266
+ process.exit(1);
267
+ return;
268
+ }
269
+ expectedRemoteMainOid = releaseRefs.mainOid;
110
270
  }
111
271
  if (workflow.targets.includes("npm")) {
112
- npmResults.push(...(await runNpmTarget(smartshellInstance, workflow)));
272
+ if (!(await ensurePublicationState())) {
273
+ return;
274
+ }
275
+ npmResults.push(
276
+ ...(await runNpmTarget(smartshellInstance, workflow, releaseCwd)),
277
+ );
113
278
  }
114
279
  if (workflow.targets.includes("docker")) {
115
- dockerResults.push(...(await runDockerTarget(smartshellInstance, workflow)));
280
+ if (!(await ensurePublicationState())) {
281
+ return;
282
+ }
283
+ dockerResults.push(
284
+ ...(await runDockerTarget(smartshellInstance, workflow, releaseCwd)),
285
+ );
116
286
  }
117
287
 
118
288
  printReleaseSummary(newVersion, gitResults, npmResults, dockerResults);
119
- if ([...gitResults, ...npmResults, ...dockerResults].some((result) => result.status === "failed")) {
289
+ if (
290
+ [...gitResults, ...npmResults, ...dockerResults].some(
291
+ (result) => result.status === "failed",
292
+ )
293
+ ) {
120
294
  process.exit(1);
121
295
  }
122
296
  };
123
297
 
124
- function resolveVersionType(argvArg: any, pendingBlock: string): commitHelpers.VersionType {
298
+ function resolveVersionType(
299
+ argvArg: any,
300
+ pendingBlock: string,
301
+ ): commitHelpers.VersionType {
125
302
  if (argvArg.major) return "major";
126
303
  if (argvArg.minor) return "minor";
127
304
  if (argvArg.patch) return "patch";
@@ -130,11 +307,12 @@ function resolveVersionType(argvArg: any, pendingBlock: string): commitHelpers.V
130
307
 
131
308
  async function runCommandStep(
132
309
  smartshellInstance: plugins.smartshell.Smartshell,
310
+ cwdArg: string,
133
311
  label: string,
134
312
  command: string,
135
313
  ): Promise<void> {
136
314
  console.log(`\n${label}`);
137
- const result = await smartshellInstance.exec(command);
315
+ const result = await smartshellInstance.exec(command, { cwd: cwdArg });
138
316
  if (result.exitCode !== 0) {
139
317
  logger.log("error", `${label} failed. Aborting release.`);
140
318
  process.exit(1);
@@ -144,9 +322,14 @@ async function runCommandStep(
144
322
 
145
323
  async function verifyCleanTree(
146
324
  smartshellInstance: plugins.smartshell.Smartshell,
325
+ cwdArg: string,
147
326
  errorMessage: string,
148
327
  ): Promise<void> {
149
- const statusResult = await smartshellInstance.exec("git status --porcelain");
328
+ const statusResult = await smartshellInstance.execSpawn(
329
+ "git",
330
+ ["status", "--porcelain"],
331
+ { cwd: cwdArg, env: releaseGitEnv },
332
+ );
150
333
  if (statusResult.stdout.trim() !== "") {
151
334
  logger.log("error", errorMessage);
152
335
  console.log(statusResult.stdout);
@@ -157,24 +340,36 @@ async function verifyCleanTree(
157
340
  async function runVersionStep(
158
341
  projectType: commitHelpers.ProjectType,
159
342
  versionType: commitHelpers.VersionType,
343
+ cwdArg: string,
160
344
  ): Promise<string> {
161
- const currentVersion = await commitHelpers.readCurrentVersion(projectType);
162
- const newVersion = commitHelpers.calculateNewVersion(currentVersion, versionType);
345
+ const currentVersion = await commitHelpers.readCurrentVersion(
346
+ projectType,
347
+ cwdArg,
348
+ );
349
+ const newVersion = commitHelpers.calculateNewVersion(
350
+ currentVersion,
351
+ versionType,
352
+ );
163
353
  logger.log("info", `Bumping version: ${currentVersion} -> ${newVersion}`);
164
354
 
165
- const commitInfo = new plugins.commitinfo.CommitInfo(paths.cwd, versionType);
355
+ const commitInfo = new plugins.commitinfo.CommitInfo(cwdArg, versionType);
166
356
  await commitInfo.writeIntoPotentialDirs();
167
- await commitHelpers.updateProjectVersionFiles(projectType, newVersion);
357
+ await commitHelpers.updateProjectVersionFiles(
358
+ projectType,
359
+ newVersion,
360
+ cwdArg,
361
+ );
168
362
  return newVersion;
169
363
  }
170
364
 
171
365
  async function runChangelogStep(
172
366
  workflow: IResolvedReleaseWorkflow,
173
367
  newVersion: string,
368
+ cwdArg: string,
174
369
  ): Promise<void> {
175
370
  const dateString = new Date().toISOString().slice(0, 10);
176
371
  await movePendingToVersion(
177
- plugins.path.join(paths.cwd, workflow.changelogFile),
372
+ plugins.path.join(cwdArg, workflow.changelogFile),
178
373
  workflow.changelogPendingSection,
179
374
  workflow.changelogVersionHeading,
180
375
  newVersion,
@@ -185,80 +380,261 @@ async function runChangelogStep(
185
380
  async function runReleaseCommitStep(
186
381
  smartshellInstance: plugins.smartshell.Smartshell,
187
382
  newVersion: string,
188
- ): Promise<void> {
189
- await smartshellInstance.exec("git add -A");
190
- const result = await smartshellInstance.exec(`git commit -m ${shellQuote(`v${newVersion}`)}`);
383
+ cwdArg: string,
384
+ ): Promise<string> {
385
+ const addResult = await smartshellInstance.execSpawn("git", ["add", "-A"], {
386
+ cwd: cwdArg,
387
+ env: releaseGitEnv,
388
+ });
389
+ if (addResult.exitCode !== 0) {
390
+ throw new Error("Staging release metadata failed.");
391
+ }
392
+ const result = await smartshellInstance.execSpawn(
393
+ "git",
394
+ ["commit", "-m", `v${newVersion}`],
395
+ { cwd: cwdArg, env: releaseGitEnv },
396
+ );
191
397
  if (result.exitCode !== 0) {
192
- logger.log("error", "Release commit failed.");
193
- process.exit(1);
398
+ throw new Error("Release commit failed.");
399
+ }
400
+ const branchResult = await smartshellInstance.execSpawn(
401
+ "git",
402
+ ["symbolic-ref", "--quiet", "HEAD"],
403
+ { cwd: cwdArg, env: releaseGitEnv, silent: true },
404
+ );
405
+ if (
406
+ branchResult.exitCode !== 0 ||
407
+ branchResult.stdout.trim() !== "refs/heads/main"
408
+ ) {
409
+ throw new Error("The release commit was not created on main.");
410
+ }
411
+ const headOid = await resolveGitObjectOid(
412
+ smartshellInstance,
413
+ cwdArg,
414
+ "HEAD^{commit}",
415
+ );
416
+ const mainOid = await resolveGitObjectOid(
417
+ smartshellInstance,
418
+ cwdArg,
419
+ "refs/heads/main^{commit}",
420
+ );
421
+ if (headOid !== mainOid) {
422
+ throw new Error("HEAD and main diverged after creating the release commit.");
194
423
  }
424
+ return mainOid;
195
425
  }
196
426
 
197
427
  async function runTagStep(
198
428
  smartshellInstance: plugins.smartshell.Smartshell,
199
429
  newVersion: string,
430
+ releaseCommitOidArg: string,
431
+ cwdArg: string,
200
432
  ): Promise<void> {
201
- const result = await smartshellInstance.exec(`git tag v${newVersion} -m ${shellQuote(`v${newVersion}`)}`);
433
+ const result = await smartshellInstance.execSpawn(
434
+ "git",
435
+ [
436
+ "tag",
437
+ "-a",
438
+ `v${newVersion}`,
439
+ "-m",
440
+ `v${newVersion}`,
441
+ releaseCommitOidArg,
442
+ ],
443
+ { cwd: cwdArg, env: releaseGitEnv },
444
+ );
202
445
  if (result.exitCode !== 0) {
203
- logger.log("error", "Release tag failed.");
204
- process.exit(1);
446
+ throw new Error("Release tag failed.");
205
447
  }
206
448
  }
207
449
 
450
+ interface IReleaseRefs {
451
+ mainOid: string;
452
+ tagOid: string;
453
+ }
454
+
455
+ const resolveGitObjectOid = async (
456
+ smartshellInstanceArg: plugins.smartshell.Smartshell,
457
+ cwdArg: string,
458
+ revisionArg: string,
459
+ ): Promise<string> => {
460
+ const result = await smartshellInstanceArg.execSpawn(
461
+ "git",
462
+ ["rev-parse", "--verify", revisionArg],
463
+ { cwd: cwdArg, env: releaseGitEnv, silent: true },
464
+ );
465
+ const oid = result.stdout.trim();
466
+ if (result.exitCode !== 0 || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(oid)) {
467
+ throw new Error(`Unable to resolve release Git object ${revisionArg}.`);
468
+ }
469
+ return oid;
470
+ };
471
+
472
+ const captureReleaseRefs = async (
473
+ smartshellInstanceArg: plugins.smartshell.Smartshell,
474
+ cwdArg: string,
475
+ newVersionArg: string,
476
+ expectedMainOidArg?: string,
477
+ ): Promise<IReleaseRefs> => {
478
+ const mainOid = await resolveGitObjectOid(
479
+ smartshellInstanceArg,
480
+ cwdArg,
481
+ "refs/heads/main^{commit}",
482
+ );
483
+ const tagOid = await resolveGitObjectOid(
484
+ smartshellInstanceArg,
485
+ cwdArg,
486
+ `refs/tags/v${newVersionArg}^{tag}`,
487
+ );
488
+ const tagCommitOid = await resolveGitObjectOid(
489
+ smartshellInstanceArg,
490
+ cwdArg,
491
+ `refs/tags/v${newVersionArg}^{commit}`,
492
+ );
493
+ if (
494
+ (expectedMainOidArg && mainOid !== expectedMainOidArg) ||
495
+ tagCommitOid !== mainOid
496
+ ) {
497
+ throw new Error(
498
+ "The release tag is not bound to the captured main release commit.",
499
+ );
500
+ }
501
+ return { mainOid, tagOid };
502
+ };
503
+
504
+ const verifyReleaseRefs = async (
505
+ smartshellInstanceArg: plugins.smartshell.Smartshell,
506
+ cwdArg: string,
507
+ newVersionArg: string,
508
+ expectedRefsArg: IReleaseRefs,
509
+ ): Promise<void> => {
510
+ const currentRefs = await captureReleaseRefs(
511
+ smartshellInstanceArg,
512
+ cwdArg,
513
+ newVersionArg,
514
+ expectedRefsArg.mainOid,
515
+ );
516
+ if (
517
+ currentRefs.mainOid !== expectedRefsArg.mainOid ||
518
+ currentRefs.tagOid !== expectedRefsArg.tagOid
519
+ ) {
520
+ throw new Error(
521
+ "Local release refs changed before publication. Inspect main and the release tag before retrying.",
522
+ );
523
+ }
524
+ };
525
+
208
526
  async function runGitTarget(
209
527
  smartshellInstance: plugins.smartshell.Smartshell,
210
528
  workflow: IResolvedReleaseWorkflow,
529
+ cwdArg: string,
530
+ newVersionArg: string,
531
+ releaseRefsArg: IReleaseRefs,
532
+ pushUrlArg?: string,
533
+ expectedRemoteMainOidArg?: string,
211
534
  ): Promise<ITargetResult[]> {
212
- const currentBranchResult = await smartshellInstance.exec("git branch --show-current");
213
- const currentBranch = currentBranchResult.stdout.trim() || "master";
214
- const commands: Array<{ target: string; command: string }> = [];
535
+ const destination = pushUrlArg || workflow.gitRemote;
536
+ const targets: string[] = [];
537
+ const refspecs: string[] = [];
215
538
  if (workflow.pushBranch) {
216
- commands.push({
217
- target: `${workflow.gitRemote}/${currentBranch}`,
218
- command: `git push ${workflow.gitRemote} ${currentBranch}`,
219
- });
539
+ targets.push(`${workflow.gitRemote}/main`);
540
+ refspecs.push("refs/heads/main:refs/heads/main");
220
541
  }
221
542
  if (workflow.pushTags) {
222
- commands.push({
223
- target: `${workflow.gitRemote}/tags`,
224
- command: `git push ${workflow.gitRemote} --tags`,
225
- });
543
+ targets.push(`${workflow.gitRemote}/v${newVersionArg}`);
544
+ refspecs.push(`refs/tags/v${newVersionArg}:refs/tags/v${newVersionArg}`);
545
+ }
546
+ if (refspecs.length === 0) {
547
+ return [];
226
548
  }
227
549
 
228
- const results: ITargetResult[] = [];
229
- for (const { target, command } of commands) {
230
- const result = await smartshellInstance.exec(command);
231
- results.push({
232
- target,
233
- status: result.exitCode === 0 ? "success" : "failed",
234
- message: result.exitCode === 0 ? undefined : "push failed",
235
- });
550
+ const args = buildReleaseGitPushArgs({
551
+ destination,
552
+ newVersion: newVersionArg,
553
+ pushBranch: workflow.pushBranch,
554
+ pushTags: workflow.pushTags,
555
+ mainOid: releaseRefsArg.mainOid,
556
+ tagOid: releaseRefsArg.tagOid,
557
+ expectedRemoteMainOid: expectedRemoteMainOidArg,
558
+ });
559
+ const result = await smartshellInstance.execSpawn("git", args, {
560
+ cwd: cwdArg,
561
+ env: releaseGitEnv,
562
+ });
563
+ return targets.map((target) => ({
564
+ target,
565
+ status: result.exitCode === 0 ? "success" : "failed",
566
+ message:
567
+ result.exitCode === 0
568
+ ? undefined
569
+ : firstMeaningfulLine(result.combinedOutput),
570
+ }));
571
+ }
572
+
573
+ export interface IBuildReleaseGitPushArgsOptions {
574
+ destination: string;
575
+ newVersion: string;
576
+ pushBranch: boolean;
577
+ pushTags: boolean;
578
+ mainOid: string;
579
+ tagOid: string;
580
+ expectedRemoteMainOid?: string;
581
+ }
582
+
583
+ export function buildReleaseGitPushArgs(
584
+ optionsArg: IBuildReleaseGitPushArgsOptions,
585
+ ): string[] {
586
+ const refspecs: string[] = [];
587
+ if (optionsArg.pushBranch) {
588
+ refspecs.push(`${optionsArg.mainOid}:refs/heads/main`);
236
589
  }
237
- return results;
590
+ if (optionsArg.pushTags) {
591
+ refspecs.push(`${optionsArg.tagOid}:refs/tags/v${optionsArg.newVersion}`);
592
+ }
593
+ const args = ["push", "--no-follow-tags"];
594
+ if (refspecs.length > 1) {
595
+ args.push("--atomic");
596
+ }
597
+ if (optionsArg.pushBranch && optionsArg.expectedRemoteMainOid) {
598
+ args.push(
599
+ `--force-with-lease=refs/heads/main:${optionsArg.expectedRemoteMainOid}`,
600
+ );
601
+ }
602
+ args.push(optionsArg.destination, ...refspecs);
603
+ return args;
238
604
  }
239
605
 
240
606
  async function runNpmTarget(
241
607
  smartshellInstance: plugins.smartshell.Smartshell,
242
608
  workflow: IResolvedReleaseWorkflow,
609
+ cwdArg: string,
243
610
  ): Promise<ITargetResult[]> {
244
611
  if (!workflow.npmEnabled) {
245
612
  return [{ target: "npm", status: "skipped", message: "disabled" }];
246
613
  }
247
614
  if (workflow.npmRegistries.length === 0) {
248
- return [{ target: "npm", status: "failed", message: "no registries configured" }];
615
+ return [
616
+ { target: "npm", status: "failed", message: "no registries configured" },
617
+ ];
249
618
  }
250
619
 
251
620
  const results: ITargetResult[] = [];
252
621
  for (const registry of workflow.npmRegistries) {
253
622
  const command = `pnpm publish --registry=${registry} --access=${workflow.npmAccessLevel}`;
254
- const result = await smartshellInstance.exec(command);
623
+ const result = await smartshellInstance.exec(command, { cwd: cwdArg });
255
624
  const output = result.combinedOutput;
256
625
  if (result.exitCode === 0) {
257
626
  results.push({ target: registry, status: "success" });
258
- } else if (isAlreadyPublishedOutput(output) && workflow.npmAlreadyPublished === "success") {
627
+ } else if (
628
+ isAlreadyPublishedOutput(output) &&
629
+ workflow.npmAlreadyPublished === "success"
630
+ ) {
259
631
  results.push({ target: registry, status: "already-published" });
260
632
  } else {
261
- results.push({ target: registry, status: "failed", message: firstMeaningfulLine(output) });
633
+ results.push({
634
+ target: registry,
635
+ status: "failed",
636
+ message: firstMeaningfulLine(output),
637
+ });
262
638
  }
263
639
  }
264
640
  return results;
@@ -267,6 +643,7 @@ async function runNpmTarget(
267
643
  async function runDockerTarget(
268
644
  smartshellInstance: plugins.smartshell.Smartshell,
269
645
  workflow: IResolvedReleaseWorkflow,
646
+ cwdArg: string,
270
647
  ): Promise<ITargetResult[]> {
271
648
  if (!workflow.dockerEnabled) {
272
649
  return [{ target: "docker", status: "skipped", message: "disabled" }];
@@ -275,24 +652,33 @@ async function runDockerTarget(
275
652
  let packageJson: unknown;
276
653
  try {
277
654
  packageJson = JSON.parse(
278
- await plugins.fs.readFile(plugins.path.join(paths.cwd, "package.json"), "utf8"),
655
+ await plugins.fs.readFile(
656
+ plugins.path.join(cwdArg, "package.json"),
657
+ "utf8",
658
+ ),
279
659
  );
280
660
  } catch {
281
661
  packageJson = undefined;
282
662
  }
283
663
  if (!getDeclaredTsdockerRange(packageJson)) {
284
- return [{
285
- target: "tsdocker",
286
- status: "failed",
287
- message: "Docker releases require @git.zone/tsdocker as a project dependency",
288
- }];
289
- }
290
- if (!(await hasInstalledProjectTsdockerBinary(paths.cwd))) {
291
- return [{
292
- target: "tsdocker",
293
- status: "failed",
294
- message: "Docker releases require the installed project-local tSDocker binary",
295
- }];
664
+ return [
665
+ {
666
+ target: "tsdocker",
667
+ status: "failed",
668
+ message:
669
+ "Docker releases require @git.zone/tsdocker as a project dependency",
670
+ },
671
+ ];
672
+ }
673
+ if (!(await hasInstalledProjectTsdockerBinary(cwdArg))) {
674
+ return [
675
+ {
676
+ target: "tsdocker",
677
+ status: "failed",
678
+ message:
679
+ "Docker releases require the installed project-local tSDocker binary",
680
+ },
681
+ ];
296
682
  }
297
683
 
298
684
  const minimumTsdockerVersion = getTsdockerMinimumVersion(
@@ -300,33 +686,43 @@ async function runDockerTarget(
300
686
  workflow.dockerTest,
301
687
  );
302
688
  if (minimumTsdockerVersion) {
303
- const versionResult = await smartshellInstance.exec("pnpm exec tsdocker --version");
689
+ const versionResult = await smartshellInstance.exec(
690
+ "pnpm exec tsdocker --version",
691
+ { cwd: cwdArg },
692
+ );
304
693
  if (
305
- versionResult.exitCode !== 0
306
- || !supportsTsdockerMinimumVersion(
694
+ versionResult.exitCode !== 0 ||
695
+ !supportsTsdockerMinimumVersion(
307
696
  versionResult.combinedOutput,
308
697
  minimumTsdockerVersion,
309
698
  )
310
699
  ) {
311
- const minimumVersion = formatTsdockerMinimumVersion(minimumTsdockerVersion);
312
- return [{
313
- target: "tsdocker",
314
- status: "failed",
315
- message: `Docker release options require project-local @git.zone/tsdocker >= ${minimumVersion}`,
316
- }];
700
+ const minimumVersion = formatTsdockerMinimumVersion(
701
+ minimumTsdockerVersion,
702
+ );
703
+ return [
704
+ {
705
+ target: "tsdocker",
706
+ status: "failed",
707
+ message: `Docker release options require project-local @git.zone/tsdocker >= ${minimumVersion}`,
708
+ },
709
+ ];
317
710
  }
318
711
  }
319
712
 
320
713
  const command = buildTsdockerPushCommand(workflow);
321
- const result = await smartshellInstance.exec(command);
714
+ const result = await smartshellInstance.exec(command, { cwd: cwdArg });
322
715
  const output = result.combinedOutput;
323
- return [{
324
- target: workflow.dockerPatterns.length > 0
325
- ? `tsdocker:${workflow.dockerPatterns.join(",")}`
326
- : "tsdocker",
327
- status: result.exitCode === 0 ? "success" : "failed",
328
- message: result.exitCode === 0 ? undefined : firstMeaningfulLine(output),
329
- }];
716
+ return [
717
+ {
718
+ target:
719
+ workflow.dockerPatterns.length > 0
720
+ ? `tsdocker:${workflow.dockerPatterns.join(",")}`
721
+ : "tsdocker",
722
+ status: result.exitCode === 0 ? "success" : "failed",
723
+ message: result.exitCode === 0 ? undefined : firstMeaningfulLine(output),
724
+ },
725
+ ];
330
726
  }
331
727
 
332
728
  type TDockerCommandWorkflow = Pick<
@@ -341,7 +737,9 @@ type TDockerCommandWorkflow = Pick<
341
737
  | "dockerPatterns"
342
738
  >;
343
739
 
344
- export function buildTsdockerPushCommand(workflow: TDockerCommandWorkflow): string {
740
+ export function buildTsdockerPushCommand(
741
+ workflow: TDockerCommandWorkflow,
742
+ ): string {
345
743
  const commandParts = ["pnpm", "exec", "tsdocker", "push"];
346
744
  for (const pattern of workflow.dockerPatterns) {
347
745
  commandParts.push(shellQuote(pattern));
@@ -350,7 +748,9 @@ export function buildTsdockerPushCommand(workflow: TDockerCommandWorkflow): stri
350
748
  commandParts.push(`--registry=${shellQuote(workflow.dockerRegistry)}`);
351
749
  }
352
750
  if (workflow.dockerBuildRegistries.length > 0) {
353
- commandParts.push(`--build-registries=${shellQuote(workflow.dockerBuildRegistries.join(","))}`);
751
+ commandParts.push(
752
+ `--build-registries=${shellQuote(workflow.dockerBuildRegistries.join(","))}`,
753
+ );
354
754
  }
355
755
  if (workflow.dockerTest) {
356
756
  commandParts.push("--test");
@@ -363,7 +763,11 @@ export function buildTsdockerPushCommand(workflow: TDockerCommandWorkflow): stri
363
763
  }
364
764
  if (workflow.dockerParallel === true) {
365
765
  commandParts.push("--parallel");
366
- } else if (typeof workflow.dockerParallel === "number" && Number.isFinite(workflow.dockerParallel) && workflow.dockerParallel > 0) {
766
+ } else if (
767
+ typeof workflow.dockerParallel === "number" &&
768
+ Number.isFinite(workflow.dockerParallel) &&
769
+ workflow.dockerParallel > 0
770
+ ) {
367
771
  commandParts.push(`--parallel=${Math.floor(workflow.dockerParallel)}`);
368
772
  }
369
773
  if (workflow.dockerContext) {
@@ -373,36 +777,64 @@ export function buildTsdockerPushCommand(workflow: TDockerCommandWorkflow): stri
373
777
  }
374
778
 
375
779
  function isAlreadyPublishedOutput(output: string): boolean {
376
- return /previously published versions|cannot publish over|already exists/i.test(output);
780
+ return /previously published versions|cannot publish over|already exists/i.test(
781
+ output,
782
+ );
377
783
  }
378
784
 
379
785
  function firstMeaningfulLine(output: string): string {
380
- return output
381
- .split("\n")
382
- .map((line) => line.trim())
383
- .find((line) => line.length > 0) || "command failed";
786
+ return (
787
+ output
788
+ .split("\n")
789
+ .map((line) => line.trim())
790
+ .find((line) => line.length > 0) || "command failed"
791
+ );
384
792
  }
385
793
 
386
794
  function shellQuote(value: string): string {
387
795
  return `'${value.replaceAll("'", "'\\''")}'`;
388
796
  }
389
797
 
390
- function printReleasePlan(workflow: IResolvedReleaseWorkflow): void {
798
+ function printReleasePlan(
799
+ workflow: IResolvedReleaseWorkflow,
800
+ branchContextArg: IReleaseBranchContext,
801
+ ): void {
391
802
  console.log("");
392
803
  console.log("gitzone release - resolved workflow");
393
804
  console.log(`confirmation: ${workflow.confirmation}`);
394
805
  console.log(`plan: ${workflow.plan.join(" -> ")}`);
395
- console.log(`targets: ${workflow.targets.length > 0 ? workflow.targets.join(", ") : "none"}`);
396
- console.log(`changelog: ${workflow.changelogFile}#${workflow.changelogPendingSection}`);
806
+ console.log(`source branch: ${branchContextArg.sourceBranch}`);
807
+ if (branchContextArg.sourceBranch !== "main") {
808
+ console.log("release branch: main (fast-forward integration requested)");
809
+ console.log(
810
+ `remote main ancestry: ${branchContextArg.planAncestryVerified ? "verified" : "deferred until execution"}`,
811
+ );
812
+ }
813
+ console.log(
814
+ `targets: ${workflow.targets.length > 0 ? workflow.targets.join(", ") : "none"}`,
815
+ );
816
+ console.log(
817
+ `changelog: ${workflow.changelogFile}#${workflow.changelogPendingSection}`,
818
+ );
397
819
  if (workflow.targets.includes("npm")) {
398
- console.log(`npm registries: ${workflow.npmRegistries.length > 0 ? workflow.npmRegistries.join(", ") : "none"}`);
820
+ console.log(
821
+ `npm registries: ${workflow.npmRegistries.length > 0 ? workflow.npmRegistries.join(", ") : "none"}`,
822
+ );
399
823
  }
400
824
  if (workflow.targets.includes("docker")) {
401
825
  console.log(`docker engine: ${workflow.dockerEngine}`);
402
- console.log(`docker registry: ${workflow.dockerRegistry || "all configured registries"}`);
403
- console.log(`docker build authentication: ${workflow.dockerBuildRegistries.length > 0 ? workflow.dockerBuildRegistries.join(", ") : "all configured registries"}`);
404
- console.log(`docker image tests: ${workflow.dockerTest ? "required before destination publication" : "not requested"}`);
405
- console.log(`docker patterns: ${workflow.dockerPatterns.length > 0 ? workflow.dockerPatterns.join(", ") : "all Dockerfiles"}`);
826
+ console.log(
827
+ `docker registry: ${workflow.dockerRegistry || "all configured registries"}`,
828
+ );
829
+ console.log(
830
+ `docker build authentication: ${workflow.dockerBuildRegistries.length > 0 ? workflow.dockerBuildRegistries.join(", ") : "all configured registries"}`,
831
+ );
832
+ console.log(
833
+ `docker image tests: ${workflow.dockerTest ? "required before destination publication" : "not requested"}`,
834
+ );
835
+ console.log(
836
+ `docker patterns: ${workflow.dockerPatterns.length > 0 ? workflow.dockerPatterns.join(", ") : "all Dockerfiles"}`,
837
+ );
406
838
  console.log(`docker options: ${formatDockerOptions(workflow)}`);
407
839
  }
408
840
  console.log("");
@@ -410,13 +842,17 @@ function printReleasePlan(workflow: IResolvedReleaseWorkflow): void {
410
842
 
411
843
  function formatDockerOptions(workflow: IResolvedReleaseWorkflow): string {
412
844
  const options: string[] = [];
413
- if (workflow.dockerRegistry) options.push(`registry=${workflow.dockerRegistry}`);
845
+ if (workflow.dockerRegistry)
846
+ options.push(`registry=${workflow.dockerRegistry}`);
414
847
  if (workflow.dockerBuildRegistries.length > 0) {
415
848
  options.push(`buildRegistries=${workflow.dockerBuildRegistries.join(",")}`);
416
849
  }
417
850
  if (workflow.dockerTest) options.push("test");
418
851
  if (workflow.dockerCached) options.push("cached");
419
- if (workflow.dockerParallel) options.push(`parallel=${workflow.dockerParallel === true ? "true" : workflow.dockerParallel}`);
852
+ if (workflow.dockerParallel)
853
+ options.push(
854
+ `parallel=${workflow.dockerParallel === true ? "true" : workflow.dockerParallel}`,
855
+ );
420
856
  if (workflow.dockerNoBuild) options.push("no-build");
421
857
  if (workflow.dockerContext) options.push(`context=${workflow.dockerContext}`);
422
858
  return options.length > 0 ? options.join(", ") : "default";
@@ -435,21 +871,27 @@ function printReleaseSummary(
435
871
  if (gitResults.length > 0) {
436
872
  console.log("git:");
437
873
  for (const result of gitResults) {
438
- console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
874
+ console.log(
875
+ ` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`,
876
+ );
439
877
  }
440
878
  }
441
879
 
442
880
  if (npmResults.length > 0) {
443
881
  console.log("npm:");
444
882
  for (const result of npmResults) {
445
- console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
883
+ console.log(
884
+ ` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`,
885
+ );
446
886
  }
447
887
  }
448
888
 
449
889
  if (dockerResults.length > 0) {
450
890
  console.log("docker:");
451
891
  for (const result of dockerResults) {
452
- console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
892
+ console.log(
893
+ ` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`,
894
+ );
453
895
  }
454
896
  }
455
897
  }
@@ -459,17 +901,44 @@ export function showHelp(mode?: ICliMode): void {
459
901
  printJson({
460
902
  command: "release",
461
903
  usage: "gitzone release [options]",
462
- description: "Creates a versioned release from pending changelog entries and publishes configured artifacts.",
904
+ description:
905
+ "Creates a versioned release from pending changelog entries and publishes configured artifacts.",
463
906
  flags: [
464
- { flag: "-y, --yes", description: "Run without interactive confirmation" },
907
+ {
908
+ flag: "-y, --yes",
909
+ description: "Run without interactive confirmation",
910
+ },
465
911
  { flag: "-t, --test", description: "Enable release preflight tests" },
466
- { flag: "-b, --build", description: "Enable release preflight build" },
912
+ {
913
+ flag: "-b, --build",
914
+ description:
915
+ "Enable the build after local release metadata is created",
916
+ },
917
+ {
918
+ flag: "--no-build",
919
+ description: "Disable the post-metadata release build",
920
+ },
467
921
  { flag: "-p, --push", description: "Enable the git release target" },
468
- { flag: "--target <names>", description: "Release only selected targets: git,npm,docker" },
922
+ {
923
+ flag: "--target <names>",
924
+ description: "Release only selected targets: git,npm,docker",
925
+ },
469
926
  { flag: "--npm", description: "Enable the npm release target" },
470
927
  { flag: "--docker", description: "Enable the tsdocker release target" },
471
- { flag: "--no-publish", description: "Run release core and git target only" },
472
- { flag: "--plan", description: "Show resolved workflow without mutating files" },
928
+ {
929
+ flag: "--no-publish",
930
+ description: "Run release core and git target only",
931
+ },
932
+ {
933
+ flag: "--merge",
934
+ description:
935
+ "Fast-forward and lease-push a cleanly rebased feature branch before release metadata",
936
+ },
937
+ {
938
+ flag: "--plan",
939
+ description:
940
+ "Show the workflow without fetching or mutating refs, files, the index, or worktrees",
941
+ },
473
942
  ],
474
943
  });
475
944
  return;
@@ -483,13 +952,23 @@ export function showHelp(mode?: ICliMode): void {
483
952
  console.log("Flags:");
484
953
  console.log(" -y, --yes Run without interactive confirmation");
485
954
  console.log(" -t, --test Enable release preflight tests");
486
- console.log(" -b, --build Enable release preflight build");
955
+ console.log(
956
+ " -b, --build Build after creating local release metadata",
957
+ );
958
+ console.log(" --no-build Disable the post-metadata release build");
487
959
  console.log(" -p, --push Enable the git release target");
488
- console.log(" --target <names> Release only selected targets: git,npm,docker");
960
+ console.log(
961
+ " --target <names> Release only selected targets: git,npm,docker",
962
+ );
489
963
  console.log(" --npm Enable the npm release target");
490
964
  console.log(" --docker Enable the tsdocker release target");
491
965
  console.log(" --no-publish Run release core and git target only");
966
+ console.log(
967
+ " --merge Fast-forward and lease-push a cleanly rebased feature branch before release metadata",
968
+ );
492
969
  console.log(" --major|--minor|--patch Override inferred semver level");
493
- console.log(" --plan Show resolved workflow without mutating files");
970
+ console.log(
971
+ " --plan Show workflow without fetching or mutating refs, files, index, or worktrees",
972
+ );
494
973
  console.log("");
495
974
  }