Skip to content

Commit 603d122

Browse files
authored
Add flow for monorepo w/ independent versions (#6)
This commit introduces a rough cut of a flow which supports a monorepo with an independent versioning strategy. This flow: * Generates a "release specification" file containing all of the workspace packages within the repo (this will be changed to all _updated_ packages in a future commit) * Opens the user's editor (if one can is detected) and waits for them to edit it * Parses the edited version of the release spec * Applies the release spec by: * Updating the version of the root package with the current date (note: the format of the version string will be changed in a future commit) * Updating the versions of all of the packages listed in the release spec * Adds new sections to the changelogs of all of the packages listed in the release spec In addition: * Bump dev version of Node to v16
1 parent d52af09 commit 603d122

39 files changed

+5821
-40
lines changed

.eslintrc.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,8 @@ module.exports = {
2929
next: 'multiline-block-like',
3030
},
3131
],
32-
// This prevents using bulleted/numbered lists in JSDoc blocks.
33-
// See: <https://github.com/gajus/eslint-plugin-jsdoc/issues/541>
34-
'jsdoc/check-indentation': 'off',
3532
// It's common for scripts to access `process.env`
3633
'node/no-process-env': 'off',
37-
// It's common for scripts to exit explicitly
38-
'node/no-process-exit': 'off',
3934
},
4035

4136
overrides: [

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v14
1+
v16

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ For more on how to use this tool, please see the [docs](./docs).
2424

2525
### Setup
2626

27-
- Install [Node.js](https://nodejs.org) version 12
27+
- Install [Node.js](https://nodejs.org) version 16
2828
- If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended) running `nvm use` will automatically choose the right node version for you.
29+
- Note that the version of Node used for development (in `.nvmrc`) is intentionally higher than version used for consumption (as `engines` in `package.json`), as we have not fully phased out legacy versions of Node from our products yet.
2930
- Install [Yarn v3](https://yarnpkg.com/getting-started/install)
3031
- Run `yarn install` to install dependencies and run any required post-install scripts
3132

jest.config.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ module.exports = {
2828
coverageDirectory: 'coverage',
2929

3030
// An array of regexp pattern strings used to skip coverage collection
31-
// coveragePathIgnorePatterns: [
32-
// "/node_modules/"
33-
// ],
31+
coveragePathIgnorePatterns: [
32+
'/node_modules/',
33+
'/src/cli.ts',
34+
'/src/command-line-arguments.ts',
35+
],
3436

3537
// Indicates which provider should be used to instrument code for coverage
3638
coverageProvider: 'babel',
@@ -135,7 +137,7 @@ module.exports = {
135137
// setupFiles: [],
136138

137139
// A list of paths to modules that run some code to configure or set up the testing framework before each test
138-
// setupFilesAfterEnv: [],
140+
setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'],
139141

140142
// The number of seconds after which a test is considered as slow and reported as such in the results.
141143
// slowTestThreshold: 5,

package.json

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
"type": "git",
77
"url": "https://github.com/MetaMask/create-release-branch.git"
88
},
9-
"main": "dist/index.js",
10-
"types": "dist/index.d.ts",
9+
"bin": "dist/cli.js",
1110
"files": [
1211
"dist/"
1312
],
@@ -18,19 +17,36 @@
1817
"lint:eslint": "eslint . --cache --ext js,ts",
1918
"lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write",
2019
"lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' '**/*.yml' '!.yarnrc.yml' --ignore-path .gitignore --no-error-on-unmatched-pattern",
21-
"prepack": "yarn build",
20+
"prepack": "yarn build:clean && chmod +x dist/cli.js && yarn lint && yarn test",
2221
"test": "jest && jest-it-up",
2322
"test:watch": "jest --watch"
2423
},
24+
"dependencies": {
25+
"@metamask/action-utils": "^0.0.2",
26+
"@metamask/utils": "^2.0.0",
27+
"debug": "^4.3.4",
28+
"execa": "^5.0.0",
29+
"glob": "^8.0.3",
30+
"pony-cause": "^2.1.0",
31+
"semver": "^7.3.7",
32+
"which": "^2.0.2",
33+
"yaml": "^2.1.1",
34+
"yargs": "^17.5.1"
35+
},
2536
"devDependencies": {
2637
"@lavamoat/allow-scripts": "^2.0.3",
2738
"@metamask/auto-changelog": "^2.3.0",
2839
"@metamask/eslint-config": "^9.0.0",
2940
"@metamask/eslint-config-jest": "^9.0.0",
3041
"@metamask/eslint-config-nodejs": "^9.0.0",
3142
"@metamask/eslint-config-typescript": "^9.0.1",
43+
"@types/debug": "^4.1.7",
3244
"@types/jest": "^28.1.4",
45+
"@types/jest-when": "^3.5.2",
3346
"@types/node": "^17.0.23",
47+
"@types/rimraf": "^3.0.2",
48+
"@types/which": "^2.0.1",
49+
"@types/yargs": "^17.0.10",
3450
"@typescript-eslint/eslint-plugin": "^4.21.0",
3551
"@typescript-eslint/parser": "^4.21.0",
3652
"eslint": "^7.23.0",
@@ -42,9 +58,12 @@
4258
"eslint-plugin-prettier": "^3.3.1",
4359
"jest": "^28.0.0",
4460
"jest-it-up": "^2.0.2",
61+
"jest-when": "^3.5.1",
62+
"nanoid": "^3.3.4",
4563
"prettier": "^2.2.1",
4664
"prettier-plugin-packagejson": "^2.2.17",
4765
"rimraf": "^3.0.2",
66+
"stdio-mock": "^1.2.0",
4867
"ts-jest": "^28.0.0",
4968
"ts-node": "^10.7.0",
5069
"typescript": "^4.2.4"

src/cli.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { main } from './main';
2+
3+
/**
4+
* The entrypoint to this tool.
5+
*/
6+
async function cli() {
7+
await main({
8+
argv: process.argv,
9+
cwd: process.cwd(),
10+
stdout: process.stdout,
11+
stderr: process.stderr,
12+
});
13+
}
14+
15+
cli().catch((error) => {
16+
console.error(error);
17+
process.exitCode = 1;
18+
});

src/command-line-arguments.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import yargs from 'yargs/yargs';
2+
import { hideBin } from 'yargs/helpers';
3+
4+
export interface CommandLineArguments {
5+
projectDirectory: string;
6+
tempDirectory: string | undefined;
7+
reset: boolean;
8+
}
9+
10+
/**
11+
* Parses the arguments provided on the command line using `yargs`.
12+
*
13+
* @param argv - The name of this executable and its arguments (as obtained via
14+
* `process.argv`).
15+
* @returns A promise for the `yargs` arguments object.
16+
*/
17+
export async function readCommandLineArguments(
18+
argv: string[],
19+
): Promise<CommandLineArguments> {
20+
return await yargs(hideBin(argv))
21+
.usage(
22+
'This tool prepares your project for a new release by bumping versions and updating changelogs.',
23+
)
24+
.option('project-directory', {
25+
alias: 'd',
26+
describe: 'The directory that holds your project.',
27+
default: '.',
28+
})
29+
.option('temp-directory', {
30+
describe:
31+
'The directory that is used to hold temporary files, such as the release spec template.',
32+
type: 'string',
33+
})
34+
.option('reset', {
35+
describe:
36+
'Removes any cached files from a previous run that may have been created.',
37+
type: 'boolean',
38+
default: false,
39+
})
40+
.help()
41+
.strict()
42+
.parse();
43+
}

src/editor.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { when } from 'jest-when';
2+
import { determineEditor } from './editor';
3+
import * as envModule from './env';
4+
import * as miscUtils from './misc-utils';
5+
6+
jest.mock('./env');
7+
jest.mock('./misc-utils');
8+
9+
describe('editor', () => {
10+
describe('determineEditor', () => {
11+
it('returns information about the editor from EDITOR if it resolves to an executable', async () => {
12+
jest
13+
.spyOn(envModule, 'getEnvironmentVariables')
14+
.mockReturnValue({ EDITOR: 'editor', TODAY: undefined });
15+
when(jest.spyOn(miscUtils, 'resolveExecutable'))
16+
.calledWith('editor')
17+
.mockResolvedValue('/path/to/resolved-editor');
18+
19+
expect(await determineEditor()).toStrictEqual({
20+
path: '/path/to/resolved-editor',
21+
args: [],
22+
});
23+
});
24+
25+
it('falls back to VSCode if it exists and if EDITOR does not point to an executable', async () => {
26+
jest
27+
.spyOn(envModule, 'getEnvironmentVariables')
28+
.mockReturnValue({ EDITOR: 'editor', TODAY: undefined });
29+
when(jest.spyOn(miscUtils, 'resolveExecutable'))
30+
.calledWith('editor')
31+
.mockResolvedValue(null)
32+
.calledWith('code')
33+
.mockResolvedValue('/path/to/code');
34+
35+
expect(await determineEditor()).toStrictEqual({
36+
path: '/path/to/code',
37+
args: ['--wait'],
38+
});
39+
});
40+
41+
it('returns null if resolving EDITOR returns null and resolving VSCode returns null', async () => {
42+
jest
43+
.spyOn(envModule, 'getEnvironmentVariables')
44+
.mockReturnValue({ EDITOR: 'editor', TODAY: undefined });
45+
when(jest.spyOn(miscUtils, 'resolveExecutable'))
46+
.calledWith('editor')
47+
.mockResolvedValue(null)
48+
.calledWith('code')
49+
.mockResolvedValue(null);
50+
51+
expect(await determineEditor()).toBeNull();
52+
});
53+
54+
it('returns null if resolving EDITOR returns null and resolving VSCode throws', async () => {
55+
jest
56+
.spyOn(envModule, 'getEnvironmentVariables')
57+
.mockReturnValue({ EDITOR: 'editor', TODAY: undefined });
58+
when(jest.spyOn(miscUtils, 'resolveExecutable'))
59+
.calledWith('editor')
60+
.mockResolvedValue(null)
61+
.calledWith('code')
62+
.mockRejectedValue(new Error('some error'));
63+
64+
expect(await determineEditor()).toBeNull();
65+
});
66+
67+
it('returns null if resolving EDITOR throws and resolving VSCode returns null', async () => {
68+
jest
69+
.spyOn(envModule, 'getEnvironmentVariables')
70+
.mockReturnValue({ EDITOR: 'editor', TODAY: undefined });
71+
when(jest.spyOn(miscUtils, 'resolveExecutable'))
72+
.calledWith('editor')
73+
.mockRejectedValue(new Error('some error'))
74+
.calledWith('code')
75+
.mockResolvedValue(null);
76+
77+
expect(await determineEditor()).toBeNull();
78+
});
79+
80+
it('returns null if resolving EDITOR throws and resolving VSCode throws', async () => {
81+
jest
82+
.spyOn(envModule, 'getEnvironmentVariables')
83+
.mockReturnValue({ EDITOR: 'editor', TODAY: undefined });
84+
when(jest.spyOn(miscUtils, 'resolveExecutable'))
85+
.calledWith('editor')
86+
.mockRejectedValue(new Error('some error'))
87+
.calledWith('code')
88+
.mockRejectedValue(new Error('some error'));
89+
90+
expect(await determineEditor()).toBeNull();
91+
});
92+
93+
it('returns null if EDITOR is unset and resolving VSCode returns null', async () => {
94+
jest
95+
.spyOn(envModule, 'getEnvironmentVariables')
96+
.mockReturnValue({ EDITOR: undefined, TODAY: undefined });
97+
when(jest.spyOn(miscUtils, 'resolveExecutable'))
98+
.calledWith('code')
99+
.mockResolvedValue(null);
100+
101+
expect(await determineEditor()).toBeNull();
102+
});
103+
104+
it('returns null if EDITOR is unset and resolving VSCode throws', async () => {
105+
jest
106+
.spyOn(envModule, 'getEnvironmentVariables')
107+
.mockReturnValue({ EDITOR: undefined, TODAY: undefined });
108+
when(jest.spyOn(miscUtils, 'resolveExecutable'))
109+
.calledWith('code')
110+
.mockRejectedValue(new Error('some error'));
111+
112+
expect(await determineEditor()).toBeNull();
113+
});
114+
});
115+
});

src/editor.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { getEnvironmentVariables } from './env';
2+
import { debug, resolveExecutable } from './misc-utils';
3+
4+
/**
5+
* Information about the editor present on the user's computer.
6+
*
7+
* @property path - The path to the executable representing the editor.
8+
* @property args - Command-line arguments to pass to the executable when
9+
* calling it.
10+
*/
11+
export interface Editor {
12+
path: string;
13+
args: string[];
14+
}
15+
16+
/**
17+
* Looks for an executable that represents a code editor on your computer. Tries
18+
* the `EDITOR` environment variable first, falling back to the executable that
19+
* represents VSCode (`code`).
20+
*
21+
* @returns A promise that contains information about the found editor (path and
22+
* arguments), or null otherwise.
23+
*/
24+
export async function determineEditor(): Promise<Editor | null> {
25+
let executablePath: string | null = null;
26+
const executableArgs: string[] = [];
27+
const { EDITOR } = getEnvironmentVariables();
28+
29+
if (EDITOR !== undefined) {
30+
try {
31+
executablePath = await resolveExecutable(EDITOR);
32+
} catch (error) {
33+
debug(
34+
`Could not resolve executable ${EDITOR} (${error}), falling back to VSCode`,
35+
);
36+
}
37+
}
38+
39+
if (executablePath === null) {
40+
try {
41+
executablePath = await resolveExecutable('code');
42+
// Waits until the file is closed before returning
43+
executableArgs.push('--wait');
44+
} catch (error) {
45+
debug(
46+
`Could not resolve path to VSCode: ${error}, continuing regardless`,
47+
);
48+
}
49+
}
50+
51+
if (executablePath !== null) {
52+
return { path: executablePath, args: executableArgs };
53+
}
54+
55+
return null;
56+
}

src/env.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { getEnvironmentVariables } from './env';
2+
3+
describe('env', () => {
4+
describe('getEnvironmentVariables', () => {
5+
let existingProcessEnv: NodeJS.ProcessEnv;
6+
7+
beforeEach(() => {
8+
existingProcessEnv = { ...process.env };
9+
});
10+
11+
afterEach(() => {
12+
Object.keys(existingProcessEnv).forEach((key) => {
13+
process.env[key] = existingProcessEnv[key];
14+
});
15+
});
16+
17+
it('returns only the environment variables from process.env that we use in this tool', () => {
18+
process.env.EDITOR = 'editor';
19+
process.env.TODAY = 'today';
20+
process.env.EXTRA = 'extra';
21+
22+
expect(getEnvironmentVariables()).toStrictEqual({
23+
EDITOR: 'editor',
24+
TODAY: 'today',
25+
});
26+
});
27+
});
28+
});

0 commit comments

Comments
 (0)