@aidc-toolkit/dev 0.9.20-beta → 1.0.22-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.
@@ -1,156 +1,130 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
- import * as path from "node:path";
4
3
  import * as process from "node:process";
5
- import sharedConfigurationJSON from "../../config/publish.json";
6
- import localConfigurationJSON from "../../config/publish.local.json";
4
+ import {
5
+ type Configuration,
6
+ loadConfiguration,
7
+ type Phase,
8
+ type PhaseState,
9
+ type Repository,
10
+ saveConfiguration,
11
+ SHARED_CONFIGURATION_PATH
12
+ } from "./configuration.js";
7
13
  import { logger, setLogLevel } from "./logger.js";
8
- import { omit, pick } from "./type-helper.js";
14
+ import { pick } from "./type-helper.js";
9
15
 
10
- const SHARED_CONFIGURATION_PATH = "config/publish.json";
11
- const LOCAL_CONFIGURATION_PATH = "config/publish.local.json";
16
+ export const PACKAGE_CONFIGURATION_PATH = "package.json";
12
17
 
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);
18
+ export const PACKAGE_LOCK_CONFIGURATION_PATH = "package-lock.json";
16
19
 
17
20
  /**
18
- * Repository.
21
+ * Configuration layout of package.json (relevant attributes only).
19
22
  */
20
- export interface Repository {
21
- /**
22
- * Directory in which repository resides, if different from repository name.
23
- */
24
- directory?: string;
25
-
23
+ export interface PackageConfiguration {
26
24
  /**
27
- * Dependency type, dictating how it is published.
25
+ * Name.
28
26
  */
29
- dependencyType: string;
27
+ readonly name: string;
30
28
 
31
29
  /**
32
- * Platform if building across platforms (local configuration), e.g., macOS hosting Windows on Parallels.
30
+ * Version.
33
31
  */
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
- };
32
+ version: string;
45
33
 
46
34
  /**
47
- * Additional dependencies not included in package configuration.
35
+ * Development dependencies.
48
36
  */
49
- additionalDependencies?: string[];
37
+ readonly devDependencies?: Record<string, string>;
50
38
 
51
39
  /**
52
- * Paths to exclude from consideration when checking for changes.
40
+ * Dependencies.
53
41
  */
54
- excludePaths?: string[];
42
+ readonly dependencies?: Record<string, string>;
43
+ }
55
44
 
45
+ /**
46
+ * Repository state, derived from package configuration and updated during publishing.
47
+ */
48
+ interface RepositoryState {
56
49
  /**
57
- * Date/time in ISO format the last alpha version was published.
50
+ * Repository name from configuration.
58
51
  */
59
- lastAlphaPublished?: string;
52
+ readonly repositoryName: string;
60
53
 
61
54
  /**
62
- * Date/time in ISO format the last beta version was published.
55
+ * Repository from configuration.
63
56
  */
64
- lastBetaPublished?: string;
57
+ repository: Repository;
65
58
 
66
59
  /**
67
- * Last beta tag.
60
+ * Phase state.
68
61
  */
69
- lastBetaTag?: string;
62
+ phaseState: PhaseState;
70
63
 
71
64
  /**
72
- * Current step in beta publication; used to resume after failure recovery.
65
+ * Phase date/time or undefined if phase never before published.
73
66
  */
74
- publishBetaStep?: string | undefined;
67
+ phaseDateTime: Date | undefined;
75
68
 
76
69
  /**
77
- * Date/time in ISO format the last production version was published.
70
+ * NPM platform arguments if any.
78
71
  */
79
- lastProductionPublished?: string;
72
+ readonly npmPlatformArgs: readonly string[];
80
73
 
81
74
  /**
82
- * Last production version.
75
+ * Branch.
83
76
  */
84
- lastProductionVersion?: string;
85
- }
77
+ readonly branch: string;
86
78
 
87
- /**
88
- * Configuration layout of merged publish.json and publish.local.json.
89
- */
90
- export interface Configuration {
91
79
  /**
92
- * Organization that owns the repositories.
80
+ * Package configuration.
93
81
  */
94
- organization: string;
82
+ readonly packageConfiguration: PackageConfiguration;
95
83
 
96
84
  /**
97
- * Log level (local configuration).
85
+ * Major version.
98
86
  */
99
- logLevel?: string;
87
+ majorVersion: number;
100
88
 
101
89
  /**
102
- * Registry hosting organization's alpha repositories (local configuration).
90
+ * Minor version.
103
91
  */
104
- alphaRegistry: string;
92
+ minorVersion: number;
105
93
 
106
94
  /**
107
- * Repositories.
95
+ * Patch version.
108
96
  */
109
- repositories: Record<string, Repository>;
110
- }
111
-
112
- export const PACKAGE_CONFIGURATION_PATH = "package.json";
97
+ patchVersion: number;
113
98
 
114
- export const PACKAGE_LOCK_CONFIGURATION_PATH = "package-lock.json";
115
-
116
- /**
117
- * Configuration layout of package.json (relevant attributes only).
118
- */
119
- export interface PackageConfiguration {
120
99
  /**
121
- * Name.
100
+ * Pre-release identifier or null if none.
122
101
  */
123
- name: string;
102
+ preReleaseIdentifier: string | null;
124
103
 
125
104
  /**
126
- * Version.
105
+ * Dependency package names extracted directly from package configuration.
127
106
  */
128
- version: string;
107
+ readonly dependencyPackageNames: readonly string[];
129
108
 
130
109
  /**
131
- * Development dependencies.
110
+ * All dependency package names in publication order.
132
111
  */
133
- devDependencies?: Record<string, string>;
112
+ readonly allDependencyPackageNames: readonly string[];
134
113
 
135
114
  /**
136
- * Dependencies.
115
+ * True if any dependencies have been updated.
137
116
  */
138
- dependencies?: Record<string, string>;
117
+ readonly anyDependenciesUpdated: boolean;
139
118
  }
140
119
 
141
- /**
142
- * Release type.
143
- */
144
- type ReleaseType = "alpha" | "beta" | "production";
145
-
146
120
  /**
147
121
  * Publish base class.
148
122
  */
149
123
  export abstract class Publish {
150
124
  /**
151
- * Release type.
125
+ * Phase.
152
126
  */
153
- private readonly _releaseType: ReleaseType;
127
+ private readonly _phase: Phase;
154
128
 
155
129
  /**
156
130
  * If true, outputs what would be run rather than running it.
@@ -173,64 +147,14 @@ export abstract class Publish {
173
147
  private readonly _atOrganizationRegistry: string;
174
148
 
175
149
  /**
176
- * All organization dependencies, keyed on repository name.
150
+ * Repository states, keyed on repository name.
177
151
  */
178
- private readonly _allOrganizationDependencies: Record<string, Record<string, string>>;
152
+ private readonly _repositoryStates: Record<string, Readonly<RepositoryState>>;
179
153
 
180
154
  /**
181
- * Current repository name.
155
+ * Current repository state.
182
156
  */
183
- private _repositoryName!: string;
184
-
185
- /**
186
- * Current repository.
187
- */
188
- private _repository!: Repository;
189
-
190
- /**
191
- * NPM platform arguments if any.
192
- */
193
- private _npmPlatformArgs!: string[];
194
-
195
- /**
196
- * Branch.
197
- */
198
- private _branch!: string;
199
-
200
- /**
201
- * Package configuration.
202
- */
203
- private _packageConfiguration!: PackageConfiguration;
204
-
205
- /**
206
- * Major version.
207
- */
208
- private _majorVersion!: number;
209
-
210
- /**
211
- * Minor version.
212
- */
213
- private _minorVersion!: number;
214
-
215
- /**
216
- * Patch version.
217
- */
218
- private _patchVersion!: number;
219
-
220
- /**
221
- * Pre-release identifier or null if none.
222
- */
223
- private _preReleaseIdentifier!: string | null;
224
-
225
- /**
226
- * Dependencies that belong to the organization, keyed on repository name.
227
- */
228
- private _organizationDependencies!: Record<string, string>;
229
-
230
- /**
231
- * True if any organization dependency has been updated.
232
- */
233
- private _organizationDependenciesUpdated!: boolean;
157
+ private _repositoryState: RepositoryState | undefined;
234
158
 
235
159
  /**
236
160
  * Constructor.
@@ -241,36 +165,29 @@ export abstract class Publish {
241
165
  * @param dryRun
242
166
  * If true, outputs what would be run rather than running it.
243
167
  */
244
- protected constructor(releaseType: ReleaseType, dryRun: boolean) {
245
- this._releaseType = releaseType;
168
+ protected constructor(releaseType: Phase, dryRun: boolean) {
169
+ this._phase = releaseType;
246
170
  this._dryRun = dryRun;
247
171
 
248
- // Merge shared and local configurations.
249
- this._configuration = {
250
- ...omit(sharedConfigurationJSON, "repositories"),
251
- ...omit(localConfigurationJSON, "repositories"),
252
- repositories: Object.fromEntries(Object.entries(sharedConfigurationJSON.repositories).map(([repositoryName, repository]) => [repositoryName, {
253
- ...repository,
254
- ...((localConfigurationJSON.repositories as Record<string, Partial<Repository> | undefined>)[repositoryName] ?? {})
255
- }]))
256
- };
172
+ this._configuration = loadConfiguration();
257
173
 
258
174
  this._atOrganization = `@${this.configuration.organization}`;
259
175
 
260
176
  this._atOrganizationRegistry = `${this.atOrganization}:registry${releaseType === "alpha" ? `=${this.configuration.alphaRegistry}` : ""}`;
261
177
 
262
- this._allOrganizationDependencies = {};
263
-
264
178
  if (this._configuration.logLevel !== undefined) {
265
179
  setLogLevel(this._configuration.logLevel);
266
180
  }
181
+
182
+ this._repositoryStates = {};
183
+ this._repositoryState = undefined;
267
184
  }
268
185
 
269
186
  /**
270
- * Get the release type.
187
+ * Get the phase.
271
188
  */
272
- protected get releaseType(): ReleaseType {
273
- return this._releaseType;
189
+ protected get phase(): Phase {
190
+ return this._phase;
274
191
  }
275
192
 
276
193
  /**
@@ -302,88 +219,83 @@ export abstract class Publish {
302
219
  }
303
220
 
304
221
  /**
305
- * Get all organization dependencies, keyed on repository name.
306
- */
307
- protected get allOrganizationDependencies(): Record<string, Record<string, string>> {
308
- return this._allOrganizationDependencies;
309
- }
310
-
311
- /**
312
- * Get the current repository name.
313
- */
314
- protected get repositoryName(): string {
315
- return this._repositoryName;
316
- }
317
-
318
- /**
319
- * Get the current repository.
320
- */
321
- protected get repository(): Repository {
322
- return this._repository;
323
- }
324
-
325
- /**
326
- * Get the NPM platform arguments if any.
222
+ * Get the repository states, keyed on repository name.
327
223
  */
328
- get npmPlatformArgs(): string[] {
329
- return this._npmPlatformArgs;
224
+ protected get repositoryStates(): Record<string, Readonly<RepositoryState>> {
225
+ return this._repositoryStates;
330
226
  }
331
227
 
332
228
  /**
333
- * Get the branch.
229
+ * Get the current repository state.
334
230
  */
335
- protected get branch(): string {
336
- return this._branch;
337
- }
231
+ protected get repositoryState(): RepositoryState {
232
+ // Repository state should be accessed only during active publication.
233
+ if (this._repositoryState === undefined) {
234
+ throw new Error("Repository state not defined");
235
+ }
338
236
 
339
- /**
340
- * Get the package configuration.
341
- */
342
- protected get packageConfiguration(): PackageConfiguration {
343
- return this._packageConfiguration;
237
+ return this._repositoryState;
344
238
  }
345
239
 
346
240
  /**
347
- * Get the major version.
241
+ * Get the dependency version for a dependency repository.
242
+ *
243
+ * @param dependencyRepositoryName
244
+ * Dependency repository name.
245
+ *
246
+ * @param dependencyRepository
247
+ * Dependency repository.
248
+ *
249
+ * @returns
250
+ * Dependency version.
348
251
  */
349
- protected get majorVersion(): number {
350
- return this._majorVersion;
351
- }
252
+ protected abstract dependencyVersionFor(dependencyRepositoryName: string, dependencyRepository: Repository): string;
352
253
 
353
254
  /**
354
- * Get the minor version.
255
+ * Determine the latest date/time or undefined if all undefined.
256
+ *
257
+ * @param initialDateTime
258
+ * Initial date/time.
259
+ *
260
+ * @param additionalDateTimes
261
+ * Additional date/times.
262
+ *
263
+ * @returns
264
+ * Latest date/time.
355
265
  */
356
- protected get minorVersion(): number {
357
- return this._minorVersion;
358
- }
266
+ protected latestDateTime(initialDateTime: Date, ...additionalDateTimes: Array<Date | undefined>): Date {
267
+ let latestDateTime = initialDateTime;
359
268
 
360
- /**
361
- * Get the patch version.
362
- */
363
- protected get patchVersion(): number {
364
- return this._patchVersion;
365
- }
269
+ for (const dateTime of additionalDateTimes) {
270
+ if (dateTime !== undefined && latestDateTime.getTime() < dateTime.getTime()) {
271
+ latestDateTime = dateTime;
272
+ }
273
+ }
366
274
 
367
- /**
368
- * Get the pre-release identifier.
369
- */
370
- protected get preReleaseIdentifier(): string | null {
371
- return this._preReleaseIdentifier;
275
+ return latestDateTime;
372
276
  }
373
277
 
374
278
  /**
375
- * Get dependencies that belong to the organization, keyed on repository name.
279
+ * Get the phase date/time for a repository.
280
+ *
281
+ * @param repository
282
+ * Repository.
283
+ *
284
+ * @param phaseDateTime
285
+ * Initial phase date/time.
286
+ *
287
+ * @returns
288
+ * Phase date/time or undefined if phase never before published.
376
289
  */
377
- protected get organizationDependencies(): Record<string, string> {
378
- return this._organizationDependencies;
379
- }
290
+ protected abstract getPhaseDateTime(repository: Repository, phaseDateTime: Date): Date;
380
291
 
381
292
  /**
382
- * Determine if any organization dependency has been updated.
293
+ * Determine if branch is valid for the phase.
294
+ *
295
+ * @returns
296
+ * True if branch is valid for the phase.
383
297
  */
384
- protected get organizationDependenciesUpdated(): boolean {
385
- return this._organizationDependenciesUpdated;
386
- }
298
+ protected abstract isValidBranch(): boolean;
387
299
 
388
300
  /**
389
301
  * Run a command and optionally capture its output.
@@ -461,22 +373,61 @@ export abstract class Publish {
461
373
  return parsedDependency.length === 2 && parsedDependency[0] === this.atOrganization ? parsedDependency[1] : null;
462
374
  }
463
375
 
376
+ /**
377
+ * Determine if an organization dependency has been updated.
378
+ *
379
+ * @param phaseDateTime
380
+ * Phase date/time of the current repository.
381
+ *
382
+ * @param dependencyRepositoryName
383
+ * Dependency repository name.
384
+ *
385
+ * @param isAdditional
386
+ * True if this is an additional dependency.
387
+ *
388
+ * @returns
389
+ * True if organization dependency has been updated.
390
+ */
391
+ private isOrganizationDependencyUpdated(phaseDateTime: Date, dependencyRepositoryName: string, isAdditional: boolean): boolean {
392
+ const dependencyString = !isAdditional ? "Dependency" : "Additional dependency";
393
+
394
+ // If dependency repository state exists, so does dependency repository.
395
+ if (!(dependencyRepositoryName in this.repositoryStates)) {
396
+ throw new Error(`${dependencyString} repository ${dependencyRepositoryName} not yet published`);
397
+ }
398
+
399
+ const dependencyRepository = this.configuration.repositories[dependencyRepositoryName];
400
+ const dependencyPhaseState = dependencyRepository.phaseStates[this.phase];
401
+
402
+ if (dependencyPhaseState === undefined) {
403
+ throw new Error(`*** Internal error *** ${dependencyString} ${dependencyRepositoryName} does not have state for ${this.phase} phase`);
404
+ }
405
+
406
+ const isUpdated = phaseDateTime.getTime() < this.getPhaseDateTime(dependencyRepository, dependencyPhaseState.dateTime).getTime();
407
+
408
+ if (isUpdated) {
409
+ logger.trace(`Dependency repository ${dependencyRepositoryName} updated`);
410
+ }
411
+
412
+ return isUpdated;
413
+ }
414
+
464
415
  /**
465
416
  * Determine if there have been any changes to the current repository.
466
417
  *
467
- * @param lastPublished
468
- * Date/time in ISO format to check against.
418
+ * @param phaseDateTime
419
+ * Phase date/time to check against or undefined if phase never before published.
469
420
  *
470
421
  * @param ignoreGitHub
471
422
  * If true, ignore .github directory.
472
423
  *
473
424
  * @returns
474
- * True if there is no last published date/time or if there have been any changes since then.
425
+ * True if there have been any changes since the phase date/time.
475
426
  */
476
- protected anyChanges(lastPublished: string | undefined, ignoreGitHub: boolean): boolean {
427
+ protected anyChanges(phaseDateTime: Date | undefined, ignoreGitHub: boolean): boolean {
477
428
  let anyChanges: boolean;
478
429
 
479
- const excludePaths = this.repository.excludePaths ?? [];
430
+ const excludePaths = this.repositoryState.repository.excludePaths ?? [];
480
431
 
481
432
  const changedFilesSet = new Set<string>();
482
433
 
@@ -515,13 +466,14 @@ export abstract class Publish {
515
466
  }
516
467
  }
517
468
 
518
- if (this.releaseType !== "alpha" && this.run(true, true, "git", "fetch", "--porcelain", "--dry-run").length !== 0) {
469
+ if (this.phase !== "alpha" && this.run(true, true, "git", "fetch", "--porcelain", "--dry-run").length !== 0) {
519
470
  throw new Error("Remote repository has outstanding changes");
520
471
  }
521
472
 
522
- if (lastPublished !== undefined) {
473
+ // Phase date/time is undefined if never before published.
474
+ if (phaseDateTime !== undefined) {
523
475
  // Get all files committed since last published.
524
- for (const line of this.run(true, true, "git", "log", "--since", lastPublished, "--name-status", "--pretty=oneline")) {
476
+ for (const line of this.run(true, true, "git", "log", "--since", phaseDateTime.toISOString(), "--name-status", "--pretty=oneline")) {
525
477
  // Header starts with 40-character SHA.
526
478
  if (/^[0-9a-f]{40} /.test(line)) {
527
479
  logger.debug(`Commit SHA ${line.substring(0, 40)}`);
@@ -549,12 +501,12 @@ export abstract class Publish {
549
501
  }
550
502
 
551
503
  // Beta or production publication requires that repository be fully committed except for excluded paths.
552
- if (this.releaseType !== "alpha" && changedFilesSet.size !== committedCount) {
504
+ if (this.phase !== "alpha" && changedFilesSet.size !== committedCount) {
553
505
  throw new Error("Repository has uncommitted changes");
554
506
  }
555
507
  }
556
508
 
557
- const lastPublishedDateTime = new Date(lastPublished);
509
+ const lastPublishedDateTime = new Date(phaseDateTime);
558
510
 
559
511
  anyChanges = false;
560
512
 
@@ -574,7 +526,7 @@ export abstract class Publish {
574
526
  logger.info("No changes");
575
527
  }
576
528
  } else {
577
- logger.info("No last published");
529
+ logger.info("Never published");
578
530
 
579
531
  // No last published, so there must have been changes.
580
532
  anyChanges = true;
@@ -620,10 +572,12 @@ export abstract class Publish {
620
572
  * Save package configuration.
621
573
  */
622
574
  protected savePackageConfiguration(): void {
575
+ const packageConfiguration = this.repositoryState.packageConfiguration;
576
+
623
577
  if (this.dryRun) {
624
- logger.info(`Dry run: Saving package configuration\n${JSON.stringify(pick(this.packageConfiguration, "name", "version", "devDependencies", "dependencies"), null, 2)}\n`);
578
+ logger.info(`Dry run: Saving package configuration\n${JSON.stringify(pick(packageConfiguration, "name", "version", "devDependencies", "dependencies"), null, 2)}\n`);
625
579
  } else {
626
- fs.writeFileSync(PACKAGE_CONFIGURATION_PATH, `${JSON.stringify(this.packageConfiguration, null, 2)}\n`);
580
+ fs.writeFileSync(PACKAGE_CONFIGURATION_PATH, `${JSON.stringify(packageConfiguration, null, 2)}\n`);
627
581
  }
628
582
  }
629
583
 
@@ -643,23 +597,25 @@ export abstract class Publish {
643
597
  * Pre-release identifier or undefined if no change.
644
598
  */
645
599
  protected updatePackageVersion(majorVersion: number | undefined, minorVersion: number | undefined, patchVersion: number | undefined, preReleaseIdentifier: string | null | undefined): void {
600
+ const repositoryState = this.repositoryState;
601
+
646
602
  if (majorVersion !== undefined) {
647
- this._majorVersion = majorVersion;
603
+ repositoryState.majorVersion = majorVersion;
648
604
  }
649
605
 
650
606
  if (minorVersion !== undefined) {
651
- this._minorVersion = minorVersion;
607
+ repositoryState.minorVersion = minorVersion;
652
608
  }
653
609
 
654
610
  if (patchVersion !== undefined) {
655
- this._patchVersion = patchVersion;
611
+ repositoryState.patchVersion = patchVersion;
656
612
  }
657
613
 
658
614
  if (preReleaseIdentifier !== undefined) {
659
- this._preReleaseIdentifier = preReleaseIdentifier;
615
+ repositoryState.preReleaseIdentifier = preReleaseIdentifier;
660
616
  }
661
617
 
662
- this.packageConfiguration.version = `${this.majorVersion}.${this.minorVersion}.${this.patchVersion}${this.preReleaseIdentifier !== null ? `-${this.preReleaseIdentifier}` : ""}`;
618
+ repositoryState.packageConfiguration.version = `${repositoryState.majorVersion}.${repositoryState.minorVersion}.${repositoryState.patchVersion}${repositoryState.preReleaseIdentifier !== null ? `-${repositoryState.preReleaseIdentifier}` : ""}`;
663
619
 
664
620
  this.savePackageConfiguration();
665
621
  }
@@ -668,11 +624,11 @@ export abstract class Publish {
668
624
  * Update organization dependencies.
669
625
  */
670
626
  protected updateOrganizationDependencies(): void {
671
- const organizationDependencies = Object.values(this.organizationDependencies);
627
+ const repositoryState = this.repositoryState;
672
628
 
673
- logger.debug(`Updating organization dependencies [${organizationDependencies.join(", ")}]`);
629
+ logger.debug(`Updating organization dependencies [${repositoryState.allDependencyPackageNames.join(", ")}]`);
674
630
 
675
- this.run(false, false, "npm", "update", ...organizationDependencies, ...this.npmPlatformArgs);
631
+ this.run(false, false, "npm", "update", ...repositoryState.allDependencyPackageNames, ...this.repositoryState.npmPlatformArgs);
676
632
  }
677
633
 
678
634
  /**
@@ -682,40 +638,40 @@ export abstract class Publish {
682
638
  * Files to commit; if none, defaults to "--all".
683
639
  */
684
640
  protected commitUpdatedPackageVersion(...files: string[]): void {
685
- this.commitModified(`Updated to version ${this.packageConfiguration.version}.`, ...files);
641
+ this.commitModified(`Updated to version ${this.repositoryState.packageConfiguration.version}.`, ...files);
686
642
  }
687
643
 
688
644
  /**
689
- * Save the current configuration.
645
+ * Update the phase state. This will replace the phase state object in the repository and repository state and may
646
+ * update the phase date/time in the repository state.
647
+ *
648
+ * @param phaseState
649
+ * Partial phases state. Only those properties provided will be updated.
690
650
  */
691
- protected saveConfiguration(): void {
692
- const saveSharedRepositories: Record<string, Omit<Repository, "platform" | "lastAlphaPublished">> = {};
693
- const saveLocalRepositories: Record<string, Pick<Repository, "platform" | "lastAlphaPublished">> = {};
651
+ protected updatePhaseState(phaseState: Partial<PhaseState>): void {
652
+ const repositoryState = this.repositoryState;
694
653
 
695
- for (const [repositoryName, repository] of Object.entries(this.configuration.repositories)) {
696
- saveSharedRepositories[repositoryName] = omit(repository, "platform", "lastAlphaPublished");
697
- saveLocalRepositories[repositoryName] = pick(repository, "platform", "lastAlphaPublished");
698
- }
699
-
700
- const saveSharedConfigurationJSON = JSON.stringify({
701
- ...omit(this.configuration, "logLevel", "alphaRegistry", "repositories"),
702
- repositories: saveSharedRepositories
703
- }, null, 2);
654
+ const updatedPhaseState = {
655
+ ...this.repositoryState.phaseState,
656
+ ...phaseState
657
+ };
704
658
 
705
- const saveLocalConfigurationJSON = JSON.stringify({
706
- ...pick(this.configuration, "logLevel", "alphaRegistry"),
707
- repositories: saveLocalRepositories
708
- }, null, 2);
659
+ repositoryState.repository.phaseStates[this.phase] = updatedPhaseState;
660
+ repositoryState.phaseState = updatedPhaseState;
709
661
 
710
- if (this.dryRun) {
711
- logger.info(`Dry run: Saving shared configuration\n${saveSharedConfigurationJSON}\n`);
712
- logger.info(`Dry run: Saving local configuration\n${saveLocalConfigurationJSON}\n`);
713
- } else {
714
- fs.writeFileSync(SHARED_CONFIGURATION_FULL_PATH, saveSharedConfigurationJSON);
715
- fs.writeFileSync(LOCAL_CONFIGURATION_FULL_PATH, saveLocalConfigurationJSON);
662
+ // Setting the phase date/time overrides the logic of its initial determination.
663
+ if (phaseState.dateTime !== undefined) {
664
+ repositoryState.phaseDateTime = phaseState.dateTime;
716
665
  }
717
666
  }
718
667
 
668
+ /**
669
+ * Save the configuration.
670
+ */
671
+ private saveConfiguration(): void {
672
+ saveConfiguration(this.configuration, this.dryRun);
673
+ }
674
+
719
675
  /**
720
676
  * Publish current repository.
721
677
  */
@@ -728,20 +684,6 @@ export abstract class Publish {
728
684
  const startDirectory = process.cwd();
729
685
 
730
686
  for (const [repositoryName, repository] of Object.entries(this.configuration.repositories)) {
731
- this._repositoryName = repositoryName;
732
- this._repository = repository;
733
-
734
- this._npmPlatformArgs = repository.platform !== undefined ?
735
- [
736
- "--cpu",
737
- repository.platform.cpu,
738
- "--os",
739
- repository.platform.os
740
- ] :
741
- [];
742
-
743
- this._branch = this.run(true, true, "git", "branch", "--show-current")[0];
744
-
745
687
  // All repositories are expected to be children of the parent of this repository.
746
688
  const directory = `../${repository.directory ?? repositoryName}`;
747
689
 
@@ -750,173 +692,156 @@ export abstract class Publish {
750
692
 
751
693
  process.chdir(directory);
752
694
 
753
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Package configuration format is known.
754
- this._packageConfiguration = JSON.parse(fs.readFileSync(PACKAGE_CONFIGURATION_PATH).toString()) as PackageConfiguration;
695
+ let phaseState = repository.phaseStates[this.phase];
755
696
 
756
- const version = this.packageConfiguration.version;
697
+ // Create phase state if necessary.
698
+ if (phaseState === undefined) {
699
+ phaseState = {
700
+ dateTime: new Date(0)
701
+ };
757
702
 
758
- const parsedVersion = /^(\d+)\.(\d+)\.(\d+)(-(alpha|beta))?$/.exec(version);
759
-
760
- if (parsedVersion === null) {
761
- throw new Error(`Invalid package version ${version}`);
703
+ repository.phaseStates[this.phase] = phaseState;
762
704
  }
763
705
 
764
- this._majorVersion = Number(parsedVersion[1]);
765
- this._minorVersion = Number(parsedVersion[2]);
766
- this._patchVersion = Number(parsedVersion[3]);
767
- this._preReleaseIdentifier = parsedVersion.length === 6 ? parsedVersion[5] : null;
706
+ try {
707
+ const phaseDateTime = this.getPhaseDateTime(repository, phaseState.dateTime);
768
708
 
769
- const parsedBranch = /^v(\d+)\.(\d+)$/.exec(this.branch);
709
+ const npmPlatformArgs = repository.platform !== undefined ?
710
+ [
711
+ "--cpu",
712
+ repository.platform.cpu,
713
+ "--os",
714
+ repository.platform.os
715
+ ] :
716
+ [];
770
717
 
771
- if (this.releaseType === "beta" && parsedBranch === null) {
772
- throw new Error(`Beta release must be from version branch v${this.majorVersion}.${this.minorVersion}`);
773
- }
718
+ const branch = this.run(true, true, "git", "branch", "--show-current")[0];
774
719
 
775
- if (this.releaseType === "production" && this.branch !== "main") {
776
- throw new Error("Production release must be from main branch");
777
- }
720
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Package configuration format is known.
721
+ const packageConfiguration = JSON.parse(fs.readFileSync(PACKAGE_CONFIGURATION_PATH).toString()) as PackageConfiguration;
778
722
 
779
- this._organizationDependencies = {};
723
+ const version = packageConfiguration.version;
780
724
 
781
- for (const currentDependencies of [this.packageConfiguration.devDependencies, this.packageConfiguration.dependencies]) {
782
- if (currentDependencies !== undefined) {
783
- for (const dependency of Object.keys(currentDependencies)) {
784
- const dependencyRepositoryName = this.dependencyRepositoryName(dependency);
725
+ const parsedVersion = /^(\d+)\.(\d+)\.(\d+)(-(alpha|beta))?$/.exec(version);
785
726
 
786
- if (dependencyRepositoryName !== null) {
787
- logger.trace(`Organization dependency from package configuration ${dependencyRepositoryName}:${dependency}`);
727
+ if (parsedVersion === null) {
728
+ throw new Error(`Invalid package version ${version}`);
729
+ }
788
730
 
789
- this.organizationDependencies[dependencyRepositoryName] = dependency;
731
+ const majorVersion = Number(parsedVersion[1]);
732
+ const minorVersion = Number(parsedVersion[2]);
733
+ const patchVersion = Number(parsedVersion[3]);
734
+ const preReleaseIdentifier = parsedVersion.length === 6 ? parsedVersion[5] : null;
790
735
 
791
- const dependencyRepository = this.configuration.repositories[dependencyRepositoryName];
736
+ const dependencyPackageNames: string[] = [];
737
+ const allDependencyPackageNames: string[] = [];
792
738
 
793
- // Dependency changes will ultimately be discarded if there are no changes and no updates to organization dependencies.
794
- switch (this.releaseType) {
795
- case "alpha":
796
- currentDependencies[dependency] = "alpha";
797
- break;
739
+ let anyDependenciesUpdated = false;
798
740
 
799
- case "beta":
800
- switch (dependencyRepository.dependencyType) {
801
- case "external":
802
- currentDependencies[dependency] = "beta";
803
- break;
741
+ for (const dependencies of [packageConfiguration.devDependencies ?? {}, packageConfiguration.dependencies ?? {}]) {
742
+ for (const dependencyPackageName of Object.keys(dependencies)) {
743
+ const dependencyRepositoryName = this.dependencyRepositoryName(dependencyPackageName);
804
744
 
805
- case "internal":
806
- {
807
- const lastBetaTag = dependencyRepository.lastBetaTag;
745
+ // Dependency repository name is null if dependency is not within the organization.
746
+ if (dependencyRepositoryName !== null) {
747
+ logger.trace(`Organization dependency ${dependencyPackageName} from package configuration`);
808
748
 
809
- if (lastBetaTag === undefined) {
810
- throw new Error(`Internal error, last beta tag not set for ${dependencyRepositoryName}`);
811
- }
749
+ // Check every dependency for logging purposes.
750
+ if (this.isOrganizationDependencyUpdated(phaseDateTime, dependencyRepositoryName, false)) {
751
+ anyDependenciesUpdated = true;
752
+ }
812
753
 
813
- currentDependencies[dependency] = `${this.configuration.organization}/${dependencyRepositoryName}#${lastBetaTag}`;
814
- }
815
- break;
754
+ for (const dependencyDependencyPackageName of this.repositoryStates[dependencyRepositoryName].allDependencyPackageNames) {
755
+ if (!allDependencyPackageNames.includes(dependencyDependencyPackageName)) {
756
+ logger.trace(`Organization dependency ${dependencyDependencyPackageName} from dependencies`);
816
757
 
817
- default:
818
- throw new Error(`Invalid dependency type "${dependencyRepository.dependencyType}" for dependency ${dependencyRepositoryName}`);
819
- }
820
- break;
758
+ allDependencyPackageNames.push(dependencyDependencyPackageName);
759
+ }
760
+ }
821
761
 
822
- case "production":
823
- {
824
- const lastProductionVersion = dependencyRepository.lastProductionVersion;
762
+ dependencyPackageNames.push(dependencyPackageName);
825
763
 
826
- if (lastProductionVersion === undefined) {
827
- throw new Error(`Internal error, last production version not set for ${dependencyRepositoryName}`);
828
- }
764
+ // Current dependency package name goes in last to preserve hierarchy.
765
+ allDependencyPackageNames.push(dependencyPackageName);
829
766
 
830
- currentDependencies[dependency] = `^${lastProductionVersion}`;
831
- }
832
- break;
833
- }
767
+ // Dependency changes will ultimately be discarded if there are no changes and no updates to repository states.
768
+ dependencies[dependencyPackageName] = this.dependencyVersionFor(dependencyRepositoryName, this.configuration.repositories[dependencyRepositoryName]);
834
769
  }
835
770
  }
836
771
  }
837
- }
838
772
 
839
- if (repository.additionalDependencies !== undefined) {
840
- for (const additionalDependencyName of repository.additionalDependencies) {
841
- if (additionalDependencyName in this.organizationDependencies) {
842
- logger.warn(`Additional dependency ${additionalDependencyName} already exists`);
843
- } else {
844
- const dependency = `${this.atOrganization}/${additionalDependencyName}`;
773
+ if (repository.additionalDependencies !== undefined) {
774
+ const additionalRepositoryNames: string[] = [];
845
775
 
846
- logger.trace(`Organization dependency from additional dependencies ${additionalDependencyName}:${dependency}`);
776
+ for (const additionalDependencyRepositoryName of repository.additionalDependencies) {
777
+ const additionalDependencyPackageName = `${this.atOrganization}/${additionalDependencyRepositoryName}`;
847
778
 
848
- this.organizationDependencies[additionalDependencyName] = dependency;
849
- }
850
- }
851
- }
852
-
853
- // Add dependency repositories of dependency repositories.
854
- for (const dependencyRepositoryName of Object.keys(this.organizationDependencies)) {
855
- const dependencyOrganizationDependencies = this.allOrganizationDependencies[dependencyRepositoryName];
779
+ if (allDependencyPackageNames.includes(additionalDependencyPackageName) || additionalRepositoryNames.includes(additionalDependencyRepositoryName)) {
780
+ logger.warn(`Additional dependency repository ${additionalDependencyRepositoryName} already a dependency`);
781
+ } else {
782
+ logger.trace(`Organization dependency ${additionalDependencyRepositoryName} from additional dependencies`);
856
783
 
857
- for (const [dependencyDependencyRepositoryName, dependencyDependency] of Object.entries(dependencyOrganizationDependencies)) {
858
- if (!(dependencyDependencyRepositoryName in this.organizationDependencies)) {
859
- logger.trace(`Organization dependency from dependencies ${dependencyDependencyRepositoryName}:${dependencyDependency}`);
784
+ // Check every dependency for logging purposes.
785
+ if (this.isOrganizationDependencyUpdated(phaseDateTime, additionalDependencyRepositoryName, true)) {
786
+ anyDependenciesUpdated = true;
787
+ }
860
788
 
861
- this.organizationDependencies[dependencyDependencyRepositoryName] = dependencyDependency;
789
+ additionalRepositoryNames.push(additionalDependencyRepositoryName);
790
+ }
862
791
  }
863
792
  }
864
- }
865
-
866
- // Save organization dependencies for future repositories.
867
- this.allOrganizationDependencies[repositoryName] = this.organizationDependencies;
868
-
869
- let getLastPublished: (repository: Repository) => string | undefined;
870
-
871
- switch (this.releaseType) {
872
- case "alpha":
873
- getLastPublished = repository => repository.lastAlphaPublished;
874
- break;
875
-
876
- case "beta":
877
- getLastPublished = repository => repository.lastBetaPublished;
878
- break;
879
-
880
- case "production":
881
- getLastPublished = repository => repository.lastProductionPublished;
882
- break;
883
- }
884
793
 
885
- const lastPublished = getLastPublished(repository);
886
-
887
- this._organizationDependenciesUpdated = false;
888
-
889
- for (const dependencyRepositoryName of Object.keys(this.organizationDependencies)) {
890
- const dependencyRepository = this.configuration.repositories[dependencyRepositoryName];
891
- const dependencyLastPublished = getLastPublished(dependencyRepository);
892
-
893
- if (dependencyLastPublished === undefined) {
894
- throw new Error(`Internal error, last ${this.releaseType} published not set for ${dependencyRepositoryName}`);
794
+ this._repositoryState = {
795
+ repositoryName,
796
+ repository,
797
+ phaseState,
798
+ phaseDateTime,
799
+ npmPlatformArgs,
800
+ branch,
801
+ packageConfiguration,
802
+ majorVersion,
803
+ minorVersion,
804
+ patchVersion,
805
+ preReleaseIdentifier,
806
+ dependencyPackageNames,
807
+ allDependencyPackageNames,
808
+ anyDependenciesUpdated
809
+ };
810
+
811
+ // Save repository state for future repositories.
812
+ this.repositoryStates[repositoryName] = this._repositoryState;
813
+
814
+ if (!this.isValidBranch()) {
815
+ throw new Error(`Branch ${branch} is not valid for ${this.phase} phase`);
895
816
  }
896
817
 
897
- if (lastPublished === undefined || dependencyLastPublished > lastPublished) {
898
- logger.info(`Dependency ${dependencyRepositoryName} recently published`);
818
+ const parsedBranch = /^v(\d+)\.(\d+)/.exec(branch);
899
819
 
900
- // At least one dependency repository has been published since the last publication of this repository.
901
- this._organizationDependenciesUpdated = true;
902
- }
903
- }
820
+ // If this is a version branch, update the package version if required.
821
+ if (parsedBranch !== null) {
822
+ const branchMajorVersion = Number(parsedBranch[1]);
823
+ const branchMinorVersion = Number(parsedBranch[2]);
904
824
 
905
- if (parsedBranch !== null) {
906
- const branchMajorVersion = Number(parsedBranch[1]);
907
- const branchMinorVersion = Number(parsedBranch[2]);
825
+ // If in a version branch and version doesn't match, update it.
826
+ if (majorVersion !== branchMajorVersion || minorVersion !== branchMinorVersion) {
827
+ if (majorVersion !== branchMajorVersion ? majorVersion !== branchMajorVersion - 1 : minorVersion !== branchMinorVersion - 1) {
828
+ throw new Error(`Invalid transition from ${majorVersion}.${minorVersion} to ${branchMajorVersion}.${branchMinorVersion}`);
829
+ }
908
830
 
909
- // If in a version branch and version doesn't match, update it.
910
- if (this.majorVersion !== branchMajorVersion || this.minorVersion !== branchMinorVersion) {
911
- this.updatePackageVersion(branchMajorVersion, branchMinorVersion, 0, null);
912
- this.commitUpdatedPackageVersion(PACKAGE_CONFIGURATION_PATH);
831
+ this.updatePackageVersion(branchMajorVersion, branchMinorVersion, 0, null);
832
+ this.commitUpdatedPackageVersion(PACKAGE_CONFIGURATION_PATH);
833
+ }
913
834
  }
914
- }
915
835
 
916
- try {
917
836
  // eslint-disable-next-line no-await-in-loop -- Next iteration requires previous to finish.
918
837
  await this.publish();
919
838
  } finally {
839
+ // Clear repository state to prevent accidental access.
840
+ this._repositoryState = undefined;
841
+
842
+ // Return to the start directory.
843
+ process.chdir(startDirectory);
844
+
920
845
  this.saveConfiguration();
921
846
  }
922
847
  // Non-external repositories may be private and not accessible to all developers.
@@ -925,15 +850,12 @@ export abstract class Publish {
925
850
  }
926
851
  }
927
852
 
928
- // Return to the start directory.
929
- process.chdir(startDirectory);
930
-
931
853
  this.finalizeAll();
932
854
 
933
855
  this.saveConfiguration();
934
856
 
935
- if (this.releaseType !== "alpha") {
936
- this.commitModified(`Published ${this.releaseType} release.`, SHARED_CONFIGURATION_PATH);
857
+ if (this.phase !== "alpha") {
858
+ this.commitModified(`Published ${this.phase} release.`, SHARED_CONFIGURATION_PATH);
937
859
  }
938
860
  }
939
861