@git.zone/cli 2.14.2 → 2.15.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 (46) hide show
  1. package/.smartconfig.json +36 -11
  2. package/assets/templates/service/npmextra.json +8 -2
  3. package/assets/templates/smartconfig/_smartconfig.json +5 -1
  4. package/assets/templates/website/npmextra.json +8 -2
  5. package/dist_ts/00_commitinfo_data.js +1 -1
  6. package/dist_ts/gitzone.cli.js +8 -1
  7. package/dist_ts/helpers.changelog.d.ts +16 -0
  8. package/dist_ts/helpers.changelog.js +114 -0
  9. package/dist_ts/helpers.smartconfigmigrations.d.ts +7 -0
  10. package/dist_ts/helpers.smartconfigmigrations.js +161 -0
  11. package/dist_ts/helpers.workflow.d.ts +97 -0
  12. package/dist_ts/helpers.workflow.js +258 -0
  13. package/dist_ts/mod_commit/index.js +190 -303
  14. package/dist_ts/mod_commit/mod.helpers.d.ts +23 -0
  15. package/dist_ts/mod_commit/mod.helpers.js +22 -15
  16. package/dist_ts/mod_commit/mod.ui.d.ts +1 -1
  17. package/dist_ts/mod_commit/mod.ui.js +7 -2
  18. package/dist_ts/mod_config/classes.commitconfig.d.ts +6 -0
  19. package/dist_ts/mod_config/classes.commitconfig.js +28 -4
  20. package/dist_ts/mod_config/classes.releaseconfig.js +13 -7
  21. package/dist_ts/mod_config/index.js +77 -29
  22. package/dist_ts/mod_format/formatters/smartconfig.formatter.js +3 -57
  23. package/dist_ts/mod_release/index.d.ts +3 -0
  24. package/dist_ts/mod_release/index.js +299 -0
  25. package/dist_ts/mod_release/mod.plugins.d.ts +3 -0
  26. package/dist_ts/mod_release/mod.plugins.js +4 -0
  27. package/dist_ts/mod_standard/index.js +16 -3
  28. package/license +2 -2
  29. package/package.json +20 -30
  30. package/readme.hints.md +15 -17
  31. package/readme.md +239 -421
  32. package/ts/00_commitinfo_data.ts +1 -1
  33. package/ts/gitzone.cli.ts +8 -0
  34. package/ts/helpers.changelog.ts +165 -0
  35. package/ts/helpers.smartconfigmigrations.ts +192 -0
  36. package/ts/helpers.workflow.ts +387 -0
  37. package/ts/mod_commit/index.ts +233 -435
  38. package/ts/mod_commit/mod.helpers.ts +28 -16
  39. package/ts/mod_commit/mod.ui.ts +7 -2
  40. package/ts/mod_config/classes.commitconfig.ts +33 -3
  41. package/ts/mod_config/classes.releaseconfig.ts +14 -7
  42. package/ts/mod_config/index.ts +89 -28
  43. package/ts/mod_format/formatters/smartconfig.formatter.ts +2 -62
  44. package/ts/mod_release/index.ts +393 -0
  45. package/ts/mod_release/mod.plugins.ts +5 -0
  46. package/ts/mod_standard/index.ts +15 -2
@@ -0,0 +1,393 @@
1
+ import * as plugins from "./mod.plugins.js";
2
+ import * as paths from "../paths.js";
3
+ import { logger } from "../gitzone.logging.js";
4
+ import type { ICliMode } from "../helpers.climode.js";
5
+ import { getCliMode, printJson } from "../helpers.climode.js";
6
+ import {
7
+ inferVersionTypeFromPending,
8
+ movePendingToVersion,
9
+ readPendingChangelog,
10
+ } from "../helpers.changelog.js";
11
+ import {
12
+ resolveReleaseWorkflow,
13
+ type IResolvedReleaseWorkflow,
14
+ } from "../helpers.workflow.js";
15
+ import * as commitHelpers from "../mod_commit/mod.helpers.js";
16
+
17
+ type TTargetStatus = "success" | "already-published" | "skipped" | "failed";
18
+
19
+ interface ITargetResult {
20
+ target: string;
21
+ status: TTargetStatus;
22
+ message?: string;
23
+ }
24
+
25
+ export const run = async (argvArg: any) => {
26
+ const mode = await getCliMode(argvArg);
27
+ const subcommand = argvArg._?.[1];
28
+
29
+ if (mode.help || subcommand === "help") {
30
+ showHelp(mode);
31
+ return;
32
+ }
33
+
34
+ if (mode.json) {
35
+ printJson({
36
+ ok: false,
37
+ error: "JSON output is not supported for mutating release workflows yet. Use `gitzone release --plan` for a human-readable plan.",
38
+ });
39
+ return;
40
+ }
41
+
42
+ const workflow = await resolveReleaseWorkflow(argvArg);
43
+ printReleasePlan(workflow);
44
+ if (workflow.confirmation === "plan") {
45
+ return;
46
+ }
47
+
48
+ const smartshellInstance = new plugins.smartshell.Smartshell({
49
+ executor: "bash",
50
+ sourceFilePaths: [],
51
+ });
52
+
53
+ const pending = await readPendingChangelog(
54
+ plugins.path.join(paths.cwd, workflow.changelogFile),
55
+ workflow.changelogPendingSection,
56
+ );
57
+ if (pending.isEmpty && !argvArg["allow-empty"] && !argvArg.allowEmpty) {
58
+ logger.log("error", "No pending changelog entries. Nothing to release.");
59
+ process.exit(1);
60
+ }
61
+
62
+ const versionType = resolveVersionType(argvArg, pending.block);
63
+ const projectType = await commitHelpers.detectProjectType();
64
+ const currentVersion = await commitHelpers.readCurrentVersion(projectType);
65
+ const plannedVersion = commitHelpers.calculateNewVersion(currentVersion, versionType);
66
+
67
+ if (workflow.confirmation === "prompt") {
68
+ if (!mode.interactive) {
69
+ throw new Error("Release confirmation requires an interactive terminal. Use `-y` or set release.confirmation to `auto`.");
70
+ }
71
+ const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
72
+ `Release v${plannedVersion} (${versionType}) now?`,
73
+ true,
74
+ );
75
+ if (!confirmed) {
76
+ logger.log("info", "Release cancelled.");
77
+ return;
78
+ }
79
+ }
80
+
81
+ let newVersion = plannedVersion;
82
+ const gitResults: ITargetResult[] = [];
83
+ const npmResults: ITargetResult[] = [];
84
+ const dockerResults: ITargetResult[] = [];
85
+
86
+ if (workflow.requireCleanTree) {
87
+ await verifyCleanTree(smartshellInstance, "Working tree is not clean. Commit or stash changes before releasing.");
88
+ }
89
+ if (workflow.runTests) {
90
+ await runCommandStep(smartshellInstance, "Running tests", workflow.testCommand);
91
+ }
92
+
93
+ newVersion = await runVersionStep(projectType, versionType);
94
+ await runChangelogStep(workflow, newVersion);
95
+ await runReleaseCommitStep(smartshellInstance, newVersion);
96
+ await runTagStep(smartshellInstance, newVersion);
97
+
98
+ if (workflow.runBuild) {
99
+ await runCommandStep(smartshellInstance, "Running release build", workflow.buildCommand);
100
+ await verifyCleanTree(smartshellInstance, "Build produced uncommitted changes. Aborting release.");
101
+ }
102
+
103
+ if (workflow.targets.includes("git")) {
104
+ gitResults.push(...(await runGitTarget(smartshellInstance, workflow)));
105
+ }
106
+ if (workflow.targets.includes("npm")) {
107
+ npmResults.push(...(await runNpmTarget(smartshellInstance, workflow)));
108
+ }
109
+ if (workflow.targets.includes("docker")) {
110
+ dockerResults.push(...(await runDockerTarget(smartshellInstance, workflow, newVersion)));
111
+ }
112
+
113
+ printReleaseSummary(newVersion, gitResults, npmResults, dockerResults);
114
+ if ([...gitResults, ...npmResults, ...dockerResults].some((result) => result.status === "failed")) {
115
+ process.exit(1);
116
+ }
117
+ };
118
+
119
+ function resolveVersionType(argvArg: any, pendingBlock: string): commitHelpers.VersionType {
120
+ if (argvArg.major) return "major";
121
+ if (argvArg.minor) return "minor";
122
+ if (argvArg.patch) return "patch";
123
+ return inferVersionTypeFromPending(pendingBlock);
124
+ }
125
+
126
+ async function runCommandStep(
127
+ smartshellInstance: plugins.smartshell.Smartshell,
128
+ label: string,
129
+ command: string,
130
+ ): Promise<void> {
131
+ console.log(`\n${label}`);
132
+ const result = await smartshellInstance.exec(command);
133
+ if (result.exitCode !== 0) {
134
+ logger.log("error", `${label} failed. Aborting release.`);
135
+ process.exit(1);
136
+ }
137
+ logger.log("success", `${label} passed.`);
138
+ }
139
+
140
+ async function verifyCleanTree(
141
+ smartshellInstance: plugins.smartshell.Smartshell,
142
+ errorMessage: string,
143
+ ): Promise<void> {
144
+ const statusResult = await smartshellInstance.exec("git status --porcelain");
145
+ if (statusResult.stdout.trim() !== "") {
146
+ logger.log("error", errorMessage);
147
+ console.log(statusResult.stdout);
148
+ process.exit(1);
149
+ }
150
+ }
151
+
152
+ async function runVersionStep(
153
+ projectType: commitHelpers.ProjectType,
154
+ versionType: commitHelpers.VersionType,
155
+ ): Promise<string> {
156
+ const currentVersion = await commitHelpers.readCurrentVersion(projectType);
157
+ const newVersion = commitHelpers.calculateNewVersion(currentVersion, versionType);
158
+ logger.log("info", `Bumping version: ${currentVersion} -> ${newVersion}`);
159
+
160
+ const commitInfo = new plugins.commitinfo.CommitInfo(paths.cwd, versionType);
161
+ await commitInfo.writeIntoPotentialDirs();
162
+ await commitHelpers.updateProjectVersionFiles(projectType, newVersion);
163
+ return newVersion;
164
+ }
165
+
166
+ async function runChangelogStep(
167
+ workflow: IResolvedReleaseWorkflow,
168
+ newVersion: string,
169
+ ): Promise<void> {
170
+ const dateString = new Date().toISOString().slice(0, 10);
171
+ await movePendingToVersion(
172
+ plugins.path.join(paths.cwd, workflow.changelogFile),
173
+ workflow.changelogPendingSection,
174
+ workflow.changelogVersionHeading,
175
+ newVersion,
176
+ dateString,
177
+ );
178
+ }
179
+
180
+ async function runReleaseCommitStep(
181
+ smartshellInstance: plugins.smartshell.Smartshell,
182
+ newVersion: string,
183
+ ): Promise<void> {
184
+ await smartshellInstance.exec("git add -A");
185
+ const result = await smartshellInstance.exec(`git commit -m ${shellQuote(`v${newVersion}`)}`);
186
+ if (result.exitCode !== 0) {
187
+ logger.log("error", "Release commit failed.");
188
+ process.exit(1);
189
+ }
190
+ }
191
+
192
+ async function runTagStep(
193
+ smartshellInstance: plugins.smartshell.Smartshell,
194
+ newVersion: string,
195
+ ): Promise<void> {
196
+ const result = await smartshellInstance.exec(`git tag v${newVersion} -m ${shellQuote(`v${newVersion}`)}`);
197
+ if (result.exitCode !== 0) {
198
+ logger.log("error", "Release tag failed.");
199
+ process.exit(1);
200
+ }
201
+ }
202
+
203
+ async function runGitTarget(
204
+ smartshellInstance: plugins.smartshell.Smartshell,
205
+ workflow: IResolvedReleaseWorkflow,
206
+ ): Promise<ITargetResult[]> {
207
+ const currentBranchResult = await smartshellInstance.exec("git branch --show-current");
208
+ const currentBranch = currentBranchResult.stdout.trim() || "master";
209
+ const commands: Array<{ target: string; command: string }> = [];
210
+ if (workflow.pushBranch) {
211
+ commands.push({
212
+ target: `${workflow.gitRemote}/${currentBranch}`,
213
+ command: `git push ${workflow.gitRemote} ${currentBranch}`,
214
+ });
215
+ }
216
+ if (workflow.pushTags) {
217
+ commands.push({
218
+ target: `${workflow.gitRemote}/tags`,
219
+ command: `git push ${workflow.gitRemote} --tags`,
220
+ });
221
+ }
222
+
223
+ const results: ITargetResult[] = [];
224
+ for (const { target, command } of commands) {
225
+ const result = await smartshellInstance.exec(command);
226
+ results.push({
227
+ target,
228
+ status: result.exitCode === 0 ? "success" : "failed",
229
+ message: result.exitCode === 0 ? undefined : "push failed",
230
+ });
231
+ }
232
+ return results;
233
+ }
234
+
235
+ async function runNpmTarget(
236
+ smartshellInstance: plugins.smartshell.Smartshell,
237
+ workflow: IResolvedReleaseWorkflow,
238
+ ): Promise<ITargetResult[]> {
239
+ if (!workflow.npmEnabled) {
240
+ return [{ target: "npm", status: "skipped", message: "disabled" }];
241
+ }
242
+ if (workflow.npmRegistries.length === 0) {
243
+ return [{ target: "npm", status: "failed", message: "no registries configured" }];
244
+ }
245
+
246
+ const results: ITargetResult[] = [];
247
+ for (const registry of workflow.npmRegistries) {
248
+ const command = `pnpm publish --registry=${registry} --access=${workflow.npmAccessLevel}`;
249
+ const result = await smartshellInstance.exec(command);
250
+ const output = `${result.stdout || ""}\n${(result as any).stderr || ""}\n${(result as any).combinedOutput || ""}`;
251
+ if (result.exitCode === 0) {
252
+ results.push({ target: registry, status: "success" });
253
+ } else if (isAlreadyPublishedOutput(output) && workflow.npmAlreadyPublished === "success") {
254
+ results.push({ target: registry, status: "already-published" });
255
+ } else {
256
+ results.push({ target: registry, status: "failed", message: firstMeaningfulLine(output) });
257
+ }
258
+ }
259
+ return results;
260
+ }
261
+
262
+ async function runDockerTarget(
263
+ smartshellInstance: plugins.smartshell.Smartshell,
264
+ workflow: IResolvedReleaseWorkflow,
265
+ newVersion: string,
266
+ ): Promise<ITargetResult[]> {
267
+ if (!workflow.dockerEnabled) {
268
+ return [{ target: "docker", status: "skipped", message: "disabled" }];
269
+ }
270
+ if (workflow.dockerImages.length === 0) {
271
+ return [{ target: "docker", status: "failed", message: "no images configured" }];
272
+ }
273
+
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
+ });
288
+ }
289
+ return results;
290
+ }
291
+
292
+ function isAlreadyPublishedOutput(output: string): boolean {
293
+ return /previously published versions|cannot publish over|already exists/i.test(output);
294
+ }
295
+
296
+ function firstMeaningfulLine(output: string): string {
297
+ return output
298
+ .split("\n")
299
+ .map((line) => line.trim())
300
+ .find((line) => line.length > 0) || "command failed";
301
+ }
302
+
303
+ function shellQuote(value: string): string {
304
+ return `'${value.replaceAll("'", "'\\''")}'`;
305
+ }
306
+
307
+ function printReleasePlan(workflow: IResolvedReleaseWorkflow): void {
308
+ console.log("");
309
+ console.log("gitzone release - resolved workflow");
310
+ console.log(`confirmation: ${workflow.confirmation}`);
311
+ console.log(`plan: ${workflow.plan.join(" -> ")}`);
312
+ console.log(`targets: ${workflow.targets.length > 0 ? workflow.targets.join(", ") : "none"}`);
313
+ console.log(`changelog: ${workflow.changelogFile}#${workflow.changelogPendingSection}`);
314
+ if (workflow.targets.includes("npm")) {
315
+ console.log(`npm registries: ${workflow.npmRegistries.length > 0 ? workflow.npmRegistries.join(", ") : "none"}`);
316
+ }
317
+ if (workflow.targets.includes("docker")) {
318
+ console.log(`docker images: ${workflow.dockerImages.length > 0 ? workflow.dockerImages.join(", ") : "none"}`);
319
+ }
320
+ console.log("");
321
+ }
322
+
323
+ function printReleaseSummary(
324
+ newVersion: string,
325
+ gitResults: ITargetResult[],
326
+ npmResults: ITargetResult[],
327
+ dockerResults: ITargetResult[],
328
+ ): void {
329
+ console.log("");
330
+ console.log(`Release v${newVersion}`);
331
+ console.log("");
332
+
333
+ if (gitResults.length > 0) {
334
+ console.log("git:");
335
+ for (const result of gitResults) {
336
+ console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
337
+ }
338
+ }
339
+
340
+ if (npmResults.length > 0) {
341
+ console.log("npm:");
342
+ for (const result of npmResults) {
343
+ console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
344
+ }
345
+ }
346
+
347
+ if (dockerResults.length > 0) {
348
+ console.log("docker:");
349
+ for (const result of dockerResults) {
350
+ console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
351
+ }
352
+ }
353
+ }
354
+
355
+ export function showHelp(mode?: ICliMode): void {
356
+ if (mode?.json) {
357
+ printJson({
358
+ command: "release",
359
+ usage: "gitzone release [options]",
360
+ description: "Creates a versioned release from pending changelog entries and publishes configured artifacts.",
361
+ flags: [
362
+ { flag: "-y, --yes", description: "Run without interactive confirmation" },
363
+ { flag: "-t, --test", description: "Enable release preflight tests" },
364
+ { flag: "-b, --build", description: "Enable release preflight build" },
365
+ { flag: "-p, --push", description: "Enable the git release target" },
366
+ { flag: "--target <names>", description: "Release only selected targets: git,npm,docker" },
367
+ { flag: "--npm", description: "Enable the npm release target" },
368
+ { flag: "--docker", description: "Enable the Docker release target" },
369
+ { flag: "--no-publish", description: "Run release core and git target only" },
370
+ { flag: "--plan", description: "Show resolved workflow without mutating files" },
371
+ ],
372
+ });
373
+ return;
374
+ }
375
+
376
+ console.log("");
377
+ console.log("Usage: gitzone release [options]");
378
+ console.log("");
379
+ console.log("Creates a versioned release from changelog Pending entries.");
380
+ console.log("");
381
+ console.log("Flags:");
382
+ console.log(" -y, --yes Run without interactive confirmation");
383
+ console.log(" -t, --test Enable release preflight tests");
384
+ console.log(" -b, --build Enable release preflight build");
385
+ console.log(" -p, --push Enable the git release target");
386
+ console.log(" --target <names> Release only selected targets: git,npm,docker");
387
+ console.log(" --npm Enable the npm release target");
388
+ console.log(" --docker Enable the Docker release target");
389
+ console.log(" --no-publish Run release core and git target only");
390
+ console.log(" --major|--minor|--patch Override inferred semver level");
391
+ console.log(" --plan Show resolved workflow without mutating files");
392
+ console.log("");
393
+ }
@@ -0,0 +1,5 @@
1
+ export * from "../plugins.js";
2
+
3
+ import * as commitinfo from "@push.rocks/commitinfo";
4
+
5
+ export { commitinfo };
@@ -17,8 +17,9 @@ const commandSummaries: ICommandHelpSummary[] = [
17
17
  {
18
18
  name: "commit",
19
19
  description:
20
- "Create semantic commits or generate read-only commit recommendations",
20
+ "Analyze changes and create semantic source commits",
21
21
  },
22
+ { name: "release", description: "Create versioned releases from pending changelog entries" },
22
23
  { name: "format", description: "Plan or apply project formatting changes" },
23
24
  { name: "config", description: "Read and change .smartconfig.json settings" },
24
25
  { name: "services", description: "Manage or configure development services" },
@@ -68,7 +69,8 @@ export let run = async (argvArg: any = {}) => {
68
69
  message: "What would you like to do?",
69
70
  default: "commit",
70
71
  choices: [
71
- { name: "Commit changes (semantic versioning)", value: "commit" },
72
+ { name: "Commit changes", value: "commit" },
73
+ { name: "Release pending changes", value: "release" },
72
74
  { name: "Format project files", value: "format" },
73
75
  { name: "Configure release settings", value: "config" },
74
76
  { name: "Create from template", value: "template" },
@@ -86,6 +88,11 @@ export let run = async (argvArg: any = {}) => {
86
88
  await modCommit.run({ _: ["commit"] });
87
89
  break;
88
90
  }
91
+ case "release": {
92
+ const modRelease = await import("../mod_release/index.js");
93
+ await modRelease.run({ _: ["release"] });
94
+ break;
95
+ }
89
96
  case "format": {
90
97
  const modFormat = await import("../mod_format/index.js");
91
98
  await modFormat.run({ interactive: true });
@@ -186,6 +193,7 @@ export async function showHelp(
186
193
  console.log(" gitzone help commit");
187
194
  console.log(" gitzone config show --json");
188
195
  console.log(" gitzone commit recommend --json");
196
+ console.log(" gitzone release --plan");
189
197
  console.log(" gitzone format plan --json");
190
198
  console.log(" gitzone services set mongodb,minio");
191
199
  console.log("");
@@ -203,6 +211,11 @@ async function showCommandHelp(
203
211
  modCommit.showHelp(mode);
204
212
  return true;
205
213
  }
214
+ case "release": {
215
+ const modRelease = await import("../mod_release/index.js");
216
+ modRelease.showHelp(mode);
217
+ return true;
218
+ }
206
219
  case "config": {
207
220
  const modConfig = await import("../mod_config/index.js");
208
221
  modConfig.showHelp(mode);