@aidc-toolkit/dev 0.9.17-beta → 0.9.19-beta

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.
@@ -0,0 +1,903 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as process from "node:process";
5
+ import sharedConfigurationJSON from "../../config/publish.json";
6
+ import localConfigurationJSON from "../../config/publish.local.json";
7
+ import { logger, setLogLevel } from "./logger.js";
8
+ import { omit, pick } from "./type-helper.js";
9
+
10
+ const SHARED_CONFIGURATION_PATH = "config/publish.json";
11
+ const LOCAL_CONFIGURATION_PATH = "config/publish.local.json";
12
+
13
+ // Configuration may be written from any directory so full paths are required.
14
+ const SHARED_CONFIGURATION_FULL_PATH = path.resolve(SHARED_CONFIGURATION_PATH);
15
+ const LOCAL_CONFIGURATION_FULL_PATH = path.resolve(LOCAL_CONFIGURATION_PATH);
16
+
17
+ /**
18
+ * Repository.
19
+ */
20
+ export interface Repository {
21
+ /**
22
+ * Directory in which repository resides, if different from repository name.
23
+ */
24
+ directory?: string;
25
+
26
+ /**
27
+ * Dependency type, dictating how it is published.
28
+ */
29
+ dependencyType: string;
30
+
31
+ /**
32
+ * Platform if building across platforms (local configuration), e.g., macOS hosting Windows on Parallels.
33
+ */
34
+ platform?: {
35
+ /**
36
+ * CPU architecture of native modules to install.
37
+ */
38
+ cpu: string;
39
+
40
+ /**
41
+ * OS of native modules to install.
42
+ */
43
+ os: string;
44
+ };
45
+
46
+ /**
47
+ * Additional dependencies not included in package configuration.
48
+ */
49
+ additionalDependencies?: string[];
50
+
51
+ /**
52
+ * Paths to exclude from consideration when checking for changes.
53
+ */
54
+ excludePaths?: string[];
55
+
56
+ /**
57
+ * Date/time in ISO format the last alpha version was published.
58
+ */
59
+ lastAlphaPublished?: string;
60
+
61
+ /**
62
+ * Current step in beta publication; used to resume after failure recovery.
63
+ */
64
+ publishBetaStep?: string | undefined;
65
+
66
+ /**
67
+ * Date/time in ISO format the last beta version was published.
68
+ */
69
+ lastBetaPublished?: string;
70
+
71
+ /**
72
+ * Date/time in ISO format the last production version was published.
73
+ */
74
+ lastProductionPublished?: string;
75
+
76
+ /**
77
+ * Last production version.
78
+ */
79
+ lastProductionVersion?: string;
80
+ }
81
+
82
+ /**
83
+ * Configuration layout of merged publish.json and publish.local.json.
84
+ */
85
+ export interface Configuration {
86
+ /**
87
+ * Organization that owns the repositories.
88
+ */
89
+ organization: string;
90
+
91
+ /**
92
+ * Log level (local configuration).
93
+ */
94
+ logLevel?: string;
95
+
96
+ /**
97
+ * Registry hosting organization's alpha repositories (local configuration).
98
+ */
99
+ alphaRegistry: string;
100
+
101
+ /**
102
+ * Repositories.
103
+ */
104
+ repositories: Record<string, Repository>;
105
+ }
106
+
107
+ export const PACKAGE_CONFIGURATION_PATH = "package.json";
108
+
109
+ export const PACKAGE_LOCK_CONFIGURATION_PATH = "package-lock.json";
110
+
111
+ /**
112
+ * Configuration layout of package.json (relevant attributes only).
113
+ */
114
+ export interface PackageConfiguration {
115
+ /**
116
+ * Name.
117
+ */
118
+ name: string;
119
+
120
+ /**
121
+ * Version.
122
+ */
123
+ version: string;
124
+
125
+ /**
126
+ * Development dependencies.
127
+ */
128
+ devDependencies?: Record<string, string>;
129
+
130
+ /**
131
+ * Dependencies.
132
+ */
133
+ dependencies?: Record<string, string>;
134
+ }
135
+
136
+ /**
137
+ * Release type.
138
+ */
139
+ type ReleaseType = "alpha" | "beta" | "production";
140
+
141
+ /**
142
+ * Publish base class.
143
+ */
144
+ export abstract class Publish {
145
+ /**
146
+ * Release type.
147
+ */
148
+ private readonly _releaseType: ReleaseType;
149
+
150
+ /**
151
+ * If true, outputs what would be run rather than running it.
152
+ */
153
+ private readonly _dryRun: boolean;
154
+
155
+ /**
156
+ * Configuration. Merger of shared and local configurations.
157
+ */
158
+ private readonly _configuration: Configuration;
159
+
160
+ /**
161
+ * At organization.
162
+ */
163
+ private readonly _atOrganization: string;
164
+
165
+ /**
166
+ * At organization registry parameter.
167
+ */
168
+ private readonly _atOrganizationRegistry: string;
169
+
170
+ /**
171
+ * All organization dependencies, keyed on repository name.
172
+ */
173
+ private readonly _allOrganizationDependencies: Record<string, Record<string, string | null>>;
174
+
175
+ /**
176
+ * Current repository name.
177
+ */
178
+ private _repositoryName!: string;
179
+
180
+ /**
181
+ * Current repository.
182
+ */
183
+ private _repository!: Repository;
184
+
185
+ /**
186
+ * NPM platform arguments if any.
187
+ */
188
+ private _npmPlatformArgs!: string[];
189
+
190
+ /**
191
+ * Branch.
192
+ */
193
+ private _branch!: string;
194
+
195
+ /**
196
+ * Package configuration.
197
+ */
198
+ private _packageConfiguration!: PackageConfiguration;
199
+
200
+ /**
201
+ * Major version.
202
+ */
203
+ private _majorVersion!: number;
204
+
205
+ /**
206
+ * Minor version.
207
+ */
208
+ private _minorVersion!: number;
209
+
210
+ /**
211
+ * Patch version.
212
+ */
213
+ private _patchVersion!: number;
214
+
215
+ /**
216
+ * Pre-release identifier or null if none.
217
+ */
218
+ private _preReleaseIdentifier!: string | null;
219
+
220
+ /**
221
+ * Dependencies that belong to the organization, keyed on repository name; null if additional (not included in
222
+ * package configuration).
223
+ */
224
+ private _organizationDependencies!: Record<string, string | null>;
225
+
226
+ /**
227
+ * True if any organization dependency has been updated.
228
+ */
229
+ private _organizationDependenciesUpdated!: boolean;
230
+
231
+ /**
232
+ * Constructor.
233
+ *
234
+ * @param releaseType
235
+ * Release type.
236
+ *
237
+ * @param dryRun
238
+ * If true, outputs what would be run rather than running it.
239
+ */
240
+ protected constructor(releaseType: ReleaseType, dryRun: boolean) {
241
+ this._releaseType = releaseType;
242
+ this._dryRun = dryRun;
243
+
244
+ // Merge shared and local configurations.
245
+ this._configuration = {
246
+ ...omit(sharedConfigurationJSON, "repositories"),
247
+ ...omit(localConfigurationJSON, "repositories"),
248
+ repositories: Object.fromEntries(Object.entries(sharedConfigurationJSON.repositories).map(([repositoryName, repository]) => [repositoryName, {
249
+ ...repository,
250
+ ...((localConfigurationJSON.repositories as Record<string, Partial<Repository> | undefined>)[repositoryName] ?? {})
251
+ }]))
252
+ };
253
+
254
+ this._atOrganization = `@${this.configuration.organization}`;
255
+
256
+ this._atOrganizationRegistry = `${this.atOrganization}:registry=${this.configuration.alphaRegistry}`;
257
+
258
+ this._allOrganizationDependencies = {};
259
+
260
+ if (this._configuration.logLevel !== undefined) {
261
+ setLogLevel(this._configuration.logLevel);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Get the release type.
267
+ */
268
+ protected get releaseType(): ReleaseType {
269
+ return this._releaseType;
270
+ }
271
+
272
+ /**
273
+ * Determine if outputs what would be run rather than running it.
274
+ */
275
+ protected get dryRun(): boolean {
276
+ return this._dryRun;
277
+ }
278
+
279
+ /**
280
+ * Get the configuration.
281
+ */
282
+ protected get configuration(): Configuration {
283
+ return this._configuration;
284
+ }
285
+
286
+ /**
287
+ * Get the at organization.
288
+ */
289
+ protected get atOrganization(): string {
290
+ return this._atOrganization;
291
+ }
292
+
293
+ /**
294
+ * Get the at organization registry parameter.
295
+ */
296
+ protected get atOrganizationRegistry(): string {
297
+ return this._atOrganizationRegistry;
298
+ }
299
+
300
+ /**
301
+ * Get all organization dependencies, keyed on repository name.
302
+ */
303
+ protected get allOrganizationDependencies(): Record<string, Record<string, string | null>> {
304
+ return this._allOrganizationDependencies;
305
+ }
306
+
307
+ /**
308
+ * Get the current repository name.
309
+ */
310
+ protected get repositoryName(): string {
311
+ return this._repositoryName;
312
+ }
313
+
314
+ /**
315
+ * Get the current repository.
316
+ */
317
+ protected get repository(): Repository {
318
+ return this._repository;
319
+ }
320
+
321
+ /**
322
+ * Get the NPM platform arguments if any.
323
+ */
324
+ get npmPlatformArgs(): string[] {
325
+ return this._npmPlatformArgs;
326
+ }
327
+
328
+ /**
329
+ * Get the branch.
330
+ */
331
+ protected get branch(): string {
332
+ return this._branch;
333
+ }
334
+
335
+ /**
336
+ * Get the package configuration.
337
+ */
338
+ protected get packageConfiguration(): PackageConfiguration {
339
+ return this._packageConfiguration;
340
+ }
341
+
342
+ /**
343
+ * Get the major version.
344
+ */
345
+ protected get majorVersion(): number {
346
+ return this._majorVersion;
347
+ }
348
+
349
+ /**
350
+ * Get the minor version.
351
+ */
352
+ protected get minorVersion(): number {
353
+ return this._minorVersion;
354
+ }
355
+
356
+ /**
357
+ * Get the patch version.
358
+ */
359
+ protected get patchVersion(): number {
360
+ return this._patchVersion;
361
+ }
362
+
363
+ /**
364
+ * Get the pre-release identifier.
365
+ */
366
+ protected get preReleaseIdentifier(): string | null {
367
+ return this._preReleaseIdentifier;
368
+ }
369
+
370
+ /**
371
+ * Get dependencies that belong to the organization, keyed on repository name.
372
+ */
373
+ protected get organizationDependencies(): Record<string, string | null> {
374
+ return this._organizationDependencies;
375
+ }
376
+
377
+ /**
378
+ * Determine if any organization dependency has been updated.
379
+ */
380
+ protected get organizationDependenciesUpdated(): boolean {
381
+ return this._organizationDependenciesUpdated;
382
+ }
383
+
384
+ /**
385
+ * Run a command and optionally capture its output.
386
+ *
387
+ * @param ignoreDryRun
388
+ * If true, dry run setting is ignored.
389
+ *
390
+ * @param captureOutput
391
+ * If true, output is captured and returned.
392
+ *
393
+ * @param command
394
+ * Command to run.
395
+ *
396
+ * @param args
397
+ * Arguments to command.
398
+ *
399
+ * @returns
400
+ * Output if captured or empty array if not.
401
+ */
402
+ protected run(ignoreDryRun: boolean, captureOutput: boolean, command: string, ...args: string[]): string[] {
403
+ if (!ignoreDryRun && captureOutput) {
404
+ throw new Error("Cannot capture output in dry run");
405
+ }
406
+
407
+ let output: string[];
408
+
409
+ const runningCommand = `Running command "${command}" with arguments [${args.join(", ")}].`;
410
+
411
+ if (this.dryRun && !ignoreDryRun) {
412
+ logger.info(`Dry run: ${runningCommand}`);
413
+
414
+ output = [];
415
+ } else {
416
+ logger.debug(runningCommand);
417
+
418
+ const spawnResult = spawnSync(command, args, {
419
+ stdio: ["inherit", captureOutput ? "pipe" : "inherit", "inherit"]
420
+ });
421
+
422
+ if (spawnResult.error !== undefined) {
423
+ throw spawnResult.error;
424
+ }
425
+
426
+ if (spawnResult.status === null) {
427
+ throw new Error(`Terminated by signal ${spawnResult.signal}`);
428
+ }
429
+
430
+ if (spawnResult.status !== 0) {
431
+ throw new Error(`Failed with status ${spawnResult.status}`);
432
+ }
433
+
434
+ // Last line is also terminated by newline and split() places empty string at the end, so use slice() to remove it.
435
+ output = captureOutput ? spawnResult.stdout.toString().split("\n").slice(0, -1) : [];
436
+
437
+ if (captureOutput) {
438
+ logger.trace(`Output:\n${output.join("\n")}`);
439
+ }
440
+ }
441
+
442
+ return output;
443
+ }
444
+
445
+ /**
446
+ * Get the repository name for a dependency if it belongs to the organization or null if not.
447
+ *
448
+ * @param dependency
449
+ * Dependency.
450
+ *
451
+ * @returns
452
+ * Repository name for dependency or null.
453
+ */
454
+ protected dependencyRepositoryName(dependency: string): string | null {
455
+ const parsedDependency = dependency.split("/");
456
+
457
+ return parsedDependency.length === 2 && parsedDependency[0] === this.atOrganization ? parsedDependency[1] : null;
458
+ }
459
+
460
+ /**
461
+ * Determine if there have been any changes to the current repository.
462
+ *
463
+ * @param lastPublished
464
+ * Date/time in ISO format to check against.
465
+ *
466
+ * @param ignoreGitHub
467
+ * If true, ignore .github directory.
468
+ *
469
+ * @returns
470
+ * True if there is no last published date/time or if there have been any changes since then.
471
+ */
472
+ protected anyChanges(lastPublished: string | undefined, ignoreGitHub: boolean): boolean {
473
+ let anyChanges: boolean;
474
+
475
+ const excludePaths = this.repository.excludePaths ?? [];
476
+
477
+ const changedFilesSet = new Set<string>();
478
+
479
+ /**
480
+ * Process a changed file.
481
+ *
482
+ * @param status
483
+ * "R" if the file has been renamed, "D" if the file has been deleted, otherwise file has been added.
484
+ *
485
+ * @param file
486
+ * Original file name if status is "R", otherwise file to be added or deleted.
487
+ *
488
+ * @param newFile
489
+ * New file name if status is "R", undefined otherwise.
490
+ */
491
+ function processChangedFile(status: string, file: string, newFile: string | undefined): void {
492
+ // Status is "D" if deleted, "R" if renamed.
493
+ const deleteFile = status === "D" || status === "R" ? file : undefined;
494
+ const addFile = status === "R" ? newFile : status !== "D" ? file : undefined;
495
+
496
+ // Remove deleted file; anything that depends on a deleted file will have been modified.
497
+ if (deleteFile !== undefined && changedFilesSet.delete(deleteFile)) {
498
+ logger.debug(`-${deleteFile}`);
499
+ }
500
+
501
+ if (addFile !== undefined && !changedFilesSet.has(addFile)) {
502
+ // Exclude hidden files and directories except possibly .github directory, package-lock.json, test directory, and any explicitly excluded files or directories.
503
+ if (((!addFile.startsWith(".") && !addFile.includes("/.")) || (!ignoreGitHub && addFile.startsWith(".github/"))) && addFile !== PACKAGE_LOCK_CONFIGURATION_PATH && !addFile.startsWith("test/") && excludePaths.filter(excludePath => addFile === excludePath || (excludePath.endsWith("/") && addFile.startsWith(excludePath))).length === 0) {
504
+ logger.debug(`+${addFile}`);
505
+
506
+ changedFilesSet.add(addFile);
507
+ } else {
508
+ // File is excluded.
509
+ logger.debug(`*${addFile}`);
510
+ }
511
+ }
512
+ }
513
+
514
+ if (this.releaseType !== "alpha" && this.run(true, true, "git", "fetch", "--porcelain", "--dry-run").length !== 0) {
515
+ throw new Error("Remote repository has outstanding changes");
516
+ }
517
+
518
+ if (lastPublished !== undefined) {
519
+ // Get all files committed since last published.
520
+ for (const line of this.run(true, true, "git", "log", "--since", lastPublished, "--name-status", "--pretty=oneline")) {
521
+ // Header starts with 40-character SHA.
522
+ if (/^[0-9a-f]{40} /.test(line)) {
523
+ logger.debug(`Commit SHA ${line.substring(0, 40)}`);
524
+ } else {
525
+ const [status, file, newFile] = line.split("\t");
526
+
527
+ processChangedFile(status.charAt(0), file, newFile);
528
+ }
529
+ }
530
+
531
+ // Get all uncommitted files.
532
+ const output = this.run(true, true, "git", "status", "--porcelain");
533
+
534
+ if (output.length !== 0) {
535
+ // Beta or production publication requires that repository be fully committed.
536
+ if (this.releaseType !== "alpha") {
537
+ throw new Error("Repository has uncommitted changes");
538
+ }
539
+
540
+ logger.debug("Uncommitted");
541
+
542
+ for (const line of output) {
543
+ // Line is two-character status, space, and detail.
544
+ const status = line.substring(0, 1);
545
+ const [file, newFile] = line.substring(3).split(" -> ");
546
+
547
+ processChangedFile(status, file, newFile);
548
+ }
549
+ }
550
+
551
+ const lastPublishedDateTime = new Date(lastPublished);
552
+
553
+ anyChanges = false;
554
+
555
+ for (const changedFile of changedFilesSet) {
556
+ if (fs.lstatSync(changedFile).mtime > lastPublishedDateTime) {
557
+ if (!anyChanges) {
558
+ logger.info("Changes");
559
+
560
+ anyChanges = true;
561
+ }
562
+
563
+ logger.info(`>${changedFile}`);
564
+ }
565
+ }
566
+
567
+ if (!anyChanges && this.organizationDependenciesUpdated) {
568
+ logger.info("Organization dependencies updated");
569
+
570
+ anyChanges = true;
571
+ }
572
+
573
+ if (!anyChanges) {
574
+ logger.info("No changes");
575
+ }
576
+ } else {
577
+ logger.info("No last published");
578
+
579
+ // No last published, so there must have been changes.
580
+ anyChanges = true;
581
+ }
582
+
583
+ return anyChanges;
584
+ }
585
+
586
+ /**
587
+ * Commit files that have been modified.
588
+ *
589
+ * @param message
590
+ * Commit message.
591
+ *
592
+ * @param files
593
+ * Files to commit; if none, defaults to "--all".
594
+ */
595
+ protected commitModified(message: string, ...files: string[]): void {
596
+ const modifiedFiles: string[] = [];
597
+
598
+ if (files.length === 0) {
599
+ modifiedFiles.push("--all");
600
+ } else {
601
+ for (const line of this.run(true, true, "git", "status", ...files, "--porcelain")) {
602
+ const status = line.substring(0, 3);
603
+ const modifiedFile = line.substring(3);
604
+
605
+ // Only interest is in local additions and modifications with no conflicts.
606
+ if (status !== "A " && status !== " M " && status !== "AM ") {
607
+ throw new Error(`Unsupported status "${status}" for ${modifiedFile}`);
608
+ }
609
+
610
+ modifiedFiles.push(modifiedFile);
611
+ }
612
+ }
613
+
614
+ if (modifiedFiles.length !== 0) {
615
+ this.run(false, false, "git", "commit", ...modifiedFiles, "--message", message);
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Save package configuration.
621
+ */
622
+ protected savePackageConfiguration(): void {
623
+ if (this.dryRun) {
624
+ logger.info(`Dry run: Saving package configuration\n${JSON.stringify(pick(this.packageConfiguration, "name", "version", "devDependencies", "dependencies"), null, 2)}\n`);
625
+ } else {
626
+ fs.writeFileSync(PACKAGE_CONFIGURATION_PATH, `${JSON.stringify(this.packageConfiguration, null, 2)}\n`);
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Update the package version.
632
+ *
633
+ * @param majorVersion
634
+ * Major version or undefined if no change.
635
+ *
636
+ * @param minorVersion
637
+ * Minor version or undefined if no change.
638
+ *
639
+ * @param patchVersion
640
+ * Patch version or undefined if no change.
641
+ *
642
+ * @param preReleaseIdentifier
643
+ * Pre-release identifier or undefined if no change.
644
+ */
645
+ protected updatePackageVersion(majorVersion: number | undefined, minorVersion: number | undefined, patchVersion: number | undefined, preReleaseIdentifier: string | null | undefined): void {
646
+ if (majorVersion !== undefined) {
647
+ this._majorVersion = majorVersion;
648
+ }
649
+
650
+ if (minorVersion !== undefined) {
651
+ this._minorVersion = minorVersion;
652
+ }
653
+
654
+ if (patchVersion !== undefined) {
655
+ this._patchVersion = patchVersion;
656
+ }
657
+
658
+ if (preReleaseIdentifier !== undefined) {
659
+ this._preReleaseIdentifier = preReleaseIdentifier;
660
+ }
661
+
662
+ this.packageConfiguration.version = `${this.majorVersion}.${this.minorVersion}.${this.patchVersion}${this.preReleaseIdentifier !== null ? `-${this.preReleaseIdentifier}` : ""}`;
663
+
664
+ this.savePackageConfiguration();
665
+ }
666
+
667
+ /**
668
+ * Commit changes resulting from updating the package version.
669
+ *
670
+ * @param files
671
+ * Files to commit; if none, defaults to "--all".
672
+ */
673
+ protected commitUpdatedPackageVersion(...files: string[]): void {
674
+ this.commitModified(`Updated to version ${this.packageConfiguration.version}.`, ...files);
675
+ }
676
+
677
+ /**
678
+ * Save the current configuration.
679
+ */
680
+ protected saveConfiguration(): void {
681
+ const saveSharedRepositories: Record<string, Omit<Repository, "platform" | "lastAlphaPublished">> = {};
682
+ const saveLocalRepositories: Record<string, Pick<Repository, "platform" | "lastAlphaPublished">> = {};
683
+
684
+ for (const [repositoryName, repository] of Object.entries(this.configuration.repositories)) {
685
+ saveSharedRepositories[repositoryName] = omit(repository, "platform", "lastAlphaPublished");
686
+ saveLocalRepositories[repositoryName] = pick(repository, "platform", "lastAlphaPublished");
687
+ }
688
+
689
+ const saveSharedConfigurationJSON = JSON.stringify({
690
+ ...omit(this.configuration, "logLevel", "alphaRegistry", "repositories"),
691
+ repositories: saveSharedRepositories
692
+ }, null, 2);
693
+
694
+ const saveLocalConfigurationJSON = JSON.stringify({
695
+ ...pick(this.configuration, "logLevel", "alphaRegistry"),
696
+ repositories: saveLocalRepositories
697
+ }, null, 2);
698
+
699
+ if (this.dryRun) {
700
+ logger.info(`Dry run: Saving shared configuration\n${saveSharedConfigurationJSON}\n`);
701
+ logger.info(`Dry run: Saving local configuration\n${saveLocalConfigurationJSON}\n`);
702
+ } else {
703
+ fs.writeFileSync(SHARED_CONFIGURATION_FULL_PATH, saveSharedConfigurationJSON);
704
+ fs.writeFileSync(LOCAL_CONFIGURATION_FULL_PATH, saveLocalConfigurationJSON);
705
+ }
706
+ }
707
+
708
+ /**
709
+ * Publish current repository.
710
+ */
711
+ protected abstract publish(): void | Promise<void>;
712
+
713
+ /**
714
+ * Publish all repositories.
715
+ */
716
+ async publishAll(): Promise<void> {
717
+ const startDirectory = process.cwd();
718
+
719
+ for (const [repositoryName, repository] of Object.entries(this.configuration.repositories)) {
720
+ this._repositoryName = repositoryName;
721
+ this._repository = repository;
722
+
723
+ this._npmPlatformArgs = repository.platform !== undefined ?
724
+ [
725
+ "--cpu",
726
+ repository.platform.cpu,
727
+ "--os",
728
+ repository.platform.os
729
+ ] :
730
+ [];
731
+
732
+ this._branch = this.run(true, true, "git", "branch", "--show-current")[0];
733
+
734
+ // All repositories are expected to be children of the parent of this repository.
735
+ const directory = `../${repository.directory ?? repositoryName}`;
736
+
737
+ if (fs.existsSync(directory) && fs.statSync(directory).isDirectory()) {
738
+ logger.info(`Repository ${repositoryName}...`);
739
+
740
+ process.chdir(directory);
741
+
742
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Package configuration format is known.
743
+ this._packageConfiguration = JSON.parse(fs.readFileSync(PACKAGE_CONFIGURATION_PATH).toString()) as PackageConfiguration;
744
+
745
+ const version = this.packageConfiguration.version;
746
+
747
+ const parsedVersion = /^(\d+)\.(\d+)\.(\d+)(-(alpha|beta))?$/.exec(version);
748
+
749
+ if (parsedVersion === null) {
750
+ throw new Error(`Invalid package version ${version}`);
751
+ }
752
+
753
+ this._majorVersion = Number(parsedVersion[1]);
754
+ this._minorVersion = Number(parsedVersion[2]);
755
+ this._patchVersion = Number(parsedVersion[3]);
756
+ this._preReleaseIdentifier = parsedVersion.length === 6 ? parsedVersion[5] : null;
757
+
758
+ const parsedBranch = /^v(\d+)\.(\d+)$/.exec(this.branch);
759
+
760
+ if (this.releaseType === "beta" && parsedBranch === null) {
761
+ throw new Error(`Beta release must be from version branch v${this.majorVersion}.${this.minorVersion}`);
762
+ }
763
+
764
+ if (this.releaseType === "production" && this.branch !== "main") {
765
+ throw new Error("Production release must be from main branch");
766
+ }
767
+
768
+ this._organizationDependencies = {};
769
+
770
+ for (const currentDependencies of [this.packageConfiguration.devDependencies, this.packageConfiguration.dependencies]) {
771
+ if (currentDependencies !== undefined) {
772
+ for (const dependency of Object.keys(currentDependencies)) {
773
+ const dependencyRepositoryName = this.dependencyRepositoryName(dependency);
774
+
775
+ if (dependencyRepositoryName !== null) {
776
+ logger.trace(`Organization dependency from package configuration ${dependencyRepositoryName}:${dependency}`);
777
+
778
+ this.organizationDependencies[dependencyRepositoryName] = dependency;
779
+
780
+ if (this.releaseType !== "production") {
781
+ // This change will ultimately be discarded if there are no changes and no updates to organization dependencies.
782
+ currentDependencies[dependency] = this.releaseType;
783
+ } else {
784
+ const dependencyRepository = this.configuration.repositories[dependencyRepositoryName];
785
+
786
+ const lastProductionVersion = dependencyRepository.lastProductionVersion;
787
+
788
+ if (lastProductionVersion === undefined) {
789
+ throw new Error(`Internal error, last production version not set for ${dependencyRepositoryName}`);
790
+ }
791
+
792
+ currentDependencies[dependency] = `^${lastProductionVersion}`;
793
+ }
794
+ }
795
+ }
796
+ }
797
+ }
798
+
799
+ if (repository.additionalDependencies !== undefined) {
800
+ for (const additionalDependency of repository.additionalDependencies) {
801
+ if (additionalDependency in this.organizationDependencies) {
802
+ logger.warn(`Additional dependency ${additionalDependency} already exists`);
803
+ } else {
804
+ logger.trace(`Organization dependency from additional dependencies ${additionalDependency}:null`);
805
+
806
+ this.organizationDependencies[additionalDependency] = null;
807
+ }
808
+ }
809
+ }
810
+
811
+ // Add dependency repositories of dependency repositories.
812
+ for (const dependencyRepositoryName of Object.keys(this.organizationDependencies)) {
813
+ const dependencyOrganizationDependencies = this.allOrganizationDependencies[dependencyRepositoryName];
814
+
815
+ for (const [dependencyDependencyRepositoryName, dependencyDependency] of Object.entries(dependencyOrganizationDependencies)) {
816
+ if (!(dependencyDependencyRepositoryName in this.organizationDependencies)) {
817
+ logger.trace(`Organization dependency from dependencies ${dependencyDependencyRepositoryName}:${dependencyDependency}`);
818
+
819
+ this.organizationDependencies[dependencyDependencyRepositoryName] = dependencyDependency;
820
+ }
821
+ }
822
+ }
823
+
824
+ // Save organization dependencies for future repositories.
825
+ this.allOrganizationDependencies[repositoryName] = this.organizationDependencies;
826
+
827
+ let getLastPublished: (repository: Repository) => string | undefined;
828
+
829
+ switch (this.releaseType) {
830
+ case "alpha":
831
+ getLastPublished = repository => repository.lastAlphaPublished;
832
+ break;
833
+
834
+ case "beta":
835
+ getLastPublished = repository => repository.lastBetaPublished;
836
+ break;
837
+
838
+ case "production":
839
+ getLastPublished = repository => repository.lastProductionPublished;
840
+ break;
841
+ }
842
+
843
+ const lastPublished = getLastPublished(repository);
844
+
845
+ this._organizationDependenciesUpdated = false;
846
+
847
+ for (const dependencyRepositoryName of Object.keys(this.organizationDependencies)) {
848
+ const dependencyRepository = this.configuration.repositories[dependencyRepositoryName];
849
+ const dependencyLastPublished = getLastPublished(dependencyRepository);
850
+
851
+ if (dependencyLastPublished === undefined) {
852
+ throw new Error(`Internal error, last ${this.releaseType} published not set for ${dependencyRepositoryName}`);
853
+ }
854
+
855
+ if (lastPublished === undefined || dependencyLastPublished > lastPublished) {
856
+ logger.info(`Repository ${dependencyRepositoryName} recently published`);
857
+
858
+ // At least one dependency repository has been published since the last publication of this repository.
859
+ this._organizationDependenciesUpdated = true;
860
+ }
861
+ }
862
+
863
+ if (parsedBranch !== null) {
864
+ const branchMajorVersion = Number(parsedBranch[1]);
865
+ const branchMinorVersion = Number(parsedBranch[2]);
866
+
867
+ // If in a version branch and version doesn't match, update it.
868
+ if (this.majorVersion !== branchMajorVersion || this.minorVersion !== branchMinorVersion) {
869
+ this.updatePackageVersion(branchMajorVersion, branchMinorVersion, 0, null);
870
+ this.commitUpdatedPackageVersion(PACKAGE_CONFIGURATION_PATH);
871
+ }
872
+ }
873
+
874
+ try {
875
+ // eslint-disable-next-line no-await-in-loop -- Next iteration requires previous to finish.
876
+ await this.publish();
877
+ } finally {
878
+ this.saveConfiguration();
879
+ }
880
+ // Non-external repositories may be private and not accessible to all developers.
881
+ } else if (repository.dependencyType === "external") {
882
+ throw new Error(`Repository ${repositoryName} not found`);
883
+ }
884
+ }
885
+
886
+ // Return to the start directory.
887
+ process.chdir(startDirectory);
888
+
889
+ this.finalizeAll();
890
+
891
+ this.saveConfiguration();
892
+
893
+ if (this.releaseType !== "alpha") {
894
+ this.commitModified(`Published ${this.releaseType} release.`, SHARED_CONFIGURATION_PATH);
895
+ }
896
+ }
897
+
898
+ /**
899
+ * Finalize publishing all repositories.
900
+ */
901
+ protected finalizeAll(): void {
902
+ }
903
+ }