@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
|
@@ -0,0 +1,273 @@
|
|
|
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
|
+
}
|
package/tasks/release-pr.ts
CHANGED
|
@@ -55,41 +55,6 @@ import {
|
|
|
55
55
|
|
|
56
56
|
const logger = log.logger("projen:release");
|
|
57
57
|
|
|
58
|
-
export type ReleaseBuildMode = "auto" | "local" | "remote";
|
|
59
|
-
|
|
60
|
-
/** Resolve an explicit or environment-sensitive native release build mode. */
|
|
61
|
-
export function resolveReleaseBuildMode(options: {
|
|
62
|
-
readonly requested: ReleaseBuildMode;
|
|
63
|
-
readonly canManage: boolean;
|
|
64
|
-
readonly localTools: readonly string[];
|
|
65
|
-
readonly hasLocalTargets: boolean;
|
|
66
|
-
readonly approve: boolean;
|
|
67
|
-
readonly wait: boolean;
|
|
68
|
-
readonly hostPlatform?: NodeJS.Platform;
|
|
69
|
-
}): Exclude<ReleaseBuildMode, "auto"> {
|
|
70
|
-
const local =
|
|
71
|
-
options.canManage &&
|
|
72
|
-
options.hasLocalTargets &&
|
|
73
|
-
(options.hostPlatform ?? process.platform) === "darwin" &&
|
|
74
|
-
options.localTools.length === 0 &&
|
|
75
|
-
options.approve &&
|
|
76
|
-
options.wait;
|
|
77
|
-
if (options.requested === "auto") return local ? "local" : "remote";
|
|
78
|
-
if (options.requested === "remote") return "remote";
|
|
79
|
-
if (!options.canManage) throw new Error("--build local requires maintain or admin permission");
|
|
80
|
-
if (!options.hasLocalTargets) throw new Error("--build local requires Rust release targets");
|
|
81
|
-
if ((options.hostPlatform ?? process.platform) !== "darwin") {
|
|
82
|
-
throw new Error("--build local requires macOS");
|
|
83
|
-
}
|
|
84
|
-
if (options.localTools.length > 0) {
|
|
85
|
-
throw new Error(`--build local requires: ${options.localTools.join(", ")}`);
|
|
86
|
-
}
|
|
87
|
-
if (!options.approve || !options.wait) {
|
|
88
|
-
throw new Error("--build local requires automatic merge and waiting");
|
|
89
|
-
}
|
|
90
|
-
return "local";
|
|
91
|
-
}
|
|
92
|
-
|
|
93
58
|
function git(
|
|
94
59
|
root: string,
|
|
95
60
|
args: string[],
|
|
@@ -115,6 +80,34 @@ async function waitForPullRequestMerge(
|
|
|
115
80
|
env: NodeJS.ProcessEnv,
|
|
116
81
|
timeoutMs: number,
|
|
117
82
|
): Promise<string> {
|
|
83
|
+
const checksDeadline = Date.now() + timeoutMs;
|
|
84
|
+
let checksReported = false;
|
|
85
|
+
while (Date.now() < checksDeadline) {
|
|
86
|
+
const count = Number(
|
|
87
|
+
captureTaskCommand(
|
|
88
|
+
root,
|
|
89
|
+
"gh",
|
|
90
|
+
[
|
|
91
|
+
"pr",
|
|
92
|
+
"view",
|
|
93
|
+
releaseBranch,
|
|
94
|
+
"--json",
|
|
95
|
+
"statusCheckRollup",
|
|
96
|
+
"--jq",
|
|
97
|
+
".statusCheckRollup | length",
|
|
98
|
+
],
|
|
99
|
+
{ env },
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
if (Number.isInteger(count) && count > 0) {
|
|
103
|
+
checksReported = true;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
await asyncUtils.sleep(2_000);
|
|
107
|
+
}
|
|
108
|
+
if (!checksReported) {
|
|
109
|
+
throw new Error(`Release pull request reported no checks within ${timeoutMs}ms`);
|
|
110
|
+
}
|
|
118
111
|
await runTaskCommandAsync(
|
|
119
112
|
root,
|
|
120
113
|
"gh",
|
|
@@ -145,64 +138,6 @@ async function waitForPullRequestMerge(
|
|
|
145
138
|
throw new Error(`Release pull request did not merge within ${timeoutMs}ms`);
|
|
146
139
|
}
|
|
147
140
|
|
|
148
|
-
/** Find and watch the release workflow for an exact merged commit. */
|
|
149
|
-
async function waitForReleaseWorkflow(
|
|
150
|
-
root: string,
|
|
151
|
-
baseBranch: string,
|
|
152
|
-
mergeSha: string,
|
|
153
|
-
env: NodeJS.ProcessEnv,
|
|
154
|
-
timeoutMs: number,
|
|
155
|
-
): Promise<void> {
|
|
156
|
-
const deadline = Date.now() + timeoutMs;
|
|
157
|
-
let runId = "";
|
|
158
|
-
while (Date.now() < deadline) {
|
|
159
|
-
runId = captureTaskCommand(
|
|
160
|
-
root,
|
|
161
|
-
"gh",
|
|
162
|
-
[
|
|
163
|
-
"run",
|
|
164
|
-
"list",
|
|
165
|
-
"--workflow",
|
|
166
|
-
"release.yml",
|
|
167
|
-
"--branch",
|
|
168
|
-
baseBranch,
|
|
169
|
-
"--event",
|
|
170
|
-
"push",
|
|
171
|
-
"--commit",
|
|
172
|
-
mergeSha,
|
|
173
|
-
"--limit",
|
|
174
|
-
"1",
|
|
175
|
-
"--json",
|
|
176
|
-
"databaseId",
|
|
177
|
-
"--jq",
|
|
178
|
-
".[0].databaseId // empty",
|
|
179
|
-
],
|
|
180
|
-
{ env },
|
|
181
|
-
);
|
|
182
|
-
if (runId) break;
|
|
183
|
-
await asyncUtils.sleep(5_000);
|
|
184
|
-
}
|
|
185
|
-
if (!runId) throw new Error(`Release workflow did not start within ${timeoutMs}ms`);
|
|
186
|
-
await runTaskCommandAsync(root, "gh", ["run", "watch", runId, "--exit-status"], {
|
|
187
|
-
env,
|
|
188
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async function waitForGitHubRelease(
|
|
193
|
-
root: string,
|
|
194
|
-
tag: string,
|
|
195
|
-
env: NodeJS.ProcessEnv,
|
|
196
|
-
timeoutMs: number,
|
|
197
|
-
): Promise<void> {
|
|
198
|
-
const deadline = Date.now() + timeoutMs;
|
|
199
|
-
while (Date.now() < deadline) {
|
|
200
|
-
if (taskCommandSucceeds(root, "gh", ["release", "view", tag], { env })) return;
|
|
201
|
-
await asyncUtils.sleep(2_000);
|
|
202
|
-
}
|
|
203
|
-
throw new Error(`GitHub Release ${tag} did not appear within ${timeoutMs}ms`);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
141
|
function githubAccount(root: string): {
|
|
207
142
|
hostname: string;
|
|
208
143
|
owner: string;
|
|
@@ -323,12 +258,11 @@ program
|
|
|
323
258
|
.option("--no-validate", "skip repository validation tasks, Rust tests, and TypeScript compile")
|
|
324
259
|
.option("--no-local-publish", "skip local npm, PyPI, and Cargo publication")
|
|
325
260
|
.option("--no-local-cargo", "skip local Cargo publication")
|
|
326
|
-
.option("--build <mode>", "native release build: auto, local, or remote", "auto")
|
|
327
261
|
.option("--no-approve", "open the release pull request without enabling automatic merge")
|
|
328
262
|
.option("--no-wait", "return after enabling automatic merge without watching publication")
|
|
329
263
|
.option(
|
|
330
264
|
"--wait-timeout-minutes <minutes>",
|
|
331
|
-
"maximum time for pull request checks
|
|
265
|
+
"maximum time for pull request checks and merge",
|
|
332
266
|
"120",
|
|
333
267
|
)
|
|
334
268
|
.action(
|
|
@@ -348,7 +282,6 @@ program
|
|
|
348
282
|
validate: boolean;
|
|
349
283
|
localPublish: boolean;
|
|
350
284
|
localCargo: boolean;
|
|
351
|
-
build: ReleaseBuildMode;
|
|
352
285
|
approve: boolean;
|
|
353
286
|
wait: boolean;
|
|
354
287
|
waitTimeoutMinutes: string;
|
|
@@ -367,29 +300,21 @@ program
|
|
|
367
300
|
throw new Error("--wait-timeout-minutes must be a positive integer");
|
|
368
301
|
}
|
|
369
302
|
const waitTimeoutMs = waitTimeoutMinutes * 60_000;
|
|
303
|
+
if (opts.approve && !opts.wait) {
|
|
304
|
+
throw new Error("automatic merge requires waiting so the draft candidate can be built");
|
|
305
|
+
}
|
|
370
306
|
const currentBranch = git(root, ["branch", "--show-current"], { capture: true });
|
|
371
307
|
if (!currentBranch) throw new Error("Release preparation requires a local branch");
|
|
372
308
|
const account = githubAccount(root);
|
|
373
|
-
if (!
|
|
374
|
-
throw new Error("
|
|
309
|
+
if (!account.canManage) {
|
|
310
|
+
throw new Error("Release preparation requires maintain or admin permission");
|
|
311
|
+
}
|
|
312
|
+
if (hasLocalReleaseTargets(root)) {
|
|
313
|
+
const localTools = missingLocalReleaseTools(root);
|
|
314
|
+
if (localTools.length > 0) {
|
|
315
|
+
throw new Error(`Local release candidate build requires: ${localTools.join(", ")}`);
|
|
316
|
+
}
|
|
375
317
|
}
|
|
376
|
-
const inspectLocalTools =
|
|
377
|
-
opts.build === "local" || (opts.build === "auto" && account.canManage);
|
|
378
|
-
const localTools = inspectLocalTools ? missingLocalReleaseTools(root) : [];
|
|
379
|
-
const buildMode = resolveReleaseBuildMode({
|
|
380
|
-
requested: opts.build,
|
|
381
|
-
canManage: account.canManage,
|
|
382
|
-
localTools,
|
|
383
|
-
hasLocalTargets: hasLocalReleaseTargets(root),
|
|
384
|
-
approve: opts.approve,
|
|
385
|
-
wait: opts.wait,
|
|
386
|
-
});
|
|
387
|
-
logger.info("selected native release build", {
|
|
388
|
-
requested: opts.build,
|
|
389
|
-
resolved: buildMode,
|
|
390
|
-
manager: account.canManage,
|
|
391
|
-
...(localTools.length > 0 ? { missing: localTools } : {}),
|
|
392
|
-
});
|
|
393
318
|
git(root, ["fetch", "--tags", "origin", opts.base]);
|
|
394
319
|
const comparisonBase = resolveBaseVersion(root, [opts.prefix], {
|
|
395
320
|
fetch: false,
|
|
@@ -529,13 +454,7 @@ program
|
|
|
529
454
|
git(releaseRoot, ["add", "-A"]);
|
|
530
455
|
const staged = git(releaseRoot, ["diff", "--cached", "--name-only"], { capture: true });
|
|
531
456
|
if (staged) {
|
|
532
|
-
git(releaseRoot, [
|
|
533
|
-
"commit",
|
|
534
|
-
"-m",
|
|
535
|
-
`chore(release): ${next.version}`,
|
|
536
|
-
"-m",
|
|
537
|
-
`Release-Build: ${buildMode}`,
|
|
538
|
-
]);
|
|
457
|
+
git(releaseRoot, ["commit", "-m", `chore(release): ${next.version}`]);
|
|
539
458
|
} else if (!worktreeExists) {
|
|
540
459
|
throw new Error("Release preparation produced no changes");
|
|
541
460
|
}
|
|
@@ -550,9 +469,7 @@ program
|
|
|
550
469
|
"",
|
|
551
470
|
`Source commit: ${git(releaseRoot, ["rev-parse", `${releaseBranch}^`], { capture: true })}`,
|
|
552
471
|
"",
|
|
553
|
-
"Merging this PR updates VERSION on main and
|
|
554
|
-
"",
|
|
555
|
-
`Native build: ${buildMode}.`,
|
|
472
|
+
"Merging this PR updates VERSION on main and prepares a draft GitHub Release.",
|
|
556
473
|
...(releaseSummary ? ["", releaseSummary] : []),
|
|
557
474
|
].join("\n");
|
|
558
475
|
const githubEnvironment = {
|
|
@@ -601,35 +518,35 @@ program
|
|
|
601
518
|
waitTimeoutMs,
|
|
602
519
|
);
|
|
603
520
|
logger.info("release pull request merged", { releaseBranch, mergeSha });
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
521
|
+
git(root, ["fetch", "origin", opts.base]);
|
|
522
|
+
git(releaseRoot, ["switch", "--detach", mergeSha]);
|
|
523
|
+
runTaskCommand(releaseRoot, process.execPath, ["install"]);
|
|
524
|
+
const releaseCandidateScript = join(
|
|
525
|
+
releaseRoot,
|
|
526
|
+
"node_modules/@dbx-tools/projen/tasks/release-candidate.ts",
|
|
527
|
+
);
|
|
528
|
+
runTaskCommand(
|
|
529
|
+
releaseRoot,
|
|
530
|
+
process.execPath,
|
|
531
|
+
[
|
|
532
|
+
releaseCandidateScript,
|
|
533
|
+
"--root",
|
|
614
534
|
releaseRoot,
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
);
|
|
628
|
-
}
|
|
535
|
+
"--version",
|
|
536
|
+
next.version,
|
|
537
|
+
"--tag",
|
|
538
|
+
releaseTag,
|
|
539
|
+
"--sha",
|
|
540
|
+
mergeSha,
|
|
541
|
+
"--notes-file",
|
|
542
|
+
`docs/releases/v${next.version}.md`,
|
|
543
|
+
"--upload",
|
|
544
|
+
],
|
|
545
|
+
{ env: githubEnvironment },
|
|
546
|
+
);
|
|
629
547
|
git(root, ["worktree", "remove", "--force", releaseRoot]);
|
|
630
548
|
git(root, ["branch", "--delete", "--force", releaseBranch]);
|
|
631
|
-
|
|
632
|
-
logger.success(`published ${releaseTag} from ${mergeSha}`);
|
|
549
|
+
logger.success(`prepared draft ${releaseTag} from ${mergeSha}`);
|
|
633
550
|
return;
|
|
634
551
|
}
|
|
635
552
|
git(root, ["worktree", "remove", "--force", releaseRoot]);
|