@git.zone/cli 4.0.0 → 6.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/.smartconfig.json +0 -1
- 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 -2
- package/dist_ts/helpers.workflow.js +102 -19
- package/dist_ts/mod_commit/mod.helpers.d.ts +3 -3
- package/dist_ts/mod_commit/mod.helpers.js +10 -10
- package/dist_ts/mod_config/index.js +22 -23
- 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 +358 -0
- package/dist_ts/mod_release/helpers.releasebranch.d.ts +47 -0
- package/dist_ts/mod_release/helpers.releasebranch.js +627 -0
- 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 -1
- package/dist_ts/mod_release/index.js +539 -209
- package/package.json +1 -1
- package/readme.hints.md +47 -1
- package/readme.md +81 -38
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/helpers.climode.ts +34 -1
- package/ts/helpers.workflow.ts +155 -19
- package/ts/mod_commit/mod.helpers.ts +12 -8
- package/ts/mod_config/index.ts +21 -22
- package/ts/mod_release/classes.releasejournal.ts +740 -0
- package/ts/mod_release/helpers.npmartifact.ts +553 -0
- package/ts/mod_release/helpers.releasebranch.ts +1344 -0
- package/ts/mod_release/helpers.releasepublication.ts +641 -0
- package/ts/mod_release/index.ts +906 -262
package/ts/mod_release/index.ts
CHANGED
|
@@ -9,25 +9,54 @@ 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 {
|
|
18
|
+
import {
|
|
19
|
+
inspectReleaseBranch,
|
|
20
|
+
integrateReleaseBranch,
|
|
21
|
+
prepareReleaseIntegration,
|
|
22
|
+
revalidatePreparedReleaseBranch,
|
|
23
|
+
revalidateReleasePublicationState,
|
|
24
|
+
releaseGitEnv,
|
|
25
|
+
resolveReleaseMergeFlag,
|
|
26
|
+
type IReleaseBranchContext,
|
|
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";
|
|
21
47
|
|
|
22
|
-
|
|
48
|
+
export { buildReleaseGitPushArgs } from "./helpers.releasepublication.js";
|
|
23
49
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
};
|
|
29
58
|
|
|
30
|
-
|
|
59
|
+
const runInternal = async (argvArg: any): Promise<void> => {
|
|
31
60
|
const mode = await getCliMode(argvArg);
|
|
32
61
|
const subcommand = argvArg._?.[1];
|
|
33
62
|
|
|
@@ -36,47 +65,88 @@ export const run = async (argvArg: any) => {
|
|
|
36
65
|
return;
|
|
37
66
|
}
|
|
38
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
|
+
|
|
39
80
|
if (mode.json) {
|
|
40
81
|
printJson({
|
|
41
82
|
ok: false,
|
|
42
|
-
error:
|
|
83
|
+
error:
|
|
84
|
+
"JSON output is not supported for mutating release workflows yet. Use `gitzone release --plan` for a human-readable plan.",
|
|
43
85
|
});
|
|
44
86
|
return;
|
|
45
87
|
}
|
|
46
88
|
|
|
89
|
+
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
90
|
+
executor: "bash",
|
|
91
|
+
sourceFilePaths: [],
|
|
92
|
+
});
|
|
93
|
+
const mergeRequested = resolveReleaseMergeFlag(argvArg);
|
|
47
94
|
const workflow = await resolveReleaseWorkflow(argvArg);
|
|
48
|
-
|
|
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
|
+
});
|
|
106
|
+
printReleasePlan(workflow, branchContext);
|
|
49
107
|
if (workflow.confirmation === "plan") {
|
|
50
108
|
return;
|
|
51
109
|
}
|
|
52
110
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
111
|
+
await assertFreshReleaseCapabilities(
|
|
112
|
+
smartshellInstance,
|
|
113
|
+
workflow,
|
|
114
|
+
branchContext.sourceCwd,
|
|
115
|
+
branchContext.pushUrl,
|
|
116
|
+
);
|
|
57
117
|
|
|
58
118
|
const pending = await readPendingChangelog(
|
|
59
|
-
plugins.path.join(
|
|
119
|
+
plugins.path.join(branchContext.sourceCwd, workflow.changelogFile),
|
|
60
120
|
workflow.changelogPendingSection,
|
|
61
121
|
);
|
|
62
122
|
if (pending.isEmpty && !argvArg["allow-empty"] && !argvArg.allowEmpty) {
|
|
63
|
-
|
|
64
|
-
process.exit(1);
|
|
123
|
+
throw new Error("No pending changelog entries. Nothing to release.");
|
|
65
124
|
}
|
|
66
125
|
|
|
67
126
|
const versionType = resolveVersionType(argvArg, pending.block);
|
|
68
|
-
const projectType = await commitHelpers.detectProjectType(
|
|
69
|
-
|
|
70
|
-
|
|
127
|
+
const projectType = await commitHelpers.detectProjectType(
|
|
128
|
+
branchContext.sourceCwd,
|
|
129
|
+
);
|
|
130
|
+
const currentVersion = await commitHelpers.readCurrentVersion(
|
|
131
|
+
projectType,
|
|
132
|
+
branchContext.sourceCwd,
|
|
133
|
+
);
|
|
134
|
+
const plannedVersion = commitHelpers.calculateNewVersion(
|
|
135
|
+
currentVersion,
|
|
136
|
+
versionType,
|
|
137
|
+
);
|
|
71
138
|
|
|
72
139
|
if (workflow.confirmation === "prompt") {
|
|
73
140
|
if (!mode.interactive) {
|
|
74
|
-
throw new Error(
|
|
141
|
+
throw new Error(
|
|
142
|
+
"Release confirmation requires an interactive terminal. Use `-y` or set release.confirmation to `auto`.",
|
|
143
|
+
);
|
|
75
144
|
}
|
|
76
|
-
const confirmed =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
145
|
+
const confirmed =
|
|
146
|
+
await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
|
147
|
+
`Release v${plannedVersion} (${versionType}) now?`,
|
|
148
|
+
true,
|
|
149
|
+
);
|
|
80
150
|
if (!confirmed) {
|
|
81
151
|
logger.log("info", "Release cancelled.");
|
|
82
152
|
return;
|
|
@@ -84,44 +154,513 @@ export const run = async (argvArg: any) => {
|
|
|
84
154
|
}
|
|
85
155
|
|
|
86
156
|
let newVersion = plannedVersion;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (workflow.requireCleanTree) {
|
|
92
|
-
await verifyCleanTree(smartshellInstance, "Working tree is not clean. Commit or stash changes before releasing.");
|
|
93
|
-
}
|
|
157
|
+
branchContext = await prepareReleaseIntegration(
|
|
158
|
+
smartshellInstance,
|
|
159
|
+
branchContext,
|
|
160
|
+
);
|
|
94
161
|
if (workflow.runTests) {
|
|
95
|
-
await runCommandStep(
|
|
162
|
+
await runCommandStep(
|
|
163
|
+
smartshellInstance,
|
|
164
|
+
branchContext.sourceCwd,
|
|
165
|
+
"Running tests",
|
|
166
|
+
workflow.testCommand,
|
|
167
|
+
);
|
|
96
168
|
}
|
|
97
169
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
170
|
+
let releaseCwd = branchContext.sourceCwd;
|
|
171
|
+
let releasePushUrl = branchContext.pushUrl;
|
|
172
|
+
let expectedRemoteMainOid = branchContext.remoteMainOid;
|
|
173
|
+
if (branchContext.sourceBranch === "main") {
|
|
174
|
+
await revalidatePreparedReleaseBranch(smartshellInstance, branchContext);
|
|
175
|
+
} else {
|
|
176
|
+
const integration = await integrateReleaseBranch(
|
|
177
|
+
smartshellInstance,
|
|
178
|
+
branchContext,
|
|
179
|
+
);
|
|
180
|
+
releaseCwd = integration.releaseCwd;
|
|
181
|
+
releasePushUrl = integration.pushUrl;
|
|
182
|
+
expectedRemoteMainOid = integration.expectedRemoteMainOid;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
newVersion = await runVersionStep(projectType, versionType, releaseCwd);
|
|
186
|
+
await runChangelogStep(workflow, newVersion, releaseCwd);
|
|
187
|
+
const releaseCommitOid = await runReleaseCommitStep(
|
|
188
|
+
smartshellInstance,
|
|
189
|
+
newVersion,
|
|
190
|
+
releaseCwd,
|
|
191
|
+
);
|
|
192
|
+
await runTagStep(
|
|
193
|
+
smartshellInstance,
|
|
194
|
+
newVersion,
|
|
195
|
+
releaseCommitOid,
|
|
196
|
+
releaseCwd,
|
|
197
|
+
);
|
|
198
|
+
const releaseRefs = await captureReleaseRefs(
|
|
199
|
+
smartshellInstance,
|
|
200
|
+
releaseCwd,
|
|
201
|
+
newVersion,
|
|
202
|
+
releaseCommitOid,
|
|
203
|
+
);
|
|
102
204
|
|
|
103
205
|
if (workflow.runBuild) {
|
|
104
|
-
await runCommandStep(
|
|
105
|
-
|
|
206
|
+
await runCommandStep(
|
|
207
|
+
smartshellInstance,
|
|
208
|
+
releaseCwd,
|
|
209
|
+
"Running release build",
|
|
210
|
+
workflow.buildCommand,
|
|
211
|
+
);
|
|
212
|
+
await verifyCleanTree(
|
|
213
|
+
smartshellInstance,
|
|
214
|
+
releaseCwd,
|
|
215
|
+
"Build produced uncommitted changes. Aborting release.",
|
|
216
|
+
);
|
|
106
217
|
}
|
|
107
218
|
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
);
|
|
110
268
|
}
|
|
111
|
-
if (
|
|
112
|
-
|
|
269
|
+
if (workflowArg.targets.includes("git") && !pushUrlArg) {
|
|
270
|
+
throw new Error("The Git release target requires one resolved push destination.");
|
|
113
271
|
}
|
|
114
|
-
if (
|
|
115
|
-
|
|
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,
|
|
365
|
+
);
|
|
366
|
+
await verifyCleanTree(
|
|
367
|
+
optionsArg.smartshell,
|
|
368
|
+
optionsArg.releaseCwd,
|
|
369
|
+
"npm pack lifecycle scripts changed the release worktree. Aborting release.",
|
|
370
|
+
);
|
|
371
|
+
await optionsArg.ensurePublicationState();
|
|
372
|
+
}
|
|
373
|
+
|
|
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 });
|
|
460
|
+
return;
|
|
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
|
+
};
|
|
470
|
+
|
|
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.",
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
if (argvArg._?.length !== 3) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
"Usage: gitzone release resume <version> [-y] [--recover-attempt=<id>]",
|
|
505
|
+
);
|
|
506
|
+
}
|
|
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);
|
|
116
545
|
}
|
|
117
546
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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.");
|
|
121
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.");
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
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);
|
|
122
613
|
};
|
|
123
614
|
|
|
124
|
-
|
|
615
|
+
const assertResumeConfiguration = async (
|
|
616
|
+
workflowArg: IResolvedReleaseResumeConfiguration,
|
|
617
|
+
journalArg: IReleaseJournal,
|
|
618
|
+
cwdArg: string,
|
|
619
|
+
): Promise<void> => {
|
|
620
|
+
if (
|
|
621
|
+
journalArg.git.state !== "skipped" &&
|
|
622
|
+
workflowArg.gitRemote !== journalArg.git.remote
|
|
623
|
+
) {
|
|
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);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
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
|
+
|
|
660
|
+
function resolveVersionType(
|
|
661
|
+
argvArg: any,
|
|
662
|
+
pendingBlock: string,
|
|
663
|
+
): commitHelpers.VersionType {
|
|
125
664
|
if (argvArg.major) return "major";
|
|
126
665
|
if (argvArg.minor) return "minor";
|
|
127
666
|
if (argvArg.patch) return "patch";
|
|
@@ -130,51 +669,71 @@ function resolveVersionType(argvArg: any, pendingBlock: string): commitHelpers.V
|
|
|
130
669
|
|
|
131
670
|
async function runCommandStep(
|
|
132
671
|
smartshellInstance: plugins.smartshell.Smartshell,
|
|
672
|
+
cwdArg: string,
|
|
133
673
|
label: string,
|
|
134
674
|
command: string,
|
|
135
675
|
): Promise<void> {
|
|
136
676
|
console.log(`\n${label}`);
|
|
137
|
-
const result = await smartshellInstance.exec(command);
|
|
677
|
+
const result = await smartshellInstance.exec(command, { cwd: cwdArg });
|
|
138
678
|
if (result.exitCode !== 0) {
|
|
139
|
-
|
|
140
|
-
process.exit(1);
|
|
679
|
+
throw new Error(`${label} failed. Aborting release.`);
|
|
141
680
|
}
|
|
142
681
|
logger.log("success", `${label} passed.`);
|
|
143
682
|
}
|
|
144
683
|
|
|
145
684
|
async function verifyCleanTree(
|
|
146
685
|
smartshellInstance: plugins.smartshell.Smartshell,
|
|
686
|
+
cwdArg: string,
|
|
147
687
|
errorMessage: string,
|
|
148
688
|
): Promise<void> {
|
|
149
|
-
const statusResult = await smartshellInstance.
|
|
689
|
+
const statusResult = await smartshellInstance.execSpawn(
|
|
690
|
+
"git",
|
|
691
|
+
["status", "--porcelain"],
|
|
692
|
+
{
|
|
693
|
+
cwd: cwdArg,
|
|
694
|
+
env: releaseGitEnv,
|
|
695
|
+
timeout: 60_000,
|
|
696
|
+
timeoutKillGraceMs: 5_000,
|
|
697
|
+
},
|
|
698
|
+
);
|
|
150
699
|
if (statusResult.stdout.trim() !== "") {
|
|
151
|
-
|
|
152
|
-
console.log(statusResult.stdout);
|
|
153
|
-
process.exit(1);
|
|
700
|
+
throw new Error(errorMessage);
|
|
154
701
|
}
|
|
155
702
|
}
|
|
156
703
|
|
|
157
704
|
async function runVersionStep(
|
|
158
705
|
projectType: commitHelpers.ProjectType,
|
|
159
706
|
versionType: commitHelpers.VersionType,
|
|
707
|
+
cwdArg: string,
|
|
160
708
|
): Promise<string> {
|
|
161
|
-
const currentVersion = await commitHelpers.readCurrentVersion(
|
|
162
|
-
|
|
709
|
+
const currentVersion = await commitHelpers.readCurrentVersion(
|
|
710
|
+
projectType,
|
|
711
|
+
cwdArg,
|
|
712
|
+
);
|
|
713
|
+
const newVersion = commitHelpers.calculateNewVersion(
|
|
714
|
+
currentVersion,
|
|
715
|
+
versionType,
|
|
716
|
+
);
|
|
163
717
|
logger.log("info", `Bumping version: ${currentVersion} -> ${newVersion}`);
|
|
164
718
|
|
|
165
|
-
const commitInfo = new plugins.commitinfo.CommitInfo(
|
|
719
|
+
const commitInfo = new plugins.commitinfo.CommitInfo(cwdArg, versionType);
|
|
166
720
|
await commitInfo.writeIntoPotentialDirs();
|
|
167
|
-
await commitHelpers.updateProjectVersionFiles(
|
|
721
|
+
await commitHelpers.updateProjectVersionFiles(
|
|
722
|
+
projectType,
|
|
723
|
+
newVersion,
|
|
724
|
+
cwdArg,
|
|
725
|
+
);
|
|
168
726
|
return newVersion;
|
|
169
727
|
}
|
|
170
728
|
|
|
171
729
|
async function runChangelogStep(
|
|
172
730
|
workflow: IResolvedReleaseWorkflow,
|
|
173
731
|
newVersion: string,
|
|
732
|
+
cwdArg: string,
|
|
174
733
|
): Promise<void> {
|
|
175
734
|
const dateString = new Date().toISOString().slice(0, 10);
|
|
176
735
|
await movePendingToVersion(
|
|
177
|
-
plugins.path.join(
|
|
736
|
+
plugins.path.join(cwdArg, workflow.changelogFile),
|
|
178
737
|
workflow.changelogPendingSection,
|
|
179
738
|
workflow.changelogVersionHeading,
|
|
180
739
|
newVersion,
|
|
@@ -185,149 +744,172 @@ async function runChangelogStep(
|
|
|
185
744
|
async function runReleaseCommitStep(
|
|
186
745
|
smartshellInstance: plugins.smartshell.Smartshell,
|
|
187
746
|
newVersion: string,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
747
|
+
cwdArg: string,
|
|
748
|
+
): Promise<string> {
|
|
749
|
+
const addResult = await smartshellInstance.execSpawn("git", ["add", "-A"], {
|
|
750
|
+
cwd: cwdArg,
|
|
751
|
+
env: releaseGitEnv,
|
|
752
|
+
timeout: 60_000,
|
|
753
|
+
timeoutKillGraceMs: 5_000,
|
|
754
|
+
});
|
|
755
|
+
if (addResult.exitCode !== 0) {
|
|
756
|
+
throw new Error("Staging release metadata failed.");
|
|
757
|
+
}
|
|
758
|
+
const result = await smartshellInstance.execSpawn(
|
|
759
|
+
"git",
|
|
760
|
+
["commit", "-m", `v${newVersion}`],
|
|
761
|
+
{
|
|
762
|
+
cwd: cwdArg,
|
|
763
|
+
env: releaseGitEnv,
|
|
764
|
+
timeout: 60_000,
|
|
765
|
+
timeoutKillGraceMs: 5_000,
|
|
766
|
+
},
|
|
767
|
+
);
|
|
191
768
|
if (result.exitCode !== 0) {
|
|
192
|
-
|
|
193
|
-
|
|
769
|
+
throw new Error("Release commit failed.");
|
|
770
|
+
}
|
|
771
|
+
const branchResult = await smartshellInstance.execSpawn(
|
|
772
|
+
"git",
|
|
773
|
+
["symbolic-ref", "--quiet", "HEAD"],
|
|
774
|
+
{
|
|
775
|
+
cwd: cwdArg,
|
|
776
|
+
env: releaseGitEnv,
|
|
777
|
+
silent: true,
|
|
778
|
+
timeout: 60_000,
|
|
779
|
+
timeoutKillGraceMs: 5_000,
|
|
780
|
+
},
|
|
781
|
+
);
|
|
782
|
+
if (
|
|
783
|
+
branchResult.exitCode !== 0 ||
|
|
784
|
+
branchResult.stdout.trim() !== "refs/heads/main"
|
|
785
|
+
) {
|
|
786
|
+
throw new Error("The release commit was not created on main.");
|
|
787
|
+
}
|
|
788
|
+
const headOid = await resolveGitObjectOid(
|
|
789
|
+
smartshellInstance,
|
|
790
|
+
cwdArg,
|
|
791
|
+
"HEAD^{commit}",
|
|
792
|
+
);
|
|
793
|
+
const mainOid = await resolveGitObjectOid(
|
|
794
|
+
smartshellInstance,
|
|
795
|
+
cwdArg,
|
|
796
|
+
"refs/heads/main^{commit}",
|
|
797
|
+
);
|
|
798
|
+
if (headOid !== mainOid) {
|
|
799
|
+
throw new Error("HEAD and main diverged after creating the release commit.");
|
|
194
800
|
}
|
|
801
|
+
return mainOid;
|
|
195
802
|
}
|
|
196
803
|
|
|
197
804
|
async function runTagStep(
|
|
198
805
|
smartshellInstance: plugins.smartshell.Smartshell,
|
|
199
806
|
newVersion: string,
|
|
807
|
+
releaseCommitOidArg: string,
|
|
808
|
+
cwdArg: string,
|
|
200
809
|
): Promise<void> {
|
|
201
|
-
const result = await smartshellInstance.
|
|
810
|
+
const result = await smartshellInstance.execSpawn(
|
|
811
|
+
"git",
|
|
812
|
+
[
|
|
813
|
+
"tag",
|
|
814
|
+
"-a",
|
|
815
|
+
`v${newVersion}`,
|
|
816
|
+
"-m",
|
|
817
|
+
`v${newVersion}`,
|
|
818
|
+
releaseCommitOidArg,
|
|
819
|
+
],
|
|
820
|
+
{
|
|
821
|
+
cwd: cwdArg,
|
|
822
|
+
env: releaseGitEnv,
|
|
823
|
+
timeout: 60_000,
|
|
824
|
+
timeoutKillGraceMs: 5_000,
|
|
825
|
+
},
|
|
826
|
+
);
|
|
202
827
|
if (result.exitCode !== 0) {
|
|
203
|
-
|
|
204
|
-
process.exit(1);
|
|
828
|
+
throw new Error("Release tag failed.");
|
|
205
829
|
}
|
|
206
830
|
}
|
|
207
831
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
): Promise<ITargetResult[]> {
|
|
212
|
-
const currentBranchResult = await smartshellInstance.exec("git branch --show-current");
|
|
213
|
-
const currentBranch = currentBranchResult.stdout.trim() || "master";
|
|
214
|
-
const commands: Array<{ target: string; command: string }> = [];
|
|
215
|
-
if (workflow.pushBranch) {
|
|
216
|
-
commands.push({
|
|
217
|
-
target: `${workflow.gitRemote}/${currentBranch}`,
|
|
218
|
-
command: `git push ${workflow.gitRemote} ${currentBranch}`,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
if (workflow.pushTags) {
|
|
222
|
-
commands.push({
|
|
223
|
-
target: `${workflow.gitRemote}/tags`,
|
|
224
|
-
command: `git push ${workflow.gitRemote} --tags`,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const results: ITargetResult[] = [];
|
|
229
|
-
for (const { target, command } of commands) {
|
|
230
|
-
const result = await smartshellInstance.exec(command);
|
|
231
|
-
results.push({
|
|
232
|
-
target,
|
|
233
|
-
status: result.exitCode === 0 ? "success" : "failed",
|
|
234
|
-
message: result.exitCode === 0 ? undefined : "push failed",
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
return results;
|
|
832
|
+
interface IReleaseRefs {
|
|
833
|
+
mainOid: string;
|
|
834
|
+
tagOid: string;
|
|
238
835
|
}
|
|
239
836
|
|
|
240
|
-
async
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
results.push({ target: registry, status: "already-published" });
|
|
260
|
-
} else {
|
|
261
|
-
results.push({ target: registry, status: "failed", message: firstMeaningfulLine(output) });
|
|
262
|
-
}
|
|
837
|
+
const resolveGitObjectOid = async (
|
|
838
|
+
smartshellInstanceArg: plugins.smartshell.Smartshell,
|
|
839
|
+
cwdArg: string,
|
|
840
|
+
revisionArg: string,
|
|
841
|
+
): Promise<string> => {
|
|
842
|
+
const result = await smartshellInstanceArg.execSpawn(
|
|
843
|
+
"git",
|
|
844
|
+
["rev-parse", "--verify", revisionArg],
|
|
845
|
+
{
|
|
846
|
+
cwd: cwdArg,
|
|
847
|
+
env: releaseGitEnv,
|
|
848
|
+
silent: true,
|
|
849
|
+
timeout: 60_000,
|
|
850
|
+
timeoutKillGraceMs: 5_000,
|
|
851
|
+
},
|
|
852
|
+
);
|
|
853
|
+
const oid = result.stdout.trim();
|
|
854
|
+
if (result.exitCode !== 0 || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(oid)) {
|
|
855
|
+
throw new Error(`Unable to resolve release Git object ${revisionArg}.`);
|
|
263
856
|
}
|
|
264
|
-
return
|
|
265
|
-
}
|
|
857
|
+
return oid;
|
|
858
|
+
};
|
|
266
859
|
|
|
267
|
-
async
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
860
|
+
const captureReleaseRefs = async (
|
|
861
|
+
smartshellInstanceArg: plugins.smartshell.Smartshell,
|
|
862
|
+
cwdArg: string,
|
|
863
|
+
newVersionArg: string,
|
|
864
|
+
expectedMainOidArg?: string,
|
|
865
|
+
): Promise<IReleaseRefs> => {
|
|
866
|
+
const mainOid = await resolveGitObjectOid(
|
|
867
|
+
smartshellInstanceArg,
|
|
868
|
+
cwdArg,
|
|
869
|
+
"refs/heads/main^{commit}",
|
|
870
|
+
);
|
|
871
|
+
const tagOid = await resolveGitObjectOid(
|
|
872
|
+
smartshellInstanceArg,
|
|
873
|
+
cwdArg,
|
|
874
|
+
`refs/tags/v${newVersionArg}^{tag}`,
|
|
875
|
+
);
|
|
876
|
+
const tagCommitOid = await resolveGitObjectOid(
|
|
877
|
+
smartshellInstanceArg,
|
|
878
|
+
cwdArg,
|
|
879
|
+
`refs/tags/v${newVersionArg}^{commit}`,
|
|
880
|
+
);
|
|
881
|
+
if (
|
|
882
|
+
(expectedMainOidArg && mainOid !== expectedMainOidArg) ||
|
|
883
|
+
tagCommitOid !== mainOid
|
|
884
|
+
) {
|
|
885
|
+
throw new Error(
|
|
886
|
+
"The release tag is not bound to the captured main release commit.",
|
|
887
|
+
);
|
|
273
888
|
}
|
|
889
|
+
return { mainOid, tagOid };
|
|
890
|
+
};
|
|
274
891
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
892
|
+
const verifyReleaseRefs = async (
|
|
893
|
+
smartshellInstanceArg: plugins.smartshell.Smartshell,
|
|
894
|
+
cwdArg: string,
|
|
895
|
+
newVersionArg: string,
|
|
896
|
+
expectedRefsArg: IReleaseRefs,
|
|
897
|
+
): Promise<void> => {
|
|
898
|
+
const currentRefs = await captureReleaseRefs(
|
|
899
|
+
smartshellInstanceArg,
|
|
900
|
+
cwdArg,
|
|
901
|
+
newVersionArg,
|
|
902
|
+
expectedRefsArg.mainOid,
|
|
903
|
+
);
|
|
904
|
+
if (
|
|
905
|
+
currentRefs.mainOid !== expectedRefsArg.mainOid ||
|
|
906
|
+
currentRefs.tagOid !== expectedRefsArg.tagOid
|
|
907
|
+
) {
|
|
908
|
+
throw new Error(
|
|
909
|
+
"Local release refs changed before publication. Inspect main and the release tag before retrying.",
|
|
279
910
|
);
|
|
280
|
-
} catch {
|
|
281
|
-
packageJson = undefined;
|
|
282
|
-
}
|
|
283
|
-
if (!getDeclaredTsdockerRange(packageJson)) {
|
|
284
|
-
return [{
|
|
285
|
-
target: "tsdocker",
|
|
286
|
-
status: "failed",
|
|
287
|
-
message: "Docker releases require @git.zone/tsdocker as a project dependency",
|
|
288
|
-
}];
|
|
289
|
-
}
|
|
290
|
-
if (!(await hasInstalledProjectTsdockerBinary(paths.cwd))) {
|
|
291
|
-
return [{
|
|
292
|
-
target: "tsdocker",
|
|
293
|
-
status: "failed",
|
|
294
|
-
message: "Docker releases require the installed project-local tSDocker binary",
|
|
295
|
-
}];
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const minimumTsdockerVersion = getTsdockerMinimumVersion(
|
|
299
|
-
workflow.dockerBuildRegistries.length > 0,
|
|
300
|
-
workflow.dockerTest,
|
|
301
|
-
);
|
|
302
|
-
if (minimumTsdockerVersion) {
|
|
303
|
-
const versionResult = await smartshellInstance.exec("pnpm exec tsdocker --version");
|
|
304
|
-
if (
|
|
305
|
-
versionResult.exitCode !== 0
|
|
306
|
-
|| !supportsTsdockerMinimumVersion(
|
|
307
|
-
versionResult.combinedOutput,
|
|
308
|
-
minimumTsdockerVersion,
|
|
309
|
-
)
|
|
310
|
-
) {
|
|
311
|
-
const minimumVersion = formatTsdockerMinimumVersion(minimumTsdockerVersion);
|
|
312
|
-
return [{
|
|
313
|
-
target: "tsdocker",
|
|
314
|
-
status: "failed",
|
|
315
|
-
message: `Docker release options require project-local @git.zone/tsdocker >= ${minimumVersion}`,
|
|
316
|
-
}];
|
|
317
|
-
}
|
|
318
911
|
}
|
|
319
|
-
|
|
320
|
-
const command = buildTsdockerPushCommand(workflow);
|
|
321
|
-
const result = await smartshellInstance.exec(command);
|
|
322
|
-
const output = result.combinedOutput;
|
|
323
|
-
return [{
|
|
324
|
-
target: workflow.dockerPatterns.length > 0
|
|
325
|
-
? `tsdocker:${workflow.dockerPatterns.join(",")}`
|
|
326
|
-
: "tsdocker",
|
|
327
|
-
status: result.exitCode === 0 ? "success" : "failed",
|
|
328
|
-
message: result.exitCode === 0 ? undefined : firstMeaningfulLine(output),
|
|
329
|
-
}];
|
|
330
|
-
}
|
|
912
|
+
};
|
|
331
913
|
|
|
332
914
|
type TDockerCommandWorkflow = Pick<
|
|
333
915
|
IResolvedReleaseWorkflow,
|
|
@@ -341,7 +923,9 @@ type TDockerCommandWorkflow = Pick<
|
|
|
341
923
|
| "dockerPatterns"
|
|
342
924
|
>;
|
|
343
925
|
|
|
344
|
-
export function buildTsdockerPushCommand(
|
|
926
|
+
export function buildTsdockerPushCommand(
|
|
927
|
+
workflow: TDockerCommandWorkflow,
|
|
928
|
+
): string {
|
|
345
929
|
const commandParts = ["pnpm", "exec", "tsdocker", "push"];
|
|
346
930
|
for (const pattern of workflow.dockerPatterns) {
|
|
347
931
|
commandParts.push(shellQuote(pattern));
|
|
@@ -350,7 +934,9 @@ export function buildTsdockerPushCommand(workflow: TDockerCommandWorkflow): stri
|
|
|
350
934
|
commandParts.push(`--registry=${shellQuote(workflow.dockerRegistry)}`);
|
|
351
935
|
}
|
|
352
936
|
if (workflow.dockerBuildRegistries.length > 0) {
|
|
353
|
-
commandParts.push(
|
|
937
|
+
commandParts.push(
|
|
938
|
+
`--build-registries=${shellQuote(workflow.dockerBuildRegistries.join(","))}`,
|
|
939
|
+
);
|
|
354
940
|
}
|
|
355
941
|
if (workflow.dockerTest) {
|
|
356
942
|
commandParts.push("--test");
|
|
@@ -363,7 +949,11 @@ export function buildTsdockerPushCommand(workflow: TDockerCommandWorkflow): stri
|
|
|
363
949
|
}
|
|
364
950
|
if (workflow.dockerParallel === true) {
|
|
365
951
|
commandParts.push("--parallel");
|
|
366
|
-
} else if (
|
|
952
|
+
} else if (
|
|
953
|
+
typeof workflow.dockerParallel === "number" &&
|
|
954
|
+
Number.isFinite(workflow.dockerParallel) &&
|
|
955
|
+
workflow.dockerParallel > 0
|
|
956
|
+
) {
|
|
367
957
|
commandParts.push(`--parallel=${Math.floor(workflow.dockerParallel)}`);
|
|
368
958
|
}
|
|
369
959
|
if (workflow.dockerContext) {
|
|
@@ -372,37 +962,50 @@ export function buildTsdockerPushCommand(workflow: TDockerCommandWorkflow): stri
|
|
|
372
962
|
return commandParts.join(" ");
|
|
373
963
|
}
|
|
374
964
|
|
|
375
|
-
function isAlreadyPublishedOutput(output: string): boolean {
|
|
376
|
-
return /previously published versions|cannot publish over|already exists/i.test(output);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function firstMeaningfulLine(output: string): string {
|
|
380
|
-
return output
|
|
381
|
-
.split("\n")
|
|
382
|
-
.map((line) => line.trim())
|
|
383
|
-
.find((line) => line.length > 0) || "command failed";
|
|
384
|
-
}
|
|
385
|
-
|
|
386
965
|
function shellQuote(value: string): string {
|
|
387
966
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
388
967
|
}
|
|
389
968
|
|
|
390
|
-
function printReleasePlan(
|
|
969
|
+
function printReleasePlan(
|
|
970
|
+
workflow: IResolvedReleaseWorkflow,
|
|
971
|
+
branchContextArg: IReleaseBranchContext,
|
|
972
|
+
): void {
|
|
391
973
|
console.log("");
|
|
392
974
|
console.log("gitzone release - resolved workflow");
|
|
393
975
|
console.log(`confirmation: ${workflow.confirmation}`);
|
|
394
976
|
console.log(`plan: ${workflow.plan.join(" -> ")}`);
|
|
395
|
-
console.log(`
|
|
396
|
-
|
|
977
|
+
console.log(`source branch: ${branchContextArg.sourceBranch}`);
|
|
978
|
+
if (branchContextArg.sourceBranch !== "main") {
|
|
979
|
+
console.log("release branch: main (fast-forward integration requested)");
|
|
980
|
+
console.log(
|
|
981
|
+
`remote main ancestry: ${branchContextArg.planAncestryVerified ? "verified" : "deferred until execution"}`,
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
console.log(
|
|
985
|
+
`targets: ${workflow.targets.length > 0 ? workflow.targets.join(", ") : "none"}`,
|
|
986
|
+
);
|
|
987
|
+
console.log(
|
|
988
|
+
`changelog: ${workflow.changelogFile}#${workflow.changelogPendingSection}`,
|
|
989
|
+
);
|
|
397
990
|
if (workflow.targets.includes("npm")) {
|
|
398
|
-
console.log(
|
|
991
|
+
console.log(
|
|
992
|
+
`npm registries: ${workflow.npmRegistries.length > 0 ? workflow.npmRegistries.join(", ") : "none"}`,
|
|
993
|
+
);
|
|
399
994
|
}
|
|
400
995
|
if (workflow.targets.includes("docker")) {
|
|
401
996
|
console.log(`docker engine: ${workflow.dockerEngine}`);
|
|
402
|
-
console.log(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
console.log(
|
|
997
|
+
console.log(
|
|
998
|
+
`docker registry: ${workflow.dockerRegistry || "all configured registries"}`,
|
|
999
|
+
);
|
|
1000
|
+
console.log(
|
|
1001
|
+
`docker build authentication: ${workflow.dockerBuildRegistries.length > 0 ? workflow.dockerBuildRegistries.join(", ") : "all configured registries"}`,
|
|
1002
|
+
);
|
|
1003
|
+
console.log(
|
|
1004
|
+
`docker image tests: ${workflow.dockerTest ? "required before destination publication" : "not requested"}`,
|
|
1005
|
+
);
|
|
1006
|
+
console.log(
|
|
1007
|
+
`docker patterns: ${workflow.dockerPatterns.length > 0 ? workflow.dockerPatterns.join(", ") : "all Dockerfiles"}`,
|
|
1008
|
+
);
|
|
406
1009
|
console.log(`docker options: ${formatDockerOptions(workflow)}`);
|
|
407
1010
|
}
|
|
408
1011
|
console.log("");
|
|
@@ -410,86 +1013,127 @@ function printReleasePlan(workflow: IResolvedReleaseWorkflow): void {
|
|
|
410
1013
|
|
|
411
1014
|
function formatDockerOptions(workflow: IResolvedReleaseWorkflow): string {
|
|
412
1015
|
const options: string[] = [];
|
|
413
|
-
if (workflow.dockerRegistry)
|
|
1016
|
+
if (workflow.dockerRegistry)
|
|
1017
|
+
options.push(`registry=${workflow.dockerRegistry}`);
|
|
414
1018
|
if (workflow.dockerBuildRegistries.length > 0) {
|
|
415
1019
|
options.push(`buildRegistries=${workflow.dockerBuildRegistries.join(",")}`);
|
|
416
1020
|
}
|
|
417
1021
|
if (workflow.dockerTest) options.push("test");
|
|
418
1022
|
if (workflow.dockerCached) options.push("cached");
|
|
419
|
-
if (workflow.dockerParallel)
|
|
1023
|
+
if (workflow.dockerParallel)
|
|
1024
|
+
options.push(
|
|
1025
|
+
`parallel=${workflow.dockerParallel === true ? "true" : workflow.dockerParallel}`,
|
|
1026
|
+
);
|
|
420
1027
|
if (workflow.dockerNoBuild) options.push("no-build");
|
|
421
1028
|
if (workflow.dockerContext) options.push(`context=${workflow.dockerContext}`);
|
|
422
1029
|
return options.length > 0 ? options.join(", ") : "default";
|
|
423
1030
|
}
|
|
424
1031
|
|
|
425
|
-
function printReleaseSummary(
|
|
426
|
-
newVersion: string,
|
|
427
|
-
gitResults: ITargetResult[],
|
|
428
|
-
npmResults: ITargetResult[],
|
|
429
|
-
dockerResults: ITargetResult[],
|
|
430
|
-
): void {
|
|
431
|
-
console.log("");
|
|
432
|
-
console.log(`Release v${newVersion}`);
|
|
433
|
-
console.log("");
|
|
434
|
-
|
|
435
|
-
if (gitResults.length > 0) {
|
|
436
|
-
console.log("git:");
|
|
437
|
-
for (const result of gitResults) {
|
|
438
|
-
console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if (npmResults.length > 0) {
|
|
443
|
-
console.log("npm:");
|
|
444
|
-
for (const result of npmResults) {
|
|
445
|
-
console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (dockerResults.length > 0) {
|
|
450
|
-
console.log("docker:");
|
|
451
|
-
for (const result of dockerResults) {
|
|
452
|
-
console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
1032
|
export function showHelp(mode?: ICliMode): void {
|
|
458
1033
|
if (mode?.json) {
|
|
459
1034
|
printJson({
|
|
460
1035
|
command: "release",
|
|
461
|
-
usage: "gitzone release [options]",
|
|
462
|
-
description:
|
|
1036
|
+
usage: "gitzone release [inspect [version]|resume <version>] [options]",
|
|
1037
|
+
description:
|
|
1038
|
+
"Creates a versioned release from pending changelog entries and publishes configured artifacts.",
|
|
463
1039
|
flags: [
|
|
464
|
-
{
|
|
1040
|
+
{
|
|
1041
|
+
flag: "-y, --yes",
|
|
1042
|
+
description: "Run without interactive confirmation",
|
|
1043
|
+
},
|
|
465
1044
|
{ flag: "-t, --test", description: "Enable release preflight tests" },
|
|
466
|
-
{
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
{
|
|
472
|
-
|
|
1045
|
+
{
|
|
1046
|
+
flag: "-b, --build",
|
|
1047
|
+
description:
|
|
1048
|
+
"Enable the build after local release metadata is created",
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
flag: "--no-build",
|
|
1052
|
+
description: "Disable the post-metadata release build",
|
|
1053
|
+
},
|
|
1054
|
+
{ flag: "-p, --push", description: "Explicitly select the git target" },
|
|
1055
|
+
{
|
|
1056
|
+
flag: "--target <names>",
|
|
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",
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
flag: "--no-publish",
|
|
1067
|
+
description: "Remove npm and Docker without implicitly enabling Git",
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
flag: "--merge",
|
|
1071
|
+
description:
|
|
1072
|
+
"Fast-forward and lease-push a cleanly rebased feature branch before release metadata",
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
flag: "--plan",
|
|
1076
|
+
description:
|
|
1077
|
+
"Show the workflow without fetching or mutating refs, files, the index, or worktrees",
|
|
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
|
+
},
|
|
473
1092
|
],
|
|
474
1093
|
});
|
|
475
1094
|
return;
|
|
476
1095
|
}
|
|
477
1096
|
|
|
478
1097
|
console.log("");
|
|
479
|
-
console.log(
|
|
1098
|
+
console.log(
|
|
1099
|
+
"Usage: gitzone release [inspect [version]|resume <version>] [options]",
|
|
1100
|
+
);
|
|
480
1101
|
console.log("");
|
|
481
1102
|
console.log("Creates a versioned release from changelog Pending entries.");
|
|
482
1103
|
console.log("");
|
|
483
1104
|
console.log("Flags:");
|
|
484
1105
|
console.log(" -y, --yes Run without interactive confirmation");
|
|
485
1106
|
console.log(" -t, --test Enable release preflight tests");
|
|
486
|
-
console.log(
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
console.log(" --
|
|
490
|
-
console.log(" --
|
|
491
|
-
console.log(
|
|
1107
|
+
console.log(
|
|
1108
|
+
" -b, --build Build after creating local release metadata",
|
|
1109
|
+
);
|
|
1110
|
+
console.log(" --no-build Disable the post-metadata release build");
|
|
1111
|
+
console.log(" -p, --push Explicitly select the git target");
|
|
1112
|
+
console.log(
|
|
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",
|
|
1121
|
+
);
|
|
1122
|
+
console.log(
|
|
1123
|
+
" --merge Fast-forward and lease-push a cleanly rebased feature branch before release metadata",
|
|
1124
|
+
);
|
|
492
1125
|
console.log(" --major|--minor|--patch Override inferred semver level");
|
|
493
|
-
console.log(
|
|
1126
|
+
console.log(
|
|
1127
|
+
" --plan Show workflow without fetching or mutating refs, files, index, or worktrees",
|
|
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
|
+
);
|
|
494
1138
|
console.log("");
|
|
495
1139
|
}
|