@dbx-tools/projen 0.9.6 → 0.9.8
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 +5 -1
- package/index.ts +2 -1
- package/package.json +4 -4
- package/src/_rust-project.ts +46 -0
- package/src/_rust-release-workflow.ts +157 -52
- package/src/barrels.ts +3 -4
- package/src/bun-app.ts +96 -85
- package/src/bun-workflow.ts +60 -52
- package/src/openapi.ts +15 -8
- package/src/project-py.ts +22 -17
- package/src/project-rs.ts +48 -11
- package/src/release-catalog.ts +4 -1
- package/src/release.ts +206 -170
- package/src/watch.ts +4 -1
- package/src/workspace-lock.ts +22 -0
- package/tasks/publish-uniffi-local.ts +80 -7
- package/tasks/release-pr.ts +215 -204
- package/tasks/sync.ts +6 -5
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
|
|
@@ -95,9 +96,9 @@ if (!process.argv.includes("--watch")) {
|
|
|
95
96
|
* re-emitting it is how we say "stop" in a language the supervisor understands.
|
|
96
97
|
*
|
|
97
98
|
* Killing is asynchronous (concurrently shells out to `ps` to walk each process tree)
|
|
98
|
-
* and can outlive `result
|
|
99
|
-
*
|
|
100
|
-
*
|
|
99
|
+
* and can outlive `result`. The timer keeps the process open until the kills
|
|
100
|
+
* land, which prevents orphaned watchers after SIGTERM. It also stops waiting
|
|
101
|
+
* if one of them wedges.
|
|
101
102
|
*/
|
|
102
103
|
function stop(): void {
|
|
103
104
|
if (stopping) return;
|