@aidc-toolkit/dev 0.9.16-beta → 0.9.17-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.
package/src/release.ts DELETED
@@ -1,428 +0,0 @@
1
- /* eslint-disable no-console -- Console application. */
2
-
3
- import * as fs from "fs";
4
- import * as path from "node:path";
5
- import * as util from "node:util";
6
- import { Octokit } from "octokit";
7
- import { parse as yamlParse } from "yaml";
8
-
9
- import configurationJSON from "../config/release.json";
10
- import secureConfigurationJSON from "../config/release.secure.json";
11
- import { run } from "./command-util.js";
12
-
13
- /**
14
- * Configuration layout of release.json.
15
- */
16
- interface Configuration {
17
- /**
18
- * Organization that owns the repositories.
19
- */
20
- organization: string;
21
-
22
- /**
23
- * If true, the fact that the repository is uncommitted is ignored. For development and testing purposes only.
24
- */
25
- ignoreUncommitted?: boolean;
26
-
27
- /**
28
- * Repositories.
29
- */
30
- repositories: Record<string, {
31
- /**
32
- * Directory in which repository resides, if different from repository name.
33
- */
34
- directory?: string;
35
-
36
- /**
37
- * Version for repository. Not all repositories will be in sync with the version.
38
- */
39
- version: string;
40
- }>;
41
- }
42
-
43
- /**
44
- * Configuration layout of release.secure.json.
45
- */
46
- interface SecureConfiguration {
47
- token: string;
48
- }
49
-
50
- const configuration: Configuration = configurationJSON;
51
- const secureConfiguration: SecureConfiguration = secureConfigurationJSON;
52
-
53
- /**
54
- * Configuration layout of package.json (relevant attributes only).
55
- */
56
- interface PackageConfiguration {
57
- /**
58
- * Version.
59
- */
60
- version: string;
61
-
62
- /**
63
- * If true, package is private and not referenced by others.
64
- */
65
- private?: boolean;
66
-
67
- /**
68
- * Development dependencies.
69
- */
70
- devDependencies?: Record<string, string>;
71
-
72
- /**
73
- * Dependencies.
74
- */
75
- dependencies?: Record<string, string>;
76
- }
77
-
78
- /**
79
- * Configuration layout of release.yml workflow (relevant attributes only).
80
- */
81
- interface WorkflowConfiguration {
82
- /**
83
- * Workflow name.
84
- */
85
- name: string;
86
-
87
- /**
88
- * Workflow trigger.
89
- */
90
- on: {
91
- /**
92
- * Push trigger.
93
- */
94
- push?: {
95
- /**
96
- * Push branches.
97
- */
98
- branches?: string[];
99
- };
100
-
101
- /**
102
- * Release trigger.
103
- */
104
- release?: {
105
- /**
106
- * Release types.
107
- */
108
- types?: string[];
109
- };
110
- };
111
- }
112
-
113
- /**
114
- * Supported states.
115
- */
116
- type State = "skipped" | "install" | "build" | "commit" | "tag" | "push" | "workflow (push)" | "release" | "workflow (release)" | "restore alpha" | "complete";
117
-
118
- /**
119
- * Release.
120
- */
121
- async function release(): Promise<void> {
122
- // State may be written from any directory so full path is required.
123
- const statePath = path.resolve("config/release.state.json");
124
-
125
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Format is controlled by this process.
126
- const state: Record<string, State | undefined> = fs.existsSync(statePath) ? JSON.parse(fs.readFileSync(statePath).toString()) : {};
127
-
128
- /**
129
- * Save the current state.
130
- */
131
- function saveState(): void {
132
- fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`);
133
- }
134
-
135
- /**
136
- * Execute a step.
137
- *
138
- * @param name
139
- * Repository name.
140
- *
141
- * @param stepState
142
- * State at which step takes place.
143
- *
144
- * @param callback
145
- * Callback to execute step.
146
- *
147
- * @returns
148
- * Promise.
149
- */
150
- async function step(name: string, stepState: State, callback: () => (void | Promise<void>)): Promise<void> {
151
- const repositoryState = state[name];
152
-
153
- if (repositoryState === undefined || repositoryState === stepState) {
154
- state[name] = stepState;
155
-
156
- try {
157
- const result = callback();
158
-
159
- if (result instanceof Promise) {
160
- await result;
161
- }
162
-
163
- state[name] = undefined;
164
- } finally {
165
- saveState();
166
- }
167
- }
168
- }
169
-
170
- const atOrganization = `@${configuration.organization}`;
171
-
172
- /**
173
- * Update dependencies from the organization.
174
- *
175
- * @param dependencies
176
- * Dependencies.
177
- *
178
- * @param restoreAlpha
179
- * If true, "alpha" is restored as the version for development.
180
- *
181
- * @returns
182
- * True if any dependencies were updated.
183
- */
184
- function updateDependencies(dependencies: Record<string, string> | undefined, restoreAlpha: boolean): boolean {
185
- let anyUpdated = false;
186
-
187
- if (dependencies !== undefined) {
188
- // eslint-disable-next-line guard-for-in -- Dependency record type is shallow.
189
- for (const dependency in dependencies) {
190
- const [dependencyAtOrganization, dependencyRepositoryName] = dependency.split("/");
191
-
192
- if (dependencyAtOrganization === atOrganization) {
193
- dependencies[dependency] = !restoreAlpha ? `^${configuration.repositories[dependencyRepositoryName].version}` : "alpha";
194
- anyUpdated = true;
195
- }
196
- }
197
- }
198
-
199
- return anyUpdated;
200
- }
201
-
202
- const octokit = new Octokit({
203
- auth: secureConfiguration.token,
204
- userAgent: `${configuration.organization} release`
205
- });
206
-
207
- let allSkipped = true;
208
- let firstRepository = true;
209
-
210
- for (const name of Object.keys(configuration.repositories)) {
211
- const repository = configuration.repositories[name];
212
-
213
- console.log(`Repository ${name}...`);
214
-
215
- // All repositories are expected to be children of the parent of this repository.
216
- process.chdir(`../${repository.directory ?? name}`);
217
-
218
- // Repository must be on main branch.
219
- if (run(true, "git", "branch", "--show-current")[0] !== "main") {
220
- throw new Error("Repository is not on main branch");
221
- }
222
-
223
- // Repository must be fully committed except for untracked files.
224
- if (!(configuration.ignoreUncommitted ?? false) && state[name] === undefined && run(true, "git", "status", "--short", "--untracked-files=no").length !== 0) {
225
- throw new Error("Repository has uncommitted changes");
226
- }
227
-
228
- const tag = `v${repository.version}`;
229
-
230
- const octokitParameterBase = {
231
- owner: configuration.organization,
232
- repo: name
233
- };
234
-
235
- const packageConfigurationPath = "package.json";
236
-
237
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Package configuration format is known.
238
- const packageConfiguration: PackageConfiguration = JSON.parse(fs.readFileSync(packageConfigurationPath).toString());
239
-
240
- let skipRepository: boolean;
241
-
242
- switch (state[name]) {
243
- case undefined:
244
- // No steps have yet been taken; skip if repository is already at the required version.
245
- skipRepository = packageConfiguration.version === repository.version;
246
-
247
- if (!skipRepository) {
248
- allSkipped = false;
249
-
250
- packageConfiguration.version = repository.version;
251
-
252
- updateDependencies(packageConfiguration.devDependencies, false);
253
- updateDependencies(packageConfiguration.dependencies, false);
254
-
255
- fs.writeFileSync(packageConfigurationPath, `${JSON.stringify(packageConfiguration, null, 2)}\n`);
256
- } else {
257
- if (!allSkipped) {
258
- throw new Error(`Repository ${name} is supposed to be skipped but at least one prior repository has been updated`);
259
- }
260
-
261
- // First repository is excluded as it hosts development artefacts only, including the configuration file for this process.
262
- if (!firstRepository && run(true, "git", "tag", "--points-at", "HEAD", tag).length === 0) {
263
- throw new Error(`Repository ${name} has at least one commit since version ${repository.version}`);
264
- }
265
-
266
- state[name] = "skipped";
267
- }
268
- break;
269
-
270
- case "skipped":
271
- // Repository was skipped on the prior run.
272
- skipRepository = true;
273
- break;
274
-
275
- case "complete":
276
- // Repository was fully updated on the prior run.
277
- skipRepository = true;
278
-
279
- allSkipped = false;
280
- break;
281
-
282
- default:
283
- // Repository failed at some step on the prior run.
284
- skipRepository = false;
285
-
286
- allSkipped = false;
287
- break;
288
- }
289
-
290
- if (!skipRepository) {
291
- const workflowsPath = ".github/workflows/";
292
-
293
- let hasPushWorkflow = false;
294
- let hasReleaseWorkflow = false;
295
-
296
- if (fs.existsSync(workflowsPath)) {
297
- for (const workflowFile of fs.readdirSync(workflowsPath).filter(workflowFile => workflowFile.endsWith(".yml"))) {
298
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Workflow configuration format is known.
299
- const workflowOn = (yamlParse(fs.readFileSync(path.resolve(workflowsPath, workflowFile)).toString()) as WorkflowConfiguration).on;
300
-
301
- if (workflowOn.push !== undefined && (workflowOn.push.branches === undefined || workflowOn.push.branches.includes("main"))) {
302
- hasPushWorkflow = true;
303
- }
304
-
305
- if (workflowOn.release !== undefined && (workflowOn.release.types === undefined || workflowOn.release.types.includes("published"))) {
306
- hasReleaseWorkflow = true;
307
- }
308
- }
309
- }
310
-
311
- /**
312
- * Validate the workflow by waiting for it to complete.
313
- */
314
- async function validateWorkflow(): Promise<void> {
315
- const commitSHA = run(true, "git", "rev-parse", "HEAD")[0];
316
-
317
- let completed = false;
318
- let queryCount = 0;
319
- let workflowRunID = -1;
320
-
321
- do {
322
- await util.promisify(setTimeout)(2000);
323
-
324
- const response = await octokit.rest.actions.listWorkflowRunsForRepo({
325
- ...octokitParameterBase,
326
- head_sha: commitSHA
327
- });
328
-
329
- for (const workflowRun of response.data.workflow_runs) {
330
- if (workflowRun.status !== "completed") {
331
- if (workflowRun.id === workflowRunID) {
332
- process.stdout.write(".");
333
- } else if (workflowRunID === -1) {
334
- workflowRunID = workflowRun.id;
335
-
336
- console.log(`Workflow run ID ${workflowRunID}`);
337
- } else {
338
- throw new Error(`Parallel workflow runs for SHA ${commitSHA}`);
339
- }
340
- } else if (workflowRun.id === workflowRunID) {
341
- process.stdout.write("\n");
342
-
343
- if (workflowRun.conclusion !== "success") {
344
- throw new Error(`Workflow ${workflowRun.conclusion}`);
345
- }
346
-
347
- completed = true;
348
- }
349
- }
350
-
351
- // Abort if workflow run not started after 10 queries.
352
- if (++queryCount === 10 && workflowRunID === -1) {
353
- throw new Error(`Workflow run not started for SHA ${commitSHA}`);
354
- }
355
- } while (!completed);
356
- }
357
-
358
- await step(name, "install", () => {
359
- run(false, "npm", "install");
360
- });
361
-
362
- await step(name, "build", () => {
363
- run(false, "npm", "run", "build", "--if-present");
364
- });
365
-
366
- await step(name, "commit", () => {
367
- run(false, "git", "commit", "--all", `--message=Updated to version ${repository.version}.`);
368
- });
369
-
370
- await step(name, "tag", () => {
371
- run(false, "git", "tag", tag);
372
- });
373
-
374
- await step(name, "push", () => {
375
- run(false, "git", "push", "--atomic", "origin", "main", tag);
376
- });
377
-
378
- if (hasPushWorkflow) {
379
- await step(name, "workflow (push)", async () => {
380
- await validateWorkflow();
381
- });
382
- }
383
-
384
- await step(name, "release", async () => {
385
- const versionSplit = repository.version.split("-");
386
- const prerelease = versionSplit.length !== 1;
387
-
388
- await octokit.rest.repos.createRelease({
389
- ...octokitParameterBase,
390
- tag_name: tag,
391
- name: `${prerelease ? `${versionSplit[1].substring(0, 1).toUpperCase()}${versionSplit[1].substring(1)} r` : "R"}elease ${versionSplit[0]}`,
392
- // TODO Remove "false" override.
393
- prerelease: false
394
- });
395
- });
396
-
397
- if (hasReleaseWorkflow) {
398
- await step(name, "workflow (release)", async () => {
399
- await validateWorkflow();
400
- });
401
- }
402
-
403
- await step(name, "restore alpha", () => {
404
- // Restore dependencies to "alpha" version for development.
405
- const devDependenciesUpdated = updateDependencies(packageConfiguration.devDependencies, true);
406
- const dependenciesUpdated = updateDependencies(packageConfiguration.dependencies, true);
407
-
408
- if (devDependenciesUpdated || dependenciesUpdated) {
409
- fs.writeFileSync(packageConfigurationPath, `${JSON.stringify(packageConfiguration, null, 2)}\n`);
410
- run(false, "git", "commit", "--all", "--message=Restored alpha version.");
411
- }
412
- });
413
-
414
- state[name] = "complete";
415
- }
416
-
417
- saveState();
418
-
419
- firstRepository = false;
420
- }
421
-
422
- // All repositories released.
423
- fs.rmSync(statePath);
424
- }
425
-
426
- await release().catch((e: unknown) => {
427
- console.error(e);
428
- });