@git.zone/cli 6.7.2 → 6.8.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/mod_deprecate/helpers.deprecation.js +21 -8
- package/dist_ts/mod_release/helpers.npmartifact.d.ts +3 -0
- package/dist_ts/mod_release/helpers.npmartifact.js +16 -1
- package/dist_ts/mod_release/helpers.releasepreparation.d.ts +21 -0
- package/dist_ts/mod_release/helpers.releasepreparation.js +196 -0
- package/dist_ts/mod_release/index.d.ts +2 -0
- package/dist_ts/mod_release/index.js +123 -7
- package/package.json +4 -4
- package/readme.md +46 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mod_deprecate/helpers.deprecation.ts +34 -13
- package/ts/mod_release/helpers.npmartifact.ts +30 -0
- package/ts/mod_release/helpers.releasepreparation.ts +308 -0
- package/ts/mod_release/index.ts +174 -6
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import * as plugins from "./mod.plugins.js";
|
|
2
|
+
import type { IResolvedReleaseWorkflow } from "../helpers.workflow.js";
|
|
3
|
+
import { readPendingChangelog } from "../helpers.changelog.js";
|
|
4
|
+
import { InterProcessLock } from "../mod_services/classes.interprocesslock.js";
|
|
5
|
+
import { ReleaseJournalStore } from "./classes.releasejournal.js";
|
|
6
|
+
import {
|
|
7
|
+
releaseGitEnv,
|
|
8
|
+
type IReleaseBranchContext,
|
|
9
|
+
} from "./helpers.releasebranch.js";
|
|
10
|
+
import {
|
|
11
|
+
assertNpmVersionAbsent,
|
|
12
|
+
assertNpmPackageName,
|
|
13
|
+
hashReleaseDestination,
|
|
14
|
+
} from "./helpers.npmartifact.js";
|
|
15
|
+
import { canonicalReleaseJson } from "./helpers.tsdockerprotocol.js";
|
|
16
|
+
|
|
17
|
+
interface IRecoveryRefs {
|
|
18
|
+
mainOid: string;
|
|
19
|
+
tagOid: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IReleasePreparationRecovery {
|
|
23
|
+
smartshell: plugins.smartshell.Smartshell;
|
|
24
|
+
workflow: IResolvedReleaseWorkflow;
|
|
25
|
+
branch: IReleaseBranchContext;
|
|
26
|
+
refs: IRecoveryRefs;
|
|
27
|
+
version: string;
|
|
28
|
+
store: ReleaseJournalStore;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const git = async (
|
|
32
|
+
options: IReleasePreparationRecovery,
|
|
33
|
+
args: string[],
|
|
34
|
+
cwd = options.branch.sourceCwd,
|
|
35
|
+
) => {
|
|
36
|
+
const result = await options.smartshell.execSpawn("git", args, {
|
|
37
|
+
cwd,
|
|
38
|
+
env: releaseGitEnv,
|
|
39
|
+
silent: true,
|
|
40
|
+
timeout: 60_000,
|
|
41
|
+
timeoutKillGraceMs: 5_000,
|
|
42
|
+
});
|
|
43
|
+
if (result.exitCode !== 0)
|
|
44
|
+
throw new Error(`Release recovery Git validation failed: ${args[0]}.`);
|
|
45
|
+
return result.stdout.trim();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const assertPackageReleaseRecovery = async (
|
|
49
|
+
options: IReleasePreparationRecovery,
|
|
50
|
+
): Promise<string> => {
|
|
51
|
+
const { workflow, branch, version } = options;
|
|
52
|
+
if (
|
|
53
|
+
workflow.targets.length !== 2 ||
|
|
54
|
+
!workflow.targets.includes("git") ||
|
|
55
|
+
!workflow.targets.includes("npm") ||
|
|
56
|
+
workflow.npmPackageSource !== "root" ||
|
|
57
|
+
!workflow.pushBranch ||
|
|
58
|
+
!workflow.pushTags ||
|
|
59
|
+
branch.sourceBranch !== "main" ||
|
|
60
|
+
!branch.pushUrl ||
|
|
61
|
+
!branch.remoteMainOid
|
|
62
|
+
) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Release recover requires configured Git and root npm targets from clean main.",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const manifest = JSON.parse(
|
|
68
|
+
await plugins.fs.readFile(
|
|
69
|
+
plugins.path.join(branch.sourceCwd, "package.json"),
|
|
70
|
+
"utf8",
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
if (manifest.version !== version || typeof manifest.name !== "string") {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"Recovery package identity does not match the existing release version.",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
assertNpmPackageName(manifest.name);
|
|
79
|
+
const pending = await readPendingChangelog(
|
|
80
|
+
plugins.path.join(branch.sourceCwd, workflow.changelogFile),
|
|
81
|
+
workflow.changelogPendingSection,
|
|
82
|
+
);
|
|
83
|
+
const changelog = await plugins.fs.readFile(
|
|
84
|
+
plugins.path.join(branch.sourceCwd, workflow.changelogFile),
|
|
85
|
+
"utf8",
|
|
86
|
+
);
|
|
87
|
+
const headings = changelog
|
|
88
|
+
.split(/\r?\n/)
|
|
89
|
+
.filter(
|
|
90
|
+
(line) =>
|
|
91
|
+
/^##\s/.test(line) && line !== `## ${workflow.changelogPendingSection}`,
|
|
92
|
+
);
|
|
93
|
+
if (
|
|
94
|
+
!pending.isEmpty ||
|
|
95
|
+
!new RegExp(
|
|
96
|
+
`^## \\d{4}-\\d{2}-\\d{2} - ${version.replace(/\./g, "\\.")}\\s*$`,
|
|
97
|
+
).test(headings[0] ?? "")
|
|
98
|
+
) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"Recovery requires the completed changelog section for this version and no Pending changes.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (
|
|
104
|
+
(await git(options, [
|
|
105
|
+
"show",
|
|
106
|
+
"-s",
|
|
107
|
+
"--format=%s",
|
|
108
|
+
options.refs.mainOid,
|
|
109
|
+
])) !== `v${version}` ||
|
|
110
|
+
(
|
|
111
|
+
await git(options, [
|
|
112
|
+
"rev-list",
|
|
113
|
+
"--parents",
|
|
114
|
+
"-n",
|
|
115
|
+
"1",
|
|
116
|
+
options.refs.mainOid,
|
|
117
|
+
])
|
|
118
|
+
).split(" ").length !== 2
|
|
119
|
+
) {
|
|
120
|
+
throw new Error("Recovery requires an existing non-merge release commit.");
|
|
121
|
+
}
|
|
122
|
+
if (
|
|
123
|
+
await git(options, [
|
|
124
|
+
"diff",
|
|
125
|
+
`${options.refs.mainOid}^`,
|
|
126
|
+
options.refs.mainOid,
|
|
127
|
+
"--",
|
|
128
|
+
".smartconfig.json",
|
|
129
|
+
])
|
|
130
|
+
) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"Release configuration changed inside the release metadata commit.",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return manifest.name;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const assertRecoveryUnpublished = async (
|
|
139
|
+
options: IReleasePreparationRecovery,
|
|
140
|
+
packageName: string,
|
|
141
|
+
): Promise<void> => {
|
|
142
|
+
const output = await git(options, [
|
|
143
|
+
"ls-remote",
|
|
144
|
+
options.branch.pushUrl!,
|
|
145
|
+
`refs/tags/v${options.version}`,
|
|
146
|
+
`refs/tags/v${options.version}^{}`,
|
|
147
|
+
]);
|
|
148
|
+
if (output)
|
|
149
|
+
throw new Error(
|
|
150
|
+
"Recovery cannot adopt an existing remote release tag. Use its original journal.",
|
|
151
|
+
);
|
|
152
|
+
for (const registry of options.workflow.npmRegistries)
|
|
153
|
+
await assertNpmVersionAbsent(registry, packageName, options.version);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const preparationBinding = (options: IReleasePreparationRecovery) => ({
|
|
157
|
+
kind: "gitzone-package-release-preparation",
|
|
158
|
+
schemaVersion: 1,
|
|
159
|
+
version: options.version,
|
|
160
|
+
...options.refs,
|
|
161
|
+
destinationHash: hashReleaseDestination(options.branch.pushUrl!),
|
|
162
|
+
expectedRemoteMainOid: options.branch.remoteMainOid,
|
|
163
|
+
// Capture resolved configuration, including build/test commands, so retries
|
|
164
|
+
// cannot silently prepare a different release.
|
|
165
|
+
workflow: JSON.parse(
|
|
166
|
+
JSON.stringify({ ...options.workflow, confirmation: "auto" }),
|
|
167
|
+
),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const bindPreparation = async (
|
|
171
|
+
path: string,
|
|
172
|
+
expected: string,
|
|
173
|
+
): Promise<void> => {
|
|
174
|
+
try {
|
|
175
|
+
const stat = await plugins.fs.lstat(path);
|
|
176
|
+
if (
|
|
177
|
+
!stat.isFile() ||
|
|
178
|
+
stat.isSymbolicLink() ||
|
|
179
|
+
stat.size > 128 * 1024 ||
|
|
180
|
+
(await plugins.fs.readFile(path, "utf8")) !== expected
|
|
181
|
+
) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
"Release recovery identity or configuration differs from its original preparation.",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
188
|
+
const handle = await plugins.fs.open(path, "wx", 0o600);
|
|
189
|
+
try {
|
|
190
|
+
await handle.writeFile(expected, "utf8");
|
|
191
|
+
await handle.sync();
|
|
192
|
+
} finally {
|
|
193
|
+
await handle.close();
|
|
194
|
+
}
|
|
195
|
+
const directory = await plugins.fs.open(plugins.path.dirname(path), "r");
|
|
196
|
+
try {
|
|
197
|
+
await directory.sync();
|
|
198
|
+
} finally {
|
|
199
|
+
await directory.close();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/** Build and seal one unpublished, already tagged package without changing refs. */
|
|
205
|
+
export const withReleasePreparationRecovery = async <T>(
|
|
206
|
+
options: IReleasePreparationRecovery,
|
|
207
|
+
ensureState: () => Promise<void>,
|
|
208
|
+
prepare: (
|
|
209
|
+
checkout: string,
|
|
210
|
+
ensureCheckout: () => Promise<void>,
|
|
211
|
+
) => Promise<T>,
|
|
212
|
+
): Promise<T> => {
|
|
213
|
+
if (!options.branch.planAncestryVerified) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
"Release recovery requires verified remote ancestry before preparation.",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const directory = options.store.getReleaseDirectory(options.version);
|
|
219
|
+
return new InterProcessLock({
|
|
220
|
+
lockPath: `${directory}.prepare.lock`,
|
|
221
|
+
description: `Release v${options.version} preparation`,
|
|
222
|
+
}).runExclusive(async () => {
|
|
223
|
+
await ensureState();
|
|
224
|
+
try {
|
|
225
|
+
await plugins.fs.lstat(directory);
|
|
226
|
+
throw new Error(
|
|
227
|
+
"Release journal already exists. Use release resume; do not rebuild its artifact.",
|
|
228
|
+
);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
231
|
+
}
|
|
232
|
+
await bindPreparation(
|
|
233
|
+
`${directory}.preparation.json`,
|
|
234
|
+
`${canonicalReleaseJson(preparationBinding(options))}\n`,
|
|
235
|
+
);
|
|
236
|
+
const root = await plugins.fs.mkdtemp(
|
|
237
|
+
plugins.path.join(plugins.os.tmpdir(), "gitzone-release-prepare-"),
|
|
238
|
+
);
|
|
239
|
+
const checkout = plugins.path.join(root, "source");
|
|
240
|
+
let added = false;
|
|
241
|
+
try {
|
|
242
|
+
await git(options, [
|
|
243
|
+
"worktree",
|
|
244
|
+
"add",
|
|
245
|
+
"--detach",
|
|
246
|
+
checkout,
|
|
247
|
+
options.refs.mainOid,
|
|
248
|
+
]);
|
|
249
|
+
added = true;
|
|
250
|
+
const install = await options.smartshell.execSpawn(
|
|
251
|
+
"pnpm",
|
|
252
|
+
["install", "--frozen-lockfile"],
|
|
253
|
+
{
|
|
254
|
+
cwd: checkout,
|
|
255
|
+
timeout: 10 * 60_000,
|
|
256
|
+
timeoutKillGraceMs: 5_000,
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
if (install.exitCode !== 0)
|
|
260
|
+
throw new Error("Release recovery dependency installation failed.");
|
|
261
|
+
if (options.workflow.runTests)
|
|
262
|
+
await run(options, checkout, options.workflow.testCommand, "tests");
|
|
263
|
+
if (options.workflow.runBuild)
|
|
264
|
+
await run(options, checkout, options.workflow.buildCommand, "build");
|
|
265
|
+
await assertCheckout(options, checkout);
|
|
266
|
+
await ensureState();
|
|
267
|
+
const result = await prepare(checkout, () =>
|
|
268
|
+
assertCheckout(options, checkout),
|
|
269
|
+
);
|
|
270
|
+
await assertCheckout(options, checkout);
|
|
271
|
+
return result;
|
|
272
|
+
} finally {
|
|
273
|
+
// This path is a uniquely owned disposable checkout, never the user's
|
|
274
|
+
// source tree or runtime data. Keep registration evidence if cleanup fails.
|
|
275
|
+
if (added)
|
|
276
|
+
await git(options, ["worktree", "remove", "--force", checkout]);
|
|
277
|
+
await plugins.fs.rm(root, { recursive: true, force: true });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const run = async (
|
|
283
|
+
options: IReleasePreparationRecovery,
|
|
284
|
+
cwd: string,
|
|
285
|
+
command: string,
|
|
286
|
+
phase: string,
|
|
287
|
+
) => {
|
|
288
|
+
const result = await options.smartshell.exec(command, { cwd });
|
|
289
|
+
if (result.exitCode !== 0)
|
|
290
|
+
throw new Error(`Release recovery ${phase} failed.`);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const assertCheckout = async (
|
|
294
|
+
options: IReleasePreparationRecovery,
|
|
295
|
+
checkout: string,
|
|
296
|
+
) => {
|
|
297
|
+
if (
|
|
298
|
+
(await git(options, ["rev-parse", "HEAD"], checkout)) !==
|
|
299
|
+
options.refs.mainOid ||
|
|
300
|
+
(await git(
|
|
301
|
+
options,
|
|
302
|
+
["status", "--porcelain", "--untracked-files=all"],
|
|
303
|
+
checkout,
|
|
304
|
+
))
|
|
305
|
+
) {
|
|
306
|
+
throw new Error("Release recovery build changed its exact tracked source.");
|
|
307
|
+
}
|
|
308
|
+
};
|
package/ts/mod_release/index.ts
CHANGED
|
@@ -51,6 +51,11 @@ import {
|
|
|
51
51
|
verifyStoredNpmArtifact,
|
|
52
52
|
} from "./helpers.npmartifact.js";
|
|
53
53
|
import { executeReleasePublication } from "./helpers.releasepublication.js";
|
|
54
|
+
import {
|
|
55
|
+
assertPackageReleaseRecovery,
|
|
56
|
+
assertRecoveryUnpublished,
|
|
57
|
+
withReleasePreparationRecovery,
|
|
58
|
+
} from "./helpers.releasepreparation.js";
|
|
54
59
|
import {
|
|
55
60
|
TsdockerReleaseClient,
|
|
56
61
|
canonicalReleaseJson,
|
|
@@ -87,6 +92,10 @@ const runInternal = async (argvArg: any): Promise<void> => {
|
|
|
87
92
|
await runReleaseResume(argvArg, mode);
|
|
88
93
|
return;
|
|
89
94
|
}
|
|
95
|
+
if (subcommand === "recover") {
|
|
96
|
+
await runReleaseRecover(argvArg, mode);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
90
99
|
if (subcommand !== undefined) {
|
|
91
100
|
throw new Error(`Unknown release subcommand: ${subcommand}`);
|
|
92
101
|
}
|
|
@@ -402,6 +411,8 @@ interface ICreateReleaseJournalOptions {
|
|
|
402
411
|
smartshell: plugins.smartshell.Smartshell;
|
|
403
412
|
workflow: IResolvedReleaseWorkflow;
|
|
404
413
|
releaseCwd: string;
|
|
414
|
+
/** Exact detached checkout used only while recovering pre-journal packaging. */
|
|
415
|
+
npmSourceCwd?: string;
|
|
405
416
|
releasePushUrl?: string;
|
|
406
417
|
expectedRemoteMainOid?: string;
|
|
407
418
|
version: string;
|
|
@@ -447,6 +458,7 @@ export const planReleasePackages = async (
|
|
|
447
458
|
export const createReleaseJournal = async (
|
|
448
459
|
optionsArg: ICreateReleaseJournalOptions,
|
|
449
460
|
): Promise<{ store: ReleaseJournalStore; journal: TReleaseJournal }> => {
|
|
461
|
+
const npmSourceCwd = optionsArg.npmSourceCwd ?? optionsArg.releaseCwd;
|
|
450
462
|
const gitCommonDirectory = await resolveGitCommonDirectory(
|
|
451
463
|
optionsArg.smartshell,
|
|
452
464
|
optionsArg.releaseCwd,
|
|
@@ -474,7 +486,7 @@ export const createReleaseJournal = async (
|
|
|
474
486
|
const packages: IReleasePackage[] = [];
|
|
475
487
|
if (optionsArg.workflow.targets.includes("npm")) {
|
|
476
488
|
if (optionsArg.workflow.npmPackageSource === "tspublish") {
|
|
477
|
-
const plan = await planReleasePackages(
|
|
489
|
+
const plan = await planReleasePackages(npmSourceCwd);
|
|
478
490
|
if (plan.version !== optionsArg.version)
|
|
479
491
|
throw new Error("tspublish plan version differs from the release.");
|
|
480
492
|
const outputDirectory = plugins.path.join(
|
|
@@ -482,7 +494,7 @@ export const createReleaseJournal = async (
|
|
|
482
494
|
"prepared",
|
|
483
495
|
);
|
|
484
496
|
const prepared = await new plugins.tspublish.TsPublish().prepare(
|
|
485
|
-
|
|
497
|
+
npmSourceCwd,
|
|
486
498
|
outputDirectory,
|
|
487
499
|
);
|
|
488
500
|
if (
|
|
@@ -509,14 +521,14 @@ export const createReleaseJournal = async (
|
|
|
509
521
|
} else {
|
|
510
522
|
artifact = await packNpmArtifact(
|
|
511
523
|
optionsArg.smartshell,
|
|
512
|
-
|
|
524
|
+
npmSourceCwd,
|
|
513
525
|
temporaryDirectory,
|
|
514
526
|
optionsArg.version,
|
|
515
527
|
);
|
|
516
528
|
}
|
|
517
529
|
await verifyCleanTree(
|
|
518
530
|
optionsArg.smartshell,
|
|
519
|
-
|
|
531
|
+
npmSourceCwd,
|
|
520
532
|
"npm pack lifecycle scripts changed the release worktree. Aborting release.",
|
|
521
533
|
);
|
|
522
534
|
await optionsArg.ensurePublicationState();
|
|
@@ -650,6 +662,153 @@ const runReleaseInspect = async (
|
|
|
650
662
|
}
|
|
651
663
|
};
|
|
652
664
|
|
|
665
|
+
const runReleaseRecover = async (
|
|
666
|
+
argvArg: any,
|
|
667
|
+
mode: ICliMode,
|
|
668
|
+
): Promise<void> => {
|
|
669
|
+
const allowed = new Set([
|
|
670
|
+
"_",
|
|
671
|
+
"$0",
|
|
672
|
+
"y",
|
|
673
|
+
"yes",
|
|
674
|
+
"plan",
|
|
675
|
+
"h",
|
|
676
|
+
"help",
|
|
677
|
+
"plain",
|
|
678
|
+
"quiet",
|
|
679
|
+
"agent",
|
|
680
|
+
"interactive",
|
|
681
|
+
"check-updates",
|
|
682
|
+
"checkUpdates",
|
|
683
|
+
]);
|
|
684
|
+
if (
|
|
685
|
+
argvArg._?.length !== 3 ||
|
|
686
|
+
Object.keys(argvArg).some((key) => !allowed.has(key)) ||
|
|
687
|
+
(argvArg.plan !== undefined && typeof argvArg.plan !== "boolean")
|
|
688
|
+
) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
"Usage: gitzone release recover <version> [-y|--plan]. Target, version, build and test overrides are not allowed.",
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
const version = normalizeReleaseVersion(argvArg._[2]);
|
|
694
|
+
const shell = new plugins.smartshell.Smartshell({
|
|
695
|
+
executor: "bash",
|
|
696
|
+
sourceFilePaths: [],
|
|
697
|
+
});
|
|
698
|
+
const store = new ReleaseJournalStore(
|
|
699
|
+
await resolveGitCommonDirectory(shell, paths.cwd),
|
|
700
|
+
);
|
|
701
|
+
const existing = await plugins.fs
|
|
702
|
+
.lstat(store.getReleaseDirectory(version))
|
|
703
|
+
.then(() => true)
|
|
704
|
+
.catch((error: NodeJS.ErrnoException) => {
|
|
705
|
+
if (error.code === "ENOENT") return false;
|
|
706
|
+
throw error;
|
|
707
|
+
});
|
|
708
|
+
if (existing) {
|
|
709
|
+
if (argvArg.plan) {
|
|
710
|
+
printReleaseJournalSummary(await store.read(version));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
await runReleaseResume(
|
|
714
|
+
{ _: ["release", "resume", version], yes: mode.yes },
|
|
715
|
+
mode,
|
|
716
|
+
);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const workflow = await resolveReleaseWorkflow({ yes: true });
|
|
720
|
+
assertReleaseConfiguration(workflow, false);
|
|
721
|
+
let branch = await inspectReleaseBranch({
|
|
722
|
+
smartshell: shell,
|
|
723
|
+
cwd: paths.cwd,
|
|
724
|
+
gitRemote: workflow.gitRemote,
|
|
725
|
+
gitTargetActive: true,
|
|
726
|
+
pushBranch: workflow.pushBranch,
|
|
727
|
+
pushTags: workflow.pushTags,
|
|
728
|
+
mergeRequested: false,
|
|
729
|
+
planMode: argvArg.plan === true,
|
|
730
|
+
});
|
|
731
|
+
const refs = await captureReleaseRefs(
|
|
732
|
+
shell,
|
|
733
|
+
paths.cwd,
|
|
734
|
+
version,
|
|
735
|
+
branch.sourceOid,
|
|
736
|
+
);
|
|
737
|
+
const options = { smartshell: shell, workflow, branch, refs, version, store };
|
|
738
|
+
const packageName = await assertPackageReleaseRecovery(options);
|
|
739
|
+
if (!branch.planAncestryVerified) {
|
|
740
|
+
console.log(
|
|
741
|
+
"Remote ancestry is deferred until execution fetches the missing commit.",
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
console.log(
|
|
745
|
+
`Recover existing ${packageName}@${version}\ncommit: ${refs.mainOid}\ntag object: ${refs.tagOid}\nGit destination: ${branch.gitRemote}\nnpm registries: ${workflow.npmRegistries.join(", ")}\nBuild and pack an isolated checkout, install its journal, then publish configured targets.`,
|
|
746
|
+
);
|
|
747
|
+
if (argvArg.plan) return;
|
|
748
|
+
await assertFreshReleaseCapabilities(
|
|
749
|
+
shell,
|
|
750
|
+
workflow,
|
|
751
|
+
paths.cwd,
|
|
752
|
+
branch.pushUrl,
|
|
753
|
+
);
|
|
754
|
+
if (!mode.yes) {
|
|
755
|
+
if (!mode.interactive)
|
|
756
|
+
throw new Error(
|
|
757
|
+
"Release recovery requires an interactive terminal or -y.",
|
|
758
|
+
);
|
|
759
|
+
if (
|
|
760
|
+
!(await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
|
761
|
+
`Recover and publish existing v${version}?`,
|
|
762
|
+
false,
|
|
763
|
+
))
|
|
764
|
+
)
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
branch = await prepareReleaseIntegration(shell, branch);
|
|
768
|
+
options.branch = branch;
|
|
769
|
+
const ensureState = async (remote = branch.remoteMainOid) => {
|
|
770
|
+
await verifyReleaseRefs(shell, paths.cwd, version, refs);
|
|
771
|
+
await revalidateReleasePublicationState(
|
|
772
|
+
shell,
|
|
773
|
+
branch,
|
|
774
|
+
refs.mainOid,
|
|
775
|
+
remote,
|
|
776
|
+
);
|
|
777
|
+
};
|
|
778
|
+
const ensureUnpublished = async () => {
|
|
779
|
+
await ensureState();
|
|
780
|
+
await assertRecoveryUnpublished(options, packageName);
|
|
781
|
+
};
|
|
782
|
+
const context = await withReleasePreparationRecovery(
|
|
783
|
+
options,
|
|
784
|
+
ensureUnpublished,
|
|
785
|
+
async (checkout, ensureCheckout) =>
|
|
786
|
+
createReleaseJournal({
|
|
787
|
+
smartshell: shell,
|
|
788
|
+
workflow,
|
|
789
|
+
releaseCwd: paths.cwd,
|
|
790
|
+
npmSourceCwd: checkout,
|
|
791
|
+
releasePushUrl: branch.pushUrl,
|
|
792
|
+
expectedRemoteMainOid: branch.remoteMainOid,
|
|
793
|
+
version,
|
|
794
|
+
refs,
|
|
795
|
+
ensurePublicationState: async () => {
|
|
796
|
+
await ensureCheckout();
|
|
797
|
+
await ensureUnpublished();
|
|
798
|
+
},
|
|
799
|
+
}),
|
|
800
|
+
);
|
|
801
|
+
const result = await executeReleasePublication({
|
|
802
|
+
smartshell: shell,
|
|
803
|
+
cwd: paths.cwd,
|
|
804
|
+
store: context.store,
|
|
805
|
+
journal: context.journal,
|
|
806
|
+
pushUrl: branch.pushUrl,
|
|
807
|
+
ensurePublicationState: ensureState,
|
|
808
|
+
});
|
|
809
|
+
printReleaseJournalSummary(result);
|
|
810
|
+
};
|
|
811
|
+
|
|
653
812
|
const assertResumeArguments = (argvArg: any): void => {
|
|
654
813
|
const forbiddenKeys = [
|
|
655
814
|
"allow-empty",
|
|
@@ -1284,7 +1443,8 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
1284
1443
|
if (mode?.json) {
|
|
1285
1444
|
printJson({
|
|
1286
1445
|
command: "release",
|
|
1287
|
-
usage:
|
|
1446
|
+
usage:
|
|
1447
|
+
"gitzone release [inspect [version]|resume <version>|recover <version>] [options]",
|
|
1288
1448
|
description:
|
|
1289
1449
|
"Creates a versioned release from pending changelog entries and publishes configured artifacts.",
|
|
1290
1450
|
flags: [
|
|
@@ -1338,6 +1498,11 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
1338
1498
|
description:
|
|
1339
1499
|
"Resume exact journaled publication without rebuilding or repacking npm artifacts",
|
|
1340
1500
|
},
|
|
1501
|
+
{
|
|
1502
|
+
flag: "recover <version>",
|
|
1503
|
+
description:
|
|
1504
|
+
"Recover a local root npm release tag after build or pack failure before journal creation",
|
|
1505
|
+
},
|
|
1341
1506
|
{
|
|
1342
1507
|
flag: "--recover-attempt <id>",
|
|
1343
1508
|
description:
|
|
@@ -1350,7 +1515,7 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
1350
1515
|
|
|
1351
1516
|
console.log("");
|
|
1352
1517
|
console.log(
|
|
1353
|
-
"Usage: gitzone release [inspect [version]|resume <version>] [options]",
|
|
1518
|
+
"Usage: gitzone release [inspect [version]|resume <version>|recover <version>] [options]",
|
|
1354
1519
|
);
|
|
1355
1520
|
console.log("");
|
|
1356
1521
|
console.log("Creates a versioned release from changelog Pending entries.");
|
|
@@ -1386,6 +1551,9 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
1386
1551
|
console.log(
|
|
1387
1552
|
" resume <version> Resume exact journaled publication without rebuilding or repacking npm artifacts",
|
|
1388
1553
|
);
|
|
1554
|
+
console.log(
|
|
1555
|
+
" recover <version> Recover an unpublished root npm release from its existing local tag",
|
|
1556
|
+
);
|
|
1389
1557
|
console.log(
|
|
1390
1558
|
" --recover-attempt Recover one exact 32-character hexadecimal attempt ID after proving its publisher stopped",
|
|
1391
1559
|
);
|