@git.zone/cli 5.0.0 → 6.0.1
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/assets/templates/ci_default/.gitea/workflows/default_tags.yaml +0 -21
- package/assets/templates/ci_default_gitlab/.gitlab-ci.yml +0 -13
- package/assets/templates/ci_default_private/.gitea/workflows/default_tags.yaml +0 -21
- package/assets/templates/ci_default_private_gitlab/.gitlab-ci.yml +0 -13
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/helpers.climode.js +31 -2
- package/dist_ts/helpers.workflow.d.ts +10 -0
- package/dist_ts/helpers.workflow.js +80 -8
- package/dist_ts/mod_release/classes.releasejournal.d.ts +78 -0
- package/dist_ts/mod_release/classes.releasejournal.js +511 -0
- package/dist_ts/mod_release/helpers.npmartifact.d.ts +32 -0
- package/dist_ts/mod_release/helpers.npmartifact.js +360 -0
- package/dist_ts/mod_release/helpers.releasebranch.d.ts +1 -0
- package/dist_ts/mod_release/helpers.releasebranch.js +54 -6
- package/dist_ts/mod_release/helpers.releasepublication.d.ts +24 -0
- package/dist_ts/mod_release/helpers.releasepublication.js +293 -0
- package/dist_ts/mod_release/index.d.ts +1 -11
- package/dist_ts/mod_release/index.js +410 -288
- package/package.json +1 -1
- package/readme.hints.md +47 -1
- package/readme.md +61 -26
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/helpers.climode.ts +34 -1
- package/ts/helpers.workflow.ts +114 -7
- package/ts/mod_release/classes.releasejournal.ts +740 -0
- package/ts/mod_release/helpers.npmartifact.ts +555 -0
- package/ts/mod_release/helpers.releasebranch.ts +67 -5
- package/ts/mod_release/helpers.releasepublication.ts +641 -0
- package/ts/mod_release/index.ts +576 -411
package/ts/mod_release/index.ts
CHANGED
|
@@ -9,15 +9,12 @@ import {
|
|
|
9
9
|
readPendingChangelog,
|
|
10
10
|
} from "../helpers.changelog.js";
|
|
11
11
|
import {
|
|
12
|
-
formatTsdockerMinimumVersion,
|
|
13
|
-
getDeclaredTsdockerRange,
|
|
14
|
-
getTsdockerMinimumVersion,
|
|
15
12
|
resolveReleaseWorkflow,
|
|
16
|
-
|
|
13
|
+
resolveReleaseResumeConfiguration,
|
|
14
|
+
type IResolvedReleaseResumeConfiguration,
|
|
17
15
|
type IResolvedReleaseWorkflow,
|
|
18
16
|
} from "../helpers.workflow.js";
|
|
19
17
|
import * as commitHelpers from "../mod_commit/mod.helpers.js";
|
|
20
|
-
import { hasInstalledProjectTsdockerBinary } from "../helpers.tsdocker.js";
|
|
21
18
|
import {
|
|
22
19
|
inspectReleaseBranch,
|
|
23
20
|
integrateReleaseBranch,
|
|
@@ -28,16 +25,38 @@ import {
|
|
|
28
25
|
resolveReleaseMergeFlag,
|
|
29
26
|
type IReleaseBranchContext,
|
|
30
27
|
} from "./helpers.releasebranch.js";
|
|
28
|
+
import {
|
|
29
|
+
ReleaseJournalStore,
|
|
30
|
+
createInitialTargetStatus,
|
|
31
|
+
finalizeJournalCompletion,
|
|
32
|
+
normalizeReleaseGitRemoteName,
|
|
33
|
+
normalizeReleaseVersion,
|
|
34
|
+
resolveGitCommonDirectory,
|
|
35
|
+
type IReleaseArtifact,
|
|
36
|
+
type IReleaseJournal,
|
|
37
|
+
} from "./classes.releasejournal.js";
|
|
38
|
+
import {
|
|
39
|
+
assertNoLegacyNpmPublisher,
|
|
40
|
+
assertPnpmReleaseCapability,
|
|
41
|
+
hashReleaseDestination,
|
|
42
|
+
normalizeNpmRegistryUrl,
|
|
43
|
+
packNpmArtifact,
|
|
44
|
+
verifyStoredNpmArtifact,
|
|
45
|
+
} from "./helpers.npmartifact.js";
|
|
46
|
+
import { executeReleasePublication } from "./helpers.releasepublication.js";
|
|
31
47
|
|
|
32
|
-
|
|
48
|
+
export { buildReleaseGitPushArgs } from "./helpers.releasepublication.js";
|
|
33
49
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
export const run = async (argvArg: any): Promise<void> => {
|
|
51
|
+
try {
|
|
52
|
+
await runInternal(argvArg);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logger.log("error", error instanceof Error ? error.message : String(error));
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
39
58
|
|
|
40
|
-
|
|
59
|
+
const runInternal = async (argvArg: any): Promise<void> => {
|
|
41
60
|
const mode = await getCliMode(argvArg);
|
|
42
61
|
const subcommand = argvArg._?.[1];
|
|
43
62
|
|
|
@@ -46,6 +65,18 @@ export const run = async (argvArg: any) => {
|
|
|
46
65
|
return;
|
|
47
66
|
}
|
|
48
67
|
|
|
68
|
+
if (subcommand === "inspect") {
|
|
69
|
+
await runReleaseInspect(argvArg, mode);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (subcommand === "resume") {
|
|
73
|
+
await runReleaseResume(argvArg, mode);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (subcommand !== undefined) {
|
|
77
|
+
throw new Error(`Unknown release subcommand: ${subcommand}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
49
80
|
if (mode.json) {
|
|
50
81
|
printJson({
|
|
51
82
|
ok: false,
|
|
@@ -59,44 +90,37 @@ export const run = async (argvArg: any) => {
|
|
|
59
90
|
executor: "bash",
|
|
60
91
|
sourceFilePaths: [],
|
|
61
92
|
});
|
|
62
|
-
|
|
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
|
-
}
|
|
93
|
+
const mergeRequested = resolveReleaseMergeFlag(argvArg);
|
|
70
94
|
const workflow = await resolveReleaseWorkflow(argvArg);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
83
|
-
} catch (error) {
|
|
84
|
-
logger.log("error", error instanceof Error ? error.message : String(error));
|
|
85
|
-
process.exit(1);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
95
|
+
assertReleaseConfiguration(workflow);
|
|
96
|
+
let branchContext: IReleaseBranchContext = await inspectReleaseBranch({
|
|
97
|
+
smartshell: smartshellInstance,
|
|
98
|
+
cwd: paths.cwd,
|
|
99
|
+
gitRemote: workflow.gitRemote,
|
|
100
|
+
gitTargetActive: workflow.targets.includes("git"),
|
|
101
|
+
pushBranch: workflow.pushBranch,
|
|
102
|
+
pushTags: workflow.pushTags,
|
|
103
|
+
mergeRequested,
|
|
104
|
+
planMode: workflow.confirmation === "plan",
|
|
105
|
+
});
|
|
88
106
|
printReleasePlan(workflow, branchContext);
|
|
89
107
|
if (workflow.confirmation === "plan") {
|
|
90
108
|
return;
|
|
91
109
|
}
|
|
92
110
|
|
|
111
|
+
await assertFreshReleaseCapabilities(
|
|
112
|
+
smartshellInstance,
|
|
113
|
+
workflow,
|
|
114
|
+
branchContext.sourceCwd,
|
|
115
|
+
branchContext.pushUrl,
|
|
116
|
+
);
|
|
117
|
+
|
|
93
118
|
const pending = await readPendingChangelog(
|
|
94
119
|
plugins.path.join(branchContext.sourceCwd, workflow.changelogFile),
|
|
95
120
|
workflow.changelogPendingSection,
|
|
96
121
|
);
|
|
97
122
|
if (pending.isEmpty && !argvArg["allow-empty"] && !argvArg.allowEmpty) {
|
|
98
|
-
|
|
99
|
-
process.exit(1);
|
|
123
|
+
throw new Error("No pending changelog entries. Nothing to release.");
|
|
100
124
|
}
|
|
101
125
|
|
|
102
126
|
const versionType = resolveVersionType(argvArg, pending.block);
|
|
@@ -130,20 +154,10 @@ export const run = async (argvArg: any) => {
|
|
|
130
154
|
}
|
|
131
155
|
|
|
132
156
|
let newVersion = plannedVersion;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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;
|
|
146
|
-
}
|
|
157
|
+
branchContext = await prepareReleaseIntegration(
|
|
158
|
+
smartshellInstance,
|
|
159
|
+
branchContext,
|
|
160
|
+
);
|
|
147
161
|
if (workflow.runTests) {
|
|
148
162
|
await runCommandStep(
|
|
149
163
|
smartshellInstance,
|
|
@@ -157,33 +171,15 @@ export const run = async (argvArg: any) => {
|
|
|
157
171
|
let releasePushUrl = branchContext.pushUrl;
|
|
158
172
|
let expectedRemoteMainOid = branchContext.remoteMainOid;
|
|
159
173
|
if (branchContext.sourceBranch === "main") {
|
|
160
|
-
|
|
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
|
-
}
|
|
174
|
+
await revalidatePreparedReleaseBranch(smartshellInstance, branchContext);
|
|
170
175
|
} else {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
}
|
|
176
|
+
const integration = await integrateReleaseBranch(
|
|
177
|
+
smartshellInstance,
|
|
178
|
+
branchContext,
|
|
179
|
+
);
|
|
180
|
+
releaseCwd = integration.releaseCwd;
|
|
181
|
+
releasePushUrl = integration.pushUrl;
|
|
182
|
+
expectedRemoteMainOid = integration.expectedRemoteMainOid;
|
|
187
183
|
}
|
|
188
184
|
|
|
189
185
|
newVersion = await runVersionStep(projectType, versionType, releaseCwd);
|
|
@@ -220,81 +216,447 @@ export const run = async (argvArg: any) => {
|
|
|
220
216
|
);
|
|
221
217
|
}
|
|
222
218
|
|
|
223
|
-
const ensurePublicationState = async (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
219
|
+
const ensurePublicationState = async (
|
|
220
|
+
expectedRemoteMainOidArg = expectedRemoteMainOid,
|
|
221
|
+
): Promise<void> => {
|
|
222
|
+
await verifyReleaseRefs(
|
|
223
|
+
smartshellInstance,
|
|
224
|
+
releaseCwd,
|
|
225
|
+
newVersion,
|
|
226
|
+
releaseRefs,
|
|
227
|
+
);
|
|
228
|
+
await revalidateReleasePublicationState(
|
|
229
|
+
smartshellInstance,
|
|
230
|
+
branchContext,
|
|
231
|
+
releaseRefs.mainOid,
|
|
232
|
+
expectedRemoteMainOidArg,
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
await ensurePublicationState();
|
|
237
|
+
const journalContext = await createReleaseJournal({
|
|
238
|
+
smartshell: smartshellInstance,
|
|
239
|
+
workflow,
|
|
240
|
+
releaseCwd,
|
|
241
|
+
releasePushUrl,
|
|
242
|
+
expectedRemoteMainOid,
|
|
243
|
+
version: newVersion,
|
|
244
|
+
refs: releaseRefs,
|
|
245
|
+
ensurePublicationState,
|
|
246
|
+
});
|
|
247
|
+
const finalJournal = await executeReleasePublication({
|
|
248
|
+
smartshell: smartshellInstance,
|
|
249
|
+
cwd: releaseCwd,
|
|
250
|
+
store: journalContext.store,
|
|
251
|
+
journal: journalContext.journal,
|
|
252
|
+
pushUrl: releasePushUrl,
|
|
253
|
+
ensurePublicationState,
|
|
254
|
+
});
|
|
255
|
+
printReleaseJournalSummary(finalJournal);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const assertFreshReleaseCapabilities = async (
|
|
259
|
+
smartshellArg: plugins.smartshell.Smartshell,
|
|
260
|
+
workflowArg: IResolvedReleaseWorkflow,
|
|
261
|
+
cwdArg: string,
|
|
262
|
+
pushUrlArg?: string,
|
|
263
|
+
): Promise<void> => {
|
|
264
|
+
if (workflowArg.targets.includes("docker")) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"Journaled Docker publication requires the planned structured tSDocker digest capability. Disable the Docker target until that upstream release is installed.",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (workflowArg.targets.includes("git") && !pushUrlArg) {
|
|
270
|
+
throw new Error("The Git release target requires one resolved push destination.");
|
|
271
|
+
}
|
|
272
|
+
if (
|
|
273
|
+
workflowArg.targets.includes("git") ||
|
|
274
|
+
workflowArg.targets.includes("npm")
|
|
275
|
+
) {
|
|
276
|
+
await assertNoLegacyNpmPublisher(cwdArg);
|
|
277
|
+
}
|
|
278
|
+
if (workflowArg.targets.includes("npm")) {
|
|
279
|
+
await assertPnpmReleaseCapability(smartshellArg, cwdArg);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const assertReleaseConfiguration = (
|
|
284
|
+
workflowArg: IResolvedReleaseWorkflow,
|
|
285
|
+
): void => {
|
|
286
|
+
if (workflowArg.targets.includes("git")) {
|
|
287
|
+
normalizeReleaseGitRemoteName(workflowArg.gitRemote);
|
|
288
|
+
}
|
|
289
|
+
if (!workflowArg.targets.includes("npm")) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (workflowArg.npmRegistries.length === 0) {
|
|
293
|
+
throw new Error("The npm release target requires at least one registry.");
|
|
294
|
+
}
|
|
295
|
+
if (workflowArg.npmAccessLevel !== "public") {
|
|
296
|
+
throw new Error(
|
|
297
|
+
"Journaled npm publication currently requires public access so every destination can be verified anonymously.",
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (
|
|
301
|
+
workflowArg.npmAlreadyPublished !== "success" &&
|
|
302
|
+
workflowArg.npmAlreadyPublished !== "error"
|
|
303
|
+
) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
"release.targets.npm.alreadyPublished must be success or error.",
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const canonicalRegistries = workflowArg.npmRegistries.map(
|
|
309
|
+
normalizeNpmRegistryUrl,
|
|
310
|
+
);
|
|
311
|
+
if (
|
|
312
|
+
new Set(canonicalRegistries).size !== canonicalRegistries.length ||
|
|
313
|
+
canonicalRegistries.some(
|
|
314
|
+
(registryArg, indexArg) => registryArg !== workflowArg.npmRegistries[indexArg],
|
|
315
|
+
)
|
|
316
|
+
) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
"npm release registries must be unique canonical credential-free http(s) URLs.",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
interface ICreateReleaseJournalOptions {
|
|
324
|
+
smartshell: plugins.smartshell.Smartshell;
|
|
325
|
+
workflow: IResolvedReleaseWorkflow;
|
|
326
|
+
releaseCwd: string;
|
|
327
|
+
releasePushUrl?: string;
|
|
328
|
+
expectedRemoteMainOid?: string;
|
|
329
|
+
version: string;
|
|
330
|
+
refs: IReleaseRefs;
|
|
331
|
+
ensurePublicationState: (expectedRemoteMainOidArg?: string) => Promise<void>;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const createReleaseJournal = async (
|
|
335
|
+
optionsArg: ICreateReleaseJournalOptions,
|
|
336
|
+
): Promise<{ store: ReleaseJournalStore; journal: IReleaseJournal }> => {
|
|
337
|
+
const gitCommonDirectory = await resolveGitCommonDirectory(
|
|
338
|
+
optionsArg.smartshell,
|
|
339
|
+
optionsArg.releaseCwd,
|
|
340
|
+
);
|
|
341
|
+
const store = new ReleaseJournalStore(gitCommonDirectory);
|
|
342
|
+
const releaseDirectory = store.getReleaseDirectory(optionsArg.version);
|
|
343
|
+
const releaseDirectoryExists = await plugins.fs
|
|
344
|
+
.lstat(releaseDirectory)
|
|
345
|
+
.then(() => true)
|
|
346
|
+
.catch((error: NodeJS.ErrnoException) => {
|
|
347
|
+
if (error.code === "ENOENT") return false;
|
|
348
|
+
throw error;
|
|
349
|
+
});
|
|
350
|
+
if (releaseDirectoryExists) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`Release journal v${optionsArg.version} already exists. Inspect or resume it instead of regenerating artifacts.`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let temporaryDirectory = await store.createTemporaryDirectory(optionsArg.version);
|
|
357
|
+
try {
|
|
358
|
+
let artifact: IReleaseArtifact | null = null;
|
|
359
|
+
if (optionsArg.workflow.targets.includes("npm")) {
|
|
360
|
+
artifact = await packNpmArtifact(
|
|
361
|
+
optionsArg.smartshell,
|
|
362
|
+
optionsArg.releaseCwd,
|
|
363
|
+
temporaryDirectory,
|
|
364
|
+
optionsArg.version,
|
|
236
365
|
);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
"
|
|
241
|
-
error instanceof Error ? error.message : String(error),
|
|
366
|
+
await verifyCleanTree(
|
|
367
|
+
optionsArg.smartshell,
|
|
368
|
+
optionsArg.releaseCwd,
|
|
369
|
+
"npm pack lifecycle scripts changed the release worktree. Aborting release.",
|
|
242
370
|
);
|
|
243
|
-
|
|
244
|
-
return false;
|
|
371
|
+
await optionsArg.ensurePublicationState();
|
|
245
372
|
}
|
|
246
|
-
};
|
|
247
373
|
|
|
248
|
-
|
|
374
|
+
const gitEnabled = optionsArg.workflow.targets.includes("git");
|
|
375
|
+
if (
|
|
376
|
+
gitEnabled &&
|
|
377
|
+
(!optionsArg.releasePushUrl || !optionsArg.expectedRemoteMainOid)
|
|
378
|
+
) {
|
|
379
|
+
throw new Error("Git release destination state is incomplete.");
|
|
380
|
+
}
|
|
381
|
+
const now = new Date().toISOString();
|
|
382
|
+
const gitStatus = createInitialTargetStatus(gitEnabled);
|
|
383
|
+
const journal = finalizeJournalCompletion({
|
|
384
|
+
kind: "gitzone-release-journal",
|
|
385
|
+
schemaVersion: 1,
|
|
386
|
+
revision: 1,
|
|
387
|
+
release: {
|
|
388
|
+
version: optionsArg.version,
|
|
389
|
+
tag: `v${optionsArg.version}`,
|
|
390
|
+
mainOid: optionsArg.refs.mainOid,
|
|
391
|
+
tagOid: optionsArg.refs.tagOid,
|
|
392
|
+
},
|
|
393
|
+
artifact,
|
|
394
|
+
git: {
|
|
395
|
+
state: gitStatus.state,
|
|
396
|
+
attempts: gitStatus.attempts,
|
|
397
|
+
attempt: gitStatus.attempt,
|
|
398
|
+
error: gitStatus.error,
|
|
399
|
+
remote: gitEnabled ? optionsArg.workflow.gitRemote : null,
|
|
400
|
+
destinationHash: gitEnabled
|
|
401
|
+
? hashReleaseDestination(optionsArg.releasePushUrl as string)
|
|
402
|
+
: null,
|
|
403
|
+
expectedRemoteMainOid: gitEnabled
|
|
404
|
+
? (optionsArg.expectedRemoteMainOid as string)
|
|
405
|
+
: null,
|
|
406
|
+
},
|
|
407
|
+
npm: {
|
|
408
|
+
access: "public",
|
|
409
|
+
tag: "latest",
|
|
410
|
+
alreadyPublished: optionsArg.workflow.targets.includes("npm")
|
|
411
|
+
? optionsArg.workflow.npmAlreadyPublished
|
|
412
|
+
: "success",
|
|
413
|
+
registries: optionsArg.workflow.targets.includes("npm")
|
|
414
|
+
? optionsArg.workflow.npmRegistries.map((registryArg) => {
|
|
415
|
+
const status = createInitialTargetStatus(true);
|
|
416
|
+
return {
|
|
417
|
+
state: status.state,
|
|
418
|
+
attempts: status.attempts,
|
|
419
|
+
attempt: status.attempt,
|
|
420
|
+
error: status.error,
|
|
421
|
+
registry: registryArg,
|
|
422
|
+
};
|
|
423
|
+
})
|
|
424
|
+
: [],
|
|
425
|
+
},
|
|
426
|
+
createdAt: now,
|
|
427
|
+
updatedAt: now,
|
|
428
|
+
completedAt: null,
|
|
429
|
+
}, now);
|
|
430
|
+
const installed = await store.installPrepared(temporaryDirectory, journal);
|
|
431
|
+
temporaryDirectory = "";
|
|
432
|
+
return { store, journal: installed };
|
|
433
|
+
} finally {
|
|
434
|
+
if (temporaryDirectory) {
|
|
435
|
+
await plugins.fs.rm(temporaryDirectory, { recursive: true, force: true });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const runReleaseInspect = async (
|
|
441
|
+
argvArg: any,
|
|
442
|
+
modeArg: ICliMode,
|
|
443
|
+
): Promise<void> => {
|
|
444
|
+
if (argvArg._?.length > 3) {
|
|
445
|
+
throw new Error("Usage: gitzone release inspect [version] [--json]");
|
|
446
|
+
}
|
|
447
|
+
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
448
|
+
executor: "bash",
|
|
449
|
+
sourceFilePaths: [],
|
|
450
|
+
});
|
|
451
|
+
const store = new ReleaseJournalStore(
|
|
452
|
+
await resolveGitCommonDirectory(smartshellInstance, paths.cwd),
|
|
453
|
+
);
|
|
454
|
+
const requestedVersion = argvArg._?.[2];
|
|
455
|
+
const journals = requestedVersion
|
|
456
|
+
? [await store.read(normalizeReleaseVersion(requestedVersion))]
|
|
457
|
+
: await store.list();
|
|
458
|
+
if (modeArg.json) {
|
|
459
|
+
printJson({ ok: true, journals });
|
|
249
460
|
return;
|
|
250
461
|
}
|
|
462
|
+
if (journals.length === 0) {
|
|
463
|
+
console.log("No release journals found.");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
for (const journal of journals) {
|
|
467
|
+
printReleaseJournalSummary(journal);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
251
470
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
471
|
+
const assertResumeArguments = (argvArg: any): void => {
|
|
472
|
+
const forbiddenKeys = [
|
|
473
|
+
"allow-empty",
|
|
474
|
+
"allowEmpty",
|
|
475
|
+
"b",
|
|
476
|
+
"build",
|
|
477
|
+
"docker",
|
|
478
|
+
"git",
|
|
479
|
+
"major",
|
|
480
|
+
"merge",
|
|
481
|
+
"minor",
|
|
482
|
+
"npm",
|
|
483
|
+
"p",
|
|
484
|
+
"patch",
|
|
485
|
+
"plan",
|
|
486
|
+
"publish",
|
|
487
|
+
"push",
|
|
488
|
+
"t",
|
|
489
|
+
"target",
|
|
490
|
+
"targets",
|
|
491
|
+
"test",
|
|
492
|
+
];
|
|
493
|
+
if (
|
|
494
|
+
forbiddenKeys.some((keyArg) =>
|
|
495
|
+
Object.prototype.hasOwnProperty.call(argvArg, keyArg),
|
|
496
|
+
)
|
|
497
|
+
) {
|
|
498
|
+
throw new Error(
|
|
499
|
+
"release resume rejects fresh-release target, integration, build, test, and version overrides.",
|
|
263
500
|
);
|
|
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;
|
|
270
501
|
}
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
npmResults.push(
|
|
276
|
-
...(await runNpmTarget(smartshellInstance, workflow, releaseCwd)),
|
|
502
|
+
if (argvArg._?.length !== 3) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
"Usage: gitzone release resume <version> [-y] [--recover-attempt=<id>]",
|
|
277
505
|
);
|
|
278
506
|
}
|
|
279
|
-
|
|
280
|
-
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const runReleaseResume = async (
|
|
510
|
+
argvArg: any,
|
|
511
|
+
modeArg: ICliMode,
|
|
512
|
+
): Promise<void> => {
|
|
513
|
+
assertResumeArguments(argvArg);
|
|
514
|
+
if (modeArg.json) {
|
|
515
|
+
throw new Error("JSON output is read-only. Use release inspect --json.");
|
|
516
|
+
}
|
|
517
|
+
const recoveryAttemptId =
|
|
518
|
+
argvArg["recover-attempt"] || argvArg.recoverAttempt;
|
|
519
|
+
if (
|
|
520
|
+
recoveryAttemptId !== undefined &&
|
|
521
|
+
(typeof recoveryAttemptId !== "string" ||
|
|
522
|
+
!/^[0-9a-f]{32}$/.test(recoveryAttemptId))
|
|
523
|
+
) {
|
|
524
|
+
throw new Error("--recover-attempt requires one exact 32-character owner ID.");
|
|
525
|
+
}
|
|
526
|
+
const version = normalizeReleaseVersion(argvArg._[2]);
|
|
527
|
+
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
528
|
+
executor: "bash",
|
|
529
|
+
sourceFilePaths: [],
|
|
530
|
+
});
|
|
531
|
+
const store = new ReleaseJournalStore(
|
|
532
|
+
await resolveGitCommonDirectory(smartshellInstance, paths.cwd),
|
|
533
|
+
);
|
|
534
|
+
const journal = await store.read(version);
|
|
535
|
+
const gitEnabled = journal.git.state !== "skipped";
|
|
536
|
+
const resumeConfiguration = await resolveReleaseResumeConfiguration({
|
|
537
|
+
git: gitEnabled,
|
|
538
|
+
npm: journal.npm.registries.length > 0,
|
|
539
|
+
});
|
|
540
|
+
await assertResumeConfiguration(resumeConfiguration, journal, paths.cwd);
|
|
541
|
+
if (journal.artifact) {
|
|
542
|
+
await assertPnpmReleaseCapability(smartshellInstance, paths.cwd);
|
|
543
|
+
await assertNoLegacyNpmPublisher(paths.cwd);
|
|
544
|
+
await verifyStoredNpmArtifact(store.getArtifactPath(version), journal.artifact);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const branchContext = await inspectReleaseBranch({
|
|
548
|
+
smartshell: smartshellInstance,
|
|
549
|
+
cwd: paths.cwd,
|
|
550
|
+
gitRemote: journal.git.remote || resumeConfiguration.gitRemote,
|
|
551
|
+
gitTargetActive: gitEnabled,
|
|
552
|
+
pushBranch: true,
|
|
553
|
+
pushTags: true,
|
|
554
|
+
mergeRequested: false,
|
|
555
|
+
planMode: false,
|
|
556
|
+
});
|
|
557
|
+
if (branchContext.sourceOid !== journal.release.mainOid) {
|
|
558
|
+
throw new Error("Resume must run from the journaled release commit on main.");
|
|
559
|
+
}
|
|
560
|
+
const refs = await captureReleaseRefs(
|
|
561
|
+
smartshellInstance,
|
|
562
|
+
paths.cwd,
|
|
563
|
+
version,
|
|
564
|
+
journal.release.mainOid,
|
|
565
|
+
);
|
|
566
|
+
if (refs.tagOid !== journal.release.tagOid) {
|
|
567
|
+
throw new Error("The local release tag no longer matches the journal.");
|
|
568
|
+
}
|
|
569
|
+
const pushUrl = gitEnabled ? branchContext.pushUrl : undefined;
|
|
570
|
+
if (
|
|
571
|
+
gitEnabled &&
|
|
572
|
+
(!pushUrl || hashReleaseDestination(pushUrl) !== journal.git.destinationHash)
|
|
573
|
+
) {
|
|
574
|
+
throw new Error("The Git push destination changed after journal creation.");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!modeArg.yes) {
|
|
578
|
+
if (!modeArg.interactive) {
|
|
579
|
+
throw new Error("Release resume requires an interactive terminal or -y.");
|
|
580
|
+
}
|
|
581
|
+
const confirmed =
|
|
582
|
+
await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
|
583
|
+
`Resume release v${version} now?`,
|
|
584
|
+
true,
|
|
585
|
+
);
|
|
586
|
+
if (!confirmed) {
|
|
587
|
+
logger.log("info", "Release resume cancelled.");
|
|
281
588
|
return;
|
|
282
589
|
}
|
|
283
|
-
dockerResults.push(
|
|
284
|
-
...(await runDockerTarget(smartshellInstance, workflow, releaseCwd)),
|
|
285
|
-
);
|
|
286
590
|
}
|
|
287
591
|
|
|
288
|
-
|
|
592
|
+
const ensurePublicationState = async (
|
|
593
|
+
expectedRemoteMainOidArg?: string,
|
|
594
|
+
): Promise<void> => {
|
|
595
|
+
await verifyReleaseRefs(smartshellInstance, paths.cwd, version, refs);
|
|
596
|
+
await revalidateReleasePublicationState(
|
|
597
|
+
smartshellInstance,
|
|
598
|
+
branchContext,
|
|
599
|
+
journal.release.mainOid,
|
|
600
|
+
expectedRemoteMainOidArg,
|
|
601
|
+
);
|
|
602
|
+
};
|
|
603
|
+
const finalJournal = await executeReleasePublication({
|
|
604
|
+
smartshell: smartshellInstance,
|
|
605
|
+
cwd: paths.cwd,
|
|
606
|
+
store,
|
|
607
|
+
journal,
|
|
608
|
+
pushUrl,
|
|
609
|
+
recoveryAttemptId,
|
|
610
|
+
ensurePublicationState,
|
|
611
|
+
});
|
|
612
|
+
printReleaseJournalSummary(finalJournal);
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const assertResumeConfiguration = async (
|
|
616
|
+
workflowArg: IResolvedReleaseResumeConfiguration,
|
|
617
|
+
journalArg: IReleaseJournal,
|
|
618
|
+
cwdArg: string,
|
|
619
|
+
): Promise<void> => {
|
|
289
620
|
if (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
)
|
|
621
|
+
journalArg.git.state !== "skipped" &&
|
|
622
|
+
workflowArg.gitRemote !== journalArg.git.remote
|
|
293
623
|
) {
|
|
294
|
-
|
|
624
|
+
throw new Error("Current Git release remote does not match the journal.");
|
|
625
|
+
}
|
|
626
|
+
const currentRegistries = workflowArg.npmRegistries.map(
|
|
627
|
+
normalizeNpmRegistryUrl,
|
|
628
|
+
);
|
|
629
|
+
if (
|
|
630
|
+
journalArg.npm.registries.length > 0 &&
|
|
631
|
+
(JSON.stringify(currentRegistries) !==
|
|
632
|
+
JSON.stringify(
|
|
633
|
+
journalArg.npm.registries.map((registryArg) => registryArg.registry),
|
|
634
|
+
) ||
|
|
635
|
+
workflowArg.npmAccessLevel !== journalArg.npm.access ||
|
|
636
|
+
workflowArg.npmAlreadyPublished !== journalArg.npm.alreadyPublished)
|
|
637
|
+
) {
|
|
638
|
+
throw new Error("Current npm release configuration does not match the journal.");
|
|
639
|
+
}
|
|
640
|
+
if (journalArg.git.state !== "skipped" || journalArg.artifact) {
|
|
641
|
+
await assertNoLegacyNpmPublisher(cwdArg);
|
|
295
642
|
}
|
|
296
643
|
};
|
|
297
644
|
|
|
645
|
+
const printReleaseJournalSummary = (journalArg: IReleaseJournal): void => {
|
|
646
|
+
console.log("");
|
|
647
|
+
console.log(`Release v${journalArg.release.version}`);
|
|
648
|
+
console.log(`journal revision: ${journalArg.revision}`);
|
|
649
|
+
console.log(
|
|
650
|
+
`artifact: ${journalArg.artifact ? `${journalArg.artifact.integrity} (${journalArg.artifact.sha256})` : "none"}`,
|
|
651
|
+
);
|
|
652
|
+
console.log(`git: ${journalArg.git.state}`);
|
|
653
|
+
for (const registry of journalArg.npm.registries) {
|
|
654
|
+
console.log(`npm ${registry.registry}: ${registry.state}`);
|
|
655
|
+
}
|
|
656
|
+
console.log(`completed: ${journalArg.completedAt || "no"}`);
|
|
657
|
+
console.log("");
|
|
658
|
+
};
|
|
659
|
+
|
|
298
660
|
function resolveVersionType(
|
|
299
661
|
argvArg: any,
|
|
300
662
|
pendingBlock: string,
|
|
@@ -314,8 +676,7 @@ async function runCommandStep(
|
|
|
314
676
|
console.log(`\n${label}`);
|
|
315
677
|
const result = await smartshellInstance.exec(command, { cwd: cwdArg });
|
|
316
678
|
if (result.exitCode !== 0) {
|
|
317
|
-
|
|
318
|
-
process.exit(1);
|
|
679
|
+
throw new Error(`${label} failed. Aborting release.`);
|
|
319
680
|
}
|
|
320
681
|
logger.log("success", `${label} passed.`);
|
|
321
682
|
}
|
|
@@ -328,12 +689,15 @@ async function verifyCleanTree(
|
|
|
328
689
|
const statusResult = await smartshellInstance.execSpawn(
|
|
329
690
|
"git",
|
|
330
691
|
["status", "--porcelain"],
|
|
331
|
-
{
|
|
692
|
+
{
|
|
693
|
+
cwd: cwdArg,
|
|
694
|
+
env: releaseGitEnv,
|
|
695
|
+
timeout: 60_000,
|
|
696
|
+
timeoutKillGraceMs: 5_000,
|
|
697
|
+
},
|
|
332
698
|
);
|
|
333
699
|
if (statusResult.stdout.trim() !== "") {
|
|
334
|
-
|
|
335
|
-
console.log(statusResult.stdout);
|
|
336
|
-
process.exit(1);
|
|
700
|
+
throw new Error(errorMessage);
|
|
337
701
|
}
|
|
338
702
|
}
|
|
339
703
|
|
|
@@ -385,6 +749,8 @@ async function runReleaseCommitStep(
|
|
|
385
749
|
const addResult = await smartshellInstance.execSpawn("git", ["add", "-A"], {
|
|
386
750
|
cwd: cwdArg,
|
|
387
751
|
env: releaseGitEnv,
|
|
752
|
+
timeout: 60_000,
|
|
753
|
+
timeoutKillGraceMs: 5_000,
|
|
388
754
|
});
|
|
389
755
|
if (addResult.exitCode !== 0) {
|
|
390
756
|
throw new Error("Staging release metadata failed.");
|
|
@@ -392,7 +758,12 @@ async function runReleaseCommitStep(
|
|
|
392
758
|
const result = await smartshellInstance.execSpawn(
|
|
393
759
|
"git",
|
|
394
760
|
["commit", "-m", `v${newVersion}`],
|
|
395
|
-
{
|
|
761
|
+
{
|
|
762
|
+
cwd: cwdArg,
|
|
763
|
+
env: releaseGitEnv,
|
|
764
|
+
timeout: 60_000,
|
|
765
|
+
timeoutKillGraceMs: 5_000,
|
|
766
|
+
},
|
|
396
767
|
);
|
|
397
768
|
if (result.exitCode !== 0) {
|
|
398
769
|
throw new Error("Release commit failed.");
|
|
@@ -400,7 +771,13 @@ async function runReleaseCommitStep(
|
|
|
400
771
|
const branchResult = await smartshellInstance.execSpawn(
|
|
401
772
|
"git",
|
|
402
773
|
["symbolic-ref", "--quiet", "HEAD"],
|
|
403
|
-
{
|
|
774
|
+
{
|
|
775
|
+
cwd: cwdArg,
|
|
776
|
+
env: releaseGitEnv,
|
|
777
|
+
silent: true,
|
|
778
|
+
timeout: 60_000,
|
|
779
|
+
timeoutKillGraceMs: 5_000,
|
|
780
|
+
},
|
|
404
781
|
);
|
|
405
782
|
if (
|
|
406
783
|
branchResult.exitCode !== 0 ||
|
|
@@ -440,7 +817,12 @@ async function runTagStep(
|
|
|
440
817
|
`v${newVersion}`,
|
|
441
818
|
releaseCommitOidArg,
|
|
442
819
|
],
|
|
443
|
-
{
|
|
820
|
+
{
|
|
821
|
+
cwd: cwdArg,
|
|
822
|
+
env: releaseGitEnv,
|
|
823
|
+
timeout: 60_000,
|
|
824
|
+
timeoutKillGraceMs: 5_000,
|
|
825
|
+
},
|
|
444
826
|
);
|
|
445
827
|
if (result.exitCode !== 0) {
|
|
446
828
|
throw new Error("Release tag failed.");
|
|
@@ -460,7 +842,13 @@ const resolveGitObjectOid = async (
|
|
|
460
842
|
const result = await smartshellInstanceArg.execSpawn(
|
|
461
843
|
"git",
|
|
462
844
|
["rev-parse", "--verify", revisionArg],
|
|
463
|
-
{
|
|
845
|
+
{
|
|
846
|
+
cwd: cwdArg,
|
|
847
|
+
env: releaseGitEnv,
|
|
848
|
+
silent: true,
|
|
849
|
+
timeout: 60_000,
|
|
850
|
+
timeoutKillGraceMs: 5_000,
|
|
851
|
+
},
|
|
464
852
|
);
|
|
465
853
|
const oid = result.stdout.trim();
|
|
466
854
|
if (result.exitCode !== 0 || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(oid)) {
|
|
@@ -523,208 +911,6 @@ const verifyReleaseRefs = async (
|
|
|
523
911
|
}
|
|
524
912
|
};
|
|
525
913
|
|
|
526
|
-
async function runGitTarget(
|
|
527
|
-
smartshellInstance: plugins.smartshell.Smartshell,
|
|
528
|
-
workflow: IResolvedReleaseWorkflow,
|
|
529
|
-
cwdArg: string,
|
|
530
|
-
newVersionArg: string,
|
|
531
|
-
releaseRefsArg: IReleaseRefs,
|
|
532
|
-
pushUrlArg?: string,
|
|
533
|
-
expectedRemoteMainOidArg?: string,
|
|
534
|
-
): Promise<ITargetResult[]> {
|
|
535
|
-
const destination = pushUrlArg || workflow.gitRemote;
|
|
536
|
-
const targets: string[] = [];
|
|
537
|
-
const refspecs: string[] = [];
|
|
538
|
-
if (workflow.pushBranch) {
|
|
539
|
-
targets.push(`${workflow.gitRemote}/main`);
|
|
540
|
-
refspecs.push("refs/heads/main:refs/heads/main");
|
|
541
|
-
}
|
|
542
|
-
if (workflow.pushTags) {
|
|
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 [];
|
|
548
|
-
}
|
|
549
|
-
|
|
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`);
|
|
589
|
-
}
|
|
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;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
async function runNpmTarget(
|
|
607
|
-
smartshellInstance: plugins.smartshell.Smartshell,
|
|
608
|
-
workflow: IResolvedReleaseWorkflow,
|
|
609
|
-
cwdArg: string,
|
|
610
|
-
): Promise<ITargetResult[]> {
|
|
611
|
-
if (!workflow.npmEnabled) {
|
|
612
|
-
return [{ target: "npm", status: "skipped", message: "disabled" }];
|
|
613
|
-
}
|
|
614
|
-
if (workflow.npmRegistries.length === 0) {
|
|
615
|
-
return [
|
|
616
|
-
{ target: "npm", status: "failed", message: "no registries configured" },
|
|
617
|
-
];
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const results: ITargetResult[] = [];
|
|
621
|
-
for (const registry of workflow.npmRegistries) {
|
|
622
|
-
const command = `pnpm publish --registry=${registry} --access=${workflow.npmAccessLevel}`;
|
|
623
|
-
const result = await smartshellInstance.exec(command, { cwd: cwdArg });
|
|
624
|
-
const output = result.combinedOutput;
|
|
625
|
-
if (result.exitCode === 0) {
|
|
626
|
-
results.push({ target: registry, status: "success" });
|
|
627
|
-
} else if (
|
|
628
|
-
isAlreadyPublishedOutput(output) &&
|
|
629
|
-
workflow.npmAlreadyPublished === "success"
|
|
630
|
-
) {
|
|
631
|
-
results.push({ target: registry, status: "already-published" });
|
|
632
|
-
} else {
|
|
633
|
-
results.push({
|
|
634
|
-
target: registry,
|
|
635
|
-
status: "failed",
|
|
636
|
-
message: firstMeaningfulLine(output),
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
return results;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
async function runDockerTarget(
|
|
644
|
-
smartshellInstance: plugins.smartshell.Smartshell,
|
|
645
|
-
workflow: IResolvedReleaseWorkflow,
|
|
646
|
-
cwdArg: string,
|
|
647
|
-
): Promise<ITargetResult[]> {
|
|
648
|
-
if (!workflow.dockerEnabled) {
|
|
649
|
-
return [{ target: "docker", status: "skipped", message: "disabled" }];
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
let packageJson: unknown;
|
|
653
|
-
try {
|
|
654
|
-
packageJson = JSON.parse(
|
|
655
|
-
await plugins.fs.readFile(
|
|
656
|
-
plugins.path.join(cwdArg, "package.json"),
|
|
657
|
-
"utf8",
|
|
658
|
-
),
|
|
659
|
-
);
|
|
660
|
-
} catch {
|
|
661
|
-
packageJson = undefined;
|
|
662
|
-
}
|
|
663
|
-
if (!getDeclaredTsdockerRange(packageJson)) {
|
|
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
|
-
];
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
const minimumTsdockerVersion = getTsdockerMinimumVersion(
|
|
685
|
-
workflow.dockerBuildRegistries.length > 0,
|
|
686
|
-
workflow.dockerTest,
|
|
687
|
-
);
|
|
688
|
-
if (minimumTsdockerVersion) {
|
|
689
|
-
const versionResult = await smartshellInstance.exec(
|
|
690
|
-
"pnpm exec tsdocker --version",
|
|
691
|
-
{ cwd: cwdArg },
|
|
692
|
-
);
|
|
693
|
-
if (
|
|
694
|
-
versionResult.exitCode !== 0 ||
|
|
695
|
-
!supportsTsdockerMinimumVersion(
|
|
696
|
-
versionResult.combinedOutput,
|
|
697
|
-
minimumTsdockerVersion,
|
|
698
|
-
)
|
|
699
|
-
) {
|
|
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
|
-
];
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const command = buildTsdockerPushCommand(workflow);
|
|
714
|
-
const result = await smartshellInstance.exec(command, { cwd: cwdArg });
|
|
715
|
-
const output = result.combinedOutput;
|
|
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
|
-
];
|
|
726
|
-
}
|
|
727
|
-
|
|
728
914
|
type TDockerCommandWorkflow = Pick<
|
|
729
915
|
IResolvedReleaseWorkflow,
|
|
730
916
|
| "dockerRegistry"
|
|
@@ -776,21 +962,6 @@ export function buildTsdockerPushCommand(
|
|
|
776
962
|
return commandParts.join(" ");
|
|
777
963
|
}
|
|
778
964
|
|
|
779
|
-
function isAlreadyPublishedOutput(output: string): boolean {
|
|
780
|
-
return /previously published versions|cannot publish over|already exists/i.test(
|
|
781
|
-
output,
|
|
782
|
-
);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function firstMeaningfulLine(output: string): string {
|
|
786
|
-
return (
|
|
787
|
-
output
|
|
788
|
-
.split("\n")
|
|
789
|
-
.map((line) => line.trim())
|
|
790
|
-
.find((line) => line.length > 0) || "command failed"
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
965
|
function shellQuote(value: string): string {
|
|
795
966
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
796
967
|
}
|
|
@@ -858,49 +1029,11 @@ function formatDockerOptions(workflow: IResolvedReleaseWorkflow): string {
|
|
|
858
1029
|
return options.length > 0 ? options.join(", ") : "default";
|
|
859
1030
|
}
|
|
860
1031
|
|
|
861
|
-
function printReleaseSummary(
|
|
862
|
-
newVersion: string,
|
|
863
|
-
gitResults: ITargetResult[],
|
|
864
|
-
npmResults: ITargetResult[],
|
|
865
|
-
dockerResults: ITargetResult[],
|
|
866
|
-
): void {
|
|
867
|
-
console.log("");
|
|
868
|
-
console.log(`Release v${newVersion}`);
|
|
869
|
-
console.log("");
|
|
870
|
-
|
|
871
|
-
if (gitResults.length > 0) {
|
|
872
|
-
console.log("git:");
|
|
873
|
-
for (const result of gitResults) {
|
|
874
|
-
console.log(
|
|
875
|
-
` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`,
|
|
876
|
-
);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
if (npmResults.length > 0) {
|
|
881
|
-
console.log("npm:");
|
|
882
|
-
for (const result of npmResults) {
|
|
883
|
-
console.log(
|
|
884
|
-
` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`,
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
if (dockerResults.length > 0) {
|
|
890
|
-
console.log("docker:");
|
|
891
|
-
for (const result of dockerResults) {
|
|
892
|
-
console.log(
|
|
893
|
-
` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`,
|
|
894
|
-
);
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
|
|
899
1032
|
export function showHelp(mode?: ICliMode): void {
|
|
900
1033
|
if (mode?.json) {
|
|
901
1034
|
printJson({
|
|
902
1035
|
command: "release",
|
|
903
|
-
usage: "gitzone release [options]",
|
|
1036
|
+
usage: "gitzone release [inspect [version]|resume <version>] [options]",
|
|
904
1037
|
description:
|
|
905
1038
|
"Creates a versioned release from pending changelog entries and publishes configured artifacts.",
|
|
906
1039
|
flags: [
|
|
@@ -918,16 +1051,20 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
918
1051
|
flag: "--no-build",
|
|
919
1052
|
description: "Disable the post-metadata release build",
|
|
920
1053
|
},
|
|
921
|
-
{ flag: "-p, --push", description: "
|
|
1054
|
+
{ flag: "-p, --push", description: "Explicitly select the git target" },
|
|
922
1055
|
{
|
|
923
1056
|
flag: "--target <names>",
|
|
924
|
-
description:
|
|
1057
|
+
description:
|
|
1058
|
+
"Replace configured targets with a non-empty subset of git,npm,docker",
|
|
1059
|
+
},
|
|
1060
|
+
{ flag: "--npm", description: "Explicitly select the npm target" },
|
|
1061
|
+
{
|
|
1062
|
+
flag: "--docker",
|
|
1063
|
+
description: "Select Docker and fail closed under journal schema v1",
|
|
925
1064
|
},
|
|
926
|
-
{ flag: "--npm", description: "Enable the npm release target" },
|
|
927
|
-
{ flag: "--docker", description: "Enable the tsdocker release target" },
|
|
928
1065
|
{
|
|
929
1066
|
flag: "--no-publish",
|
|
930
|
-
description: "
|
|
1067
|
+
description: "Remove npm and Docker without implicitly enabling Git",
|
|
931
1068
|
},
|
|
932
1069
|
{
|
|
933
1070
|
flag: "--merge",
|
|
@@ -939,13 +1076,28 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
939
1076
|
description:
|
|
940
1077
|
"Show the workflow without fetching or mutating refs, files, the index, or worktrees",
|
|
941
1078
|
},
|
|
1079
|
+
{
|
|
1080
|
+
flag: "inspect [version]",
|
|
1081
|
+
description: "Read one or all durable release journals without mutation",
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
flag: "resume <version>",
|
|
1085
|
+
description: "Resume exact Git/npm publication without rebuilding or repacking",
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
flag: "--recover-attempt <id>",
|
|
1089
|
+
description:
|
|
1090
|
+
"Recover one exact 32-character hexadecimal attempt ID after proving it stopped",
|
|
1091
|
+
},
|
|
942
1092
|
],
|
|
943
1093
|
});
|
|
944
1094
|
return;
|
|
945
1095
|
}
|
|
946
1096
|
|
|
947
1097
|
console.log("");
|
|
948
|
-
console.log(
|
|
1098
|
+
console.log(
|
|
1099
|
+
"Usage: gitzone release [inspect [version]|resume <version>] [options]",
|
|
1100
|
+
);
|
|
949
1101
|
console.log("");
|
|
950
1102
|
console.log("Creates a versioned release from changelog Pending entries.");
|
|
951
1103
|
console.log("");
|
|
@@ -956,13 +1108,17 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
956
1108
|
" -b, --build Build after creating local release metadata",
|
|
957
1109
|
);
|
|
958
1110
|
console.log(" --no-build Disable the post-metadata release build");
|
|
959
|
-
console.log(" -p, --push
|
|
1111
|
+
console.log(" -p, --push Explicitly select the git target");
|
|
960
1112
|
console.log(
|
|
961
|
-
" --target <names>
|
|
1113
|
+
" --target <names> Replace configured targets with a non-empty subset of git,npm,docker",
|
|
1114
|
+
);
|
|
1115
|
+
console.log(" --npm Explicitly select the npm target");
|
|
1116
|
+
console.log(
|
|
1117
|
+
" --docker Select Docker (fails closed under journal schema v1)",
|
|
1118
|
+
);
|
|
1119
|
+
console.log(
|
|
1120
|
+
" --no-publish Remove npm and Docker without implicitly enabling Git",
|
|
962
1121
|
);
|
|
963
|
-
console.log(" --npm Enable the npm release target");
|
|
964
|
-
console.log(" --docker Enable the tsdocker release target");
|
|
965
|
-
console.log(" --no-publish Run release core and git target only");
|
|
966
1122
|
console.log(
|
|
967
1123
|
" --merge Fast-forward and lease-push a cleanly rebased feature branch before release metadata",
|
|
968
1124
|
);
|
|
@@ -970,5 +1126,14 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
970
1126
|
console.log(
|
|
971
1127
|
" --plan Show workflow without fetching or mutating refs, files, index, or worktrees",
|
|
972
1128
|
);
|
|
1129
|
+
console.log(
|
|
1130
|
+
" inspect [version] Read one or all durable release journals without mutation",
|
|
1131
|
+
);
|
|
1132
|
+
console.log(
|
|
1133
|
+
" resume <version> Resume exact Git/npm publication without rebuilding or repacking",
|
|
1134
|
+
);
|
|
1135
|
+
console.log(
|
|
1136
|
+
" --recover-attempt Recover one exact 32-character hexadecimal attempt ID after proving its publisher stopped",
|
|
1137
|
+
);
|
|
973
1138
|
console.log("");
|
|
974
1139
|
}
|