Skip to content

Commit 2ea22c8

Browse files
committed
Add tool for reviewing per-package changes
In order to review a release candidate PR, especially a large one, it is best to cross-check the new changelog entries in the PR with the set of changes that were made since the previous release. In the context of a monorepo such as this one, we must consider packages when determing the set of changes, as it's the packages we are publishing, not the entire repo. This is complicated by the fact that it is technically possible to issue partial releases — that is, when the scope of a new release is decided, it is possible to include only a subset of the total set of changed packages. More concretely, if package A, B, and C are changed, but only B is included in a release, then a future release will include A and/or C. When _that_ release occurs, care must be taken to exclude the changes in B which have already been released from the changelog. In this kind of scenario, the best way to verify that the changelog is being updated appropriately is to look at each package individually and view the changes that have taken place since its previous release (not the previous release of the whole monorepo). This tool aids with such a comparison. Given the name of a workspace such as `@metamask/address-book-controller`, it will compare the tag for its latest version and HEAD and allow you to either view a set of commits that have occurred in this timeframe along with their diffs, or view the consolidated diff agnostic of commits. Note that `git` has been customized to _not_ run its output through a pager, which means you'll have to do this yourself: scripts/changes-since-last-release.ts --format per-commit @metamask/approval-controller | less -r scripts/changes-since-last-release.ts --format together @metamask/transaction-controller | less -r
1 parent 992e968 commit 2ea22c8

File tree

3 files changed

+167
-0
lines changed

3 files changed

+167
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"prettier": "^2.6.2",
5959
"prettier-plugin-packagejson": "^2.2.17",
6060
"rimraf": "^3.0.2",
61+
"semver": "^7.5.1",
6162
"simple-git-hooks": "^2.8.0",
6263
"ts-node": "^10.9.1",
6364
"typescript": "~4.6.3",
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!yarn ts-node
2+
3+
import execa from 'execa';
4+
import semver from 'semver';
5+
import yargs from 'yargs/yargs';
6+
import { hideBin } from 'yargs/helpers';
7+
8+
type Workspace = {
9+
location: string;
10+
name: string;
11+
};
12+
13+
enum Format {
14+
PerCommit = 'per-commit',
15+
Together = 'together',
16+
}
17+
18+
class KnownError extends Error {
19+
// do nothing
20+
}
21+
22+
main().catch((error) => {
23+
if (error instanceof KnownError) {
24+
process.stderr.write(`${error.message}\n`);
25+
} else {
26+
console.error(error);
27+
}
28+
process.exitCode = 1;
29+
});
30+
31+
/**
32+
* Show changes for a workspace package since its last release.
33+
*
34+
* Here's a couple of examples of how you can call this script:
35+
*
36+
* ```
37+
* scripts/changes-since-last-release.ts --format per-commit @metamask/transaction-controller | less -r
38+
* scripts/changes-since-last-release.ts --format together @metamask/transaction-controller | less -r
39+
* ```
40+
*/
41+
async function main() {
42+
const { packageName, format } = await yargs(hideBin(process.argv))
43+
.scriptName('scripts/changes-since-last-release.ts')
44+
.command(
45+
'$0 <package-name>',
46+
'Show changes for a workspace package since its previous release.',
47+
(y) => {
48+
y.positional('package-name', {
49+
describe: 'The name of the package to show changes for.',
50+
});
51+
},
52+
)
53+
.option('format', {
54+
alias: 'f',
55+
describe: 'How to show the changes: divided by commit or all together',
56+
choices: Object.values(Format),
57+
default: Format.PerCommit,
58+
})
59+
.string('_')
60+
.help()
61+
.strict()
62+
.parse();
63+
64+
const workspaces = await getWorkspaces();
65+
const workspace = workspaces.find((w) => w.name === packageName);
66+
if (!workspace) {
67+
throw new KnownError(`Could not map ${packageName} to a workspace`);
68+
}
69+
70+
const tags = await getTags();
71+
const tagsForWorkspace = tags.filter((line) =>
72+
line.startsWith(`${workspace.name}@`),
73+
);
74+
if (tagsForWorkspace.length === 0) {
75+
throw new KnownError(`No version tags found for ${workspace.name}.`);
76+
}
77+
const versions = tagsForWorkspace.map((tag) => {
78+
const versionString = tag.split('@', 3)[2];
79+
const parsedVersionString = semver.parse(versionString);
80+
if (!parsedVersionString) {
81+
throw new Error(`Invalid version tag ${tag}`);
82+
}
83+
return parsedVersionString;
84+
});
85+
const sortedVersions = versions.sort((a, b) => a.compare(b));
86+
const latestVersion = sortedVersions[sortedVersions.length - 1];
87+
88+
console.log(
89+
`Showing changes for \u001B[34m${
90+
workspace.name
91+
}\u001B[0m since \u001B[34m${latestVersion.toString()}\u001B[0m.\n`,
92+
);
93+
await showDiff(workspace, latestVersion, { format });
94+
}
95+
96+
/**
97+
* Retrieve the set of Yarn workspaces in this project.
98+
*
99+
* @returns The set of workspaces.
100+
*/
101+
async function getWorkspaces(): Promise<Workspace[]> {
102+
const { stdout } = await execa('yarn', ['workspaces', 'list', '--json']);
103+
const workspaces = stdout.split('\n').map((line) => JSON.parse(line));
104+
return workspaces.filter((workspace) => workspace.location !== '.');
105+
}
106+
107+
/**
108+
* Retrieve the set of Git tags in this project.
109+
*
110+
* @returns The set of tags.
111+
*/
112+
async function getTags(): Promise<string[]> {
113+
const { stdout } = await execa('git', ['tag']);
114+
return stdout.split('\n');
115+
}
116+
117+
/**
118+
* Show the set of changes in the latest release of the given package.
119+
*
120+
* @param workspace - The workspace that represents the package.
121+
* @param latestVersion - The version (as a `semver` object).
122+
* @param options - An options bag.
123+
* @param options.format - How to show the changes: divided by commit or all
124+
* together.
125+
*/
126+
async function showDiff(
127+
workspace: Workspace,
128+
latestVersion: semver.SemVer,
129+
{ format = Format.PerCommit } = {},
130+
): Promise<void> {
131+
const gitOptions = ['--no-pager'];
132+
const gitCommandOptions = ['--color'];
133+
const args =
134+
format === Format.PerCommit
135+
? [
136+
...gitOptions,
137+
'log',
138+
...gitCommandOptions,
139+
'-p',
140+
`${workspace.name}@${latestVersion.toString()}..HEAD`,
141+
'--',
142+
workspace.location,
143+
]
144+
: [
145+
...gitOptions,
146+
'diff',
147+
...gitCommandOptions,
148+
`${workspace.name}@${latestVersion.toString()}..HEAD`,
149+
workspace.location,
150+
];
151+
await execa('git', args, {
152+
stdio: 'inherit',
153+
});
154+
}

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,6 +1464,7 @@ __metadata:
14641464
prettier: ^2.6.2
14651465
prettier-plugin-packagejson: ^2.2.17
14661466
rimraf: ^3.0.2
1467+
semver: ^7.5.1
14671468
simple-git-hooks: ^2.8.0
14681469
ts-node: ^10.9.1
14691470
typescript: ~4.6.3
@@ -8632,6 +8633,17 @@ __metadata:
86328633
languageName: node
86338634
linkType: hard
86348635

8636+
"semver@npm:^7.5.1":
8637+
version: 7.5.1
8638+
resolution: "semver@npm:7.5.1"
8639+
dependencies:
8640+
lru-cache: ^6.0.0
8641+
bin:
8642+
semver: bin/semver.js
8643+
checksum: d16dbedad53c65b086f79524b9ef766bf38670b2395bdad5c957f824dcc566b624988013564f4812bcace3f9d405355c3635e2007396a39d1bffc71cfec4a2fc
8644+
languageName: node
8645+
linkType: hard
8646+
86358647
"semver@npm:~5.4.1":
86368648
version: 5.4.1
86378649
resolution: "semver@npm:5.4.1"

0 commit comments

Comments
 (0)