@git.zone/cli 4.0.0 → 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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@git.zone/cli",
3
3
  "private": false,
4
- "version": "4.0.0",
4
+ "version": "5.0.0",
5
5
  "description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
6
6
  "main": "dist_ts/index.js",
7
7
  "typings": "dist_ts/index.d.ts",
package/readme.md CHANGED
@@ -133,23 +133,28 @@ Commit flags:
133
133
 
134
134
  ## Release Workflow
135
135
 
136
- `gitzone release` performs the release core once, then publishes to configured targets.
136
+ `gitzone release` releases from `main`. Running it on another branch fails before release metadata or ref mutation unless the bare `--merge` flag is explicitly supplied.
137
+
138
+ `--merge` is intentionally strict: the source and existing `main` worktree must be clean, the source must be linearly rebased onto current remote `main`, and the Git target must push both the branch and tag. GitZone pins the single configured push URL, compare-and-swap fast-forwards local `main`, updates its verified worktree, and lease-pushes that exact source commit before creating release metadata. It rejects diverged or merged history, local replacement refs or grafts, stale worktrees, incomplete shallow history, changed destinations, and remote races. If the pre-release push fails, GitZone restores local `main` only when doing so cannot overwrite concurrent work. This plumbing-level integration intentionally does not run merge hooks or write `ORIG_HEAD`.
139
+
140
+ `gitzone release` performs the release core once, then publishes to configured targets. An active Git target must succeed before npm or Docker publication can start.
137
141
 
138
142
  The release core is not configurable plumbing. It always follows the same professional release transaction:
139
143
 
140
- 1. Run configured preflight checks.
141
- 2. Read `changelog.md` `## Pending` entries.
142
- 3. Infer or accept a semver bump.
143
- 4. Update version files and baked commit info.
144
- 5. Move pending changelog entries into the new version section.
145
- 6. Create the local release commit.
146
- 7. Create the local release tag.
144
+ 1. Verify the release branch, clean state, and `main` worktree ownership.
145
+ 2. Read `changelog.md` `## Pending` entries and infer or accept a semver bump.
146
+ 3. Run configured tests.
147
+ 4. With `--merge`, fast-forward and lease-push `main` before release metadata is created.
148
+ 5. Update version files and baked commit info.
149
+ 6. Move pending changelog entries into the new version section.
150
+ 7. Create the local release commit and tag on `main`.
151
+ 8. Run the configured release build, require a clean tree, and revalidate the release checkout.
147
152
 
148
153
  Targets decide what happens after that:
149
154
 
150
155
  | Target | What it does |
151
156
  | --- | --- |
152
- | `git` | Pushes the release commit and tags, often triggering remote CI release builds |
157
+ | `git` | Atomically pushes the exact `main` release commit and new tag, often triggering remote CI release builds |
153
158
  | `npm` | Publishes the package to configured npm registries |
154
159
  | `docker` | Delegates container builds and pushes to `tsdocker` |
155
160
 
@@ -160,6 +165,9 @@ gitzone release --plan
160
165
  # Release to configured targets
161
166
  gitzone release
162
167
 
168
+ # From a clean feature branch already rebased onto current main
169
+ gitzone release --merge
170
+
163
171
  # Release only to npm
164
172
  gitzone release --target npm
165
173
 
@@ -179,15 +187,16 @@ Release flags:
179
187
  | --- | --- |
180
188
  | `-y`, `--yes` | Run without interactive confirmation |
181
189
  | `-t`, `--test` | Enable preflight tests |
182
- | `-b`, `--build` | Enable preflight build |
190
+ | `-b`, `--build` | Enable the release build after local release metadata is created |
183
191
  | `-p`, `--push` | Enable the `git` target |
184
192
  | `--target <csv>` | Use only selected targets, e.g. `git,npm` |
185
193
  | `--npm` | Enable the `npm` target |
186
194
  | `--docker` | Enable the `docker` target |
187
195
  | `--no-publish` | Keep release core and `git` target only |
188
- | `--no-build` | Disable preflight build for this run |
196
+ | `--no-build` | Disable the post-metadata release build for this run |
197
+ | `--merge` | Fast-forward and lease-push a cleanly rebased feature branch into `main`, then release from `main` |
189
198
  | `--major`, `--minor`, `--patch` | Override inferred semver level |
190
- | `--plan` | Show resolved workflow only |
199
+ | `--plan` | Show the resolved workflow without fetching or mutating refs, files, the index, or worktrees |
191
200
 
192
201
  ## Standard Changelog
193
202
 
@@ -223,7 +232,6 @@ CLI workflow config lives under `@git.zone/cli` in `.smartconfig.json`. Docker b
223
232
  "release": {
224
233
  "confirmation": "prompt",
225
234
  "preflight": {
226
- "requireCleanTree": true,
227
235
  "test": false,
228
236
  "build": true
229
237
  },
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@git.zone/cli',
6
- version: '4.0.0',
6
+ version: '5.0.0',
7
7
  description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
8
8
  }
@@ -70,7 +70,6 @@ export interface IReleaseWorkflowConfig {
70
70
  source?: "pendingChangelog" | "manual";
71
71
  };
72
72
  preflight?: {
73
- requireCleanTree?: boolean;
74
73
  test?: boolean;
75
74
  build?: boolean;
76
75
  testCommand?: string;
@@ -101,7 +100,6 @@ export interface IResolvedReleaseWorkflow {
101
100
  confirmation: TConfirmationMode;
102
101
  plan: string[];
103
102
  targets: TReleaseTarget[];
104
- requireCleanTree: boolean;
105
103
  runTests: boolean;
106
104
  runBuild: boolean;
107
105
  testCommand: string;
@@ -202,6 +200,22 @@ export const resolveDockerTestSetting = (valueArg: unknown): boolean => {
202
200
  return valueArg;
203
201
  };
204
202
 
203
+ const resolveGitPushSetting = (
204
+ valueArg: unknown,
205
+ settingArg: "pushBranch" | "pushTags",
206
+ gitEnabledArg: boolean,
207
+ ): boolean => {
208
+ if (!gitEnabledArg) {
209
+ return valueArg === undefined ? true : valueArg === true;
210
+ }
211
+ if (valueArg !== undefined && valueArg !== true) {
212
+ throw new Error(
213
+ `release.targets.git.${settingArg} must be true when the Git target is enabled`,
214
+ );
215
+ }
216
+ return true;
217
+ };
218
+
205
219
  const isStringRecord = (valueArg: unknown): valueArg is Record<string, unknown> => {
206
220
  return typeof valueArg === "object" && valueArg !== null && !Array.isArray(valueArg);
207
221
  };
@@ -340,15 +354,19 @@ const getTargetOverride = (argvArg: any): TReleaseTarget[] | undefined => {
340
354
  return targets.length > 0 ? targets : undefined;
341
355
  };
342
356
 
357
+ const releaseTargetOrder: TReleaseTarget[] = ["git", "npm", "docker"];
358
+
343
359
  const buildReleasePlan = (options: {
344
- requireCleanTree: boolean;
360
+ mergeRequested: boolean;
345
361
  runTests: boolean;
346
362
  runBuild: boolean;
347
363
  targets: TReleaseTarget[];
348
364
  }): string[] => {
349
- const plan: string[] = [];
350
- if (options.requireCleanTree) plan.push("preflight.cleanTree");
365
+ const plan: string[] = ["preflight.branch", "preflight.cleanTree"];
351
366
  if (options.runTests) plan.push("preflight.test");
367
+ if (options.mergeRequested) {
368
+ plan.push("integration.main", "integration.pushMain");
369
+ }
352
370
  plan.push("core.version", "core.changelog", "core.commit", "core.tag");
353
371
  if (options.runBuild) plan.push("core.build");
354
372
  for (const target of options.targets) {
@@ -421,6 +439,16 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
421
439
  const npmEnabled = npmConfig.enabled ?? npmRegistries.length > 0;
422
440
  const gitEnabled = gitConfig.enabled ?? true;
423
441
  const dockerEnabled = dockerConfig.enabled ?? false;
442
+ const pushBranch = resolveGitPushSetting(
443
+ gitConfig.pushBranch,
444
+ "pushBranch",
445
+ gitEnabled,
446
+ );
447
+ const pushTags = resolveGitPushSetting(
448
+ gitConfig.pushTags,
449
+ "pushTags",
450
+ gitEnabled,
451
+ );
424
452
 
425
453
  let confirmation = normalizeConfirmation(releaseConfig.confirmation, "prompt");
426
454
  if (argvArg.plan) {
@@ -429,14 +457,12 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
429
457
  confirmation = "auto";
430
458
  }
431
459
 
432
- let requireCleanTree = releaseConfig.preflight?.requireCleanTree ?? true;
433
460
  let runTests = releaseConfig.preflight?.test ?? false;
434
461
  let runBuild = releaseConfig.preflight?.build ?? true;
435
462
  if (argvArg.t || argvArg.test) runTests = true;
436
463
  if (argvArg.b || argvArg.build) runBuild = true;
437
464
  if (isDisabled(argvArg, "test")) runTests = false;
438
465
  if (isDisabled(argvArg, "build")) runBuild = false;
439
- if (isDisabled(argvArg, "preflight")) requireCleanTree = false;
440
466
 
441
467
  const configuredTargets: TReleaseTarget[] = [];
442
468
  if (gitEnabled) configuredTargets.push("git");
@@ -449,13 +475,16 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
449
475
  if (isDisabled(argvArg, "publish")) {
450
476
  targets = targets.filter((target) => target === "git");
451
477
  }
452
- targets = unique(targets);
478
+ const selectedTargets = unique(targets);
479
+ targets = releaseTargetOrder.filter((target) =>
480
+ selectedTargets.includes(target),
481
+ );
482
+ const mergeRequested = argvArg.merge === true;
453
483
 
454
484
  return {
455
485
  confirmation,
456
- plan: buildReleasePlan({ requireCleanTree, runTests, runBuild, targets }),
486
+ plan: buildReleasePlan({ mergeRequested, runTests, runBuild, targets }),
457
487
  targets,
458
- requireCleanTree,
459
488
  runTests,
460
489
  runBuild,
461
490
  testCommand: releaseConfig.preflight?.testCommand || "pnpm test",
@@ -465,8 +494,8 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
465
494
  changelogVersionHeading: "## {{date}} - {{version}}",
466
495
  gitEnabled,
467
496
  gitRemote: gitConfig.remote || "origin",
468
- pushBranch: gitConfig.pushBranch ?? true,
469
- pushTags: gitConfig.pushTags ?? true,
497
+ pushBranch,
498
+ pushTags,
470
499
  npmEnabled,
471
500
  npmRegistries,
472
501
  npmAccessLevel: npmConfig.accessLevel || "public",
@@ -37,9 +37,9 @@ export async function detectCurrentBranch(): Promise<string> {
37
37
  * Detects the project type based on presence of package.json and/or deno.json
38
38
  * @returns The project type
39
39
  */
40
- export async function detectProjectType(): Promise<ProjectType> {
41
- const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
42
- const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
40
+ export async function detectProjectType(cwdArg = paths.cwd): Promise<ProjectType> {
41
+ const packageJsonPath = plugins.path.join(cwdArg, 'package.json');
42
+ const denoJsonPath = plugins.path.join(cwdArg, 'deno.json');
43
43
 
44
44
  const hasPackageJson = await plugins.smartfs.file(packageJsonPath).exists();
45
45
  const hasDenoJson = await plugins.smartfs.file(denoJsonPath).exists();
@@ -96,9 +96,12 @@ export function calculateNewVersion(currentVersion: string, versionType: Version
96
96
  * @param projectType The project type to determine which file to read
97
97
  * @returns The current version string
98
98
  */
99
- export async function readCurrentVersion(projectType: ProjectType): Promise<string> {
99
+ export async function readCurrentVersion(
100
+ projectType: ProjectType,
101
+ cwdArg = paths.cwd,
102
+ ): Promise<string> {
100
103
  if (projectType === 'npm' || projectType === 'both') {
101
- const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
104
+ const packageJsonPath = plugins.path.join(cwdArg, 'package.json');
102
105
  const content = (await plugins.smartfs
103
106
  .file(packageJsonPath)
104
107
  .encoding('utf8')
@@ -110,7 +113,7 @@ export async function readCurrentVersion(projectType: ProjectType): Promise<stri
110
113
  }
111
114
  return packageJson.version;
112
115
  } else {
113
- const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
116
+ const denoJsonPath = plugins.path.join(cwdArg, 'deno.json');
114
117
  const content = (await plugins.smartfs
115
118
  .file(denoJsonPath)
116
119
  .encoding('utf8')
@@ -148,10 +151,11 @@ export async function updateVersionFile(filePath: string, newVersion: string): P
148
151
  export async function updateProjectVersionFiles(
149
152
  projectType: ProjectType,
150
153
  newVersion: string,
154
+ cwdArg = paths.cwd,
151
155
  ): Promise<string[]> {
152
156
  const filesToUpdate: string[] = [];
153
- const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
154
- const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
157
+ const packageJsonPath = plugins.path.join(cwdArg, 'package.json');
158
+ const denoJsonPath = plugins.path.join(cwdArg, 'deno.json');
155
159
 
156
160
  if (projectType === 'npm' || projectType === 'both') {
157
161
  await updateVersionFile(packageJsonPath, newVersion);
@@ -293,7 +293,7 @@ async function handleShow(mode: ICliMode): Promise<void> {
293
293
  const targets = release.targets || {};
294
294
  printConfigSection("Release Workflow", [
295
295
  ["confirmation", formatValue(release.confirmation)],
296
- ["require clean tree", formatValue(release.preflight?.requireCleanTree)],
296
+ ["clean tree", "required"],
297
297
  ["run tests", formatValue(release.preflight?.test)],
298
298
  ["run build", formatValue(release.preflight?.build)],
299
299
  ["test command", formatValue(release.preflight?.testCommand)],
@@ -773,12 +773,6 @@ async function handleRelease(mode: ICliMode): Promise<void> {
773
773
  choices: ["prompt", "auto", "plan"],
774
774
  default: currentRelease.confirmation || "prompt",
775
775
  });
776
- const requireCleanTree = await askValue<boolean>(interactInstance, {
777
- type: "confirm",
778
- name: "requireCleanTree",
779
- message: "Require a clean git tree before release?",
780
- default: currentRelease.preflight?.requireCleanTree ?? true,
781
- });
782
776
  const runTests = await askValue<boolean>(interactInstance, {
783
777
  type: "confirm",
784
778
  name: "runTests",
@@ -828,18 +822,8 @@ async function handleRelease(mode: ICliMode): Promise<void> {
828
822
  message: "Git remote:",
829
823
  default: currentTargets.git?.remote || "origin",
830
824
  }),
831
- pushBranch: await askValue<boolean>(interactInstance, {
832
- type: "confirm",
833
- name: "pushBranch",
834
- message: "Push release commit branch?",
835
- default: currentTargets.git?.pushBranch ?? true,
836
- }),
837
- pushTags: await askValue<boolean>(interactInstance, {
838
- type: "confirm",
839
- name: "pushTags",
840
- message: "Push release tags?",
841
- default: currentTargets.git?.pushTags ?? true,
842
- }),
825
+ pushBranch: true,
826
+ pushTags: true,
843
827
  };
844
828
  } else {
845
829
  releaseTargets.git = { ...(currentTargets.git || {}), enabled: false };
@@ -947,12 +931,13 @@ async function handleRelease(mode: ICliMode): Promise<void> {
947
931
  }
948
932
 
949
933
  setCliConfigValueInData(smartconfigData, "schemaVersion", CURRENT_GITZONE_CLI_SCHEMA_VERSION);
934
+ const currentPreflight = { ...(currentRelease.preflight || {}) };
935
+ delete currentPreflight.requireCleanTree;
950
936
  setCliConfigValueInData(smartconfigData, "release", {
951
937
  ...currentRelease,
952
938
  confirmation,
953
939
  preflight: {
954
- ...(currentRelease.preflight || {}),
955
- requireCleanTree,
940
+ ...currentPreflight,
956
941
  test: runTests,
957
942
  build: runBuild,
958
943
  testCommand: testCommand.trim(),
@@ -1704,7 +1689,7 @@ async function validateReleaseConfig(
1704
1689
  if (preflight.build === true && preflight.buildCommand === "") {
1705
1690
  findings.push({
1706
1691
  level: "warn",
1707
- message: "Release build preflight has an empty command",
1692
+ message: "Release build has an empty command",
1708
1693
  fix: "Set release.preflight.buildCommand or unset it to use pnpm build.",
1709
1694
  });
1710
1695
  }
@@ -1816,6 +1801,20 @@ async function validateGitTarget(
1816
1801
  });
1817
1802
  return;
1818
1803
  }
1804
+ if (gitTarget.pushBranch !== undefined && gitTarget.pushBranch !== true) {
1805
+ findings.push({
1806
+ level: "error",
1807
+ message: "Enabled Git release targets must push main",
1808
+ fix: "Set release.targets.git.pushBranch to true or disable the Git target.",
1809
+ });
1810
+ }
1811
+ if (gitTarget.pushTags !== undefined && gitTarget.pushTags !== true) {
1812
+ findings.push({
1813
+ level: "error",
1814
+ message: "Enabled Git release targets must push release tags",
1815
+ fix: "Set release.targets.git.pushTags to true or disable the Git target.",
1816
+ });
1817
+ }
1819
1818
  findings.push({
1820
1819
  level: "ok",
1821
1820
  message: `Git release target is enabled (${gitTarget.remote || "origin"})`,