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