@dbx-tools/projen 0.9.21 → 0.9.29
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 +77 -207
- package/index.ts +10 -14
- package/package.json +4 -4
- package/src/barrels.ts +1 -74
- package/src/module-exports.ts +175 -1
- package/src/openapi.ts +5 -72
- package/src/packages.ts +45 -0
- package/src/project-js.ts +3 -6
- package/src/project-predicate.ts +1 -2
- package/src/project-py.ts +82 -103
- package/src/project.ts +2 -2
- package/src/python-node-bindings.ts +138 -0
- package/src/python-node-bundle.ts +92 -0
- package/src/release-catalog.ts +3 -5
- package/src/{release-dispatch.ts → release-context.ts} +0 -11
- package/src/release.ts +70 -152
- package/tasks/bump.ts +12 -21
- package/tasks/databricks-cli-assets.ts +129 -0
- package/tasks/local-publish.ts +23 -107
- package/tasks/publish-npm.ts +10 -1
- package/tasks/publish-python.ts +26 -11
- package/tasks/publish.ts +12 -48
- package/tasks/python-node-bindings-watch.ts +47 -0
- package/tasks/python-node-bindings.ts +1017 -0
- package/tasks/python-release.ts +74 -0
- package/tasks/release.ts +99 -0
- package/tasks/sync.ts +13 -18
- package/tasks/version-check.ts +1 -28
- package/._LICENSE +0 -0
- package/._README.md +0 -0
- package/._index.ts +0 -0
- package/._package.json +0 -0
- package/src/.__lazy-require.ts +0 -0
- package/src/.__release-platform.ts +0 -0
- package/src/.__rust-project.ts +0 -0
- package/src/.__rust-release-workflow.ts +0 -0
- package/src/.__task-command.ts +0 -0
- package/src/._barrels.ts +0 -0
- package/src/._bun-app.ts +0 -0
- package/src/._bun-workflow.ts +0 -0
- package/src/._clean.ts +0 -0
- package/src/._codegen.ts +0 -0
- package/src/._dbx-tools-config.ts +0 -0
- package/src/._engine-root.ts +0 -0
- package/src/._generated.ts +0 -0
- package/src/._mixin.ts +0 -0
- package/src/._module-exports.ts +0 -0
- package/src/._openapi.ts +0 -0
- package/src/._packages.ts +0 -0
- package/src/._pnpm-workspace.ts +0 -0
- package/src/._project-js.ts +0 -0
- package/src/._project-predicate.ts +0 -0
- package/src/._project-py.ts +0 -0
- package/src/._project-rs.ts +0 -0
- package/src/._project.ts +0 -0
- package/src/._projen-version.ts +0 -0
- package/src/._publish.ts +0 -0
- package/src/._release-catalog.ts +0 -0
- package/src/._release-dispatch.ts +0 -0
- package/src/._release-github.ts +0 -0
- package/src/._release.ts +0 -0
- package/src/._scaffold.ts +0 -0
- package/src/._tags.ts +0 -0
- package/src/._tsconfig.ts +0 -0
- package/src/._uniffi.ts +0 -0
- package/src/._vscode.ts +0 -0
- package/src/._watch.ts +0 -0
- package/src/._workspace-lock.ts +0 -0
- package/src/._workspace-version.ts +0 -0
- package/src/_release-platform.ts +0 -78
- package/src/_rust-project.ts +0 -429
- package/src/_rust-release-workflow.ts +0 -311
- package/src/project-rs.ts +0 -1054
- package/src/release-github.ts +0 -109
- package/src/uniffi.ts +0 -65
- package/tasks/._barrels.ts +0 -0
- package/tasks/._bump.ts +0 -0
- package/tasks/._clean.ts +0 -0
- package/tasks/._compile-workspace.ts +0 -0
- package/tasks/._local-publish.ts +0 -0
- package/tasks/._openapi.ts +0 -0
- package/tasks/._projenrc.ts +0 -0
- package/tasks/._publish-npm.ts +0 -0
- package/tasks/._publish-python.ts +0 -0
- package/tasks/._publish-uniffi-local.ts +0 -0
- package/tasks/._publish.ts +0 -0
- package/tasks/._python-registry.ts +0 -0
- package/tasks/._release-assets.ts +0 -0
- package/tasks/._release-candidate.ts +0 -0
- package/tasks/._release-manifest.ts +0 -0
- package/tasks/._release-pr.ts +0 -0
- package/tasks/._release-summary.ts +0 -0
- package/tasks/._release-version.ts +0 -0
- package/tasks/._rust.ts +0 -0
- package/tasks/._sync.ts +0 -0
- package/tasks/._uniffi-python.d.ts +0 -0
- package/tasks/._uniffi-python.js +0 -0
- package/tasks/._uniffi-release.mjs +0 -0
- package/tasks/._uniffi.ts +0 -0
- package/tasks/._version-check.ts +0 -0
- package/tasks/publish-uniffi-local.ts +0 -359
- package/tasks/release-assets.ts +0 -467
- package/tasks/release-candidate.ts +0 -416
- package/tasks/release-manifest.ts +0 -273
- package/tasks/release-pr.ts +0 -589
- package/tasks/release-summary.ts +0 -379
- package/tasks/rust.ts +0 -173
- package/tasks/uniffi-python.d.ts +0 -60
- package/tasks/uniffi-python.js +0 -178
- package/tasks/uniffi-release.mjs +0 -579
- package/tasks/uniffi.ts +0 -262
|
@@ -1,416 +0,0 @@
|
|
|
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 projectUtils from "@dbx-tools/core/project-utils";
|
|
8
|
-
import { json, log } from "@dbx-tools/shared-core";
|
|
9
|
-
import { Command } from "commander";
|
|
10
|
-
import { readNpmArchiveIdentity } from "./publish-npm.ts";
|
|
11
|
-
import { buildPythonProjects } from "./publish-python.ts";
|
|
12
|
-
import { buildReleaseAssets } from "./release-assets.ts";
|
|
13
|
-
import {
|
|
14
|
-
type ReleaseArtifactInput,
|
|
15
|
-
type ReleaseArtifactRole,
|
|
16
|
-
verifyReleaseManifest,
|
|
17
|
-
writeReleaseManifest,
|
|
18
|
-
} from "./release-manifest.ts";
|
|
19
|
-
import type { RustReleaseConfiguration } from "../src/_rust-release-workflow.ts";
|
|
20
|
-
import { captureTaskCommand, runTaskCommand, taskCommandSucceeds } from "../src/_task-command.ts";
|
|
21
|
-
|
|
22
|
-
const logger = log.logger("projen:release-candidate");
|
|
23
|
-
|
|
24
|
-
function files(directory: string): string[] {
|
|
25
|
-
if (!existsSync(directory)) return [];
|
|
26
|
-
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
27
|
-
const path = join(directory, entry.name);
|
|
28
|
-
return entry.isDirectory() ? files(path) : [path];
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function pythonPackageName(path: string, version: string): string {
|
|
33
|
-
const name = basename(path);
|
|
34
|
-
const marker = `-${version}`;
|
|
35
|
-
const index = name.indexOf(marker);
|
|
36
|
-
if (index <= 0) throw new Error(`Cannot resolve Python package name from ${name}`);
|
|
37
|
-
return name.slice(0, index).replace(/[_.]+/g, "-").toLowerCase();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function role(path: string): ReleaseArtifactRole {
|
|
41
|
-
const normalized = path.split("\\").join("/");
|
|
42
|
-
if (normalized.includes("/npm-facade/")) return "facade";
|
|
43
|
-
if (normalized.includes("/npm-workspace/") || normalized.includes("/python-workspace/")) {
|
|
44
|
-
return "workspace";
|
|
45
|
-
}
|
|
46
|
-
return "native";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function releaseArtifacts(root: string, version: string): ReleaseArtifactInput[] {
|
|
50
|
-
const releaseRoot = join(root, "dist/release");
|
|
51
|
-
return files(releaseRoot)
|
|
52
|
-
.filter((path) => !path.includes(`${join(releaseRoot, "upload")}/`))
|
|
53
|
-
.flatMap((path): ReleaseArtifactInput[] => {
|
|
54
|
-
const normalized = path.split("\\").join("/");
|
|
55
|
-
if (
|
|
56
|
-
![
|
|
57
|
-
"/binary/",
|
|
58
|
-
"/npm/",
|
|
59
|
-
"/npm-facade/",
|
|
60
|
-
"/npm-workspace/",
|
|
61
|
-
"/python/",
|
|
62
|
-
"/python-workspace/",
|
|
63
|
-
].some((directory) => normalized.includes(directory))
|
|
64
|
-
) {
|
|
65
|
-
return [];
|
|
66
|
-
}
|
|
67
|
-
if (path.endsWith(".tgz")) {
|
|
68
|
-
const identity = readNpmArchiveIdentity(path);
|
|
69
|
-
if (identity.version !== version) {
|
|
70
|
-
throw new Error(`${identity.name} carries ${identity.version}, expected ${version}`);
|
|
71
|
-
}
|
|
72
|
-
return [
|
|
73
|
-
{
|
|
74
|
-
kind: "npm",
|
|
75
|
-
packageName: identity.name,
|
|
76
|
-
packageVersion: identity.version,
|
|
77
|
-
path,
|
|
78
|
-
role: role(path),
|
|
79
|
-
},
|
|
80
|
-
];
|
|
81
|
-
}
|
|
82
|
-
if (
|
|
83
|
-
path.endsWith(".whl") ||
|
|
84
|
-
(path.endsWith(".tar.gz") &&
|
|
85
|
-
(normalized.includes("/python/") || normalized.includes("/python-workspace/")))
|
|
86
|
-
) {
|
|
87
|
-
return [
|
|
88
|
-
{
|
|
89
|
-
kind: "pypi",
|
|
90
|
-
packageName: pythonPackageName(path, version),
|
|
91
|
-
packageVersion: version,
|
|
92
|
-
path,
|
|
93
|
-
role: role(path),
|
|
94
|
-
},
|
|
95
|
-
];
|
|
96
|
-
}
|
|
97
|
-
if (path.endsWith(".zip") || path.endsWith(".tar.gz")) {
|
|
98
|
-
return [{ kind: "binary", path }];
|
|
99
|
-
}
|
|
100
|
-
return [];
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function pruneDraftAssets(
|
|
105
|
-
root: string,
|
|
106
|
-
tag: string,
|
|
107
|
-
desired: ReadonlySet<string>,
|
|
108
|
-
env: NodeJS.ProcessEnv,
|
|
109
|
-
): void {
|
|
110
|
-
const current = captureTaskCommand(
|
|
111
|
-
root,
|
|
112
|
-
"gh",
|
|
113
|
-
["release", "view", tag, "--json", "assets", "--jq", ".assets[].name"],
|
|
114
|
-
{ env, check: true, stderr: "inherit" },
|
|
115
|
-
)
|
|
116
|
-
.split(/\r?\n/)
|
|
117
|
-
.filter(Boolean);
|
|
118
|
-
for (const name of current) {
|
|
119
|
-
if (!desired.has(name)) {
|
|
120
|
-
runTaskCommand(root, "gh", ["release", "delete-asset", tag, name, "--yes"], { env });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function rustReleaseConfiguration(root: string): RustReleaseConfiguration | undefined {
|
|
126
|
-
const path = join(root, ".projen/rust-release.json");
|
|
127
|
-
return existsSync(path)
|
|
128
|
-
? (json.parse(readFileSync(path, "utf8")) as RustReleaseConfiguration)
|
|
129
|
-
: undefined;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function buildFacades(root: string, version: string): void {
|
|
133
|
-
const configuration = rustReleaseConfiguration(root);
|
|
134
|
-
if (!configuration) return;
|
|
135
|
-
for (const binding of configuration.bindings) {
|
|
136
|
-
if (!binding.node || !binding.nodePackage) continue;
|
|
137
|
-
runTaskCommand(root, "node", [
|
|
138
|
-
".projen/uniffi-release.mjs",
|
|
139
|
-
"facade",
|
|
140
|
-
"--node",
|
|
141
|
-
binding.node,
|
|
142
|
-
"--node-package",
|
|
143
|
-
binding.nodePackage,
|
|
144
|
-
"--node-triple",
|
|
145
|
-
"linux-x64-gnu",
|
|
146
|
-
"--version",
|
|
147
|
-
version,
|
|
148
|
-
"--output",
|
|
149
|
-
`dist/release/${binding.crate}/facade`,
|
|
150
|
-
]);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function ensureDraftRelease(options: {
|
|
155
|
-
readonly root: string;
|
|
156
|
-
readonly sha: string;
|
|
157
|
-
readonly tag: string;
|
|
158
|
-
readonly notesFile?: string;
|
|
159
|
-
readonly env: NodeJS.ProcessEnv;
|
|
160
|
-
}): void {
|
|
161
|
-
const existingTag = captureTaskCommand(
|
|
162
|
-
options.root,
|
|
163
|
-
"git",
|
|
164
|
-
["rev-parse", "--verify", `${options.tag}^{commit}`],
|
|
165
|
-
{ env: options.env },
|
|
166
|
-
);
|
|
167
|
-
if (existingTag) {
|
|
168
|
-
if (existingTag !== options.sha) {
|
|
169
|
-
throw new Error(`Release tag ${options.tag} does not point to ${options.sha}`);
|
|
170
|
-
}
|
|
171
|
-
if (
|
|
172
|
-
captureTaskCommand(options.root, "git", ["cat-file", "-t", options.tag], {
|
|
173
|
-
env: options.env,
|
|
174
|
-
check: true,
|
|
175
|
-
stderr: "inherit",
|
|
176
|
-
}) !== "tag"
|
|
177
|
-
) {
|
|
178
|
-
throw new Error(`Release tag ${options.tag} must be annotated`);
|
|
179
|
-
}
|
|
180
|
-
} else {
|
|
181
|
-
runTaskCommand(
|
|
182
|
-
options.root,
|
|
183
|
-
"git",
|
|
184
|
-
["tag", "-a", options.tag, options.sha, "-m", options.tag],
|
|
185
|
-
{ env: options.env },
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
const remoteTag = taskCommandSucceeds(
|
|
189
|
-
options.root,
|
|
190
|
-
"git",
|
|
191
|
-
["ls-remote", "--exit-code", "--tags", "origin", `refs/tags/${options.tag}`],
|
|
192
|
-
{ env: options.env },
|
|
193
|
-
);
|
|
194
|
-
if (remoteTag) {
|
|
195
|
-
runTaskCommand(
|
|
196
|
-
options.root,
|
|
197
|
-
"git",
|
|
198
|
-
["fetch", "--force", "origin", `+refs/tags/${options.tag}:refs/tags/${options.tag}`],
|
|
199
|
-
{ env: options.env },
|
|
200
|
-
);
|
|
201
|
-
if (
|
|
202
|
-
captureTaskCommand(options.root, "git", ["rev-parse", `${options.tag}^{commit}`], {
|
|
203
|
-
env: options.env,
|
|
204
|
-
check: true,
|
|
205
|
-
stderr: "inherit",
|
|
206
|
-
}) !== options.sha
|
|
207
|
-
) {
|
|
208
|
-
throw new Error(`Remote release tag ${options.tag} does not point to ${options.sha}`);
|
|
209
|
-
}
|
|
210
|
-
if (
|
|
211
|
-
captureTaskCommand(options.root, "git", ["cat-file", "-t", options.tag], {
|
|
212
|
-
env: options.env,
|
|
213
|
-
check: true,
|
|
214
|
-
stderr: "inherit",
|
|
215
|
-
}) !== "tag"
|
|
216
|
-
) {
|
|
217
|
-
throw new Error(`Remote release tag ${options.tag} must be annotated`);
|
|
218
|
-
}
|
|
219
|
-
} else {
|
|
220
|
-
runTaskCommand(options.root, "git", ["push", "origin", `refs/tags/${options.tag}`], {
|
|
221
|
-
env: options.env,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const release = captureTaskCommand(
|
|
226
|
-
options.root,
|
|
227
|
-
"gh",
|
|
228
|
-
["release", "view", options.tag, "--json", "isDraft"],
|
|
229
|
-
{ env: options.env },
|
|
230
|
-
);
|
|
231
|
-
if (release) {
|
|
232
|
-
const state = JSON.parse(release) as { isDraft?: boolean };
|
|
233
|
-
if (state.isDraft !== true) {
|
|
234
|
-
throw new Error(`GitHub Release ${options.tag} is already published`);
|
|
235
|
-
}
|
|
236
|
-
if (options.notesFile) {
|
|
237
|
-
runTaskCommand(
|
|
238
|
-
options.root,
|
|
239
|
-
"gh",
|
|
240
|
-
["release", "edit", options.tag, "--title", options.tag, "--notes-file", options.notesFile],
|
|
241
|
-
{ env: options.env },
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
const notes =
|
|
247
|
-
options.notesFile && existsSync(options.notesFile) ? ["--notes-file", options.notesFile] : [];
|
|
248
|
-
runTaskCommand(
|
|
249
|
-
options.root,
|
|
250
|
-
"gh",
|
|
251
|
-
["release", "create", options.tag, "--draft", "--verify-tag", "--title", options.tag, ...notes],
|
|
252
|
-
{ env: options.env },
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** Build the exact release candidate and optionally attach it to a draft release. */
|
|
257
|
-
export function buildReleaseCandidate(options: {
|
|
258
|
-
readonly root: string;
|
|
259
|
-
readonly pythonRoot: string;
|
|
260
|
-
readonly sha: string;
|
|
261
|
-
readonly tag: string;
|
|
262
|
-
readonly version: string;
|
|
263
|
-
readonly notesFile?: string;
|
|
264
|
-
readonly upload?: boolean;
|
|
265
|
-
readonly env?: NodeJS.ProcessEnv;
|
|
266
|
-
}): void {
|
|
267
|
-
const root = resolve(options.root);
|
|
268
|
-
const env = options.env ?? process.env;
|
|
269
|
-
if (
|
|
270
|
-
captureTaskCommand(root, "git", ["rev-parse", "HEAD"], {
|
|
271
|
-
env,
|
|
272
|
-
check: true,
|
|
273
|
-
stderr: "inherit",
|
|
274
|
-
}) !== options.sha
|
|
275
|
-
) {
|
|
276
|
-
throw new Error(`Release candidate must be built from ${options.sha}`);
|
|
277
|
-
}
|
|
278
|
-
if (readFileSync(join(root, "VERSION"), "utf8").trim() !== options.version) {
|
|
279
|
-
throw new Error(`VERSION does not match ${options.version}`);
|
|
280
|
-
}
|
|
281
|
-
const status = captureTaskCommand(
|
|
282
|
-
root,
|
|
283
|
-
"git",
|
|
284
|
-
["status", "--porcelain=v1", "--untracked-files=all"],
|
|
285
|
-
{ env, check: true, stderr: "inherit" },
|
|
286
|
-
);
|
|
287
|
-
if (status) throw new Error("Release candidate source contains tracked changes");
|
|
288
|
-
|
|
289
|
-
rmSync(join(root, "dist/release"), { recursive: true, force: true });
|
|
290
|
-
if ((rustReleaseConfiguration(root)?.targets.length ?? 0) > 0) {
|
|
291
|
-
buildReleaseAssets({ root, version: options.version });
|
|
292
|
-
buildFacades(root, options.version);
|
|
293
|
-
}
|
|
294
|
-
const publishScript = fileURLToPath(new URL("./publish.ts", import.meta.url));
|
|
295
|
-
runTaskCommand(root, process.execPath, [
|
|
296
|
-
publishScript,
|
|
297
|
-
options.version,
|
|
298
|
-
"--output",
|
|
299
|
-
"dist/release/npm-workspace",
|
|
300
|
-
]);
|
|
301
|
-
const pythonRoot = resolve(root, options.pythonRoot);
|
|
302
|
-
if (existsSync(pythonRoot)) {
|
|
303
|
-
buildPythonProjects({
|
|
304
|
-
allowEmpty: true,
|
|
305
|
-
excludeUniFFI: true,
|
|
306
|
-
output: join(root, "dist/release/python-workspace"),
|
|
307
|
-
root: pythonRoot,
|
|
308
|
-
version: options.version,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
const uploadDirectory = join(root, "dist/release/upload");
|
|
312
|
-
const manifest = writeReleaseManifest({
|
|
313
|
-
artifacts: releaseArtifacts(root, options.version),
|
|
314
|
-
directory: uploadDirectory,
|
|
315
|
-
gitSha: options.sha,
|
|
316
|
-
tag: options.tag,
|
|
317
|
-
version: options.version,
|
|
318
|
-
});
|
|
319
|
-
logger.success("built release candidate", {
|
|
320
|
-
artifacts: manifest.artifacts.length,
|
|
321
|
-
sha: options.sha,
|
|
322
|
-
tag: options.tag,
|
|
323
|
-
});
|
|
324
|
-
if (!options.upload) return;
|
|
325
|
-
uploadReleaseCandidate(options);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/** Attach an already-built and verified candidate to its draft GitHub Release. */
|
|
329
|
-
export function uploadReleaseCandidate(options: {
|
|
330
|
-
readonly root: string;
|
|
331
|
-
readonly sha: string;
|
|
332
|
-
readonly tag: string;
|
|
333
|
-
readonly version: string;
|
|
334
|
-
readonly notesFile?: string;
|
|
335
|
-
readonly env?: NodeJS.ProcessEnv;
|
|
336
|
-
}): void {
|
|
337
|
-
const root = resolve(options.root);
|
|
338
|
-
const env = options.env ?? process.env;
|
|
339
|
-
const uploadDirectory = join(root, "dist/release/upload");
|
|
340
|
-
verifyReleaseManifest({
|
|
341
|
-
directory: uploadDirectory,
|
|
342
|
-
gitSha: options.sha,
|
|
343
|
-
tag: options.tag,
|
|
344
|
-
version: options.version,
|
|
345
|
-
});
|
|
346
|
-
const notesFile = join(root, "dist/release/release-notes.md");
|
|
347
|
-
const summary =
|
|
348
|
-
options.notesFile && existsSync(resolve(root, options.notesFile))
|
|
349
|
-
? readFileSync(resolve(root, options.notesFile), "utf8").trim()
|
|
350
|
-
: "";
|
|
351
|
-
writeFileSync(
|
|
352
|
-
notesFile,
|
|
353
|
-
[
|
|
354
|
-
`Release commit: \`${options.sha}\``,
|
|
355
|
-
"",
|
|
356
|
-
"Candidate manifest: `release-manifest.json`",
|
|
357
|
-
"",
|
|
358
|
-
"Candidate checksums: `SHA256SUMS`",
|
|
359
|
-
...(summary ? ["", summary] : []),
|
|
360
|
-
"",
|
|
361
|
-
].join("\n"),
|
|
362
|
-
);
|
|
363
|
-
ensureDraftRelease({
|
|
364
|
-
root,
|
|
365
|
-
sha: options.sha,
|
|
366
|
-
tag: options.tag,
|
|
367
|
-
notesFile,
|
|
368
|
-
env,
|
|
369
|
-
});
|
|
370
|
-
const uploadFiles = files(uploadDirectory).sort((left, right) => {
|
|
371
|
-
const priority = (path: string): number =>
|
|
372
|
-
basename(path) === "release-manifest.json" ? 2 : basename(path) === "SHA256SUMS" ? 1 : 0;
|
|
373
|
-
return priority(left) - priority(right) || left.localeCompare(right);
|
|
374
|
-
});
|
|
375
|
-
pruneDraftAssets(root, options.tag, new Set(uploadFiles.map((path) => basename(path))), env);
|
|
376
|
-
runTaskCommand(root, "gh", ["release", "upload", options.tag, ...uploadFiles, "--clobber"], {
|
|
377
|
-
env,
|
|
378
|
-
});
|
|
379
|
-
logger.success(`uploaded draft release candidate ${options.tag}`);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (import.meta.main) {
|
|
383
|
-
new Command()
|
|
384
|
-
.requiredOption("--version <version>", "release version")
|
|
385
|
-
.requiredOption("--tag <tag>", "annotated release tag")
|
|
386
|
-
.requiredOption("--sha <sha>", "exact release commit")
|
|
387
|
-
.option("--root <path>", "repository root")
|
|
388
|
-
.option("--python-root <path>", "Python package root", "packages/py")
|
|
389
|
-
.option("--notes-file <path>", "draft release notes file")
|
|
390
|
-
.option("--upload", "create or update the draft GitHub Release after building")
|
|
391
|
-
.option("--upload-existing", "upload the existing verified candidate without rebuilding")
|
|
392
|
-
.action(
|
|
393
|
-
(options: {
|
|
394
|
-
version: string;
|
|
395
|
-
tag: string;
|
|
396
|
-
sha: string;
|
|
397
|
-
root?: string;
|
|
398
|
-
pythonRoot: string;
|
|
399
|
-
notesFile?: string;
|
|
400
|
-
upload?: boolean;
|
|
401
|
-
uploadExisting?: boolean;
|
|
402
|
-
}) => {
|
|
403
|
-
const candidateOptions = {
|
|
404
|
-
root: options.root ?? projectUtils.root() ?? process.cwd(),
|
|
405
|
-
pythonRoot: options.pythonRoot,
|
|
406
|
-
sha: options.sha,
|
|
407
|
-
tag: options.tag,
|
|
408
|
-
version: options.version,
|
|
409
|
-
...(options.notesFile ? { notesFile: options.notesFile } : {}),
|
|
410
|
-
};
|
|
411
|
-
if (options.uploadExisting) uploadReleaseCandidate(candidateOptions);
|
|
412
|
-
else buildReleaseCandidate({ ...candidateOptions, upload: options.upload });
|
|
413
|
-
},
|
|
414
|
-
)
|
|
415
|
-
.parse();
|
|
416
|
-
}
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun
|
|
2
|
-
/** Create, verify, and select immutable release-candidate assets. */
|
|
3
|
-
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
import {
|
|
6
|
-
copyFileSync,
|
|
7
|
-
existsSync,
|
|
8
|
-
mkdirSync,
|
|
9
|
-
readdirSync,
|
|
10
|
-
readFileSync,
|
|
11
|
-
rmSync,
|
|
12
|
-
statSync,
|
|
13
|
-
writeFileSync,
|
|
14
|
-
} from "node:fs";
|
|
15
|
-
import { basename, join, resolve } from "node:path";
|
|
16
|
-
import { parseArgs } from "node:util";
|
|
17
|
-
|
|
18
|
-
export type ReleaseArtifactKind = "binary" | "npm" | "pypi";
|
|
19
|
-
export type ReleaseArtifactRole = "facade" | "native" | "workspace";
|
|
20
|
-
|
|
21
|
-
export interface ReleaseArtifactInput {
|
|
22
|
-
readonly kind: ReleaseArtifactKind;
|
|
23
|
-
readonly packageName?: string;
|
|
24
|
-
readonly packageVersion?: string;
|
|
25
|
-
readonly path: string;
|
|
26
|
-
readonly role?: ReleaseArtifactRole;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface ReleaseArtifact {
|
|
30
|
-
readonly kind: ReleaseArtifactKind;
|
|
31
|
-
readonly name: string;
|
|
32
|
-
readonly packageName?: string;
|
|
33
|
-
readonly packageVersion?: string;
|
|
34
|
-
readonly role?: ReleaseArtifactRole;
|
|
35
|
-
readonly sha256: string;
|
|
36
|
-
readonly size: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface ReleaseManifest {
|
|
40
|
-
readonly schemaVersion: 1;
|
|
41
|
-
readonly tag: string;
|
|
42
|
-
readonly version: string;
|
|
43
|
-
readonly gitSha: string;
|
|
44
|
-
readonly artifacts: readonly ReleaseArtifact[];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function sha256(path: string): string {
|
|
48
|
-
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Copy release artifacts into one upload directory and write its trust manifest. */
|
|
52
|
-
export function writeReleaseManifest(options: {
|
|
53
|
-
readonly artifacts: readonly ReleaseArtifactInput[];
|
|
54
|
-
readonly directory: string;
|
|
55
|
-
readonly gitSha: string;
|
|
56
|
-
readonly tag: string;
|
|
57
|
-
readonly version: string;
|
|
58
|
-
}): ReleaseManifest {
|
|
59
|
-
const directory = resolve(options.directory);
|
|
60
|
-
rmSync(directory, { recursive: true, force: true });
|
|
61
|
-
mkdirSync(directory, { recursive: true });
|
|
62
|
-
const names = new Set<string>();
|
|
63
|
-
const artifacts = options.artifacts
|
|
64
|
-
.map((artifact): ReleaseArtifact => {
|
|
65
|
-
const source = resolve(artifact.path);
|
|
66
|
-
if (!existsSync(source)) throw new Error(`Missing release artifact: ${source}`);
|
|
67
|
-
const name = basename(source);
|
|
68
|
-
if (names.has(name)) throw new Error(`Duplicate release artifact name: ${name}`);
|
|
69
|
-
names.add(name);
|
|
70
|
-
copyFileSync(source, join(directory, name));
|
|
71
|
-
return {
|
|
72
|
-
kind: artifact.kind,
|
|
73
|
-
name,
|
|
74
|
-
...(artifact.packageName ? { packageName: artifact.packageName } : {}),
|
|
75
|
-
...(artifact.packageVersion ? { packageVersion: artifact.packageVersion } : {}),
|
|
76
|
-
...(artifact.role ? { role: artifact.role } : {}),
|
|
77
|
-
sha256: sha256(source),
|
|
78
|
-
size: statSync(source).size,
|
|
79
|
-
};
|
|
80
|
-
})
|
|
81
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
82
|
-
const manifest: ReleaseManifest = {
|
|
83
|
-
schemaVersion: 1,
|
|
84
|
-
tag: options.tag,
|
|
85
|
-
version: options.version,
|
|
86
|
-
gitSha: options.gitSha,
|
|
87
|
-
artifacts,
|
|
88
|
-
};
|
|
89
|
-
writeFileSync(join(directory, "release-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
90
|
-
writeFileSync(
|
|
91
|
-
join(directory, "SHA256SUMS"),
|
|
92
|
-
`${artifacts.map((artifact) => `${artifact.sha256} ${artifact.name}`).join("\n")}\n`,
|
|
93
|
-
);
|
|
94
|
-
return manifest;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function readManifest(directory: string): ReleaseManifest {
|
|
98
|
-
const value = JSON.parse(
|
|
99
|
-
readFileSync(join(directory, "release-manifest.json"), "utf8"),
|
|
100
|
-
) as unknown;
|
|
101
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
102
|
-
throw new Error("Unsupported release manifest");
|
|
103
|
-
}
|
|
104
|
-
const record = value as Record<string, unknown>;
|
|
105
|
-
if (
|
|
106
|
-
record.schemaVersion !== 1 ||
|
|
107
|
-
typeof record.tag !== "string" ||
|
|
108
|
-
typeof record.version !== "string" ||
|
|
109
|
-
typeof record.gitSha !== "string" ||
|
|
110
|
-
!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(record.gitSha) ||
|
|
111
|
-
!Array.isArray(record.artifacts)
|
|
112
|
-
) {
|
|
113
|
-
throw new Error("Unsupported release manifest");
|
|
114
|
-
}
|
|
115
|
-
const names = new Set<string>();
|
|
116
|
-
const artifacts = record.artifacts.map((value): ReleaseArtifact => {
|
|
117
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
118
|
-
throw new Error("Invalid release artifact");
|
|
119
|
-
}
|
|
120
|
-
const artifact = value as Record<string, unknown>;
|
|
121
|
-
if (
|
|
122
|
-
!["binary", "npm", "pypi"].includes(String(artifact.kind)) ||
|
|
123
|
-
typeof artifact.name !== "string" ||
|
|
124
|
-
basename(artifact.name) !== artifact.name ||
|
|
125
|
-
artifact.name === "." ||
|
|
126
|
-
artifact.name === ".." ||
|
|
127
|
-
typeof artifact.sha256 !== "string" ||
|
|
128
|
-
!/^[0-9a-f]{64}$/.test(artifact.sha256) ||
|
|
129
|
-
!Number.isSafeInteger(artifact.size) ||
|
|
130
|
-
Number(artifact.size) < 0
|
|
131
|
-
) {
|
|
132
|
-
throw new Error("Invalid release artifact");
|
|
133
|
-
}
|
|
134
|
-
if (names.has(artifact.name)) {
|
|
135
|
-
throw new Error(`Duplicate release artifact name: ${artifact.name}`);
|
|
136
|
-
}
|
|
137
|
-
names.add(artifact.name);
|
|
138
|
-
const kind = artifact.kind as ReleaseArtifactKind;
|
|
139
|
-
const packageName = artifact.packageName;
|
|
140
|
-
const packageVersion = artifact.packageVersion;
|
|
141
|
-
const role = artifact.role;
|
|
142
|
-
if (
|
|
143
|
-
(packageName !== undefined && typeof packageName !== "string") ||
|
|
144
|
-
(packageVersion !== undefined && typeof packageVersion !== "string") ||
|
|
145
|
-
(role !== undefined && !["facade", "native", "workspace"].includes(String(role))) ||
|
|
146
|
-
(kind !== "binary" && (!packageName || !packageVersion))
|
|
147
|
-
) {
|
|
148
|
-
throw new Error(`Invalid release artifact metadata: ${artifact.name}`);
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
kind,
|
|
152
|
-
name: artifact.name,
|
|
153
|
-
...(packageName ? { packageName } : {}),
|
|
154
|
-
...(packageVersion ? { packageVersion } : {}),
|
|
155
|
-
...(role ? { role: role as ReleaseArtifactRole } : {}),
|
|
156
|
-
sha256: artifact.sha256,
|
|
157
|
-
size: Number(artifact.size),
|
|
158
|
-
};
|
|
159
|
-
});
|
|
160
|
-
return {
|
|
161
|
-
schemaVersion: 1,
|
|
162
|
-
tag: record.tag,
|
|
163
|
-
version: record.version,
|
|
164
|
-
gitSha: record.gitSha,
|
|
165
|
-
artifacts,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Verify a release bundle and optionally extract one registry/package subset. */
|
|
170
|
-
export function verifyReleaseManifest(options: {
|
|
171
|
-
readonly directory: string;
|
|
172
|
-
readonly gitSha: string;
|
|
173
|
-
readonly kind?: ReleaseArtifactKind;
|
|
174
|
-
readonly output?: string;
|
|
175
|
-
readonly packageName?: string;
|
|
176
|
-
readonly tag: string;
|
|
177
|
-
readonly version: string;
|
|
178
|
-
}): readonly ReleaseArtifact[] {
|
|
179
|
-
const directory = resolve(options.directory);
|
|
180
|
-
const manifest = readManifest(directory);
|
|
181
|
-
if (manifest.tag !== options.tag) {
|
|
182
|
-
throw new Error(`Release manifest tag ${manifest.tag} does not match ${options.tag}`);
|
|
183
|
-
}
|
|
184
|
-
if (manifest.version !== options.version) {
|
|
185
|
-
throw new Error(
|
|
186
|
-
`Release manifest version ${manifest.version} does not match ${options.version}`,
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
if (manifest.gitSha !== options.gitSha) {
|
|
190
|
-
throw new Error(`Release manifest SHA ${manifest.gitSha} does not match ${options.gitSha}`);
|
|
191
|
-
}
|
|
192
|
-
for (const artifact of manifest.artifacts) {
|
|
193
|
-
if (artifact.kind !== "binary" && artifact.packageVersion !== options.version) {
|
|
194
|
-
throw new Error(
|
|
195
|
-
`Release artifact ${artifact.name} carries ${String(artifact.packageVersion)}, expected ${options.version}`,
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
const expectedChecksums = `${manifest.artifacts
|
|
200
|
-
.map((artifact) => `${artifact.sha256} ${artifact.name}`)
|
|
201
|
-
.join("\n")}\n`;
|
|
202
|
-
const checksums = readFileSync(join(directory, "SHA256SUMS"), "utf8");
|
|
203
|
-
if (checksums !== expectedChecksums) throw new Error("SHA256SUMS does not match the manifest");
|
|
204
|
-
const selected = manifest.artifacts.filter(
|
|
205
|
-
(artifact) =>
|
|
206
|
-
(!options.kind || artifact.kind === options.kind) &&
|
|
207
|
-
(!options.packageName || artifact.packageName === options.packageName),
|
|
208
|
-
);
|
|
209
|
-
if ((options.kind || options.packageName) && selected.length === 0) {
|
|
210
|
-
throw new Error("Release manifest selection matched no artifacts");
|
|
211
|
-
}
|
|
212
|
-
if (!options.kind && !options.packageName) {
|
|
213
|
-
const expected = new Set([
|
|
214
|
-
"release-manifest.json",
|
|
215
|
-
"SHA256SUMS",
|
|
216
|
-
...manifest.artifacts.map((artifact) => artifact.name),
|
|
217
|
-
]);
|
|
218
|
-
const present = readdirSync(directory).filter((name) => !name.startsWith("."));
|
|
219
|
-
const unexpected = present.filter((name) => !expected.has(name));
|
|
220
|
-
if (unexpected.length > 0) {
|
|
221
|
-
throw new Error(`Release contains unmanifested assets: ${unexpected.sort().join(", ")}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
const output = options.output ? resolve(options.output) : undefined;
|
|
225
|
-
if (output) {
|
|
226
|
-
rmSync(output, { recursive: true, force: true });
|
|
227
|
-
mkdirSync(output, { recursive: true });
|
|
228
|
-
}
|
|
229
|
-
for (const artifact of selected) {
|
|
230
|
-
const source = join(directory, artifact.name);
|
|
231
|
-
if (!existsSync(source)) throw new Error(`Missing release asset: ${artifact.name}`);
|
|
232
|
-
if (statSync(source).size !== artifact.size) {
|
|
233
|
-
throw new Error(`Release asset size does not match: ${artifact.name}`);
|
|
234
|
-
}
|
|
235
|
-
if (sha256(source) !== artifact.sha256) {
|
|
236
|
-
throw new Error(`Release asset checksum does not match: ${artifact.name}`);
|
|
237
|
-
}
|
|
238
|
-
if (output) copyFileSync(source, join(output, artifact.name));
|
|
239
|
-
}
|
|
240
|
-
return selected;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (import.meta.main) {
|
|
244
|
-
const { positionals, values } = parseArgs({
|
|
245
|
-
args: process.argv.slice(2),
|
|
246
|
-
allowPositionals: true,
|
|
247
|
-
options: {
|
|
248
|
-
directory: { type: "string" },
|
|
249
|
-
kind: { type: "string" },
|
|
250
|
-
output: { type: "string" },
|
|
251
|
-
package: { type: "string" },
|
|
252
|
-
sha: { type: "string" },
|
|
253
|
-
tag: { type: "string" },
|
|
254
|
-
version: { type: "string" },
|
|
255
|
-
},
|
|
256
|
-
});
|
|
257
|
-
if (positionals[0] !== "verify") throw new Error("Expected verify command");
|
|
258
|
-
if (!values.directory || !values.sha || !values.tag || !values.version) {
|
|
259
|
-
throw new Error("verify requires --directory, --sha, --tag, and --version");
|
|
260
|
-
}
|
|
261
|
-
if (values.kind && !["binary", "npm", "pypi"].includes(values.kind)) {
|
|
262
|
-
throw new Error(`Unknown release artifact kind: ${values.kind}`);
|
|
263
|
-
}
|
|
264
|
-
verifyReleaseManifest({
|
|
265
|
-
directory: values.directory,
|
|
266
|
-
gitSha: values.sha,
|
|
267
|
-
...(values.kind ? { kind: values.kind as ReleaseArtifactKind } : {}),
|
|
268
|
-
...(values.output ? { output: values.output } : {}),
|
|
269
|
-
...(values.package ? { packageName: values.package } : {}),
|
|
270
|
-
tag: values.tag,
|
|
271
|
-
version: values.version,
|
|
272
|
-
});
|
|
273
|
-
}
|