From 0d8cdad9f316db3641623ccd6d3adef207b4eb6b Mon Sep 17 00:00:00 2001 From: alvarius Date: Tue, 8 Aug 2023 18:45:11 +0100 Subject: [PATCH] ci: add script to generate global changelog (#1256) --- package.json | 4 +- pnpm-lock.yaml | 42 +++++++----- scripts/changelog.ts | 159 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 17 deletions(-) create mode 100644 scripts/changelog.ts diff --git a/package.json b/package.json index ce617d7df6..6e41351f1d 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "prettier:check": "prettier --check '**/*.{ts,tsx,css,md,mdx,sol}'", "release:check": "changeset status --verbose --since=origin/main", "release:publish": "pnpm install && pnpm build && changeset publish", - "release:version": "changeset version && pnpm install --lockfile-only", + "release:version": "changeset version && pnpm install --lockfile-only && pnpx bun scripts/changelog.ts", "sort-package-json": "npx sort-package-json package.json 'packages/*/package.json' 'templates/*/package.json' 'templates/*/packages/*/package.json' 'examples/*/package.json' 'examples/*/packages/*/package.json' 'integration/*/package.json' 'integration/*/packages/*/package.json' 'docs/package.json'", "test": "pnpm recursive run test" }, @@ -31,10 +31,12 @@ "devDependencies": { "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.1", + "@types/node": "^18.15.11", "@typescript-eslint/eslint-plugin": "5.46.1", "@typescript-eslint/parser": "5.46.1", "chalk": "^5.2.0", "eslint": "8.29.0", + "execa": "^7.0.0", "husky": ">=6", "lint-staged": ">=10", "prettier": "^2.8.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9cb8b4bb47..27c213641c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@changesets/cli': specifier: ^2.26.1 version: 2.26.1 + '@types/node': + specifier: ^18.15.11 + version: 18.15.11 '@typescript-eslint/eslint-plugin': specifier: 5.46.1 version: 5.46.1(@typescript-eslint/parser@5.46.1)(eslint@8.29.0)(typescript@5.1.6) @@ -26,6 +29,9 @@ importers: eslint: specifier: 8.29.0 version: 8.29.0 + execa: + specifier: ^7.0.0 + version: 7.0.0 husky: specifier: '>=6' version: 6.0.0 @@ -2282,7 +2288,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 espree: 9.5.1 globals: 13.20.0 ignore: 5.2.4 @@ -2651,7 +2657,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -3638,7 +3644,7 @@ packages: '@typescript-eslint/scope-manager': 5.46.1 '@typescript-eslint/type-utils': 5.46.1(eslint@8.29.0)(typescript@5.1.6) '@typescript-eslint/utils': 5.46.1(eslint@8.29.0)(typescript@5.1.6) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 eslint: 8.29.0 ignore: 5.2.4 natural-compare-lite: 1.4.0 @@ -3663,7 +3669,7 @@ packages: '@typescript-eslint/scope-manager': 5.46.1 '@typescript-eslint/types': 5.46.1 '@typescript-eslint/typescript-estree': 5.46.1(typescript@5.1.6) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 eslint: 8.29.0 typescript: 5.1.6 transitivePeerDependencies: @@ -3690,7 +3696,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 5.46.1(typescript@5.1.6) '@typescript-eslint/utils': 5.46.1(eslint@8.29.0)(typescript@5.1.6) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 eslint: 8.29.0 tsutils: 3.21.0(typescript@5.1.6) typescript: 5.1.6 @@ -3714,7 +3720,7 @@ packages: dependencies: '@typescript-eslint/types': 5.46.1 '@typescript-eslint/visitor-keys': 5.46.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.0 @@ -5094,6 +5100,18 @@ packages: time-zone: 1.0.0 dev: true + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -5773,7 +5791,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.0 @@ -6052,7 +6070,6 @@ packages: onetime: 6.0.0 signal-exit: 3.0.7 strip-final-newline: 3.0.0 - dev: false /execcli@5.0.6: resolution: {integrity: sha512-du+uy/Ew2P90PKjSHI89u/XuqVaBDzvaJ6ePn40JaOy7owFQNsYDbd5AoR5A559HEAb1i5HO22rJxtgVonf5Bg==} @@ -6814,7 +6831,6 @@ packages: /human-signals@4.3.1: resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} engines: {node: '>=14.18.0'} - dev: false /husky@6.0.0: resolution: {integrity: sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ==} @@ -7159,7 +7175,6 @@ packages: /is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: false /is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} @@ -8038,7 +8053,7 @@ packages: chalk: 3.0.0 commander: 4.1.1 cosmiconfig: 6.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 dedent: 0.7.0 execa: 3.4.0 listr: 0.14.3 @@ -8439,7 +8454,6 @@ packages: /mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} - dev: false /mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} @@ -8713,7 +8727,6 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: path-key: 4.0.0 - dev: false /number-is-nan@1.0.1: resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} @@ -8839,7 +8852,6 @@ packages: engines: {node: '>=12'} dependencies: mimic-fn: 4.0.0 - dev: false /openurl@1.1.1: resolution: {integrity: sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==} @@ -9040,7 +9052,6 @@ packages: /path-key@4.0.0: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} - dev: false /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -10272,7 +10283,6 @@ packages: /strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} - dev: false /strip-hex-prefix@1.0.0: resolution: {integrity: sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==} diff --git a/scripts/changelog.ts b/scripts/changelog.ts new file mode 100644 index 0000000000..f3b4b4f7a6 --- /dev/null +++ b/scripts/changelog.ts @@ -0,0 +1,159 @@ +/** + * This is a workaround until changesets natively supports publishing a + * central changelog (https://github.com/changesets/changesets/issues/1059). + */ + +import { execa } from "execa"; +import { readFileSync, writeFileSync } from "node:fs"; + +//--------- CONSTANTS + +const REPO_URL = process.env.REPO_URL ?? "https://github.com/latticexyz/mud"; +const CHANGELOG_PATH = process.env.CHANGELOG_PATH ?? "CHANGELOG.md"; + +enum ChangeType { + PATCH, + MINOR, + MAJOR, +} + +const changeTypes = { + patch: ChangeType.PATCH, + minor: ChangeType.MINOR, + major: ChangeType.MAJOR, +} as const; + +type ChangelogItem = { + packages: { + package: string; + type: string; + }[]; + type: ChangeType; + description: string; +}; + +type GitMetadata = { + commitHash: string; + authorName: string; + authorEmail: string; + title: string; +}; + +await appendChangelog(); + +//----------- UTILS + +async function appendChangelog() { + // Reset current changelog to version on main + await execa("git", ["checkout", "main", "--", CHANGELOG_PATH]); + + // Load the current changelog + const currentChangelog = readFileSync(CHANGELOG_PATH).toString(); + + // Append the new changelog at the up + const newChangelog = await renderChangelog(); + writeFileSync(CHANGELOG_PATH, `${newChangelog}\n${currentChangelog}`); +} + +async function renderChangelog() { + const changes = await getChanges(); + const version = await getVersion(); + + return ` +# Version ${version} + +## Major changes + +${await renderChangelogItems(changes.major)} +## Minor changes + +${await renderChangelogItems(changes.minor)} +## Patch changes + +${await renderChangelogItems(changes.patch)} +`; +} + +async function renderChangelogItems(changelogItems: (ChangelogItem & GitMetadata)[]) { + let output = ""; + + for (const changelogItem of changelogItems) { + output += `1. **[${changelogItem.title}](${REPO_URL}/commit/${changelogItem.commitHash})** (${changelogItem.packages + .map((e) => e.package) + .join(", ")}) + +${changelogItem.description} +`; + } + + return output; +} + +async function getVersion() { + return "2.0.0-next.1"; +} + +async function getChanges() { + // Get the diff of the current branch to main + const changesetDiff = (await execa("git", ["diff", "main", ".changeset/pre.json"])).stdout; + + // Get the list of changesets + const addedLinesRegex = /\+\s+"([^"]+)"/g; + const addedChangesets = [...changesetDiff.matchAll(addedLinesRegex)].map((match) => match[1]); + + // Load the contents of each changeset and metadata from git + const changesets = await Promise.all( + addedChangesets.map(async (addedChangeset) => { + const changesetPath = `.changeset/${addedChangeset}.md`; + const changeset = readFileSync(changesetPath).toString(); + const gitLog = (await execa("git", ["log", changesetPath])).stdout; + return { ...parseChangeset(changeset), ...parseGitLog(gitLog) }; + }) + ); + + // Sort the changesets into patch, minor and major updates + const patch = changesets.filter((change) => change.type === ChangeType.PATCH); + const minor = changesets.filter((change) => change.type === ChangeType.MINOR); + const major = changesets.filter((change) => change.type === ChangeType.MAJOR); + + return { patch, minor, major }; +} + +function notNull(element: T | undefined | null | ""): element is T { + return Boolean(element); +} + +/** + * Parse a changeset string into a more usable format (list of updated packages, change type, description) + */ +function parseChangeset(changeset: string): ChangelogItem { + // Remove first separator + const separatorString = "---\n"; + let separatorIndex = changeset.indexOf(separatorString); + changeset = changeset.substring(separatorIndex + separatorString.length); + + // Parse list of changed packages and description + separatorIndex = changeset.indexOf(separatorString); + const packages = changeset + .substring(0, separatorIndex) + .split("\n") + .map((line) => { + const match = line.match(/"([^"]+)":\s*(\w+)/); + if (match) return { package: match[1], type: match[2] as "patch" | "minor" | "major" }; + }) + .filter(notNull); + const description = changeset.substring(separatorIndex + separatorString.length); + + // Find the strongest update type + const type = Math.max(...packages.map((change) => changeTypes[change.type])); + + return { packages, description, type }; +} + +function parseGitLog(log: string): GitMetadata { + // Thanks ChatGPT + const [, commitHash, authorName, authorEmail, title] = + log.match(/commit (\w+)[\s\S]*?Author: ([^<]+) <([^>]+)>[\s\S]*?\n\n\s{4}([^\n]+)/) ?? []; + + return { commitHash, authorName, authorEmail, title }; +}