@aidc-toolkit/dev 0.9.19-beta → 0.9.21-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,151 +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 {
23
+ export interface PackageConfiguration {
21
24
  /**
22
- * Directory in which repository resides, if different from repository name.
25
+ * Name.
23
26
  */
24
- directory?: string;
27
+ readonly name: string;
25
28
 
26
29
  /**
27
- * Dependency type, dictating how it is published.
30
+ * Version.
28
31
  */
29
- dependencyType: string;
32
+ version: string;
30
33
 
31
34
  /**
32
- * Platform if building across platforms (local configuration), e.g., macOS hosting Windows on Parallels.
35
+ * Development dependencies.
33
36
  */
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
- };
37
+ readonly devDependencies?: Record<string, string>;
45
38
 
46
39
  /**
47
- * Additional dependencies not included in package configuration.
40
+ * Dependencies.
48
41
  */
49
- additionalDependencies?: string[];
42
+ readonly dependencies?: Record<string, string>;
43
+ }
50
44
 
45
+ /**
46
+ * Repository state, derived from package configuration and updated during publishing.
47
+ */
48
+ interface RepositoryState {
51
49
  /**
52
- * Paths to exclude from consideration when checking for changes.
50
+ * Repository name from configuration.
53
51
  */
54
- excludePaths?: string[];
52
+ readonly repositoryName: string;
55
53
 
56
54
  /**
57
- * Date/time in ISO format the last alpha version was published.
55
+ * Repository from configuration.
58
56
  */
59
- lastAlphaPublished?: string;
57
+ repository: Repository;
60
58
 
61
59
  /**
62
- * Current step in beta publication; used to resume after failure recovery.
60
+ * Phase state.
63
61
  */
64
- publishBetaStep?: string | undefined;
62
+ phaseState: PhaseState;
65
63
 
66
64
  /**
67
- * Date/time in ISO format the last beta version was published.
65
+ * Phase date/time or undefined if phase never before published.
68
66
  */
69
- lastBetaPublished?: string;
67
+ phaseDateTime: Date | undefined;
70
68
 
71
69
  /**
72
- * Date/time in ISO format the last production version was published.
70
+ * NPM platform arguments if any.
73
71
  */
74
- lastProductionPublished?: string;
72
+ readonly npmPlatformArgs: readonly string[];
75
73
 
76
74
  /**
77
- * Last production version.
75
+ * Branch.
78
76
  */
79
- lastProductionVersion?: string;
80
- }
77
+ readonly branch: string;
81
78
 
82
- /**
83
- * Configuration layout of merged publish.json and publish.local.json.
84
- */
85
- export interface Configuration {
86
79
  /**
87
- * Organization that owns the repositories.
80
+ * Package configuration.
88
81
  */
89
- organization: string;
82
+ readonly packageConfiguration: PackageConfiguration;
90
83
 
91
84
  /**
92
- * Log level (local configuration).
85
+ * Major version.
93
86
  */
94
- logLevel?: string;
87
+ majorVersion: number;
95
88
 
96
89
  /**
97
- * Registry hosting organization's alpha repositories (local configuration).
90
+ * Minor version.
98
91
  */
99
- alphaRegistry: string;
92
+ minorVersion: number;
100
93
 
101
94
  /**
102
- * Repositories.
95
+ * Patch version.
103
96
  */
104
- repositories: Record<string, Repository>;
105
- }
97
+ patchVersion: number;
106
98
 
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
99
  /**
116
- * Name.
100
+ * Pre-release identifier or null if none.
117
101
  */
118
- name: string;
102
+ preReleaseIdentifier: string | null;
119
103
 
120
104
  /**
121
- * Version.
105
+ * Dependency package names extracted directly from package configuration.
122
106
  */
123
- version: string;
107
+ readonly dependencyPackageNames: readonly string[];
124
108
 
125
109
  /**
126
- * Development dependencies.
110
+ * All dependency package names in publication order.
127
111
  */
128
- devDependencies?: Record<string, string>;
112
+ readonly allDependencyPackageNames: readonly string[];
129
113
 
130
114
  /**
131
- * Dependencies.
115
+ * True if any dependencies have been updated.
132
116
  */
133
- dependencies?: Record<string, string>;
117
+ readonly anyDependenciesUpdated: boolean;
134
118
  }
135
119
 
136
- /**
137
- * Release type.
138
- */
139
- type ReleaseType = "alpha" | "beta" | "production";
140
-
141
120
  /**
142
121
  * Publish base class.
143
122
  */
144
123
  export abstract class Publish {
145
124
  /**
146
- * Release type.
125
+ * Phase.
147
126
  */
148
- private readonly _releaseType: ReleaseType;
127
+ private readonly _phase: Phase;
149
128
 
150
129
  /**
151
130
  * If true, outputs what would be run rather than running it.
@@ -168,65 +147,14 @@ export abstract class Publish {
168
147
  private readonly _atOrganizationRegistry: string;
169
148
 
170
149
  /**
171
- * All organization dependencies, keyed on repository name.
150
+ * Repository states, keyed on repository name.
172
151
  */
173
- private readonly _allOrganizationDependencies: Record<string, Record<string, string | null>>;
152
+ private readonly _repositoryStates: Record<string, Readonly<RepositoryState>>;
174
153
 
175
154
  /**
176
- * Current repository name.
155
+ * Current repository state.
177
156
  */
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;
157
+ private _repositoryState: RepositoryState | undefined;
230
158
 
231
159
  /**
232
160
  * Constructor.
@@ -237,36 +165,29 @@ export abstract class Publish {
237
165
  * @param dryRun
238
166
  * If true, outputs what would be run rather than running it.
239
167
  */
240
- protected constructor(releaseType: ReleaseType, dryRun: boolean) {
241
- this._releaseType = releaseType;
168
+ protected constructor(releaseType: Phase, dryRun: boolean) {
169
+ this._phase = releaseType;
242
170
  this._dryRun = dryRun;
243
171
 
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
- };
172
+ this._configuration = loadConfiguration();
253
173
 
254
174
  this._atOrganization = `@${this.configuration.organization}`;
255
175
 
256
- this._atOrganizationRegistry = `${this.atOrganization}:registry=${this.configuration.alphaRegistry}`;
257
-
258
- this._allOrganizationDependencies = {};
176
+ this._atOrganizationRegistry = `${this.atOrganization}:registry${releaseType === "alpha" ? `=${this.configuration.alphaRegistry}` : ""}`;
259
177
 
260
178
  if (this._configuration.logLevel !== undefined) {
261
179
  setLogLevel(this._configuration.logLevel);
262
180
  }
181
+
182
+ this._repositoryStates = {};
183
+ this._repositoryState = undefined;
263
184
  }
264
185
 
265
186
  /**
266
- * Get the release type.
187
+ * Get the phase.
267
188
  */
268
- protected get releaseType(): ReleaseType {
269
- return this._releaseType;
189
+ protected get phase(): Phase {
190
+ return this._phase;
270
191
  }
271
192
 
272
193
  /**
@@ -298,88 +219,83 @@ export abstract class Publish {
298
219
  }
299
220
 
300
221
  /**
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.
222
+ * Get the repository states, keyed on repository name.
316
223
  */
317
- protected get repository(): Repository {
318
- return this._repository;
224
+ protected get repositoryStates(): Record<string, Readonly<RepositoryState>> {
225
+ return this._repositoryStates;
319
226
  }
320
227
 
321
228
  /**
322
- * Get the NPM platform arguments if any.
229
+ * Get the current repository state.
323
230
  */
324
- get npmPlatformArgs(): string[] {
325
- return this._npmPlatformArgs;
326
- }
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
+ }
327
236
 
328
- /**
329
- * Get the branch.
330
- */
331
- protected get branch(): string {
332
- return this._branch;
237
+ return this._repositoryState;
333
238
  }
334
239
 
335
240
  /**
336
- * Get the package configuration.
337
- */
338
- protected get packageConfiguration(): PackageConfiguration {
339
- return this._packageConfiguration;
340
- }
341
-
342
- /**
343
- * 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.
344
251
  */
345
- protected get majorVersion(): number {
346
- return this._majorVersion;
347
- }
252
+ protected abstract dependencyVersionFor(dependencyRepositoryName: string, dependencyRepository: Repository): string;
348
253
 
349
254
  /**
350
- * 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.
351
265
  */
352
- protected get minorVersion(): number {
353
- return this._minorVersion;
354
- }
266
+ protected latestDateTime(initialDateTime: Date, ...additionalDateTimes: Array<Date | undefined>): Date {
267
+ let latestDateTime = initialDateTime;
355
268
 
356
- /**
357
- * Get the patch version.
358
- */
359
- protected get patchVersion(): number {
360
- return this._patchVersion;
361
- }
269
+ for (const dateTime of additionalDateTimes) {
270
+ if (dateTime !== undefined && latestDateTime.getTime() < dateTime.getTime()) {
271
+ latestDateTime = dateTime;
272
+ }
273
+ }
362
274
 
363
- /**
364
- * Get the pre-release identifier.
365
- */
366
- protected get preReleaseIdentifier(): string | null {
367
- return this._preReleaseIdentifier;
275
+ return latestDateTime;
368
276
  }
369
277
 
370
278
  /**
371
- * 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.
372
289
  */
373
- protected get organizationDependencies(): Record<string, string | null> {
374
- return this._organizationDependencies;
375
- }
290
+ protected abstract getPhaseDateTime(repository: Repository, phaseDateTime: Date): Date;
376
291
 
377
292
  /**
378
- * 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.
379
297
  */
380
- protected get organizationDependenciesUpdated(): boolean {
381
- return this._organizationDependenciesUpdated;
382
- }
298
+ protected abstract isValidBranch(): boolean;
383
299
 
384
300
  /**
385
301
  * Run a command and optionally capture its output.
@@ -457,22 +373,61 @@ export abstract class Publish {
457
373
  return parsedDependency.length === 2 && parsedDependency[0] === this.atOrganization ? parsedDependency[1] : null;
458
374
  }
459
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
+
460
415
  /**
461
416
  * Determine if there have been any changes to the current repository.
462
417
  *
463
- * @param lastPublished
464
- * 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.
465
420
  *
466
421
  * @param ignoreGitHub
467
422
  * If true, ignore .github directory.
468
423
  *
469
424
  * @returns
470
- * 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.
471
426
  */
472
- protected anyChanges(lastPublished: string | undefined, ignoreGitHub: boolean): boolean {
427
+ protected anyChanges(phaseDateTime: Date | undefined, ignoreGitHub: boolean): boolean {
473
428
  let anyChanges: boolean;
474
429
 
475
- const excludePaths = this.repository.excludePaths ?? [];
430
+ const excludePaths = this.repositoryState.repository.excludePaths ?? [];
476
431
 
477
432
  const changedFilesSet = new Set<string>();
478
433
 
@@ -511,13 +466,14 @@ export abstract class Publish {
511
466
  }
512
467
  }
513
468
 
514
- 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) {
515
470
  throw new Error("Remote repository has outstanding changes");
516
471
  }
517
472
 
518
- if (lastPublished !== undefined) {
473
+ // Phase date/time is undefined if never before published.
474
+ if (phaseDateTime !== undefined) {
519
475
  // Get all files committed since last published.
520
- 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")) {
521
477
  // Header starts with 40-character SHA.
522
478
  if (/^[0-9a-f]{40} /.test(line)) {
523
479
  logger.debug(`Commit SHA ${line.substring(0, 40)}`);
@@ -532,10 +488,7 @@ export abstract class Publish {
532
488
  const output = this.run(true, true, "git", "status", "--porcelain");
533
489
 
534
490
  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
- }
491
+ const committedCount = changedFilesSet.size;
539
492
 
540
493
  logger.debug("Uncommitted");
541
494
 
@@ -546,9 +499,14 @@ export abstract class Publish {
546
499
 
547
500
  processChangedFile(status, file, newFile);
548
501
  }
502
+
503
+ // Beta or production publication requires that repository be fully committed except for excluded paths.
504
+ if (this.phase !== "alpha" && changedFilesSet.size !== committedCount) {
505
+ throw new Error("Repository has uncommitted changes");
506
+ }
549
507
  }
550
508
 
551
- const lastPublishedDateTime = new Date(lastPublished);
509
+ const lastPublishedDateTime = new Date(phaseDateTime);
552
510
 
553
511
  anyChanges = false;
554
512
 
@@ -564,17 +522,11 @@ export abstract class Publish {
564
522
  }
565
523
  }
566
524
 
567
- if (!anyChanges && this.organizationDependenciesUpdated) {
568
- logger.info("Organization dependencies updated");
569
-
570
- anyChanges = true;
571
- }
572
-
573
525
  if (!anyChanges) {
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,27 +597,40 @@ 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
  }
666
622
 
623
+ /**
624
+ * Update organization dependencies.
625
+ */
626
+ protected updateOrganizationDependencies(): void {
627
+ const repositoryState = this.repositoryState;
628
+
629
+ logger.debug(`Updating organization dependencies [${repositoryState.allDependencyPackageNames.join(", ")}]`);
630
+
631
+ this.run(false, false, "npm", "update", ...repositoryState.allDependencyPackageNames, ...this.repositoryState.npmPlatformArgs);
632
+ }
633
+
667
634
  /**
668
635
  * Commit changes resulting from updating the package version.
669
636
  *
@@ -671,40 +638,40 @@ export abstract class Publish {
671
638
  * Files to commit; if none, defaults to "--all".
672
639
  */
673
640
  protected commitUpdatedPackageVersion(...files: string[]): void {
674
- this.commitModified(`Updated to version ${this.packageConfiguration.version}.`, ...files);
641
+ this.commitModified(`Updated to version ${this.repositoryState.packageConfiguration.version}.`, ...files);
675
642
  }
676
643
 
677
644
  /**
678
- * 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.
679
650
  */
680
- protected saveConfiguration(): void {
681
- const saveSharedRepositories: Record<string, Omit<Repository, "platform" | "lastAlphaPublished">> = {};
682
- const saveLocalRepositories: Record<string, Pick<Repository, "platform" | "lastAlphaPublished">> = {};
651
+ protected updatePhaseState(phaseState: Partial<PhaseState>): void {
652
+ const repositoryState = this.repositoryState;
683
653
 
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);
654
+ const updatedPhaseState = {
655
+ ...this.repositoryState.phaseState,
656
+ ...phaseState
657
+ };
693
658
 
694
- const saveLocalConfigurationJSON = JSON.stringify({
695
- ...pick(this.configuration, "logLevel", "alphaRegistry"),
696
- repositories: saveLocalRepositories
697
- }, null, 2);
659
+ repositoryState.repository.phaseStates[this.phase] = updatedPhaseState;
660
+ repositoryState.phaseState = updatedPhaseState;
698
661
 
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);
662
+ // Setting the phase date/time overrides the logic of its initial determination.
663
+ if (phaseState.dateTime !== undefined) {
664
+ repositoryState.phaseDateTime = phaseState.dateTime;
705
665
  }
706
666
  }
707
667
 
668
+ /**
669
+ * Save the configuration.
670
+ */
671
+ private saveConfiguration(): void {
672
+ saveConfiguration(this.configuration, this.dryRun);
673
+ }
674
+
708
675
  /**
709
676
  * Publish current repository.
710
677
  */
@@ -717,20 +684,6 @@ export abstract class Publish {
717
684
  const startDirectory = process.cwd();
718
685
 
719
686
  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
687
  // All repositories are expected to be children of the parent of this repository.
735
688
  const directory = `../${repository.directory ?? repositoryName}`;
736
689
 
@@ -739,142 +692,156 @@ export abstract class Publish {
739
692
 
740
693
  process.chdir(directory);
741
694
 
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;
695
+ let phaseState = repository.phaseStates[this.phase];
744
696
 
745
- const version = this.packageConfiguration.version;
697
+ // Create phase state if necessary.
698
+ if (phaseState === undefined) {
699
+ phaseState = {
700
+ dateTime: new Date(0)
701
+ };
746
702
 
747
- const parsedVersion = /^(\d+)\.(\d+)\.(\d+)(-(alpha|beta))?$/.exec(version);
748
-
749
- if (parsedVersion === null) {
750
- throw new Error(`Invalid package version ${version}`);
703
+ repository.phaseStates[this.phase] = phaseState;
751
704
  }
752
705
 
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;
706
+ try {
707
+ const phaseDateTime = this.getPhaseDateTime(repository, phaseState.dateTime);
757
708
 
758
- 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
+ [];
759
717
 
760
- if (this.releaseType === "beta" && parsedBranch === null) {
761
- throw new Error(`Beta release must be from version branch v${this.majorVersion}.${this.minorVersion}`);
762
- }
718
+ const branch = this.run(true, true, "git", "branch", "--show-current")[0];
763
719
 
764
- if (this.releaseType === "production" && this.branch !== "main") {
765
- throw new Error("Production release must be from main branch");
766
- }
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;
767
722
 
768
- this._organizationDependencies = {};
723
+ const version = packageConfiguration.version;
769
724
 
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);
725
+ const parsedVersion = /^(\d+)\.(\d+)\.(\d+)(-(alpha|beta))?$/.exec(version);
774
726
 
775
- if (dependencyRepositoryName !== null) {
776
- logger.trace(`Organization dependency from package configuration ${dependencyRepositoryName}:${dependency}`);
727
+ if (parsedVersion === null) {
728
+ throw new Error(`Invalid package version ${version}`);
729
+ }
777
730
 
778
- 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;
779
735
 
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];
736
+ const dependencyPackageNames: string[] = [];
737
+ const allDependencyPackageNames: string[] = [];
785
738
 
786
- const lastProductionVersion = dependencyRepository.lastProductionVersion;
739
+ let anyDependenciesUpdated = false;
787
740
 
788
- if (lastProductionVersion === undefined) {
789
- throw new Error(`Internal error, last production version not set for ${dependencyRepositoryName}`);
790
- }
741
+ for (const dependencies of [packageConfiguration.devDependencies ?? {}, packageConfiguration.dependencies ?? {}]) {
742
+ for (const dependencyPackageName of Object.keys(dependencies)) {
743
+ const dependencyRepositoryName = this.dependencyRepositoryName(dependencyPackageName);
744
+
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`);
791
748
 
792
- currentDependencies[dependency] = `^${lastProductionVersion}`;
749
+ // Check every dependency for logging purposes.
750
+ if (this.isOrganizationDependencyUpdated(phaseDateTime, dependencyRepositoryName, false)) {
751
+ anyDependenciesUpdated = true;
793
752
  }
794
- }
795
- }
796
- }
797
- }
798
753
 
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`);
754
+ for (const dependencyDependencyPackageName of this.repositoryStates[dependencyRepositoryName].allDependencyPackageNames) {
755
+ if (!allDependencyPackageNames.includes(dependencyDependencyPackageName)) {
756
+ logger.trace(`Organization dependency ${dependencyDependencyPackageName} from dependencies`);
805
757
 
806
- this.organizationDependencies[additionalDependency] = null;
807
- }
808
- }
809
- }
758
+ allDependencyPackageNames.push(dependencyDependencyPackageName);
759
+ }
760
+ }
810
761
 
811
- // Add dependency repositories of dependency repositories.
812
- for (const dependencyRepositoryName of Object.keys(this.organizationDependencies)) {
813
- const dependencyOrganizationDependencies = this.allOrganizationDependencies[dependencyRepositoryName];
762
+ dependencyPackageNames.push(dependencyPackageName);
814
763
 
815
- for (const [dependencyDependencyRepositoryName, dependencyDependency] of Object.entries(dependencyOrganizationDependencies)) {
816
- if (!(dependencyDependencyRepositoryName in this.organizationDependencies)) {
817
- logger.trace(`Organization dependency from dependencies ${dependencyDependencyRepositoryName}:${dependencyDependency}`);
764
+ // Current dependency package name goes in last to preserve hierarchy.
765
+ allDependencyPackageNames.push(dependencyPackageName);
818
766
 
819
- this.organizationDependencies[dependencyDependencyRepositoryName] = dependencyDependency;
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]);
769
+ }
820
770
  }
821
771
  }
822
- }
823
-
824
- // Save organization dependencies for future repositories.
825
- this.allOrganizationDependencies[repositoryName] = this.organizationDependencies;
826
772
 
827
- let getLastPublished: (repository: Repository) => string | undefined;
773
+ if (repository.additionalDependencies !== undefined) {
774
+ const additionalRepositoryNames: string[] = [];
828
775
 
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
- }
776
+ for (const additionalDependencyRepositoryName of repository.additionalDependencies) {
777
+ const additionalDependencyPackageName = `${this.atOrganization}/${additionalDependencyRepositoryName}`;
842
778
 
843
- const lastPublished = getLastPublished(repository);
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`);
844
783
 
845
- this._organizationDependenciesUpdated = false;
784
+ // Check every dependency for logging purposes.
785
+ if (this.isOrganizationDependencyUpdated(phaseDateTime, additionalDependencyRepositoryName, true)) {
786
+ anyDependenciesUpdated = true;
787
+ }
846
788
 
847
- for (const dependencyRepositoryName of Object.keys(this.organizationDependencies)) {
848
- const dependencyRepository = this.configuration.repositories[dependencyRepositoryName];
849
- const dependencyLastPublished = getLastPublished(dependencyRepository);
789
+ additionalRepositoryNames.push(additionalDependencyRepositoryName);
790
+ }
791
+ }
792
+ }
850
793
 
851
- if (dependencyLastPublished === undefined) {
852
- 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`);
853
816
  }
854
817
 
855
- if (lastPublished === undefined || dependencyLastPublished > lastPublished) {
856
- logger.info(`Repository ${dependencyRepositoryName} recently published`);
818
+ const parsedBranch = /^v(\d+)\.(\d+)/.exec(branch);
857
819
 
858
- // At least one dependency repository has been published since the last publication of this repository.
859
- this._organizationDependenciesUpdated = true;
860
- }
861
- }
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]);
862
824
 
863
- if (parsedBranch !== null) {
864
- const branchMajorVersion = Number(parsedBranch[1]);
865
- 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
+ }
866
830
 
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);
831
+ this.updatePackageVersion(branchMajorVersion, branchMinorVersion, 0, null);
832
+ this.commitUpdatedPackageVersion(PACKAGE_CONFIGURATION_PATH);
833
+ }
871
834
  }
872
- }
873
835
 
874
- try {
875
836
  // eslint-disable-next-line no-await-in-loop -- Next iteration requires previous to finish.
876
837
  await this.publish();
877
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
+
878
845
  this.saveConfiguration();
879
846
  }
880
847
  // Non-external repositories may be private and not accessible to all developers.
@@ -883,15 +850,12 @@ export abstract class Publish {
883
850
  }
884
851
  }
885
852
 
886
- // Return to the start directory.
887
- process.chdir(startDirectory);
888
-
889
853
  this.finalizeAll();
890
854
 
891
855
  this.saveConfiguration();
892
856
 
893
- if (this.releaseType !== "alpha") {
894
- this.commitModified(`Published ${this.releaseType} release.`, SHARED_CONFIGURATION_PATH);
857
+ if (this.phase !== "alpha") {
858
+ this.commitModified(`Published ${this.phase} release.`, SHARED_CONFIGURATION_PATH);
895
859
  }
896
860
  }
897
861