@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,144 @@
1
+ import * as fs from "node:fs";
2
+ import { PACKAGE_CONFIGURATION_PATH, PACKAGE_LOCK_CONFIGURATION_PATH, Publish } from "./publish.js";
3
+ import { logger } from "./logger.js";
4
+
5
+ const BACKUP_PACKAGE_CONFIGURATION_PATH = ".package.json";
6
+
7
+ /**
8
+ * Publish alpha versions.
9
+ */
10
+ class PublishAlpha extends Publish {
11
+ /**
12
+ * If true, update all dependencies automatically.
13
+ */
14
+ private readonly _updateAll: boolean;
15
+
16
+ /**
17
+ * Constructor.
18
+ *
19
+ * If true, outputs what would be run rather than running it.
20
+ *
21
+ * @param updateAll
22
+ * If true, update all dependencies automatically.
23
+ *
24
+ * @param dryRun
25
+ * If true, outputs what would be run rather than running it.
26
+ */
27
+ constructor(updateAll: boolean, dryRun: boolean) {
28
+ super("alpha", dryRun);
29
+
30
+ this._updateAll = updateAll;
31
+ }
32
+
33
+ /**
34
+ * @inheritDoc
35
+ */
36
+ protected publish(): void {
37
+ let anyDependencyUpdates = false;
38
+
39
+ // Check for external dependency updates, even if there are no changes.
40
+ for (const currentDependencies of [this.packageConfiguration.devDependencies, this.packageConfiguration.dependencies]) {
41
+ if (currentDependencies !== undefined) {
42
+ for (const [dependency, version] of Object.entries(currentDependencies)) {
43
+ // Ignore organization dependencies.
44
+ if (this.dependencyRepositoryName(dependency) === null && version.startsWith("^")) {
45
+ const [latestVersion] = this.run(true, true, "npm", "view", dependency, "version");
46
+
47
+ if (latestVersion !== version.substring(1)) {
48
+ logger.info(`Dependency ${dependency}@${version} ${!this._updateAll ? "pending update" : "updating"} to version ${latestVersion}.`);
49
+
50
+ if (this._updateAll) {
51
+ currentDependencies[dependency] = `^${latestVersion}`;
52
+
53
+ anyDependencyUpdates = true;
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ if (anyDependencyUpdates) {
62
+ // Save the dependency updates; this will be detected by call to anyChanges().
63
+ this.savePackageConfiguration();
64
+ }
65
+
66
+ if (this._updateAll) {
67
+ logger.debug("Updating all dependencies");
68
+
69
+ // Running this even if there are no dependency updates will update dependencies of dependencies.
70
+ this.run(false, false, "npm", "update", ...this.npmPlatformArgs);
71
+ }
72
+
73
+ const anyChanges = this.anyChanges(this.repository.lastAlphaPublished, true);
74
+
75
+ if (anyChanges) {
76
+ const switchToAlpha = this.preReleaseIdentifier !== "alpha";
77
+
78
+ if (switchToAlpha) {
79
+ // Previous publication was beta or production.
80
+ this.updatePackageVersion(undefined, undefined, this.patchVersion + 1, "alpha");
81
+
82
+ // Use specified registry for organization until no longer in alpha mode.
83
+ this.run(false, false, "npm", "set", this.atOrganizationRegistry, "--location", "project");
84
+ }
85
+
86
+ if (this.organizationDependenciesUpdated && (switchToAlpha || !this._updateAll)) {
87
+ const updateOrganizationDependencies = Object.values(this.organizationDependencies).filter(updateOrganizationDependency => updateOrganizationDependency !== null);
88
+
89
+ logger.debug(`Updating organization dependencies [${updateOrganizationDependencies.join(", ")}]`);
90
+
91
+ this.run(false, false, "npm", "update", ...updateOrganizationDependencies, ...this.npmPlatformArgs);
92
+ }
93
+ }
94
+
95
+ // Run lint if present.
96
+ this.run(false, false, "npm", "run", "lint", "--if-present");
97
+
98
+ // Run development build if present.
99
+ this.run(false, false, "npm", "run", "build:dev", "--if-present");
100
+
101
+ if (anyChanges) {
102
+ const nowISOString = new Date().toISOString();
103
+
104
+ // Nothing further required if this repository is not a dependency of others.
105
+ if (this.repository.dependencyType !== "none") {
106
+ if (!this.dryRun) {
107
+ // Backup the package configuration file.
108
+ fs.renameSync(PACKAGE_CONFIGURATION_PATH, BACKUP_PACKAGE_CONFIGURATION_PATH);
109
+ }
110
+
111
+ try {
112
+ // Package version is transient.
113
+ this.updatePackageVersion(undefined, undefined, undefined, `alpha.${nowISOString.replaceAll(/\D/g, "").substring(0, 12)}`);
114
+
115
+ // Publish to development NPM registry.
116
+ this.run(false, false, "npm", "publish", "--tag", "alpha");
117
+
118
+ // Unpublish all prior alpha versions.
119
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Output is a JSON array.
120
+ for (const version of JSON.parse(this.run(true, true, "npm", "view", this.packageConfiguration.name, "versions", "--json").join("\n")) as string[]) {
121
+ if (/^\d+.\d+.\d+-alpha.\d+$/.test(version) && version !== this.packageConfiguration.version) {
122
+ this.run(false, false, "npm", "unpublish", `${this.packageConfiguration.name}@${version}`);
123
+ }
124
+ }
125
+ } finally {
126
+ if (!this.dryRun) {
127
+ // Restore the package configuration file.
128
+ fs.rmSync(PACKAGE_CONFIGURATION_PATH);
129
+ fs.renameSync(BACKUP_PACKAGE_CONFIGURATION_PATH, PACKAGE_CONFIGURATION_PATH);
130
+ }
131
+ }
132
+ }
133
+
134
+ this.commitUpdatedPackageVersion(PACKAGE_CONFIGURATION_PATH, PACKAGE_LOCK_CONFIGURATION_PATH);
135
+
136
+ this.repository.lastAlphaPublished = nowISOString;
137
+ }
138
+ }
139
+ }
140
+
141
+ // Detailed syntax checking not required as this is an internal tool.
142
+ await new PublishAlpha(process.argv.includes("--update-all"), process.argv.includes("--dry-run")).publishAll().catch((e: unknown) => {
143
+ logger.error(e);
144
+ });
@@ -0,0 +1,293 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { setTimeout } from "node:timers/promises";
4
+ import { Octokit } from "octokit";
5
+ import { parse as yamlParse } from "yaml";
6
+ import secureConfigurationJSON from "../../config/publish.secure.json";
7
+ import { Publish } from "./publish.js";
8
+ import { logger } from "./logger.js";
9
+
10
+ /**
11
+ * Configuration layout of publish.secure.json.
12
+ */
13
+ interface SecureConfiguration {
14
+ token: string;
15
+ }
16
+
17
+ /**
18
+ * Configuration layout of release.yml workflow (relevant attributes only).
19
+ */
20
+ interface WorkflowConfiguration {
21
+ /**
22
+ * Workflow name.
23
+ */
24
+ name: string;
25
+
26
+ /**
27
+ * Workflow trigger.
28
+ */
29
+ on: {
30
+ /**
31
+ * Push trigger.
32
+ */
33
+ push?: {
34
+ /**
35
+ * Push branches.
36
+ */
37
+ branches?: string[];
38
+ };
39
+
40
+ /**
41
+ * Release trigger.
42
+ */
43
+ release?: {
44
+ /**
45
+ * Release types.
46
+ */
47
+ types?: string[];
48
+ };
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Publish steps.
54
+ */
55
+ type Step = "install" | "build" | "commit" | "tag" | "push" | "workflow (push)" | "release" | "workflow (release)";
56
+
57
+ /**
58
+ * Publish beta versions.
59
+ */
60
+ class PublishBeta extends Publish {
61
+ /**
62
+ * Secure configuration.
63
+ */
64
+ private readonly _secureConfiguration: SecureConfiguration = secureConfigurationJSON;
65
+
66
+ /**
67
+ * Octokit.
68
+ */
69
+ private readonly _octokit: Octokit;
70
+
71
+ /**
72
+ * Constructor.
73
+ *
74
+ * @param dryRun
75
+ * If true, outputs what would be run rather than running it.
76
+ */
77
+ constructor(dryRun: boolean) {
78
+ super("beta", dryRun);
79
+
80
+ this._octokit = new Octokit({
81
+ auth: this._secureConfiguration.token,
82
+ userAgent: `${this.configuration.organization} release`
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Run a step.
88
+ *
89
+ * Repository.
90
+ *
91
+ * @param step
92
+ * State at which step takes place.
93
+ *
94
+ * @param stepRunner
95
+ * Callback to execute step.
96
+ */
97
+ private async runStep(step: Step, stepRunner: () => (void | Promise<void>)): Promise<void> {
98
+ if (this.repository.publishBetaStep === undefined || this.repository.publishBetaStep === step) {
99
+ logger.debug(`Running step ${step}`);
100
+
101
+ this.repository.publishBetaStep = step;
102
+
103
+ await stepRunner();
104
+
105
+ this.repository.publishBetaStep = undefined;
106
+ } else {
107
+ logger.debug(`Skipping step ${step}`);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Validate the workflow by waiting for it to complete.
113
+ *
114
+ * Branch on which workflow is running.
115
+ */
116
+ private async validateWorkflow(): Promise<void> {
117
+ if (this.dryRun) {
118
+ logger.info("Dry run: Validate workflow");
119
+ } else {
120
+ const commitSHA = this.run(true, true, "git", "rev-parse", this.branch)[0];
121
+
122
+ let completed = false;
123
+ let queryCount = 0;
124
+ let workflowRunID = -1;
125
+
126
+ do {
127
+ // eslint-disable-next-line no-await-in-loop -- Loop depends on awaited response.
128
+ const response = await setTimeout(2000).then(
129
+ async () => this._octokit.rest.actions.listWorkflowRunsForRepo({
130
+ owner: this.configuration.organization,
131
+ repo: this.repositoryName,
132
+ head_sha: commitSHA
133
+ })
134
+ );
135
+
136
+ for (const workflowRun of response.data.workflow_runs) {
137
+ if (workflowRun.status !== "completed") {
138
+ if (workflowRun.id === workflowRunID) {
139
+ process.stdout.write(".");
140
+ } else if (workflowRunID === -1) {
141
+ workflowRunID = workflowRun.id;
142
+
143
+ logger.info(`Workflow run ID ${workflowRunID}`);
144
+ } else {
145
+ throw new Error(`Parallel workflow runs for SHA ${commitSHA}`);
146
+ }
147
+ } else if (workflowRun.id === workflowRunID) {
148
+ process.stdout.write("\n");
149
+
150
+ if (workflowRun.conclusion !== "success") {
151
+ throw new Error(`Workflow ${workflowRun.conclusion}`);
152
+ }
153
+
154
+ completed = true;
155
+ }
156
+ }
157
+
158
+ // Abort if workflow run not started after 10 queries.
159
+ if (++queryCount === 10 && workflowRunID === -1) {
160
+ throw new Error(`Workflow run not started for SHA ${commitSHA}`);
161
+ }
162
+ } while (!completed);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * @inheritDoc
168
+ */
169
+ protected async publish(): Promise<void> {
170
+ let publish: boolean;
171
+
172
+ // Scrap any incomplete publishing if pre-release identifier is not beta.
173
+ if (this.preReleaseIdentifier !== "beta") {
174
+ this.repository.publishBetaStep = undefined;
175
+ }
176
+
177
+ if (this.preReleaseIdentifier === "alpha") {
178
+ if (this.anyChanges(this.repository.lastAlphaPublished, true)) {
179
+ throw new Error("Repository has changed since last alpha published");
180
+ }
181
+
182
+ publish = true;
183
+
184
+ this.updatePackageVersion(undefined, undefined, undefined, "beta");
185
+ } else {
186
+ // Publish beta step is defined if previous attempt failed at that step.
187
+ publish = this.repository.publishBetaStep !== undefined;
188
+
189
+ // Ignore changes after publication process has started.
190
+ if (!publish && this.anyChanges(this.repository.lastBetaPublished, false)) {
191
+ throw new Error("Internal error, repository has changed without intermediate alpha publication");
192
+ }
193
+ }
194
+
195
+ if (publish) {
196
+ const tag = `v${this.packageConfiguration.version}`;
197
+
198
+ if (this.repository.publishBetaStep !== undefined) {
199
+ logger.debug(`Repository failed at step "${this.repository.publishBetaStep}" on prior run`);
200
+ }
201
+
202
+ const workflowsPath = ".github/workflows/";
203
+
204
+ let hasPushWorkflow = false;
205
+ let hasReleaseWorkflow = false;
206
+
207
+ if (fs.existsSync(workflowsPath)) {
208
+ logger.debug("Checking workflows");
209
+
210
+ for (const workflowFile of fs.readdirSync(workflowsPath).filter(workflowFile => workflowFile.endsWith(".yml"))) {
211
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Workflow configuration format is known.
212
+ const workflowOn = (yamlParse(fs.readFileSync(path.resolve(workflowsPath, workflowFile)).toString()) as WorkflowConfiguration).on;
213
+
214
+ if (workflowOn.push !== undefined && (workflowOn.push.branches === undefined || workflowOn.push.branches.includes("v*"))) {
215
+ logger.debug("Repository has push workflow");
216
+
217
+ hasPushWorkflow = true;
218
+ }
219
+
220
+ if (workflowOn.release !== undefined && (workflowOn.release.types === undefined || workflowOn.release.types.includes("published"))) {
221
+ logger.debug("Repository has release workflow");
222
+
223
+ hasReleaseWorkflow = true;
224
+ }
225
+ }
226
+ }
227
+
228
+ await this.runStep("install", () => {
229
+ this.run(false, false, "npm", "install", ...this.npmPlatformArgs);
230
+ });
231
+
232
+ await this.runStep("build", () => {
233
+ this.run(false, false, "npm", "run", "build:release", "--if-present");
234
+ });
235
+
236
+ await this.runStep("commit", () => {
237
+ this.commitUpdatedPackageVersion();
238
+ });
239
+
240
+ await this.runStep("tag", () => {
241
+ this.run(false, false, "git", "tag", tag);
242
+ });
243
+
244
+ await this.runStep("push", () => {
245
+ this.run(false, false, "git", "push", "--atomic", "origin", this.branch, tag);
246
+ });
247
+
248
+ if (hasPushWorkflow) {
249
+ await this.runStep("workflow (push)", async () => {
250
+ await this.validateWorkflow();
251
+ });
252
+ }
253
+
254
+ await this.runStep("release", async () => {
255
+ if (this.dryRun) {
256
+ logger.info("Dry run: Create release");
257
+ } else {
258
+ await this._octokit.rest.repos.createRelease({
259
+ owner: this.configuration.organization,
260
+ repo: this.repositoryName,
261
+ tag_name: tag,
262
+ name: `Release ${tag}`,
263
+ prerelease: true
264
+ });
265
+ }
266
+ });
267
+
268
+ if (hasReleaseWorkflow) {
269
+ await this.runStep("workflow (release)", async () => {
270
+ await this.validateWorkflow();
271
+ });
272
+ }
273
+
274
+ this.repository.lastBetaPublished = new Date().toISOString();
275
+ this.repository.publishBetaStep = undefined;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * @inheritDoc
281
+ */
282
+ protected override finalizeAll(): void {
283
+ // Publication complete; reset steps to undefined for next run.
284
+ for (const repository of Object.values(this.configuration.repositories)) {
285
+ repository.publishBetaStep = undefined;
286
+ }
287
+ }
288
+ }
289
+
290
+ // Detailed syntax checking not required as this is an internal tool.
291
+ await new PublishBeta(process.argv.includes("--dry-run")).publishAll().catch((e: unknown) => {
292
+ logger.error(e);
293
+ });