Skip to content

Commit f4e901f

Browse files
author
Brijesh Bittu
committed
[code-infra] Port releaseTag script
1 parent 0995a43 commit f4e901f

File tree

6 files changed

+217
-1
lines changed

6 files changed

+217
-1
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "mui-public",
33
"private": true,
4+
"version": "0.0.1",
45
"scripts": {
56
"preinstall": "npx only-allow pnpm",
67
"eslint": "eslint . --cache --report-unused-disable-directives --ext .js,.ts,.tsx --max-warnings 0",
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import gitUrlParse from 'git-url-parse';
2+
import childProcess from 'node:child_process';
3+
import path from 'path';
4+
import { promisify } from 'util';
5+
import yargs from 'yargs';
6+
7+
import { findWorkspaceRoot } from '../utils/findWorkspaceRoot.mjs';
8+
import { readJSON } from '../utils/readJson.mjs';
9+
10+
/**
11+
* Only directly call it with side-effect free commands.
12+
* Otherwise use the `exec` that's considering whether the context is supposed to be "dry" i.e. have no side-effects.
13+
*/
14+
const execActual = promisify(childProcess.exec);
15+
/**
16+
* @param {string} command
17+
* @param {unknown} [options]
18+
*/
19+
async function execDry(command, options) {
20+
// eslint-disable-next-line no-console
21+
console.log(`exec(\`${command}\`, ${JSON.stringify(options)})`);
22+
}
23+
24+
/**
25+
* Find the remote pointing to mui/material-ui.
26+
*
27+
* Conventionally this should be named `upstream` but some collaborators might've used a different naming scheme.
28+
*
29+
* @param {string} repo
30+
*/
31+
async function findMuiOrgRemote(repo) {
32+
const { stdout } = await execActual(['git', 'remote', '-v'].join(' '));
33+
const remoteLines = stdout.trim().split(/\r?\n/);
34+
35+
return remoteLines
36+
.map((remoteLine) => {
37+
const [name, url, method] = remoteLine.split(/\s/);
38+
return { name, url, method };
39+
})
40+
.find((remote) => {
41+
const parsed = gitUrlParse(remote.url);
42+
return parsed.owner === 'mui' && parsed.name === repo;
43+
});
44+
}
45+
46+
async function main(argv) {
47+
const { dryRun, repo } = argv;
48+
49+
const exec = dryRun ? execDry : execActual;
50+
51+
const rootWorkspace = findWorkspaceRoot();
52+
const rootWorkspaceManifest = await readJSON(path.join(rootWorkspace, 'package.json'));
53+
54+
const tag = `v${rootWorkspaceManifest.version}`;
55+
const message = `Version ${rootWorkspaceManifest.version}`;
56+
57+
await exec(['git', 'tag', '-a', tag, '-m', `"${message}"`].join(' '));
58+
// eslint-disable-next-line no-console -- verbose logging
59+
console.log(`Created tag '${tag}'. To remove enter 'git tag -d ${tag}'`);
60+
61+
const muiOrgRemote = await findMuiOrgRemote(repo);
62+
if (muiOrgRemote === undefined) {
63+
throw new TypeError(
64+
'Unable to find the upstream remote. It should be a remote pointing to "mui/material-ui". ' +
65+
'Did you forget to add it via `git remote add upstream [email protected]:mui/material-ui.git`? ' +
66+
'If you think this is a bug please include `git remote -v` in your report.',
67+
);
68+
}
69+
70+
await exec(['git', 'push', muiOrgRemote.name, tag].join(' '));
71+
72+
// eslint-disable-next-line no-console -- verbose logging
73+
console.log(
74+
`Pushed tag '${tag}' to ${muiOrgRemote.name}. This should not be reversed. In case of emergency enter 'git push --delete ${muiOrgRemote.name} ${tag}' to remove.`,
75+
);
76+
}
77+
78+
yargs(process.argv.slice(2))
79+
.command({
80+
command: '$0',
81+
description: 'Tags the current release and pushes these changes to mui/<repo-name>.',
82+
builder: (command) => {
83+
return command
84+
.option('dryRun', {
85+
default: false,
86+
describe: 'When true, the script will not have any permanent side-effects.',
87+
type: 'boolean',
88+
})
89+
.option('repo', {
90+
describe: 'Repository to tag',
91+
type: 'string',
92+
});
93+
},
94+
handler: main,
95+
})
96+
.help()
97+
.strict(true)
98+
.version(false)
99+
.parse();
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as path from 'node:path';
2+
import * as url from 'node:url';
3+
import * as fs from 'node:fs';
4+
5+
function findUpFile(fileName, cwd = process.cwd(), maxIterations = 5) {
6+
const pathName = path.join(cwd, fileName);
7+
if (fs.existsSync(pathName)) {
8+
return pathName;
9+
}
10+
if (maxIterations === 0) {
11+
return null;
12+
}
13+
return findUpFile(fileName, path.dirname(cwd), maxIterations - 1);
14+
}
15+
16+
/**
17+
* Returns the full path of the root directory of the monorepo.
18+
*/
19+
export function findWorkspaceRoot() {
20+
// Use this when available. Avoids the need to check for the workspace file.
21+
if (process.env.NX_WORKSPACE_ROOT) {
22+
return process.env.NX_WORKSPACE_ROOT;
23+
}
24+
25+
const workspaceFilePath = findUpFile('pnpm-workspace.yaml', process.cwd());
26+
if (workspaceFilePath) {
27+
return path.dirname(workspaceFilePath);
28+
}
29+
30+
const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url));
31+
const workspaceRoot = path.resolve(currentDirectory, '..');
32+
return workspaceRoot;
33+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import fs from 'node:fs/promises';
2+
3+
export async function readJSON(filePath) {
4+
try {
5+
const data = await fs.readFile(filePath, 'utf8');
6+
return JSON.parse(data);
7+
} catch (error) {
8+
console.error(`Error reading JSON file at ${filePath}:`, error);
9+
throw error;
10+
}
11+
}

packages/mui-internal-code-infra/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
"default": "./src/eslint/docs.mjs"
2525
}
2626
},
27+
"bin": {
28+
"mui-release-tag": "./src/cli/releaseTag.mjs"
29+
},
2730
"dependencies": {
2831
"emoji-regex": "^10.4.0",
2932
"eslint-config-airbnb-base": "^15.0.0",
@@ -37,9 +40,11 @@
3740
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
3841
"eslint-plugin-react-hooks": "^6.0.0",
3942
"eslint-plugin-testing-library": "^7.3.0",
43+
"git-url-parse": "^16.1.0",
4044
"globals": "^16.2.0",
4145
"minimatch": "^10.0.1",
42-
"typescript-eslint": "^8.33.1"
46+
"typescript-eslint": "^8.33.1",
47+
"yargs": "^18.0.0"
4348
},
4449
"peerDependencies": {
4550
"@next/eslint-plugin-next": "^14.0.0 || ^15.0.0",

pnpm-lock.yaml

Lines changed: 67 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)