@dbx-tools/projen 0.9.18 → 0.9.19
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/README.md +19 -19
- package/index.ts +1 -1
- package/package.json +4 -4
- package/src/_rust-release-workflow.ts +25 -202
- package/src/project-py.ts +60 -71
- package/src/project-rs.ts +1 -24
- package/src/release.ts +85 -142
- package/tasks/publish-npm.ts +63 -11
- package/tasks/publish-python.ts +101 -26
- package/tasks/publish.ts +22 -3
- package/tasks/release-candidate.ts +383 -0
- package/tasks/release-manifest.ts +273 -0
- package/tasks/release-pr.ts +68 -151
package/tasks/publish.ts
CHANGED
|
@@ -55,7 +55,9 @@
|
|
|
55
55
|
*/
|
|
56
56
|
import {
|
|
57
57
|
chmodSync,
|
|
58
|
+
copyFileSync,
|
|
58
59
|
existsSync,
|
|
60
|
+
mkdirSync,
|
|
59
61
|
mkdtempSync,
|
|
60
62
|
readFileSync,
|
|
61
63
|
rmSync,
|
|
@@ -234,12 +236,14 @@ const plan =
|
|
|
234
236
|
: undefined;
|
|
235
237
|
if (!version && !plan) {
|
|
236
238
|
logger.error(
|
|
237
|
-
"usage: bun tasks/publish.ts <version> [--plan <path>] [--registry <url>] [--exclude <dir>] [--dry-run] [--skip-compile]",
|
|
239
|
+
"usage: bun tasks/publish.ts <version> [--plan <path>] [--registry <url>] [--output <dir>] [--exclude <dir>] [--dry-run] [--skip-compile]",
|
|
238
240
|
);
|
|
239
241
|
process.exit(1);
|
|
240
242
|
}
|
|
241
243
|
const registryIdx = rest.indexOf("--registry");
|
|
242
244
|
const registry = registryIdx >= 0 ? rest[registryIdx + 1] : undefined;
|
|
245
|
+
const outputIdx = rest.indexOf("--output");
|
|
246
|
+
const output = outputIdx >= 0 ? resolve(root, rest[outputIdx + 1]) : undefined;
|
|
243
247
|
const dryRun = rest.includes("--dry-run");
|
|
244
248
|
const skipCompile = rest.includes("--skip-compile");
|
|
245
249
|
const concurrencyIdx = rest.indexOf("--concurrency");
|
|
@@ -371,8 +375,12 @@ for (const { dir } of publishable) {
|
|
|
371
375
|
}
|
|
372
376
|
|
|
373
377
|
logger.info(
|
|
374
|
-
`${dryRun ? "dry-run packing" : "publishing"} ${publishable.length} packages with concurrency ${concurrency}`,
|
|
378
|
+
`${output ? "packing" : dryRun ? "dry-run packing" : "publishing"} ${publishable.length} packages with concurrency ${concurrency}`,
|
|
375
379
|
);
|
|
380
|
+
if (output) {
|
|
381
|
+
rmSync(output, { recursive: true, force: true });
|
|
382
|
+
mkdirSync(output, { recursive: true });
|
|
383
|
+
}
|
|
376
384
|
await asyncUtils.mapConcurrent(
|
|
377
385
|
publishable,
|
|
378
386
|
async ({ dir, name, version: packageVersion, access }) => {
|
|
@@ -390,6 +398,15 @@ await asyncUtils.mapConcurrent(
|
|
|
390
398
|
`Packed npm access ${String(local.access)} does not match ${String(access)} for ${name}`,
|
|
391
399
|
);
|
|
392
400
|
}
|
|
401
|
+
if (output) {
|
|
402
|
+
const destination = join(output, archive.split(/[\\/]/).at(-1)!);
|
|
403
|
+
if (existsSync(destination)) {
|
|
404
|
+
throw new Error(`Duplicate npm archive name: ${destination}`);
|
|
405
|
+
}
|
|
406
|
+
copyFileSync(archive, destination);
|
|
407
|
+
logger.info(`packed ${name} @ ${packageVersion}`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
393
410
|
if (!dryRun) {
|
|
394
411
|
const published = await publishedNpmRelease(local.name, local.version, registry);
|
|
395
412
|
if (npmReleaseMatches(local, published)) {
|
|
@@ -410,4 +427,6 @@ await asyncUtils.mapConcurrent(
|
|
|
410
427
|
},
|
|
411
428
|
{ concurrency, errorMode: "settle" },
|
|
412
429
|
);
|
|
413
|
-
logger.success(
|
|
430
|
+
logger.success(
|
|
431
|
+
`${output || dryRun ? "packed" : "published"} ${publishable.length} packages${output ? ` to ${output}` : ""}`,
|
|
432
|
+
);
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Build and optionally upload the complete locally approved release candidate. */
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
8
|
+
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
9
|
+
import { json, log } from "@dbx-tools/shared-core";
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import type { RustReleaseConfiguration } from "../src/_rust-release-workflow.ts";
|
|
12
|
+
import { buildPythonProjects } from "./publish-python.ts";
|
|
13
|
+
import { readNpmArchiveIdentity } from "./publish-npm.ts";
|
|
14
|
+
import { buildReleaseAssets } from "./release-assets.ts";
|
|
15
|
+
import {
|
|
16
|
+
type ReleaseArtifactInput,
|
|
17
|
+
type ReleaseArtifactRole,
|
|
18
|
+
writeReleaseManifest,
|
|
19
|
+
} from "./release-manifest.ts";
|
|
20
|
+
|
|
21
|
+
const logger = log.logger("projen:release-candidate");
|
|
22
|
+
|
|
23
|
+
function run(root: string, command: string, args: readonly string[], env = process.env): void {
|
|
24
|
+
exec.spawnSync(command, [...args], {
|
|
25
|
+
cwd: root,
|
|
26
|
+
env,
|
|
27
|
+
stdin: "ignore",
|
|
28
|
+
stdout: "inherit",
|
|
29
|
+
stderr: "inherit",
|
|
30
|
+
check: true,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function capture(
|
|
35
|
+
root: string,
|
|
36
|
+
command: string,
|
|
37
|
+
args: readonly string[],
|
|
38
|
+
env = process.env,
|
|
39
|
+
): string {
|
|
40
|
+
return exec
|
|
41
|
+
.spawnSync(command, [...args], {
|
|
42
|
+
cwd: root,
|
|
43
|
+
env,
|
|
44
|
+
stdin: "ignore",
|
|
45
|
+
stdout: "capture",
|
|
46
|
+
stderr: "inherit",
|
|
47
|
+
check: true,
|
|
48
|
+
})
|
|
49
|
+
.stdout.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function files(directory: string): string[] {
|
|
53
|
+
if (!existsSync(directory)) return [];
|
|
54
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
55
|
+
const path = join(directory, entry.name);
|
|
56
|
+
return entry.isDirectory() ? files(path) : [path];
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function pythonPackageName(path: string, version: string): string {
|
|
61
|
+
const name = basename(path);
|
|
62
|
+
const marker = `-${version}`;
|
|
63
|
+
const index = name.indexOf(marker);
|
|
64
|
+
if (index <= 0) throw new Error(`Cannot resolve Python package name from ${name}`);
|
|
65
|
+
return name.slice(0, index).replace(/[_.]+/g, "-").toLowerCase();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function role(path: string): ReleaseArtifactRole {
|
|
69
|
+
const normalized = path.split("\\").join("/");
|
|
70
|
+
if (normalized.includes("/npm-facade/")) return "facade";
|
|
71
|
+
if (normalized.includes("/npm-workspace/") || normalized.includes("/python-workspace/")) {
|
|
72
|
+
return "workspace";
|
|
73
|
+
}
|
|
74
|
+
return "native";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function releaseArtifacts(root: string, version: string): ReleaseArtifactInput[] {
|
|
78
|
+
const releaseRoot = join(root, "dist/release");
|
|
79
|
+
return files(releaseRoot)
|
|
80
|
+
.filter((path) => !path.includes(`${join(releaseRoot, "upload")}/`))
|
|
81
|
+
.flatMap((path): ReleaseArtifactInput[] => {
|
|
82
|
+
const normalized = path.split("\\").join("/");
|
|
83
|
+
if (path.endsWith(".tgz")) {
|
|
84
|
+
const identity = readNpmArchiveIdentity(path);
|
|
85
|
+
if (identity.version !== version) {
|
|
86
|
+
throw new Error(`${identity.name} carries ${identity.version}, expected ${version}`);
|
|
87
|
+
}
|
|
88
|
+
return [
|
|
89
|
+
{
|
|
90
|
+
kind: "npm",
|
|
91
|
+
packageName: identity.name,
|
|
92
|
+
packageVersion: identity.version,
|
|
93
|
+
path,
|
|
94
|
+
role: role(path),
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
if (
|
|
99
|
+
path.endsWith(".whl") ||
|
|
100
|
+
(path.endsWith(".tar.gz") &&
|
|
101
|
+
(normalized.includes("/python/") || normalized.includes("/python-workspace/")))
|
|
102
|
+
) {
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
kind: "pypi",
|
|
106
|
+
packageName: pythonPackageName(path, version),
|
|
107
|
+
packageVersion: version,
|
|
108
|
+
path,
|
|
109
|
+
role: role(path),
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
if (path.endsWith(".zip") || path.endsWith(".tar.gz")) {
|
|
114
|
+
return [{ kind: "binary", path }];
|
|
115
|
+
}
|
|
116
|
+
return [];
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function pruneDraftAssets(
|
|
121
|
+
root: string,
|
|
122
|
+
tag: string,
|
|
123
|
+
desired: ReadonlySet<string>,
|
|
124
|
+
env: NodeJS.ProcessEnv,
|
|
125
|
+
): void {
|
|
126
|
+
const current = capture(
|
|
127
|
+
root,
|
|
128
|
+
"gh",
|
|
129
|
+
["release", "view", tag, "--json", "assets", "--jq", ".assets[].name"],
|
|
130
|
+
env,
|
|
131
|
+
)
|
|
132
|
+
.split(/\r?\n/)
|
|
133
|
+
.filter(Boolean);
|
|
134
|
+
for (const name of current) {
|
|
135
|
+
if (!desired.has(name)) {
|
|
136
|
+
run(root, "gh", ["release", "delete-asset", tag, name, "--yes"], env);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function rustReleaseConfiguration(root: string): RustReleaseConfiguration | undefined {
|
|
142
|
+
const path = join(root, ".projen/rust-release.json");
|
|
143
|
+
return existsSync(path)
|
|
144
|
+
? (json.parse(readFileSync(path, "utf8")) as RustReleaseConfiguration)
|
|
145
|
+
: undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildFacades(root: string, version: string): void {
|
|
149
|
+
const configuration = rustReleaseConfiguration(root);
|
|
150
|
+
if (!configuration) return;
|
|
151
|
+
for (const binding of configuration.bindings) {
|
|
152
|
+
if (!binding.node || !binding.nodePackage) continue;
|
|
153
|
+
run(root, "node", [
|
|
154
|
+
".projen/uniffi-release.mjs",
|
|
155
|
+
"facade",
|
|
156
|
+
"--node",
|
|
157
|
+
binding.node,
|
|
158
|
+
"--node-package",
|
|
159
|
+
binding.nodePackage,
|
|
160
|
+
"--node-triple",
|
|
161
|
+
"linux-x64-gnu",
|
|
162
|
+
"--version",
|
|
163
|
+
version,
|
|
164
|
+
"--output",
|
|
165
|
+
`dist/release/${binding.crate}/facade`,
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function ensureDraftRelease(options: {
|
|
171
|
+
readonly root: string;
|
|
172
|
+
readonly sha: string;
|
|
173
|
+
readonly tag: string;
|
|
174
|
+
readonly notesFile?: string;
|
|
175
|
+
readonly env: NodeJS.ProcessEnv;
|
|
176
|
+
}): void {
|
|
177
|
+
const existingTag = exec.spawnSync("git", ["rev-parse", "--verify", `${options.tag}^{commit}`], {
|
|
178
|
+
cwd: options.root,
|
|
179
|
+
env: options.env,
|
|
180
|
+
stdin: "ignore",
|
|
181
|
+
stdout: "capture",
|
|
182
|
+
stderr: "ignore",
|
|
183
|
+
check: false,
|
|
184
|
+
});
|
|
185
|
+
if (existingTag.exitCode === 0) {
|
|
186
|
+
if (existingTag.stdout.trim() !== options.sha) {
|
|
187
|
+
throw new Error(`Release tag ${options.tag} does not point to ${options.sha}`);
|
|
188
|
+
}
|
|
189
|
+
if (capture(options.root, "git", ["cat-file", "-t", options.tag], options.env) !== "tag") {
|
|
190
|
+
throw new Error(`Release tag ${options.tag} must be annotated`);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
run(
|
|
194
|
+
options.root,
|
|
195
|
+
"git",
|
|
196
|
+
["tag", "-a", options.tag, options.sha, "-m", options.tag],
|
|
197
|
+
options.env,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const remoteTag = exec.spawnSync(
|
|
201
|
+
"git",
|
|
202
|
+
["ls-remote", "--exit-code", "--tags", "origin", `refs/tags/${options.tag}`],
|
|
203
|
+
{
|
|
204
|
+
cwd: options.root,
|
|
205
|
+
env: options.env,
|
|
206
|
+
stdin: "ignore",
|
|
207
|
+
stdout: "ignore",
|
|
208
|
+
stderr: "ignore",
|
|
209
|
+
check: false,
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
if (remoteTag.exitCode === 0) {
|
|
213
|
+
run(
|
|
214
|
+
options.root,
|
|
215
|
+
"git",
|
|
216
|
+
["fetch", "--force", "origin", `+refs/tags/${options.tag}:refs/tags/${options.tag}`],
|
|
217
|
+
options.env,
|
|
218
|
+
);
|
|
219
|
+
if (
|
|
220
|
+
capture(options.root, "git", ["rev-parse", `${options.tag}^{commit}`], options.env) !==
|
|
221
|
+
options.sha
|
|
222
|
+
) {
|
|
223
|
+
throw new Error(`Remote release tag ${options.tag} does not point to ${options.sha}`);
|
|
224
|
+
}
|
|
225
|
+
if (capture(options.root, "git", ["cat-file", "-t", options.tag], options.env) !== "tag") {
|
|
226
|
+
throw new Error(`Remote release tag ${options.tag} must be annotated`);
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
run(options.root, "git", ["push", "origin", `refs/tags/${options.tag}`], options.env);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const release = exec.spawnSync("gh", ["release", "view", options.tag, "--json", "isDraft"], {
|
|
233
|
+
cwd: options.root,
|
|
234
|
+
env: options.env,
|
|
235
|
+
stdin: "ignore",
|
|
236
|
+
stdout: "capture",
|
|
237
|
+
stderr: "ignore",
|
|
238
|
+
check: false,
|
|
239
|
+
});
|
|
240
|
+
if (release.exitCode === 0) {
|
|
241
|
+
const state = JSON.parse(release.stdout) as { isDraft?: boolean };
|
|
242
|
+
if (state.isDraft !== true)
|
|
243
|
+
throw new Error(`GitHub Release ${options.tag} is already published`);
|
|
244
|
+
if (options.notesFile) {
|
|
245
|
+
run(
|
|
246
|
+
options.root,
|
|
247
|
+
"gh",
|
|
248
|
+
["release", "edit", options.tag, "--title", options.tag, "--notes-file", options.notesFile],
|
|
249
|
+
options.env,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const notes =
|
|
255
|
+
options.notesFile && existsSync(options.notesFile) ? ["--notes-file", options.notesFile] : [];
|
|
256
|
+
run(
|
|
257
|
+
options.root,
|
|
258
|
+
"gh",
|
|
259
|
+
["release", "create", options.tag, "--draft", "--verify-tag", "--title", options.tag, ...notes],
|
|
260
|
+
options.env,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Build the exact release candidate and optionally attach it to a draft release. */
|
|
265
|
+
export function buildReleaseCandidate(options: {
|
|
266
|
+
readonly root: string;
|
|
267
|
+
readonly sha: string;
|
|
268
|
+
readonly tag: string;
|
|
269
|
+
readonly version: string;
|
|
270
|
+
readonly notesFile?: string;
|
|
271
|
+
readonly upload?: boolean;
|
|
272
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
273
|
+
}): void {
|
|
274
|
+
const root = resolve(options.root);
|
|
275
|
+
const env = options.env ?? process.env;
|
|
276
|
+
if (capture(root, "git", ["rev-parse", "HEAD"], env) !== options.sha) {
|
|
277
|
+
throw new Error(`Release candidate must be built from ${options.sha}`);
|
|
278
|
+
}
|
|
279
|
+
if (readFileSync(join(root, "VERSION"), "utf8").trim() !== options.version) {
|
|
280
|
+
throw new Error(`VERSION does not match ${options.version}`);
|
|
281
|
+
}
|
|
282
|
+
const status = capture(root, "git", ["status", "--porcelain=v1", "--untracked-files=all"], env);
|
|
283
|
+
if (status) throw new Error("Release candidate source contains tracked changes");
|
|
284
|
+
|
|
285
|
+
rmSync(join(root, "dist/release"), { recursive: true, force: true });
|
|
286
|
+
if ((rustReleaseConfiguration(root)?.targets.length ?? 0) > 0) {
|
|
287
|
+
buildReleaseAssets({ root, version: options.version });
|
|
288
|
+
buildFacades(root, options.version);
|
|
289
|
+
}
|
|
290
|
+
const publishScript = fileURLToPath(new URL("./publish.ts", import.meta.url));
|
|
291
|
+
run(root, process.execPath, [
|
|
292
|
+
publishScript,
|
|
293
|
+
options.version,
|
|
294
|
+
"--output",
|
|
295
|
+
"dist/release/npm-workspace",
|
|
296
|
+
]);
|
|
297
|
+
const pythonRoot = join(root, "packages/py");
|
|
298
|
+
if (existsSync(pythonRoot)) {
|
|
299
|
+
buildPythonProjects({
|
|
300
|
+
allowEmpty: true,
|
|
301
|
+
excludeUniFFI: true,
|
|
302
|
+
output: join(root, "dist/release/python-workspace"),
|
|
303
|
+
root: pythonRoot,
|
|
304
|
+
version: options.version,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const uploadDirectory = join(root, "dist/release/upload");
|
|
308
|
+
const manifest = writeReleaseManifest({
|
|
309
|
+
artifacts: releaseArtifacts(root, options.version),
|
|
310
|
+
directory: uploadDirectory,
|
|
311
|
+
gitSha: options.sha,
|
|
312
|
+
tag: options.tag,
|
|
313
|
+
version: options.version,
|
|
314
|
+
});
|
|
315
|
+
logger.success("built release candidate", {
|
|
316
|
+
artifacts: manifest.artifacts.length,
|
|
317
|
+
sha: options.sha,
|
|
318
|
+
tag: options.tag,
|
|
319
|
+
});
|
|
320
|
+
if (!options.upload) return;
|
|
321
|
+
const notesFile = join(root, "dist/release/release-notes.md");
|
|
322
|
+
const summary =
|
|
323
|
+
options.notesFile && existsSync(resolve(root, options.notesFile))
|
|
324
|
+
? readFileSync(resolve(root, options.notesFile), "utf8").trim()
|
|
325
|
+
: "";
|
|
326
|
+
writeFileSync(
|
|
327
|
+
notesFile,
|
|
328
|
+
[
|
|
329
|
+
`Release commit: \`${options.sha}\``,
|
|
330
|
+
"",
|
|
331
|
+
"Candidate manifest: `release-manifest.json`",
|
|
332
|
+
"",
|
|
333
|
+
"Candidate checksums: `SHA256SUMS`",
|
|
334
|
+
...(summary ? ["", summary] : []),
|
|
335
|
+
"",
|
|
336
|
+
].join("\n"),
|
|
337
|
+
);
|
|
338
|
+
ensureDraftRelease({
|
|
339
|
+
root,
|
|
340
|
+
sha: options.sha,
|
|
341
|
+
tag: options.tag,
|
|
342
|
+
notesFile,
|
|
343
|
+
env,
|
|
344
|
+
});
|
|
345
|
+
const uploadFiles = files(uploadDirectory).sort((left, right) => {
|
|
346
|
+
const priority = (path: string): number =>
|
|
347
|
+
basename(path) === "release-manifest.json" ? 2 : basename(path) === "SHA256SUMS" ? 1 : 0;
|
|
348
|
+
return priority(left) - priority(right) || left.localeCompare(right);
|
|
349
|
+
});
|
|
350
|
+
pruneDraftAssets(root, options.tag, new Set(uploadFiles.map((path) => basename(path))), env);
|
|
351
|
+
run(root, "gh", ["release", "upload", options.tag, ...uploadFiles, "--clobber"], env);
|
|
352
|
+
logger.success(`uploaded draft release candidate ${options.tag}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (import.meta.main) {
|
|
356
|
+
new Command()
|
|
357
|
+
.requiredOption("--version <version>", "release version")
|
|
358
|
+
.requiredOption("--tag <tag>", "annotated release tag")
|
|
359
|
+
.requiredOption("--sha <sha>", "exact release commit")
|
|
360
|
+
.option("--root <path>", "repository root")
|
|
361
|
+
.option("--notes-file <path>", "draft release notes file")
|
|
362
|
+
.option("--upload", "create or update the draft GitHub Release")
|
|
363
|
+
.action(
|
|
364
|
+
(options: {
|
|
365
|
+
version: string;
|
|
366
|
+
tag: string;
|
|
367
|
+
sha: string;
|
|
368
|
+
root?: string;
|
|
369
|
+
notesFile?: string;
|
|
370
|
+
upload?: boolean;
|
|
371
|
+
}) => {
|
|
372
|
+
buildReleaseCandidate({
|
|
373
|
+
root: options.root ?? projectUtils.root() ?? process.cwd(),
|
|
374
|
+
sha: options.sha,
|
|
375
|
+
tag: options.tag,
|
|
376
|
+
version: options.version,
|
|
377
|
+
...(options.notesFile ? { notesFile: options.notesFile } : {}),
|
|
378
|
+
upload: options.upload,
|
|
379
|
+
});
|
|
380
|
+
},
|
|
381
|
+
)
|
|
382
|
+
.parse();
|
|
383
|
+
}
|