@dbx-tools/projen 0.9.7 → 0.9.9
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/index.ts +2 -1
- package/package.json +4 -4
- package/src/_rust-project.ts +46 -0
- package/src/_rust-release-workflow.ts +81 -29
- package/src/project-rs.ts +48 -11
- package/src/release-catalog.ts +4 -1
- package/src/watch.ts +4 -1
- package/src/workspace-lock.ts +22 -0
- package/tasks/publish-uniffi-local.ts +81 -7
- package/tasks/release-pr.ts +215 -204
- package/tasks/sync.ts +3 -2
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
5
|
export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
|
|
6
|
-
export const PACKAGE_VERSION = "0.9.
|
|
6
|
+
export const PACKAGE_VERSION = "0.9.9";
|
|
7
7
|
export * as barrels from "./src/barrels.ts";
|
|
8
8
|
export * as bunApp from "./src/bun-app.ts";
|
|
9
9
|
export * as bunWorkflow from "./src/bun-workflow.ts";
|
|
@@ -36,6 +36,7 @@ export * as tsconfig from "./src/tsconfig.ts";
|
|
|
36
36
|
export * as uniffi from "./src/uniffi.ts";
|
|
37
37
|
export * as vscode from "./src/vscode.ts";
|
|
38
38
|
export * as watch from "./src/watch.ts";
|
|
39
|
+
export * as workspaceLock from "./src/workspace-lock.ts";
|
|
39
40
|
export * as workspaceVersion from "./src/workspace-version.ts";
|
|
40
41
|
export { BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE, BUN_APP_OVERRIDES, RootBunfigFile, BunfigFile, BunDevServerFile, BunBuildFile } from "./src/bun-app.ts";
|
|
41
42
|
export { BUN_VERSION } from "./src/bun-workflow.ts";
|
package/package.json
CHANGED
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@clack/prompts": "^1.7.0",
|
|
33
|
-
"@dbx-tools/core": "^0.9.
|
|
34
|
-
"@dbx-tools/path": "^0.9.
|
|
35
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
33
|
+
"@dbx-tools/core": "^0.9.9",
|
|
34
|
+
"@dbx-tools/path": "^0.9.9",
|
|
35
|
+
"@dbx-tools/shared-core": "^0.9.9",
|
|
36
36
|
"commander": "^15.0.0",
|
|
37
37
|
"concurrently": "^10.0.3",
|
|
38
38
|
"constructs": "^10.6.0",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"main": "index.ts",
|
|
55
55
|
"license": "Apache-2.0",
|
|
56
|
-
"version": "0.9.
|
|
56
|
+
"version": "0.9.9",
|
|
57
57
|
"types": "index.ts",
|
|
58
58
|
"type": "module",
|
|
59
59
|
"exports": {
|
package/src/_rust-project.ts
CHANGED
|
@@ -26,6 +26,19 @@ export interface RustCliOptions {
|
|
|
26
26
|
readonly command?: string;
|
|
27
27
|
/** Root CLI help text. Defaults to the Rust package description. */
|
|
28
28
|
readonly description?: string;
|
|
29
|
+
/** Keep the binary installable by registry lookup without exposing a root command. */
|
|
30
|
+
readonly hidden?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One explicit Cargo binary target and its optional release registration. */
|
|
34
|
+
export interface RustBinaryOptions {
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly path: string;
|
|
37
|
+
readonly requiredFeatures?: readonly string[];
|
|
38
|
+
readonly release?: boolean;
|
|
39
|
+
readonly releaseExcludeOs?: readonly RustReleaseOs[];
|
|
40
|
+
readonly description?: string;
|
|
41
|
+
readonly cli?: boolean | RustCliOptions;
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
/** Explicit Cargo example target and the feature gate required to compile it. */
|
|
@@ -47,6 +60,8 @@ export interface RustCrateOptions {
|
|
|
47
60
|
/** Omit this crate and any release binary artifact from these operating systems. */
|
|
48
61
|
readonly releaseExcludeOs?: readonly RustReleaseOs[];
|
|
49
62
|
readonly dependencies?: Readonly<Record<string, CargoDependency>>;
|
|
63
|
+
/** Cargo dependencies scoped by a target cfg expression. */
|
|
64
|
+
readonly targetDependencies?: Readonly<Record<string, Readonly<Record<string, CargoDependency>>>>;
|
|
50
65
|
readonly devDependencies?: Readonly<Record<string, CargoDependency>>;
|
|
51
66
|
readonly features?: Readonly<Record<string, readonly string[]>>;
|
|
52
67
|
readonly defaultFeatures?: readonly string[];
|
|
@@ -54,6 +69,10 @@ export interface RustCrateOptions {
|
|
|
54
69
|
readonly examples?: readonly CargoExampleOptions[];
|
|
55
70
|
/** Cargo and release executable name. Defaults to the generated package name. */
|
|
56
71
|
readonly binaryName?: string;
|
|
72
|
+
/** Additional Cargo binary targets owned by this crate. */
|
|
73
|
+
readonly binaries?: readonly RustBinaryOptions[];
|
|
74
|
+
/** Cargo binary selected by `cargo run` when this crate has multiple binaries. */
|
|
75
|
+
readonly defaultRun?: string;
|
|
57
76
|
/** Publish this release binary through the generated `dbx` command registry. */
|
|
58
77
|
readonly cli?: boolean | RustCliOptions;
|
|
59
78
|
readonly bindings?: readonly ("node" | "python")[];
|
|
@@ -187,10 +206,12 @@ function packageOptions(
|
|
|
187
206
|
...(options.release !== undefined ? { release: options.release } : {}),
|
|
188
207
|
...(options.releaseExcludeOs ? { releaseExcludeOs: options.releaseExcludeOs } : {}),
|
|
189
208
|
...(options.dependencies ? { dependencies: options.dependencies } : {}),
|
|
209
|
+
...(options.targetDependencies ? { targetDependencies: options.targetDependencies } : {}),
|
|
190
210
|
...(options.devDependencies ? { devDependencies: options.devDependencies } : {}),
|
|
191
211
|
...(options.features ? { features: options.features } : {}),
|
|
192
212
|
...(options.defaultFeatures ? { defaultFeatures: options.defaultFeatures } : {}),
|
|
193
213
|
...(options.binaryName ? { binaryName: options.binaryName } : {}),
|
|
214
|
+
...(options.binaries ? { binaries: options.binaries } : {}),
|
|
194
215
|
...(options.cli !== undefined ? { cli: options.cli } : {}),
|
|
195
216
|
...(options.bindings ? { bindings: options.bindings } : {}),
|
|
196
217
|
...(options.uniffiConfig ? { uniffiConfig: options.uniffiConfig } : {}),
|
|
@@ -244,6 +265,7 @@ export class RustProject extends Project implements DBXToolsProject {
|
|
|
244
265
|
...(!workspace ? { workspace: {} } : {}),
|
|
245
266
|
package: {
|
|
246
267
|
name: this.crateName,
|
|
268
|
+
...(options.defaultRun ? { "default-run": options.defaultRun } : {}),
|
|
247
269
|
version: inheritedCargoValue(workspace, options.version, "0.1.0"),
|
|
248
270
|
edition: inheritedCargoValue(workspace, options.edition, "2021"),
|
|
249
271
|
"rust-version": inheritedCargoValue(workspace, options.rustVersion, "1.82"),
|
|
@@ -272,6 +294,13 @@ export class RustProject extends Project implements DBXToolsProject {
|
|
|
272
294
|
? [{ name: `${this.crateName}-uniffi-bindgen`, path: "uniffi-bindgen.rs" }]
|
|
273
295
|
: []),
|
|
274
296
|
...(binary ? [{ name: binaryName, path: "src/main.rs" }] : []),
|
|
297
|
+
...(options.binaries ?? []).map((target) => ({
|
|
298
|
+
name: target.name,
|
|
299
|
+
path: target.path,
|
|
300
|
+
...(target.requiredFeatures?.length
|
|
301
|
+
? { "required-features": [...target.requiredFeatures] }
|
|
302
|
+
: {}),
|
|
303
|
+
})),
|
|
275
304
|
],
|
|
276
305
|
}
|
|
277
306
|
: {}),
|
|
@@ -304,6 +333,23 @@ export class RustProject extends Project implements DBXToolsProject {
|
|
|
304
333
|
),
|
|
305
334
|
}
|
|
306
335
|
: {}),
|
|
336
|
+
...(options.targetDependencies
|
|
337
|
+
? {
|
|
338
|
+
target: Object.fromEntries(
|
|
339
|
+
Object.entries(options.targetDependencies).map(([target, dependencies]) => [
|
|
340
|
+
target,
|
|
341
|
+
{
|
|
342
|
+
dependencies: Object.fromEntries(
|
|
343
|
+
Object.entries(dependencies).map(([name, value]) => [
|
|
344
|
+
name,
|
|
345
|
+
cargoDependency(value, dependencyVersion),
|
|
346
|
+
]),
|
|
347
|
+
),
|
|
348
|
+
},
|
|
349
|
+
]),
|
|
350
|
+
),
|
|
351
|
+
}
|
|
352
|
+
: {}),
|
|
307
353
|
...(options.devDependencies
|
|
308
354
|
? {
|
|
309
355
|
"dev-dependencies": Object.fromEntries(
|
|
@@ -128,6 +128,7 @@ interface RustReleaseBinaryPlan {
|
|
|
128
128
|
readonly crate: string;
|
|
129
129
|
readonly binary: string;
|
|
130
130
|
readonly excludedOs: readonly RustReleaseOs[];
|
|
131
|
+
readonly requiredFeatures: readonly string[];
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
type RustReleaseTargetPlan = Readonly<Record<string, string | boolean | number>>;
|
|
@@ -165,13 +166,26 @@ export function planRustRelease(
|
|
|
165
166
|
nodePackage: binding.nodePackage ?? "",
|
|
166
167
|
pythonPackage: binding.pythonPackage ?? "",
|
|
167
168
|
}));
|
|
168
|
-
const releaseBinaries = packages
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
const releaseBinaries = packages.flatMap((pkg) => [
|
|
170
|
+
...(pkg.packageOptions.release
|
|
171
|
+
? [
|
|
172
|
+
{
|
|
173
|
+
crate: pkg.crateName,
|
|
174
|
+
binary: pkg.packageOptions.binaryName ?? pkg.crateName,
|
|
175
|
+
excludedOs: pkg.packageOptions.releaseExcludeOs ?? [],
|
|
176
|
+
requiredFeatures: [],
|
|
177
|
+
},
|
|
178
|
+
]
|
|
179
|
+
: []),
|
|
180
|
+
...(pkg.packageOptions.binaries ?? [])
|
|
181
|
+
.filter((binary) => binary.release)
|
|
182
|
+
.map((binary) => ({
|
|
183
|
+
crate: pkg.crateName,
|
|
184
|
+
binary: binary.name,
|
|
185
|
+
excludedOs: binary.releaseExcludeOs ?? [],
|
|
186
|
+
requiredFeatures: binary.requiredFeatures ?? [],
|
|
187
|
+
})),
|
|
188
|
+
]);
|
|
175
189
|
const publicCrates = orderRustBindings(
|
|
176
190
|
packages
|
|
177
191
|
.filter((pkg) => !pkg.packageOptions.private)
|
|
@@ -338,7 +352,31 @@ function rustBinaryCommands(plan: RustReleasePlan, independent: boolean): string
|
|
|
338
352
|
});
|
|
339
353
|
}
|
|
340
354
|
|
|
355
|
+
function rustAdditionalBinaryBuildCommands(plan: RustReleasePlan, independent: boolean): string[] {
|
|
356
|
+
return plan.releaseBinaries
|
|
357
|
+
.filter((binary) => binary.requiredFeatures.length)
|
|
358
|
+
.flatMap((binary) => {
|
|
359
|
+
const command = `cargo build --release --package "${binary.crate}" --bin "${binary.binary}" --features "${binary.requiredFeatures.join(",")}"${
|
|
360
|
+
plan.usesCargoLock ? " --locked" : ""
|
|
361
|
+
} --target "\${{ matrix.cargo }}"`;
|
|
362
|
+
const excludedCondition = binary.excludedOs
|
|
363
|
+
.map((os) => `[ "\${{ matrix.os }}" != "${os}" ]`)
|
|
364
|
+
.join(" && ");
|
|
365
|
+
const platformCommands = excludedCondition
|
|
366
|
+
? [`if ${excludedCondition}; then`, ` ${command}`, "fi"]
|
|
367
|
+
: [command];
|
|
368
|
+
if (!independent) return platformCommands;
|
|
369
|
+
const unit = defaultReleaseUnitId("rust", binary.crate);
|
|
370
|
+
return [
|
|
371
|
+
`if jq -e --arg unit "${unit}" '.units[] | select(.id == $unit)' dist/release-plan.json >/dev/null; then`,
|
|
372
|
+
...platformCommands.map((line) => ` ${line}`),
|
|
373
|
+
"fi",
|
|
374
|
+
];
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
341
378
|
function rustArtifactSteps(plan: RustReleasePlan): JobStep[] {
|
|
379
|
+
const binaryCrates = [...new Set(plan.releaseBinaries.map((binary) => binary.crate))];
|
|
342
380
|
return [
|
|
343
381
|
...plan.bindings.flatMap((binding) => [
|
|
344
382
|
...(binding.node
|
|
@@ -368,24 +406,35 @@ function rustArtifactSteps(plan: RustReleasePlan): JobStep[] {
|
|
|
368
406
|
]
|
|
369
407
|
: []),
|
|
370
408
|
]),
|
|
371
|
-
...
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
name:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
409
|
+
...binaryCrates.map((crate) => {
|
|
410
|
+
const binaries = plan.releaseBinaries.filter((binary) => binary.crate === crate);
|
|
411
|
+
const excludedOs =
|
|
412
|
+
binaries[0]?.excludedOs.filter((os) =>
|
|
413
|
+
binaries.every((binary) => binary.excludedOs.includes(os)),
|
|
414
|
+
) ?? [];
|
|
415
|
+
return {
|
|
416
|
+
name: `Upload ${crate} release binary`,
|
|
417
|
+
uses: "actions/upload-artifact@v7",
|
|
418
|
+
...(rustReleaseBinaryCondition(excludedOs)
|
|
419
|
+
? { if: rustReleaseBinaryCondition(excludedOs) }
|
|
420
|
+
: {}),
|
|
421
|
+
with: {
|
|
422
|
+
name: `${crate}-\${{ matrix.node }}-binary`,
|
|
423
|
+
path: `dist/release/${crate}/\${{ matrix.node }}/binary/*`,
|
|
424
|
+
"retention-days": 7,
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
}),
|
|
383
428
|
];
|
|
384
429
|
}
|
|
385
430
|
|
|
386
431
|
export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly JobStep[]): Job {
|
|
387
432
|
const bindingCommands = rustBindingCommands(plan, Boolean(independentSetup));
|
|
388
433
|
const binaryCommands = rustBinaryCommands(plan, Boolean(independentSetup));
|
|
434
|
+
const additionalBinaryBuildCommands = rustAdditionalBinaryBuildCommands(
|
|
435
|
+
plan,
|
|
436
|
+
Boolean(independentSetup),
|
|
437
|
+
);
|
|
389
438
|
return {
|
|
390
439
|
if: independentSetup
|
|
391
440
|
? "${{ needs.release-plan.outputs.rust_targets != '[]' && (github.event_name == 'push' || inputs.stage != 'docs') }}"
|
|
@@ -524,16 +573,19 @@ export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly
|
|
|
524
573
|
},
|
|
525
574
|
run: timedBash(
|
|
526
575
|
"rust_workspace",
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
576
|
+
[
|
|
577
|
+
independentSetup
|
|
578
|
+
? [
|
|
579
|
+
"mapfile -t PACKAGES < <(jq -r '.[]' <<<'${{ toJSON(matrix.packages) }}')",
|
|
580
|
+
"PACKAGE_ARGS=()",
|
|
581
|
+
'for PACKAGE in "${PACKAGES[@]}"; do PACKAGE_ARGS+=(--package "$PACKAGE"); done',
|
|
582
|
+
`cargo build --release --timings "\${PACKAGE_ARGS[@]}"${plan.usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"`,
|
|
583
|
+
].join("\n")
|
|
584
|
+
: `cargo build --release --timings --workspace${plan.usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"${
|
|
585
|
+
plan.hasReleaseExclusions ? " ${{ matrix.cargoExcludes }}" : ""
|
|
586
|
+
}`,
|
|
587
|
+
...additionalBinaryBuildCommands,
|
|
588
|
+
].join("\n"),
|
|
537
589
|
),
|
|
538
590
|
},
|
|
539
591
|
...(!independentSetup
|
package/src/project-rs.ts
CHANGED
|
@@ -289,9 +289,11 @@ export interface RustReleaseBinaryMapping {
|
|
|
289
289
|
readonly command: string;
|
|
290
290
|
readonly description: string;
|
|
291
291
|
readonly binaryName: string;
|
|
292
|
+
readonly hidden: boolean;
|
|
292
293
|
readonly unit: string;
|
|
293
294
|
readonly component: string;
|
|
294
295
|
readonly version: string;
|
|
296
|
+
readonly tagPrefix: string;
|
|
295
297
|
readonly tag: string;
|
|
296
298
|
readonly repository: string;
|
|
297
299
|
readonly assets: readonly RustReleaseBinaryAssetMapping[];
|
|
@@ -379,17 +381,40 @@ function planRustReleaseBinaries(
|
|
|
379
381
|
resolved: ResolvedRustWorkspaceOptions,
|
|
380
382
|
): RustReleaseBinaryMapping[] {
|
|
381
383
|
const commands = new Set<string>();
|
|
382
|
-
const binaries = packages
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
384
|
+
const binaries = packages.flatMap((pkg) => {
|
|
385
|
+
const configuredBinaries = [
|
|
386
|
+
...(pkg.packageOptions.cli
|
|
387
|
+
? [
|
|
388
|
+
{
|
|
389
|
+
name: pkg.packageOptions.binaryName ?? pkg.crateName,
|
|
390
|
+
defaultCommand: pkg.packageOptions.directory,
|
|
391
|
+
description: pkg.packageOptions.description,
|
|
392
|
+
release: pkg.packageOptions.release,
|
|
393
|
+
excludedOs: pkg.packageOptions.releaseExcludeOs,
|
|
394
|
+
cli: pkg.packageOptions.cli,
|
|
395
|
+
},
|
|
396
|
+
]
|
|
397
|
+
: []),
|
|
398
|
+
...(pkg.packageOptions.binaries ?? [])
|
|
399
|
+
.filter((binary) => Boolean(binary.cli))
|
|
400
|
+
.map((binary) => ({
|
|
401
|
+
name: binary.name,
|
|
402
|
+
defaultCommand: binary.name,
|
|
403
|
+
description: binary.description ?? pkg.packageOptions.description,
|
|
404
|
+
release: binary.release,
|
|
405
|
+
excludedOs: binary.releaseExcludeOs,
|
|
406
|
+
cli: binary.cli!,
|
|
407
|
+
})),
|
|
408
|
+
];
|
|
409
|
+
return configuredBinaries.map((binary) => {
|
|
410
|
+
if (!binary.release) {
|
|
386
411
|
throw new Error(`${pkg.crateName} must set release when cli is configured`);
|
|
387
412
|
}
|
|
388
|
-
const configured =
|
|
413
|
+
const configured = binary.cli;
|
|
389
414
|
const command =
|
|
390
415
|
typeof configured === "object" && configured.command
|
|
391
416
|
? configured.command
|
|
392
|
-
:
|
|
417
|
+
: binary.defaultCommand;
|
|
393
418
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(command)) {
|
|
394
419
|
throw new Error(`Invalid Rust CLI command: ${command}`);
|
|
395
420
|
}
|
|
@@ -399,24 +424,31 @@ function planRustReleaseBinaries(
|
|
|
399
424
|
commands.add(command);
|
|
400
425
|
const description =
|
|
401
426
|
(typeof configured === "object" ? configured.description : undefined) ??
|
|
402
|
-
|
|
427
|
+
binary.description ??
|
|
403
428
|
pkg.crateName;
|
|
404
|
-
const
|
|
405
|
-
const
|
|
429
|
+
const hidden = typeof configured === "object" && configured.hidden === true;
|
|
430
|
+
const excludedOs = new Set(binary.excludedOs ?? []);
|
|
431
|
+
const binaryName = binary.name;
|
|
406
432
|
const identity = isDBXToolsJavaScriptProject()(project)
|
|
407
433
|
? project.releaseCatalog.releaseIdentityFor(pkg)
|
|
408
434
|
: {
|
|
409
435
|
component: defaultReleaseUnitId("rust", pkg.crateName),
|
|
410
436
|
version: readWorkspaceVersion(project.outdir),
|
|
411
437
|
};
|
|
438
|
+
const tagPrefix =
|
|
439
|
+
isDBXToolsJavaScriptProject()(project) && project.releaseCatalog.mode === "independent"
|
|
440
|
+
? `${identity.component}-v`
|
|
441
|
+
: "v";
|
|
412
442
|
return {
|
|
413
443
|
command,
|
|
414
444
|
description,
|
|
415
445
|
binaryName,
|
|
446
|
+
hidden,
|
|
416
447
|
unit: "id" in identity ? identity.id : identity.component,
|
|
417
448
|
component: identity.component,
|
|
418
449
|
version: identity.version,
|
|
419
|
-
|
|
450
|
+
tagPrefix,
|
|
451
|
+
tag: `${tagPrefix}${identity.version}`,
|
|
420
452
|
repository: resolved.repository,
|
|
421
453
|
assets: resolved.nativeTargets
|
|
422
454
|
.filter((target) => !excludedOs.has(target.os))
|
|
@@ -427,6 +459,7 @@ function planRustReleaseBinaries(
|
|
|
427
459
|
})),
|
|
428
460
|
};
|
|
429
461
|
});
|
|
462
|
+
});
|
|
430
463
|
if (binaries.length && !resolved.repository) {
|
|
431
464
|
throw new Error("A repository URL is required when Rust CLI commands are configured");
|
|
432
465
|
}
|
|
@@ -894,6 +927,10 @@ export class DBXToolsRustWorkspace extends Component {
|
|
|
894
927
|
identity: pkg.crateName,
|
|
895
928
|
publish: !pkg.packageOptions.private || pkg.packageOptions.release === true,
|
|
896
929
|
sourcePaths: [`${toPosix(relative(project.outdir, pkg.outdir))}/src`],
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
for (const pkg of this.packages) {
|
|
933
|
+
project.releaseCatalog.configureProject(pkg, {
|
|
897
934
|
dependencies: [
|
|
898
935
|
...rustReleaseDependencies(
|
|
899
936
|
project,
|
|
@@ -921,7 +958,7 @@ export class DBXToolsRustWorkspace extends Component {
|
|
|
921
958
|
if (isDBXToolsJavaScriptProject()(project)) {
|
|
922
959
|
for (const binary of this.releaseBinaries) {
|
|
923
960
|
project.releaseCatalog.registerArtifact(binary.unit, {
|
|
924
|
-
id: `${binary.unit}:github-binary`,
|
|
961
|
+
id: `${binary.unit}:github-binary:${binary.binaryName}`,
|
|
925
962
|
kind: "github-binary",
|
|
926
963
|
name: binary.binaryName,
|
|
927
964
|
generated: true,
|
package/src/release-catalog.ts
CHANGED
|
@@ -311,7 +311,10 @@ export class DBXToolsReleaseCatalog extends Component {
|
|
|
311
311
|
/** Replace grouping metadata for an already registered project. */
|
|
312
312
|
configureProject(
|
|
313
313
|
project: Project,
|
|
314
|
-
options: Pick<
|
|
314
|
+
options: Pick<
|
|
315
|
+
ReleaseProjectRegistration,
|
|
316
|
+
"unit" | "component" | "publish" | "sourcePaths" | "dependencies"
|
|
317
|
+
>,
|
|
315
318
|
): void {
|
|
316
319
|
const current = this.projects.get(project);
|
|
317
320
|
if (!current) throw new Error(`Release project is not registered: ${project.outdir}`);
|
package/src/watch.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { isAbsolute, resolve } from "node:path";
|
|
|
19
19
|
import { watch as pathWatch } from "@dbx-tools/path";
|
|
20
20
|
import { asyncUtils, log } from "@dbx-tools/shared-core";
|
|
21
21
|
import { isGeneratedFile, recordedRoots, repoRoot } from "./packages.ts";
|
|
22
|
+
import { withWorkspaceMutationLock } from "./workspace-lock.ts";
|
|
22
23
|
|
|
23
24
|
const logger = log.logger("projen:watch");
|
|
24
25
|
const DEBOUNCE_MS = 250;
|
|
@@ -103,7 +104,9 @@ export function watchLoop(
|
|
|
103
104
|
.filter((p) => !ignoredPath(p));
|
|
104
105
|
pending.clear();
|
|
105
106
|
try {
|
|
106
|
-
if (relevant.length)
|
|
107
|
+
if (relevant.length) {
|
|
108
|
+
await withWorkspaceMutationLock(repoRoot, () => onBatch(relevant));
|
|
109
|
+
}
|
|
107
110
|
} catch (err) {
|
|
108
111
|
logger.error(`${tag} cycle failed:`, err instanceof Error ? err.message : err);
|
|
109
112
|
} finally {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Cross-process coordination for generated workspace mutations. */
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import * as fileLock from "@dbx-tools/core/file-lock";
|
|
4
|
+
import { log } from "@dbx-tools/shared-core";
|
|
5
|
+
|
|
6
|
+
const MUTATION_LOCK_SCOPE = "dbx-tools-workspace-mutation";
|
|
7
|
+
const logger = log.logger("projen:workspace-lock");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Serialize synthesis, generated-file watchers, and release preparation for one
|
|
11
|
+
* repository without coordinating unrelated worktrees or repositories.
|
|
12
|
+
*/
|
|
13
|
+
export function withWorkspaceMutationLock<T>(
|
|
14
|
+
root: string,
|
|
15
|
+
callback: () => T | Promise<T>,
|
|
16
|
+
): Promise<T> {
|
|
17
|
+
const repository = resolve(root);
|
|
18
|
+
return fileLock.withFileLock([MUTATION_LOCK_SCOPE, repository], callback, {
|
|
19
|
+
backends: process.platform === "win32" ? ["file"] : ["flock", "file"],
|
|
20
|
+
onWait: () => logger.info("waiting for workspace mutation lock", { repository }),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -4,7 +4,7 @@ import { arch, platform } from "node:os";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
|
-
import { log } from "@dbx-tools/shared-core";
|
|
7
|
+
import { json, log, object } from "@dbx-tools/shared-core";
|
|
8
8
|
import { parse, stringify } from "smol-toml";
|
|
9
9
|
import { captureTaskCommand, runTaskCommand, taskCommandSucceeds } from "../src/_task-command.ts";
|
|
10
10
|
import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
|
|
@@ -42,6 +42,7 @@ function pythonTag(): string {
|
|
|
42
42
|
"uv",
|
|
43
43
|
[
|
|
44
44
|
"run",
|
|
45
|
+
"--no-project",
|
|
45
46
|
"python",
|
|
46
47
|
"-c",
|
|
47
48
|
"import sysconfig; print(sysconfig.get_platform().replace('-', '_').replace('.', '_'))",
|
|
@@ -94,17 +95,90 @@ function cargoVersionExists(crateName: string, version: string, registry: string
|
|
|
94
95
|
]);
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
interface CargoMetadataPackage {
|
|
99
|
+
dependencies: Array<{ name: string; source?: string | null }>;
|
|
100
|
+
manifest_path: string;
|
|
101
|
+
name: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function orderedCargoManifests(config: RustWorkspaceMapping): string[] {
|
|
105
|
+
const configured = new Set(config.crates.map((crate) => resolve(repoRoot, crate, "Cargo.toml")));
|
|
106
|
+
const metadata = json.parseRecord(
|
|
107
|
+
run("cargo", ["metadata", "--format-version", "1", "--no-deps", "--locked"], true),
|
|
108
|
+
);
|
|
109
|
+
if (!metadata || !Array.isArray(metadata.packages)) {
|
|
110
|
+
throw new Error("Cargo metadata did not return workspace packages");
|
|
111
|
+
}
|
|
112
|
+
const packages = metadata.packages.filter(object.isRecord).map((pkg) => {
|
|
113
|
+
if (
|
|
114
|
+
typeof pkg.name !== "string" ||
|
|
115
|
+
typeof pkg.manifest_path !== "string" ||
|
|
116
|
+
!Array.isArray(pkg.dependencies)
|
|
117
|
+
) {
|
|
118
|
+
throw new Error("Cargo metadata returned an invalid package");
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
name: pkg.name,
|
|
122
|
+
manifest_path: resolve(pkg.manifest_path),
|
|
123
|
+
dependencies: pkg.dependencies.filter(object.isRecord).map((dependency) => {
|
|
124
|
+
if (typeof dependency.name !== "string") {
|
|
125
|
+
throw new Error(`Cargo metadata returned an invalid dependency for ${pkg.name}`);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
name: dependency.name,
|
|
129
|
+
source: typeof dependency.source === "string" ? dependency.source : null,
|
|
130
|
+
};
|
|
131
|
+
}),
|
|
132
|
+
} satisfies CargoMetadataPackage;
|
|
133
|
+
});
|
|
134
|
+
const publishable = new Map(
|
|
135
|
+
packages
|
|
136
|
+
.filter((pkg) => configured.has(pkg.manifest_path))
|
|
137
|
+
.filter((pkg) => {
|
|
138
|
+
const manifest = parse(readFileSync(pkg.manifest_path, "utf8")) as {
|
|
139
|
+
package?: { publish?: boolean };
|
|
140
|
+
};
|
|
141
|
+
return manifest.package?.publish !== false;
|
|
142
|
+
})
|
|
143
|
+
.map((pkg) => [pkg.name, pkg]),
|
|
144
|
+
);
|
|
145
|
+
const ordered: CargoMetadataPackage[] = [];
|
|
146
|
+
const visiting = new Set<string>();
|
|
147
|
+
const completed = new Set<string>();
|
|
148
|
+
const visit = (pkg: CargoMetadataPackage): void => {
|
|
149
|
+
if (completed.has(pkg.name)) return;
|
|
150
|
+
if (visiting.has(pkg.name)) {
|
|
151
|
+
throw new Error(`Cyclic Cargo publication dependency: ${pkg.name}`);
|
|
152
|
+
}
|
|
153
|
+
visiting.add(pkg.name);
|
|
154
|
+
for (const dependency of pkg.dependencies) {
|
|
155
|
+
if (dependency.source) continue;
|
|
156
|
+
const workspaceDependency = publishable.get(dependency.name);
|
|
157
|
+
if (workspaceDependency) visit(workspaceDependency);
|
|
158
|
+
}
|
|
159
|
+
visiting.delete(pkg.name);
|
|
160
|
+
completed.add(pkg.name);
|
|
161
|
+
ordered.push(pkg);
|
|
162
|
+
};
|
|
163
|
+
for (const pkg of publishable.values()) visit(pkg);
|
|
164
|
+
return ordered.map((pkg) => pkg.manifest_path);
|
|
165
|
+
}
|
|
166
|
+
|
|
97
167
|
function publishCargo(config: RustWorkspaceMapping, registry: string, version: string): void {
|
|
98
|
-
const manifests = config
|
|
99
|
-
.map((crate) => resolve(repoRoot, crate, "Cargo.toml"))
|
|
100
|
-
.filter((manifest) => !/^publish = false$/m.test(readFileSync(manifest, "utf8")));
|
|
168
|
+
const manifests = orderedCargoManifests(config);
|
|
101
169
|
const originals = new Map(
|
|
102
170
|
manifests.map((manifest) => [manifest, readFileSync(manifest, "utf8")]),
|
|
103
171
|
);
|
|
104
172
|
try {
|
|
105
|
-
const crateNames = manifests.map(
|
|
106
|
-
|
|
107
|
-
|
|
173
|
+
const crateNames = manifests.map((manifest) => {
|
|
174
|
+
const document = parse(readFileSync(manifest, "utf8")) as {
|
|
175
|
+
package?: { name?: string };
|
|
176
|
+
};
|
|
177
|
+
if (!document.package?.name) {
|
|
178
|
+
throw new Error(`Cargo manifest has no package name: ${manifest}`);
|
|
179
|
+
}
|
|
180
|
+
return document.package.name;
|
|
181
|
+
});
|
|
108
182
|
for (const manifest of manifests) {
|
|
109
183
|
const mode = statSync(manifest).mode;
|
|
110
184
|
const document = parse(readFileSync(manifest, "utf8")) as Record<string, unknown>;
|
package/tasks/release-pr.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
resolveBaseVersion,
|
|
49
49
|
resolveNextVersion,
|
|
50
50
|
} from "../src/workspace-version.ts";
|
|
51
|
+
import { withWorkspaceMutationLock } from "../src/workspace-lock.ts";
|
|
51
52
|
|
|
52
53
|
const logger = log.logger("projen:release");
|
|
53
54
|
|
|
@@ -303,227 +304,237 @@ program
|
|
|
303
304
|
waitTimeoutMinutes: string;
|
|
304
305
|
}) => {
|
|
305
306
|
const root = projectUtils.root() ?? process.cwd();
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const waitTimeoutMs = waitTimeoutMinutes * 60_000;
|
|
318
|
-
const currentBranch = git(root, ["branch", "--show-current"], { capture: true });
|
|
319
|
-
if (!currentBranch) throw new Error("Release preparation requires a local branch");
|
|
320
|
-
const account = githubAccount(root);
|
|
321
|
-
git(root, ["fetch", "--tags", "origin", opts.base]);
|
|
322
|
-
const comparisonBase = resolveBaseVersion(root, [opts.prefix], {
|
|
323
|
-
fetch: false,
|
|
324
|
-
includeComponentTags: false,
|
|
325
|
-
}).version;
|
|
326
|
-
const next = resolveNextVersion(root, [opts.prefix], opts.level, { fetch: false });
|
|
327
|
-
const releaseVersionScript = fileURLToPath(new URL("./release-version.ts", import.meta.url));
|
|
328
|
-
runTaskCommand(root, process.execPath, [
|
|
329
|
-
releaseVersionScript,
|
|
330
|
-
"--version",
|
|
331
|
-
next.version,
|
|
332
|
-
"--prefix",
|
|
333
|
-
opts.prefix,
|
|
334
|
-
"--assert-next",
|
|
335
|
-
]);
|
|
336
|
-
const releaseTag = `${opts.prefix}${next.version}`;
|
|
337
|
-
const releaseBranch = `release/${releaseTag}`;
|
|
338
|
-
const releaseRoot = join(root, ".worktrees", releaseTag);
|
|
339
|
-
if (currentBranch.startsWith("release/")) {
|
|
340
|
-
throw new Error("Release preparation must start from a source branch");
|
|
341
|
-
}
|
|
342
|
-
if (
|
|
343
|
-
git(root, ["ls-remote", "--tags", "origin", `refs/tags/${releaseTag}`], {
|
|
344
|
-
capture: true,
|
|
345
|
-
check: false,
|
|
346
|
-
})
|
|
347
|
-
) {
|
|
348
|
-
throw new Error(`Release tag already exists: ${releaseTag}`);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const status = git(root, ["status", "--porcelain=v1", "--untracked-files=all"], {
|
|
352
|
-
capture: true,
|
|
353
|
-
});
|
|
354
|
-
if (status) {
|
|
355
|
-
git(root, ["add", "-A"]);
|
|
356
|
-
git(root, ["commit", "-m", opts.message]);
|
|
357
|
-
}
|
|
358
|
-
if (!gitSucceeds(root, ["merge-base", "--is-ancestor", `origin/${opts.base}`, "HEAD"])) {
|
|
359
|
-
git(root, ["merge", "--no-edit", `origin/${opts.base}`]);
|
|
360
|
-
}
|
|
361
|
-
pushCurrentBranch(root, currentBranch);
|
|
362
|
-
|
|
363
|
-
const worktreeExists = existsSync(join(releaseRoot, ".git"));
|
|
364
|
-
if (!worktreeExists) {
|
|
365
|
-
git(root, ["worktree", "prune"]);
|
|
366
|
-
const localBranch = git(root, ["branch", "--list", releaseBranch], { capture: true });
|
|
367
|
-
const remoteBranch = git(
|
|
368
|
-
root,
|
|
369
|
-
["ls-remote", "--heads", "origin", `refs/heads/${releaseBranch}`],
|
|
370
|
-
{ capture: true, check: false },
|
|
371
|
-
);
|
|
372
|
-
if (localBranch || remoteBranch) {
|
|
373
|
-
throw new Error(`Release branch already exists without its worktree: ${releaseBranch}`);
|
|
307
|
+
await withWorkspaceMutationLock(root, async () => {
|
|
308
|
+
const waitTimeoutMinutes = object.toNumber(opts.waitTimeoutMinutes, {
|
|
309
|
+
separators: false,
|
|
310
|
+
percent: false,
|
|
311
|
+
});
|
|
312
|
+
if (
|
|
313
|
+
waitTimeoutMinutes === undefined ||
|
|
314
|
+
!Number.isInteger(waitTimeoutMinutes) ||
|
|
315
|
+
waitTimeoutMinutes <= 0
|
|
316
|
+
) {
|
|
317
|
+
throw new Error("--wait-timeout-minutes must be a positive integer");
|
|
374
318
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
319
|
+
const waitTimeoutMs = waitTimeoutMinutes * 60_000;
|
|
320
|
+
const currentBranch = git(root, ["branch", "--show-current"], { capture: true });
|
|
321
|
+
if (!currentBranch) throw new Error("Release preparation requires a local branch");
|
|
322
|
+
const account = githubAccount(root);
|
|
323
|
+
git(root, ["fetch", "--tags", "origin", opts.base]);
|
|
324
|
+
const comparisonBase = resolveBaseVersion(root, [opts.prefix], {
|
|
325
|
+
fetch: false,
|
|
326
|
+
includeComponentTags: false,
|
|
327
|
+
}).version;
|
|
328
|
+
const next = resolveNextVersion(root, [opts.prefix], opts.level, { fetch: false });
|
|
329
|
+
const releaseVersionScript = fileURLToPath(
|
|
330
|
+
new URL("./release-version.ts", import.meta.url),
|
|
381
331
|
);
|
|
382
|
-
|
|
383
|
-
|
|
332
|
+
runTaskCommand(root, process.execPath, [
|
|
333
|
+
releaseVersionScript,
|
|
334
|
+
"--version",
|
|
335
|
+
next.version,
|
|
336
|
+
"--prefix",
|
|
337
|
+
opts.prefix,
|
|
338
|
+
"--assert-next",
|
|
339
|
+
]);
|
|
340
|
+
const releaseTag = `${opts.prefix}${next.version}`;
|
|
341
|
+
const releaseBranch = `release/${releaseTag}`;
|
|
342
|
+
const releaseRoot = join(root, ".worktrees", releaseTag);
|
|
343
|
+
if (currentBranch.startsWith("release/")) {
|
|
344
|
+
throw new Error("Release preparation must start from a source branch");
|
|
345
|
+
}
|
|
346
|
+
if (
|
|
347
|
+
git(root, ["ls-remote", "--tags", "origin", `refs/tags/${releaseTag}`], {
|
|
348
|
+
capture: true,
|
|
349
|
+
check: false,
|
|
350
|
+
})
|
|
351
|
+
) {
|
|
352
|
+
throw new Error(`Release tag already exists: ${releaseTag}`);
|
|
384
353
|
}
|
|
385
|
-
|
|
386
|
-
|
|
354
|
+
|
|
355
|
+
const status = git(root, ["status", "--porcelain=v1", "--untracked-files=all"], {
|
|
356
|
+
capture: true,
|
|
357
|
+
});
|
|
358
|
+
if (status) {
|
|
359
|
+
git(root, ["add", "-A"]);
|
|
360
|
+
git(root, ["commit", "-m", opts.message]);
|
|
387
361
|
}
|
|
388
|
-
if (
|
|
389
|
-
git(
|
|
362
|
+
if (!gitSucceeds(root, ["merge-base", "--is-ancestor", `origin/${opts.base}`, "HEAD"])) {
|
|
363
|
+
git(root, ["merge", "--no-edit", `origin/${opts.base}`]);
|
|
390
364
|
}
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
runTaskCommand(releaseRoot, process.execPath, ["install"]);
|
|
365
|
+
pushCurrentBranch(root, currentBranch);
|
|
394
366
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
367
|
+
const worktreeExists = existsSync(join(releaseRoot, ".git"));
|
|
368
|
+
if (!worktreeExists) {
|
|
369
|
+
git(root, ["worktree", "prune"]);
|
|
370
|
+
const localBranch = git(root, ["branch", "--list", releaseBranch], { capture: true });
|
|
371
|
+
const remoteBranch = git(
|
|
372
|
+
root,
|
|
373
|
+
["ls-remote", "--heads", "origin", `refs/heads/${releaseBranch}`],
|
|
374
|
+
{ capture: true, check: false },
|
|
375
|
+
);
|
|
376
|
+
if (localBranch || remoteBranch) {
|
|
377
|
+
throw new Error(`Release branch already exists without its worktree: ${releaseBranch}`);
|
|
378
|
+
}
|
|
379
|
+
git(root, ["worktree", "add", "-b", releaseBranch, releaseRoot, "HEAD"]);
|
|
380
|
+
} else {
|
|
381
|
+
const releaseStatus = git(
|
|
382
|
+
releaseRoot,
|
|
383
|
+
["status", "--porcelain=v1", "--untracked-files=all"],
|
|
384
|
+
{ capture: true },
|
|
385
|
+
);
|
|
386
|
+
if (releaseStatus) {
|
|
387
|
+
git(releaseRoot, [
|
|
388
|
+
"stash",
|
|
389
|
+
"push",
|
|
390
|
+
"--include-untracked",
|
|
391
|
+
"--message",
|
|
392
|
+
"release-resume",
|
|
393
|
+
]);
|
|
394
|
+
}
|
|
395
|
+
if (!gitSucceeds(releaseRoot, ["merge-base", "--is-ancestor", currentBranch, "HEAD"])) {
|
|
396
|
+
git(releaseRoot, ["merge", "--no-edit", currentBranch]);
|
|
397
|
+
}
|
|
398
|
+
if (releaseStatus) {
|
|
399
|
+
git(releaseRoot, ["stash", "pop"]);
|
|
400
|
+
}
|
|
401
|
+
logger.info(`resuming ${releaseBranch} in ${releaseRoot}`);
|
|
402
|
+
}
|
|
403
|
+
runTaskCommand(releaseRoot, process.execPath, ["install"]);
|
|
409
404
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
405
|
+
const bumpScript = fileURLToPath(new URL("./bump.ts", import.meta.url));
|
|
406
|
+
const versionCheckScript = fileURLToPath(new URL("./version-check.ts", import.meta.url));
|
|
407
|
+
runTaskCommand(releaseRoot, process.execPath, [
|
|
408
|
+
bumpScript,
|
|
409
|
+
"--level",
|
|
410
|
+
opts.level,
|
|
411
|
+
"--prefix",
|
|
412
|
+
opts.prefix,
|
|
413
|
+
...opts.os.flatMap((value) => ["--os", value]),
|
|
414
|
+
...opts.arch.flatMap((value) => ["--arch", value]),
|
|
415
|
+
]);
|
|
416
|
+
if (readWorkspaceVersion(releaseRoot) !== next.version) {
|
|
417
|
+
throw new Error(`Release preparation did not produce ${next.version}`);
|
|
414
418
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
419
|
+
|
|
420
|
+
runTaskCommand(releaseRoot, process.execPath, [versionCheckScript]);
|
|
421
|
+
if (opts.validate) {
|
|
422
|
+
for (const task of opts.validateTask) {
|
|
423
|
+
runTaskCommand(releaseRoot, process.execPath, ["run", task]);
|
|
424
|
+
}
|
|
425
|
+
if (existsSync(join(releaseRoot, "Cargo.toml"))) {
|
|
426
|
+
runTaskCommand(releaseRoot, "cargo", [
|
|
427
|
+
"test",
|
|
428
|
+
"--workspace",
|
|
429
|
+
...(existsSync(join(releaseRoot, "Cargo.lock")) ? ["--locked"] : []),
|
|
430
|
+
]);
|
|
431
|
+
}
|
|
432
|
+
runTaskCommand(releaseRoot, process.execPath, ["run", "compile"]);
|
|
433
|
+
} else {
|
|
434
|
+
logger.warn("release validation skipped by --no-validate");
|
|
421
435
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
logger.warn("release validation skipped by --no-validate");
|
|
425
|
-
}
|
|
426
|
-
if (opts.localPublish) {
|
|
427
|
-
await publishLocalRelease({
|
|
428
|
-
root: releaseRoot,
|
|
429
|
-
version: next.version,
|
|
430
|
-
localRegistry: opts.localRegistry,
|
|
431
|
-
localPypi: opts.localPypi,
|
|
432
|
-
pythonRoot: opts.pythonRoot,
|
|
433
|
-
localCargo: opts.localCargo,
|
|
434
|
-
reuseValidatedNodeCompile: opts.validate,
|
|
435
|
-
});
|
|
436
|
-
} else {
|
|
437
|
-
logger.info("local publication skipped by --no-local-publish");
|
|
438
|
-
}
|
|
439
|
-
runTaskCommand(releaseRoot, process.execPath, [versionCheckScript]);
|
|
440
|
-
const releaseSummary = opts.releaseSummary
|
|
441
|
-
? await generateReleaseSummary({
|
|
436
|
+
if (opts.localPublish) {
|
|
437
|
+
await publishLocalRelease({
|
|
442
438
|
root: releaseRoot,
|
|
443
439
|
version: next.version,
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
440
|
+
localRegistry: opts.localRegistry,
|
|
441
|
+
localPypi: opts.localPypi,
|
|
442
|
+
pythonRoot: opts.pythonRoot,
|
|
443
|
+
localCargo: opts.localCargo,
|
|
444
|
+
reuseValidatedNodeCompile: opts.validate,
|
|
445
|
+
});
|
|
446
|
+
} else {
|
|
447
|
+
logger.info("local publication skipped by --no-local-publish");
|
|
448
|
+
}
|
|
449
|
+
runTaskCommand(releaseRoot, process.execPath, [versionCheckScript]);
|
|
450
|
+
const releaseSummary = opts.releaseSummary
|
|
451
|
+
? await generateReleaseSummary({
|
|
452
|
+
root: releaseRoot,
|
|
453
|
+
version: next.version,
|
|
454
|
+
fromRef: `${opts.prefix}${comparisonBase}`,
|
|
455
|
+
providers: releaseSummaryProviders(opts.releaseSummaryProviders),
|
|
456
|
+
})
|
|
457
|
+
: undefined;
|
|
448
458
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
459
|
+
git(releaseRoot, ["add", "-A"]);
|
|
460
|
+
const staged = git(releaseRoot, ["diff", "--cached", "--name-only"], { capture: true });
|
|
461
|
+
if (staged) {
|
|
462
|
+
git(releaseRoot, ["commit", "-m", `chore(release): ${next.version}`]);
|
|
463
|
+
} else if (!worktreeExists) {
|
|
464
|
+
throw new Error("Release preparation produced no changes");
|
|
465
|
+
}
|
|
466
|
+
// The source push and release commit hooks have already scanned every new
|
|
467
|
+
// byte. A new remote branch has no upstream comparison point, so the
|
|
468
|
+
// managed pre-push hook would rescan the repository's complete history.
|
|
469
|
+
git(releaseRoot, ["push", "--no-verify", "--set-upstream", "origin", releaseBranch]);
|
|
460
470
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
471
|
+
const title = `chore(release): ${next.version}`;
|
|
472
|
+
const body = [
|
|
473
|
+
`Release ${releaseTag}.`,
|
|
474
|
+
"",
|
|
475
|
+
`Source commit: ${git(releaseRoot, ["rev-parse", `${releaseBranch}^`], { capture: true })}`,
|
|
476
|
+
"",
|
|
477
|
+
"Merging this PR updates VERSION on main and starts the public release workflow.",
|
|
478
|
+
...(releaseSummary ? ["", releaseSummary] : []),
|
|
479
|
+
].join("\n");
|
|
480
|
+
const githubEnvironment = {
|
|
481
|
+
...process.env,
|
|
482
|
+
GH_HOST: account.hostname,
|
|
483
|
+
GH_REPO: githubRepositorySpecifier(account),
|
|
484
|
+
[githubTokenEnvironmentName(account.hostname)]: account.token,
|
|
485
|
+
};
|
|
486
|
+
const ensurePullRequest = (): void => {
|
|
487
|
+
if (
|
|
488
|
+
taskCommandSucceeds(root, "gh", ["pr", "view", releaseBranch], {
|
|
489
|
+
env: githubEnvironment,
|
|
490
|
+
})
|
|
491
|
+
) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
runTaskCommand(
|
|
495
|
+
root,
|
|
496
|
+
"gh",
|
|
497
|
+
[
|
|
498
|
+
"pr",
|
|
499
|
+
"create",
|
|
500
|
+
"--base",
|
|
501
|
+
opts.base,
|
|
502
|
+
"--head",
|
|
503
|
+
releaseBranch,
|
|
504
|
+
"--title",
|
|
505
|
+
title,
|
|
506
|
+
"--body",
|
|
507
|
+
body,
|
|
508
|
+
],
|
|
509
|
+
{ env: githubEnvironment },
|
|
510
|
+
);
|
|
511
|
+
};
|
|
512
|
+
ensurePullRequest();
|
|
513
|
+
if (opts.approve) {
|
|
514
|
+
runTaskCommand(root, "gh", ["pr", "merge", releaseBranch, "--auto", "--merge"], {
|
|
479
515
|
env: githubEnvironment,
|
|
480
|
-
})
|
|
481
|
-
) {
|
|
482
|
-
return;
|
|
516
|
+
});
|
|
483
517
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
"create",
|
|
490
|
-
"--base",
|
|
491
|
-
opts.base,
|
|
492
|
-
"--head",
|
|
518
|
+
git(root, ["worktree", "remove", "--force", releaseRoot]);
|
|
519
|
+
git(root, ["branch", "--delete", "--force", releaseBranch]);
|
|
520
|
+
if (opts.approve && opts.wait) {
|
|
521
|
+
const mergeSha = await waitForPullRequestMerge(
|
|
522
|
+
root,
|
|
493
523
|
releaseBranch,
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
{
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
git(root, ["worktree", "remove", "--force", releaseRoot]);
|
|
509
|
-
git(root, ["branch", "--delete", "--force", releaseBranch]);
|
|
510
|
-
if (opts.approve && opts.wait) {
|
|
511
|
-
const mergeSha = await waitForPullRequestMerge(
|
|
512
|
-
root,
|
|
513
|
-
releaseBranch,
|
|
514
|
-
githubEnvironment,
|
|
515
|
-
waitTimeoutMs,
|
|
524
|
+
githubEnvironment,
|
|
525
|
+
waitTimeoutMs,
|
|
526
|
+
);
|
|
527
|
+
logger.info("release pull request merged", { releaseBranch, mergeSha });
|
|
528
|
+
await waitForReleaseWorkflow(root, opts.base, mergeSha, githubEnvironment, waitTimeoutMs);
|
|
529
|
+
logger.success(`published ${releaseTag} from ${mergeSha}`);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
logger.success(
|
|
533
|
+
`${
|
|
534
|
+
opts.approve ? "enabled automatic merge for" : "opened"
|
|
535
|
+
} ${releaseBranch} for ${releaseTag}${opts.approve && !opts.wait ? " without waiting" : ""}`,
|
|
516
536
|
);
|
|
517
|
-
|
|
518
|
-
await waitForReleaseWorkflow(root, opts.base, mergeSha, githubEnvironment, waitTimeoutMs);
|
|
519
|
-
logger.success(`published ${releaseTag} from ${mergeSha}`);
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
logger.success(
|
|
523
|
-
`${
|
|
524
|
-
opts.approve ? "enabled automatic merge for" : "opened"
|
|
525
|
-
} ${releaseBranch} for ${releaseTag}${opts.approve && !opts.wait ? " without waiting" : ""}`,
|
|
526
|
-
);
|
|
537
|
+
});
|
|
527
538
|
},
|
|
528
539
|
);
|
|
529
540
|
|
package/tasks/sync.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { log } from "@dbx-tools/shared-core";
|
|
|
4
4
|
import concurrently from "concurrently";
|
|
5
5
|
import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
|
|
6
6
|
import { runSynth } from "../src/scaffold.ts";
|
|
7
|
+
import { withWorkspaceMutationLock } from "../src/workspace-lock.ts";
|
|
7
8
|
|
|
8
9
|
const logger = log.logger("projen:sync");
|
|
9
10
|
|
|
@@ -39,7 +40,7 @@ if (!process.argv.includes("--watch")) {
|
|
|
39
40
|
// One-shot: full synth (+install + barrels via the post-synth component). This is
|
|
40
41
|
// the scriptable path, so a failed synth stays a failed exit code.
|
|
41
42
|
logger.start("synthesizing");
|
|
42
|
-
runSynth({ post: true });
|
|
43
|
+
await withWorkspaceMutationLock(repoRoot, () => runSynth({ post: true }));
|
|
43
44
|
logger.success("synced");
|
|
44
45
|
} else {
|
|
45
46
|
// Watch: one initial full synth to bring the tree up to date, then focused
|
|
@@ -52,7 +53,7 @@ if (!process.argv.includes("--watch")) {
|
|
|
52
53
|
// fatal: errors are logged and retried, and only a stop signal ends the task.
|
|
53
54
|
logger.start("initial sync");
|
|
54
55
|
try {
|
|
55
|
-
runSynth({ post: true });
|
|
56
|
+
await withWorkspaceMutationLock(repoRoot, () => runSynth({ post: true }));
|
|
56
57
|
logger.success("synced - watching (Ctrl-C to stop)");
|
|
57
58
|
} catch (err) {
|
|
58
59
|
// A tree that doesn't synth is precisely when the watcher is most useful: the edit
|