@edifice.io/cli 1.6.0-develop-b2school.10 → 1.6.0-develop-b2school.11

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,471 +1,445 @@
1
- // @ts-check
2
- // Originally ported to TS from https://github.com/remix-run/react-router/tree/main/scripts/{version,publish}.js
3
-
4
- import { parse as parseCommit } from "@commitlint/parse";
5
- import currentGitBranch from "current-git-branch";
6
- import { execSync } from "node:child_process";
7
- import path from "node:path";
8
- import * as semver from "semver";
9
- import { simpleGit } from "simple-git";
10
- import {
11
- capitalize,
12
- getSorterFn,
13
- readPackageJson,
14
- releaseCommitMsg,
15
- updatePackageJson,
16
- } from "./utilities.js";
17
-
18
- /**
19
- * Execute a script being published
20
- * @param {import('./index').Options} options
21
- * @returns {Promise<void>}
22
- */
23
- export const publish = async (options) => {
24
- const { branchConfigs, packages, rootDir, branch, tag, ghToken } = options;
25
-
26
- const branchName = /** @type {string} */ (branch ?? currentGitBranch());
27
- const isMainBranch = branchName === "main";
28
- const npmTag = isMainBranch ? "latest" : branchName;
29
-
30
- /** @type {import('./index').BranchConfig | undefined} */
31
- const branchConfig = branchConfigs[branchName];
32
-
33
- if (!branchConfig) {
34
- throw new Error(`No publish config found for branch: ${branchName}`);
35
- }
36
-
37
- // Get tags
38
- /** @type {string[]} */
39
- const allTags = execSync("git tag").toString().split("\n");
40
-
41
- // TODO: Keep this code for now, but it's not used
42
- const filteredTags = allTags
43
- // Ensure tag is valid
44
- .filter((t) => semver.valid(t))
45
- // Filter tags based on whether the branch is a release or pre-release
46
- .filter((t) => {
47
- const isPrereleaseTag = semver.prerelease(t) !== null;
48
- const prereleaseBranch = semver.prerelease(t)?.[0];
49
- // For prerelease branches, only include tags for that branch
50
- if (branchConfig.prerelease) {
51
- return isPrereleaseTag && prereleaseBranch === branchName;
52
- }
53
- // For main branch, exclude all prereleases
54
- return !isPrereleaseTag;
55
- })
56
- // sort by latest
57
- // @ts-ignore
58
- .sort(semver.compare);
59
-
60
- /* const filteredTags = allTags
61
- // Ensure tag is valid
62
- .filter((t) => semver.valid(t))
63
- // Filter tags based on whether the branch is a release or pre-release
64
- .filter((t) => {
65
- const isPrereleaseTag = semver.prerelease(t) !== null;
66
- return branchConfig.prerelease ? isPrereleaseTag : !isPrereleaseTag;
67
- })
68
- // sort by latest
69
- // @ts-ignore
70
- .sort(semver.compare); */
71
-
72
- // Get the latest tag
73
- let latestTag = filteredTags.at(-1);
74
-
75
- // TODO: Keep this code for now, but it's not used
76
- /* if (branchConfig.prerelease) {
77
- const mainTags = execSync("git tag --merged main").toString().split("\n");
78
- const validMainTags = mainTags
79
- .filter((t) => semver.valid(t))
80
- .sort((a, b) => semver.compare(a, b));
81
-
82
- latestTag = validMainTags.at(-1) || latestTag; // Use the latest tag from main if available
83
- } */
84
-
85
- // console.log({ filteredTags: filteredTags.reverse() });
86
- let rangeFrom = latestTag;
87
-
88
- // If RELEASE_ALL is set via a commit subject or body, all packages will be
89
- // released regardless if they have changed files matching the package srcDir.
90
- let RELEASE_ALL = false;
91
-
92
- // Validate manual tag
93
- if (tag) {
94
- if (!semver.valid(tag)) {
95
- throw new Error(`tag '${tag}' is not a semantically valid version`);
96
- }
97
- if (!tag.startsWith("v")) {
98
- throw new Error(
99
- `tag must start with "v" (e.g. v0.0.0). You supplied ${tag}`,
100
- );
101
- }
102
- if (allTags.includes(tag)) {
103
- throw new Error(`tag ${tag} has already been released`);
104
- }
105
- }
106
-
107
- if (!latestTag || tag) {
108
- if (tag) {
109
- console.info(
110
- `Tag is set to ${tag}. This will force release all packages. Publishing...`,
111
- );
112
- RELEASE_ALL = true;
113
-
114
- // Is it the first release? Is it a major version?
115
- if (!latestTag || (semver.patch(tag) === 0 && semver.minor(tag) === 0)) {
116
- rangeFrom = "origin/main";
117
- latestTag = tag;
118
- }
119
- } else {
120
- throw new Error(
121
- "Could not find latest tag! To make a release tag of v0.0.1, run with TAG=v0.0.1",
122
- );
123
- }
124
- }
125
-
126
- console.info(`Git Range: ${rangeFrom}..HEAD`);
127
-
128
- const rawCommitsLog = (
129
- await simpleGit().log({ from: rangeFrom, to: "HEAD" })
130
- ).all.filter((c) => {
131
- const exclude = [
132
- c.message.startsWith("Merge branch "), // No merge commits
133
- c.message.includes("version & publish"), // No version bump commits
134
- c.message.startsWith(releaseCommitMsg("")), // No example update commits
135
- ].some(Boolean);
136
-
137
- return !exclude;
138
- });
139
-
140
- /**
141
- * Get the commits since the latest tag
142
- * @type {import('./index.js').Commit[]}
143
- */
144
- const commitsSinceLatestTag = await Promise.all(
145
- rawCommitsLog.map(async (c) => {
146
- const parsed = await parseCommit(c.message);
147
- return {
148
- hash: c.hash.substring(0, 7),
149
- body: c.body,
150
- subject: parsed.subject ?? "",
151
- author_name: c.author_name,
152
- author_email: c.author_email,
153
- type: parsed.type?.toLowerCase() ?? "other",
154
- scope: parsed.scope,
155
- };
156
- }),
157
- );
158
-
159
- console.info(
160
- `Parsing ${commitsSinceLatestTag.length} commits since ${rangeFrom}...`,
161
- );
162
-
163
- /**
164
- * Parses the commit messages, logs them, and determines the type of release needed
165
- * -1 means no release is necessary
166
- * 0 means patch release is necessary
167
- * 1 means minor release is necessary
168
- * 2 means major release is necessary
169
- * @type {number}
170
- */
171
- let recommendedReleaseLevel = commitsSinceLatestTag.reduce(
172
- (releaseLevel, commit) => {
173
- if (commit.type) {
174
- if (["fix", "refactor", "perf"].includes(commit.type)) {
175
- releaseLevel = Math.max(releaseLevel, 0);
176
- }
177
- if (["feat"].includes(commit.type)) {
178
- releaseLevel = Math.max(releaseLevel, 1);
179
- }
180
- if (commit.body.includes("BREAKING CHANGE")) {
181
- releaseLevel = Math.max(releaseLevel, 2);
182
- }
183
- if (
184
- commit.subject.includes("RELEASE_ALL") ||
185
- commit.body.includes("RELEASE_ALL")
186
- ) {
187
- RELEASE_ALL = true;
188
- }
189
- }
190
- return releaseLevel;
191
- },
192
- -1,
193
- );
194
-
195
- // If no release is semantically necessary and no manual tag is set, do not release
196
- if (recommendedReleaseLevel === -1 && !tag) {
197
- console.info(
198
- `There have been no changes since ${latestTag} that require a new version. You're good!`,
199
- );
200
- return;
201
- }
202
-
203
- // If no release is semantically necessary but a manual tag is set, do a patch release
204
- if (recommendedReleaseLevel === -1 && tag) {
205
- recommendedReleaseLevel = 0;
206
- }
207
-
208
- const releaseType = branchConfig.prerelease
209
- ? "prerelease"
210
- : /** @type {const} */ ({ 0: "patch", 1: "minor", 2: "major" })[
211
- recommendedReleaseLevel
212
- ];
213
-
214
- if (!releaseType) {
215
- throw new Error(`Invalid release level: ${recommendedReleaseLevel}`);
216
- }
217
-
218
- const version = tag
219
- ? semver.parse(tag)?.version
220
- : semver.inc(latestTag, releaseType, npmTag);
221
-
222
- if (!version) {
223
- throw new Error(
224
- [
225
- "Invalid version increment from semver.inc()",
226
- `- latestTag: ${latestTag}`,
227
- `- recommendedReleaseLevel: ${recommendedReleaseLevel}`,
228
- `- prerelease: ${branchConfig.prerelease}`,
229
- ].join("\n"),
230
- );
231
- }
232
-
233
- console.log(`Targeting version ${version}...`);
234
-
235
- /**
236
- * Uses git diff to determine which files have changed since the latest tag
237
- * @type {string[]}
238
- */
239
- const changedFiles = tag
240
- ? []
241
- : execSync(`git diff ${latestTag} --name-only`)
242
- .toString()
243
- .split("\n")
244
- .filter(Boolean);
245
-
246
- /** Uses packages and changedFiles to determine which packages have changed */
247
- const changedPackages = RELEASE_ALL
248
- ? packages
249
- : packages.filter((pkg) => {
250
- const changed = changedFiles.some(
251
- (file) =>
252
- file.startsWith(path.join(pkg.packageDir, "src")) ||
253
- // check if any files in the assets directory were modified
254
- file.startsWith(path.join(pkg.packageDir, "assets")) ||
255
- file.startsWith(path.join(pkg.packageDir, "package.json")),
256
- );
257
- return changed;
258
- });
259
-
260
- // If a package has a dependency that has been updated, we need to update the
261
- // package that depends on it as well.
262
- // run this multiple times so that dependencies of dependencies are also included
263
- for (let runs = 0; runs < 3; runs++) {
264
- for (const pkg of packages) {
265
- const packageJson = await readPackageJson(
266
- path.resolve(rootDir, pkg.packageDir, "package.json"),
267
- );
268
- const allDependencies = Object.keys(
269
- Object.assign(
270
- {},
271
- packageJson.dependencies ?? {},
272
- packageJson.peerDependencies ?? {},
273
- ),
274
- );
275
-
276
- if (
277
- allDependencies.find((dep) =>
278
- changedPackages.find((d) => d.name === dep),
279
- ) &&
280
- !changedPackages.find((d) => d.name === pkg.name)
281
- ) {
282
- console.info(` Adding dependency ${pkg.name} to changed packages`);
283
- changedPackages.push(pkg);
284
- }
285
- }
286
- }
287
-
288
- const changelogCommitsMd = await Promise.all(
289
- Object.entries(
290
- commitsSinceLatestTag.reduce((prev, curr) => {
291
- return {
292
- ...prev,
293
- [curr.type]: [...(prev[curr.type] ?? []), curr],
294
- };
295
- }, /** @type {Record<string, import('./index').Commit[]>} */ ({})),
296
- )
297
- .sort(
298
- getSorterFn(([type]) =>
299
- [
300
- "other",
301
- "examples",
302
- "docs",
303
- "ci",
304
- "test",
305
- "chore",
306
- "refactor",
307
- "perf",
308
- "fix",
309
- "feat",
310
- ].indexOf(type),
311
- ),
312
- )
313
- .reverse()
314
- .map(async ([type, commits]) => {
315
- return Promise.all(
316
- commits.map(async (commit) => {
317
- let username = "";
318
-
319
- if (ghToken) {
320
- const query = commit.author_email;
321
-
322
- const res = await fetch(
323
- `https://api.github.com/search/users?q=${query}`,
324
- {
325
- headers: {
326
- Authorization: `token ${ghToken}`,
327
- },
328
- },
329
- );
330
- const data = /** @type {unknown} */ (await res.json());
331
- if (data && typeof data === "object" && "items" in data) {
332
- if (Array.isArray(data.items) && data.items[0]) {
333
- const item = /** @type {object} */ (data.items[0]);
334
- if ("login" in item && typeof item.login === "string") {
335
- username = item.login;
336
- }
337
- }
338
- }
339
- }
340
-
341
- const scope = commit.scope ? `${commit.scope}: ` : "";
342
- const subject = commit.subject;
343
-
344
- return `- ${scope}${subject} (${commit.hash}) ${
345
- username
346
- ? `by @${username}`
347
- : `by ${commit.author_name || commit.author_email}`
348
- }`;
349
- }),
350
- ).then((c) => /** @type {const} */ ([type, c]));
351
- }),
352
- ).then((groups) => {
353
- return groups
354
- .map(([type, commits]) => {
355
- return [`### ${capitalize(type)}`, commits.join("\n")].join("\n\n");
356
- })
357
- .join("\n\n");
358
- });
359
-
360
- const date = new Intl.DateTimeFormat(undefined, {
361
- dateStyle: "short",
362
- timeStyle: "short",
363
- }).format(Date.now());
364
-
365
- const changelogMd = [
366
- `Version ${version} - ${date}${tag ? " (Manual Release)" : ""}`,
367
- "## Changes",
368
- changelogCommitsMd || "- None",
369
- "## Packages",
370
- changedPackages.map((d) => `- ${d.name}@${version}`).join("\n"),
371
- ].join("\n\n");
372
-
373
- console.info("Generating changelog...");
374
- console.info();
375
- console.info(changelogMd);
376
- console.info();
377
-
378
- if (changedPackages.length === 0) {
379
- console.info("No packages have been affected.");
380
- return;
381
- }
382
-
383
- if (!process.env.CI) {
384
- console.warn(
385
- `This is a dry run for version ${version}. Push to CI to publish for real or set CI=true to override!`,
386
- );
387
- return;
388
- }
389
-
390
- console.info(`Updating all changed packages to version ${version}...`);
391
- // Update each package to the new version
392
- for (const pkg of changedPackages) {
393
- console.info(` Updating ${pkg.name} version to ${version}...`);
394
-
395
- await updatePackageJson(
396
- path.resolve(rootDir, pkg.packageDir, "package.json"),
397
- (config) => {
398
- config.version = version;
399
- },
400
- );
401
- }
402
-
403
- console.info();
404
- console.info(`Publishing all packages to npm with tag "${npmTag}"`);
405
-
406
- // Publish each package
407
- for (const pkg of changedPackages) {
408
- const packageDir = path.join(rootDir, pkg.packageDir);
409
-
410
- const cmd = `cd ${packageDir} && pnpm publish --tag ${npmTag} --access=public --no-git-checks`;
411
- console.info(` Publishing ${pkg.name}@${version} to npm...`);
412
- execSync(cmd, {
413
- // @ts-ignore
414
- stdio: [process.stdin, process.stdout, process.stderr],
415
- });
416
- }
417
-
418
- console.info();
419
- console.info("Resetting changes...");
420
- /* execSync(
421
- `git add -A && git reset -- ${changedPackages
422
- .map((pkg) => path.resolve(rootDir, pkg.packageDir, "package.json"))
423
- .join(" ")}`,
424
- ); */
425
- execSync(
426
- `git reset -- ${changedPackages
427
- .map((pkg) => path.resolve(rootDir, pkg.packageDir, "package.json"))
428
- .join(" ")}`,
429
- );
430
- execSync(
431
- `git checkout -- ${changedPackages
432
- .map((pkg) => path.resolve(rootDir, pkg.packageDir, "package.json"))
433
- .join(" ")}`,
434
- );
435
- // execSync(`git commit -m "${releaseCommitMsg(version)}" --allow-empty -n`);
436
- console.info(" Reset changes.");
437
-
438
- /* console.info();
439
- console.info("Pushing changes...");
440
- execSync(`git push origin ${currentGitBranch()}`);
441
- console.info(" Changes pushed."); */
442
-
443
- /**
444
- * We tag the latest commit as we want to keep the history clean without any release commits
445
- */
446
- console.info();
447
- console.info(`Creating new git tag v${version}`);
448
- execSync(`git tag -a -m "v${version}" v${version}`);
449
-
450
- console.info();
451
- console.info("Pushing tags...");
452
- execSync("git push --tags");
453
- console.info(" Tags pushed.");
454
-
455
- if (ghToken && isMainBranch) {
456
- console.info();
457
- console.info("Creating github release...");
458
-
459
- // Stringify the markdown to escape any quotes
460
- execSync(
461
- `gh release create v${version} ${
462
- branchConfig.prerelease ? "--prerelease" : ""
463
- } --notes '${changelogMd.replace(/'/g, '"')}'`,
464
- { env: { ...process.env, GH_TOKEN: ghToken } },
465
- );
466
- console.info(" Github release created.");
467
- }
468
-
469
- console.info();
470
- console.info("All done!");
471
- };
1
+ // @ts-check
2
+ // Originally ported to TS from https://github.com/remix-run/react-router/tree/main/scripts/{version,publish}.js
3
+
4
+ import { parse as parseCommit } from "@commitlint/parse";
5
+ import currentGitBranch from "current-git-branch";
6
+ import { execSync } from "node:child_process";
7
+ import path from "node:path";
8
+ import * as semver from "semver";
9
+ import { simpleGit } from "simple-git";
10
+ import {
11
+ capitalize,
12
+ getSorterFn,
13
+ readPackageJson,
14
+ releaseCommitMsg,
15
+ updatePackageJson,
16
+ } from "./utilities.js";
17
+
18
+ /**
19
+ * Execute a script being published
20
+ * @param {import('./index').Options} options
21
+ * @returns {Promise<void>}
22
+ */
23
+ export const publish = async (options) => {
24
+ const { branchConfigs, packages, rootDir, branch, tag, ghToken } = options;
25
+
26
+ const branchName = /** @type {string} */ (branch ?? currentGitBranch());
27
+ const isMainBranch = branchName === "main";
28
+ const npmTag = isMainBranch ? "latest" : branchName;
29
+
30
+ /** @type {import('./index').BranchConfig | undefined} */
31
+ const branchConfig = branchConfigs[branchName];
32
+
33
+ if (!branchConfig) {
34
+ throw new Error(`No publish config found for branch: ${branchName}`);
35
+ }
36
+
37
+ // Get tags
38
+ /** @type {string[]} */
39
+ const allTags = execSync("git tag").toString().split("\n");
40
+
41
+ const filteredTags = allTags
42
+ // Ensure tag is valid
43
+ .filter((t) => semver.valid(t))
44
+ // Filter tags based on whether the branch is a release or pre-release
45
+ .filter((t) => {
46
+ const isPrereleaseTag = semver.prerelease(t) !== null;
47
+ const prereleaseBranch = semver.prerelease(t)?.[0];
48
+ // For prerelease branches, only include tags for that branch
49
+ if (branchConfig.prerelease) {
50
+ return isPrereleaseTag && prereleaseBranch === branchName;
51
+ }
52
+ // For main branch, exclude all prereleases
53
+ return !isPrereleaseTag;
54
+ })
55
+ // sort by latest
56
+ // @ts-ignore
57
+ .sort(semver.compare);
58
+
59
+ // Get the latest tag
60
+ let latestTag = filteredTags.at(-1);
61
+ let rangeFrom = latestTag;
62
+
63
+ // If RELEASE_ALL is set via a commit subject or body, all packages will be
64
+ // released regardless if they have changed files matching the package srcDir.
65
+ let RELEASE_ALL = false;
66
+
67
+ // Validate manual tag
68
+ if (tag) {
69
+ if (!semver.valid(tag)) {
70
+ throw new Error(`tag '${tag}' is not a semantically valid version`);
71
+ }
72
+ if (!tag.startsWith("v")) {
73
+ throw new Error(
74
+ `tag must start with "v" (e.g. v0.0.0). You supplied ${tag}`,
75
+ );
76
+ }
77
+ if (allTags.includes(tag)) {
78
+ throw new Error(`tag ${tag} has already been released`);
79
+ }
80
+ }
81
+
82
+ if (!latestTag || tag) {
83
+ if (tag) {
84
+ console.info(
85
+ `Tag is set to ${tag}. This will force release all packages. Publishing...`,
86
+ );
87
+ RELEASE_ALL = true;
88
+
89
+ // Is it the first release? Is it a major version?
90
+ if (!latestTag || (semver.patch(tag) === 0 && semver.minor(tag) === 0)) {
91
+ rangeFrom = "origin/main";
92
+ latestTag = tag;
93
+ }
94
+ } else {
95
+ throw new Error(
96
+ "Could not find latest tag! To make a release tag of v0.0.1, run with TAG=v0.0.1",
97
+ );
98
+ }
99
+ }
100
+
101
+ console.info(`Git Range: ${rangeFrom}..HEAD`);
102
+
103
+ const rawCommitsLog = (
104
+ await simpleGit().log({ from: rangeFrom, to: "HEAD" })
105
+ ).all.filter((c) => {
106
+ const exclude = [
107
+ c.message.startsWith("Merge branch "), // No merge commits
108
+ c.message.includes("version & publish"), // No version bump commits
109
+ c.message.startsWith(releaseCommitMsg("")), // No example update commits
110
+ ].some(Boolean);
111
+
112
+ return !exclude;
113
+ });
114
+
115
+ // /**
116
+ // * Get the commits since the latest tag
117
+ // * @type {import('./index.js').Commit[]}
118
+ // */
119
+ const commitsSinceLatestTag = await Promise.all(
120
+ rawCommitsLog.map(async (c) => {
121
+ const parsed = await parseCommit(c.message);
122
+ return {
123
+ hash: c.hash.substring(0, 7),
124
+ body: c.body,
125
+ subject: parsed.subject ?? "",
126
+ author_name: c.author_name,
127
+ author_email: c.author_email,
128
+ type: parsed.type?.toLowerCase() ?? "other",
129
+ scope: parsed.scope,
130
+ };
131
+ }),
132
+ );
133
+
134
+ console.info(
135
+ `Parsing ${commitsSinceLatestTag.length} commits since ${rangeFrom}...`,
136
+ );
137
+
138
+ /**
139
+ * Parses the commit messages, logs them, and determines the type of release needed
140
+ * -1 means no release is necessary
141
+ * 0 means patch release is necessary
142
+ * 1 means minor release is necessary
143
+ * 2 means major release is necessary
144
+ * @type {number}
145
+ */
146
+ let recommendedReleaseLevel = commitsSinceLatestTag.reduce(
147
+ (releaseLevel, commit) => {
148
+ if (commit.type) {
149
+ if (["fix", "refactor", "perf"].includes(commit.type)) {
150
+ releaseLevel = Math.max(releaseLevel, 0);
151
+ }
152
+ if (["feat"].includes(commit.type)) {
153
+ releaseLevel = Math.max(releaseLevel, 1);
154
+ }
155
+ if (commit.body.includes("BREAKING CHANGE")) {
156
+ releaseLevel = Math.max(releaseLevel, 2);
157
+ }
158
+ if (
159
+ commit.subject.includes("RELEASE_ALL") ||
160
+ commit.body.includes("RELEASE_ALL")
161
+ ) {
162
+ RELEASE_ALL = true;
163
+ }
164
+ }
165
+ return releaseLevel;
166
+ },
167
+ -1,
168
+ );
169
+
170
+ // If there is a breaking change and no manual tag is set, do not release
171
+ /* if (recommendedReleaseLevel === 2 && !tag) {
172
+ throw new Error(
173
+ 'Major versions releases must be tagged and released manually.'
174
+ );
175
+ } */
176
+
177
+ // If no release is semantically necessary and no manual tag is set, do not release
178
+ if (recommendedReleaseLevel === -1 && !tag) {
179
+ console.info(
180
+ `There have been no changes since ${latestTag} that require a new version. You're good!`,
181
+ );
182
+ return;
183
+ }
184
+
185
+ // If no release is semantically necessary but a manual tag is set, do a patch release
186
+ if (recommendedReleaseLevel === -1 && tag) {
187
+ recommendedReleaseLevel = 0;
188
+ }
189
+
190
+ const releaseType = branchConfig.prerelease
191
+ ? "prerelease"
192
+ : /** @type {const} */ ({ 0: "patch", 1: "minor", 2: "major" })[
193
+ recommendedReleaseLevel
194
+ ];
195
+
196
+ if (!releaseType) {
197
+ throw new Error(`Invalid release level: ${recommendedReleaseLevel}`);
198
+ }
199
+
200
+ const version = tag
201
+ ? semver.parse(tag)?.version
202
+ : semver.inc(latestTag, releaseType, npmTag);
203
+
204
+ if (!version) {
205
+ throw new Error(
206
+ [
207
+ "Invalid version increment from semver.inc()",
208
+ `- latestTag: ${latestTag}`,
209
+ `- recommendedReleaseLevel: ${recommendedReleaseLevel}`,
210
+ `- prerelease: ${branchConfig.prerelease}`,
211
+ ].join("\n"),
212
+ );
213
+ }
214
+
215
+ console.log(`Targeting version ${version}...`);
216
+
217
+ /**
218
+ * Uses git diff to determine which files have changed since the latest tag
219
+ * @type {string[]}
220
+ */
221
+ const changedFiles = tag
222
+ ? []
223
+ : execSync(`git diff ${latestTag} --name-only`)
224
+ .toString()
225
+ .split("\n")
226
+ .filter(Boolean);
227
+
228
+ /** Uses packages and changedFiles to determine which packages have changed */
229
+ const changedPackages = RELEASE_ALL
230
+ ? packages
231
+ : packages.filter((pkg) => {
232
+ const changed = changedFiles.some(
233
+ (file) =>
234
+ file.startsWith(path.join(pkg.packageDir, "src")) ||
235
+ // check if any files in the assets directory were modified
236
+ file.startsWith(path.join(pkg.packageDir, "assets")) ||
237
+ file.startsWith(path.join(pkg.packageDir, "package.json")),
238
+ );
239
+ return changed;
240
+ });
241
+
242
+ // If a package has a dependency that has been updated, we need to update the
243
+ // package that depends on it as well.
244
+ // run this multiple times so that dependencies of dependencies are also included
245
+ for (let runs = 0; runs < 3; runs++) {
246
+ for (const pkg of packages) {
247
+ const packageJson = await readPackageJson(
248
+ path.resolve(rootDir, pkg.packageDir, "package.json"),
249
+ );
250
+ const allDependencies = Object.keys(
251
+ Object.assign(
252
+ {},
253
+ packageJson.dependencies ?? {},
254
+ packageJson.peerDependencies ?? {},
255
+ ),
256
+ );
257
+
258
+ if (
259
+ allDependencies.find((dep) =>
260
+ changedPackages.find((d) => d.name === dep),
261
+ ) &&
262
+ !changedPackages.find((d) => d.name === pkg.name)
263
+ ) {
264
+ console.info(` Adding dependency ${pkg.name} to changed packages`);
265
+ changedPackages.push(pkg);
266
+ }
267
+ }
268
+ }
269
+
270
+ const changelogCommitsMd = await Promise.all(
271
+ Object.entries(
272
+ commitsSinceLatestTag.reduce((prev, curr) => {
273
+ return {
274
+ ...prev,
275
+ [curr.type]: [...(prev[curr.type] ?? []), curr],
276
+ };
277
+ }, /** @type {Record<string, import('./index').Commit[]>} */ ({})),
278
+ )
279
+ .sort(
280
+ getSorterFn(([type]) =>
281
+ [
282
+ "other",
283
+ "examples",
284
+ "docs",
285
+ "ci",
286
+ "test",
287
+ "chore",
288
+ "refactor",
289
+ "perf",
290
+ "fix",
291
+ "feat",
292
+ ].indexOf(type),
293
+ ),
294
+ )
295
+ .reverse()
296
+ .map(async ([type, commits]) => {
297
+ return Promise.all(
298
+ commits.map(async (commit) => {
299
+ let username = "";
300
+
301
+ if (ghToken) {
302
+ const query = commit.author_email;
303
+
304
+ const res = await fetch(
305
+ `https://api.github.com/search/users?q=${query}`,
306
+ {
307
+ headers: {
308
+ Authorization: `token ${ghToken}`,
309
+ },
310
+ },
311
+ );
312
+ const data = /** @type {unknown} */ (await res.json());
313
+ if (data && typeof data === "object" && "items" in data) {
314
+ if (Array.isArray(data.items) && data.items[0]) {
315
+ const item = /** @type {object} */ (data.items[0]);
316
+ if ("login" in item && typeof item.login === "string") {
317
+ username = item.login;
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ const scope = commit.scope ? `${commit.scope}: ` : "";
324
+ const subject = commit.subject;
325
+
326
+ return `- ${scope}${subject} (${commit.hash}) ${
327
+ username
328
+ ? `by @${username}`
329
+ : `by ${commit.author_name || commit.author_email}`
330
+ }`;
331
+ }),
332
+ ).then((c) => /** @type {const} */ ([type, c]));
333
+ }),
334
+ ).then((groups) => {
335
+ return groups
336
+ .map(([type, commits]) => {
337
+ return [`### ${capitalize(type)}`, commits.join("\n")].join("\n\n");
338
+ })
339
+ .join("\n\n");
340
+ });
341
+
342
+ const date = new Intl.DateTimeFormat(undefined, {
343
+ dateStyle: "short",
344
+ timeStyle: "short",
345
+ }).format(Date.now());
346
+
347
+ const changelogMd = [
348
+ `Version ${version} - ${date}${tag ? " (Manual Release)" : ""}`,
349
+ "## Changes",
350
+ changelogCommitsMd || "- None",
351
+ "## Packages",
352
+ changedPackages.map((d) => `- ${d.name}@${version}`).join("\n"),
353
+ ].join("\n\n");
354
+
355
+ console.info("Generating changelog...");
356
+ console.info();
357
+ console.info(changelogMd);
358
+ console.info();
359
+
360
+ if (changedPackages.length === 0) {
361
+ console.info("No packages have been affected.");
362
+ return;
363
+ }
364
+
365
+ if (!process.env.CI) {
366
+ console.warn(
367
+ `This is a dry run for version ${version}. Push to CI to publish for real or set CI=true to override!`,
368
+ );
369
+ return;
370
+ }
371
+
372
+ console.info(`Updating all changed packages to version ${version}...`);
373
+ // Update each package to the new version
374
+ for (const pkg of changedPackages) {
375
+ console.info(` Updating ${pkg.name} version to ${version}...`);
376
+
377
+ await updatePackageJson(
378
+ path.resolve(rootDir, pkg.packageDir, "package.json"),
379
+ (config) => {
380
+ config.version = version;
381
+ },
382
+ );
383
+ }
384
+
385
+ console.info();
386
+ console.info(`Publishing all packages to npm with tag "${npmTag}"`);
387
+
388
+ // Publish each package
389
+ for (const pkg of changedPackages) {
390
+ const packageDir = path.join(rootDir, pkg.packageDir);
391
+
392
+ const cmd = `cd ${packageDir} && pnpm publish --tag ${npmTag} --access=public --no-git-checks`;
393
+ console.info(` Publishing ${pkg.name}@${version} to npm...`);
394
+ execSync(cmd, {
395
+ // @ts-ignore
396
+ stdio: [process.stdin, process.stdout, process.stderr],
397
+ });
398
+ }
399
+
400
+ console.info();
401
+ console.info("Committing changes...");
402
+ execSync(
403
+ `git add -A && git reset -- ${changedPackages
404
+ .map((pkg) => path.resolve(rootDir, pkg.packageDir, "package.json"))
405
+ .join(" ")}`,
406
+ );
407
+ execSync(
408
+ `git checkout -- ${changedPackages
409
+ .map((pkg) => path.resolve(rootDir, pkg.packageDir, "package.json"))
410
+ .join(" ")}`,
411
+ );
412
+ execSync(`git commit -m "${releaseCommitMsg(version)}" --allow-empty -n`);
413
+ console.info(" Committed Changes.");
414
+
415
+ console.info();
416
+ console.info("Pushing changes...");
417
+ execSync(`git push origin ${currentGitBranch()}`);
418
+ console.info(" Changes pushed.");
419
+
420
+ console.info();
421
+ console.info(`Creating new git tag v${version}`);
422
+ execSync(`git tag -a -m "v${version}" v${version}`);
423
+
424
+ console.info();
425
+ console.info("Pushing tags...");
426
+ execSync("git push --tags");
427
+ console.info(" Tags pushed.");
428
+
429
+ if (ghToken && isMainBranch) {
430
+ console.info();
431
+ console.info("Creating github release...");
432
+
433
+ // Stringify the markdown to escape any quotes
434
+ execSync(
435
+ `gh release create v${version} ${
436
+ branchConfig.prerelease ? "--prerelease" : ""
437
+ } --notes '${changelogMd.replace(/'/g, '"')}'`,
438
+ { env: { ...process.env, GH_TOKEN: ghToken } },
439
+ );
440
+ console.info(" Github release created.");
441
+ }
442
+
443
+ console.info();
444
+ console.info("All done!");
445
+ };