Skip to content

Commit faada11

Browse files
committed
Detect affected packages from commit using git cli
Removed all references to github PR api
1 parent c58a23d commit faada11

File tree

1 file changed

+100
-135
lines changed

1 file changed

+100
-135
lines changed

packages/code-infra/src/cli/cmdPublishCanary.mjs

Lines changed: 100 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { $ } from 'execa';
1515
import gitUrlParse from 'git-url-parse';
1616
import * as semver from 'semver';
1717

18-
import { fetchCommitsBetweenRefs } from '../utils/changelog.mjs';
1918
import {
2019
getCurrentGitSha,
2120
getPackageVersionInfo,
@@ -67,112 +66,97 @@ async function getRepositoryInfo() {
6766

6867
/**
6968
* Extract package name from npm package name for label matching
70-
* @param {string} npmPackageName - npm package name (e.g., '@mui/internal-code-infra')
71-
* @returns {string} Package name for label (e.g., 'code-infra')
69+
* @param {string} commitHash - Commit hash to check changed files
70+
* @param {PublicPackage[]} allPackages - List of all package names
71+
* @returns {Promise<string[]>} Affected package names
7272
*/
73-
function extractPackageNameForLabel(npmPackageName) {
74-
// For scoped packages like @mui/internal-code-infra, extract the last part after 'internal-'
75-
if (npmPackageName.startsWith('@')) {
76-
const parts = npmPackageName.split('/');
77-
if (parts.length >= 2) {
78-
const packageName = parts[1];
79-
// Remove 'internal-' prefix if present
80-
return packageName.replace(/^internal-/, '');
73+
async function getAffectedPkgsForCommit(commitHash, allPackages) {
74+
const { stdout } = await $`git diff-tree --no-commit-id --name-only -r ${commitHash}`;
75+
const affectedFiles = stdout.trim().split('\n');
76+
/**
77+
* @type {Set<string>}
78+
*/
79+
const affectedPackages = new Set();
80+
81+
for (const filePath of affectedFiles) {
82+
for (const pkg of allPackages) {
83+
const relativePath = path.relative(pkg.path, filePath);
84+
if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
85+
affectedPackages.add(pkg.name);
86+
}
8187
}
8288
}
83-
return npmPackageName;
84-
}
8589

86-
/**
87-
* Get merged PRs since the last canary tag
88-
* @param {string} owner - Repository owner
89-
* @param {string} repo - Repository name
90-
* @returns {Promise<Array<import('../utils/changelog.mjs').FetchedCommitDetails>>} List of merged PRs
91-
*/
92-
async function getMergedPRsSinceTag(owner, repo) {
93-
const octokit = getOctokit();
94-
const changelogs = await fetchCommitsBetweenRefs({
95-
octokit,
96-
lastRelease: CANARY_TAG,
97-
release: 'master',
98-
repo,
99-
org: owner,
100-
});
101-
return changelogs;
90+
return Array.from(affectedPackages);
10291
}
10392

10493
/**
105-
* Generate changelog for a package based on PRs
106-
* @param {string} packageName - Package name (e.g., 'code-infra')
107-
* @param {Array<import('../utils/changelog.mjs').FetchedCommitDetails>} allPRs - All merged PRs
108-
* @returns {string} Generated changelog content
94+
*
95+
* @param {Object} param0
96+
* @param {string} param0.repo
97+
* @param {string} param0.owner
98+
* @returns {Promise<Awaited<ReturnType<Octokit['repos']['compareCommits']>>['data']['commits']>}
10999
*/
110-
function generateChangelogForPackage(packageName, allPRs) {
111-
const scopeLabel = `scope: ${packageName}`;
112-
113-
// Filter PRs that have the matching scope label
114-
const relevantPRs = allPRs.filter((pr) =>
115-
pr.labels.some((label) => label.toLowerCase() === scopeLabel.toLowerCase()),
100+
async function fetchCommitsBetweenRefs({ repo, owner }) {
101+
const octokit = getOctokit();
102+
/**
103+
* @typedef {Awaited<ReturnType<Octokit['repos']['compareCommits']>>['data']['commits']} Commits
104+
*/
105+
/**
106+
* @type {Commits}
107+
*/
108+
const results = [];
109+
/**
110+
* @type {any}
111+
*/
112+
const timeline = octokit.paginate.iterator(
113+
octokit.repos.compareCommitsWithBasehead.endpoint.merge({
114+
owner,
115+
repo,
116+
basehead: `${CANARY_TAG}...master`,
117+
}),
116118
);
117-
118-
if (relevantPRs.length === 0) {
119-
return '';
119+
for await (const response of timeline) {
120+
results.push(...response.data.commits);
120121
}
121-
122-
// Generate changelog content
123-
const changelogLines = relevantPRs.map((pr) => {
124-
// Check if PR has 'breaking' label (case-insensitive)
125-
const hasBreakingLabel = pr.labels.some((label) =>
126-
['breaking', 'breaking change', 'breaking-change'].includes(label.toLowerCase()),
127-
);
128-
// Check if title contains [breaking] (case-insensitive)
129-
const hasBreakingInTitle = /\[(breaking(?:\s|-)?(?:change)?)\]/i.test(pr.message);
130-
const isBreaking = hasBreakingLabel || hasBreakingInTitle;
131-
132-
// Add "Breaking: " prefix if breaking change
133-
const prefix = isBreaking ? 'Breaking: ' : '';
134-
return `- ${prefix}${pr.message} (#${pr.prNumber})`;
135-
});
136-
137-
return changelogLines.join('\n');
122+
return results;
138123
}
139124

140125
/**
141126
* Prepare changelog data for packages using GitHub API
142127
* @param {PublicPackage[]} packagesToPublish - Packages that will be published
143128
* @param {Map<string, string>} canaryVersions - Map of package names to their canary versions
144129
* @param {{owner: string, repo: string}} repoInfo - Repository information
145-
* @returns {Promise<Map<string, string>>} Map of package names to their changelogs
130+
* @returns {Promise<Map<string, string[]>>} Map of package names to their changelogs
146131
*/
147132
async function prepareChangelogsFromGitHub(packagesToPublish, canaryVersions, repoInfo) {
148-
console.log('🔍 Fetching merged PRs from GitHub API...');
149-
const allCommitPRs = await getMergedPRsSinceTag(repoInfo.owner, repoInfo.repo);
150-
console.log(`📋 Found ${allCommitPRs.length} merged PRs since last canary tag`);
133+
console.log('🔍 Fetching merged commits from GitHub API...');
134+
const commits = await fetchCommitsBetweenRefs(repoInfo);
135+
console.log(`📋 Found ${commits.length} merged commits since last canary tag`);
151136

152137
/**
153-
* @type {Map<string, string>}
138+
* @type {Map<string, string[]>}
154139
*/
155140
const changelogs = new Map();
156-
157-
for (const pkg of packagesToPublish) {
158-
const version = canaryVersions.get(pkg.name);
159-
if (!version) {
160-
continue;
141+
for (const commit of commits) {
142+
// eslint-disable-next-line no-await-in-loop
143+
const affectedPackages = await getAffectedPkgsForCommit(commit.sha, packagesToPublish);
144+
for (const pkgName of affectedPackages) {
145+
const existingChangelogs = changelogs.get(pkgName) || [];
146+
existingChangelogs.push(
147+
`- ${commit.commit.message.split('\n')[0]} (${commit.sha.slice(0, 7)})`,
148+
);
149+
changelogs.set(pkgName, existingChangelogs);
161150
}
162-
163-
const packageLabel = extractPackageNameForLabel(pkg.name);
164-
const changelog = generateChangelogForPackage(packageLabel, allCommitPRs);
165-
changelogs.set(pkg.name, changelog);
166151
}
167-
168152
return changelogs;
169153
}
170154

171155
/**
172156
* Prepare changelog data for packages
173157
* @param {PublicPackage[]} packagesToPublish - Packages that will be published
174158
* @param {Map<string, string>} canaryVersions - Map of package names to their canary versions
175-
* @returns {Promise<Map<string, string>>} Map of package names to their changelogs
159+
* @returns {Promise<Map<string, string[]>>} Map of package names to their changelogs
176160
*/
177161
async function prepareChangelogsForPackages(packagesToPublish, canaryVersions) {
178162
console.log('\n📝 Preparing changelogs for packages...');
@@ -181,7 +165,7 @@ async function prepareChangelogsForPackages(packagesToPublish, canaryVersions) {
181165
console.log(`📂 Repository: ${repoInfo.owner}/${repoInfo.repo}`);
182166

183167
/**
184-
* @type {Map<string, string>}
168+
* @type {Map<string, string[]>}
185169
*/
186170
let changelogs = new Map();
187171

@@ -194,14 +178,11 @@ async function prepareChangelogsForPackages(packagesToPublish, canaryVersions) {
194178
continue;
195179
}
196180

197-
const changelog = changelogs.get(pkg.name) || '';
181+
const changelog = changelogs.get(pkg.name) || [];
198182
console.log(`\n📦 ${pkg.name}@${version}`);
199183
if (changelog) {
200184
console.log(
201-
` Changelog:\n${changelog
202-
.split('\n')
203-
.map((/** @type {string} */ line) => ` ${line}`)
204-
.join('\n')}`,
185+
` Changelog:\n${changelog.map((/** @type {string} */ line) => ` ${line}`).join('\n')}`,
205186
);
206187
} else {
207188
console.log(' Changelog: No changes with scope labels found for this package.');
@@ -216,7 +197,7 @@ async function prepareChangelogsForPackages(packagesToPublish, canaryVersions) {
216197
* Create GitHub releases and tags for published packages
217198
* @param {PublicPackage[]} publishedPackages - Packages that were published
218199
* @param {Map<string, string>} canaryVersions - Map of package names to their canary versions
219-
* @param {Map<string, string>} changelogs - Map of package names to their changelogs
200+
* @param {Map<string, string[]>} changelogs - Map of package names to their changelogs
220201
* @param {{dryRun?: boolean}} options - Publishing options
221202
* @returns {Promise<void>}
222203
*/
@@ -228,33 +209,6 @@ async function createGitHubReleasesForPackages(
228209
) {
229210
console.log('\n🚀 Creating GitHub releases and tags for published packages...');
230211

231-
if (options.dryRun) {
232-
console.log('🧪 Dry-run mode: Would create release(s) and tag(s) for:');
233-
for (const pkg of publishedPackages) {
234-
const version = canaryVersions.get(pkg.name);
235-
if (!version) {
236-
continue;
237-
}
238-
const changelog = changelogs.get(pkg.name);
239-
const tagName = `${pkg.name}@${version}${changelog ? ' (with changelog):' : ' (no changelog)'}`;
240-
console.log(` • ${tagName}`);
241-
if (changelog) {
242-
// Draw changelog in an ASCII rectangle
243-
const lines = changelog.split('\n');
244-
const maxLength = Math.max(Math.max(...lines.map((line) => line.length)), 60);
245-
const border = `┌${'─'.repeat(maxLength + 2)}┐`;
246-
const footer = `└${'─'.repeat(maxLength + 2)}┘`;
247-
console.log(' Changelog:');
248-
console.log(` ${border}`);
249-
lines.forEach((line) => {
250-
console.log(` │ ${line.padEnd(maxLength)} │`);
251-
});
252-
console.log(` ${footer}`);
253-
}
254-
}
255-
return;
256-
}
257-
258212
const repoInfo = await getRepositoryInfo();
259213
const gitSha = await getCurrentGitSha();
260214
const octokit = getOctokit();
@@ -277,33 +231,41 @@ async function createGitHubReleasesForPackages(
277231
console.log(`\n📦 Processing ${pkg.name}@${version}...`);
278232

279233
// Create git tag
280-
// eslint-disable-next-line no-await-in-loop
281-
await $({
282-
env: {
283-
...process.env,
284-
GIT_COMMITTER_NAME: 'Code infra',
285-
GIT_COMMITTER_EMAIL: 'code-infra@mui.com',
286-
},
287-
})`git tag -a ${tagName} -m ${`Canary release ${pkg.name}@${version}`}`;
288-
289-
// eslint-disable-next-line no-await-in-loop
290-
await $`git push origin ${tagName}`;
291-
console.log(`✅ Created and pushed git tag: ${tagName}`);
292-
293-
// Create GitHub release
294-
// eslint-disable-next-line no-await-in-loop
295-
const res = await octokit.repos.createRelease({
296-
owner: repoInfo.owner,
297-
repo: repoInfo.repo,
298-
tag_name: tagName,
299-
target_commitish: gitSha,
300-
name: releaseName,
301-
body: changelog,
302-
draft: false,
303-
prerelease: true, // Mark as prerelease since these are canary versions
304-
});
234+
if (options.dryRun) {
235+
console.log(`🏷️ Would create and push git tag: ${tagName} (dry-run)`);
236+
console.log(`📝 Would publish a Github release:`);
237+
console.log(` - Name: ${releaseName}`);
238+
console.log(` - Tag: ${tagName}`);
239+
console.log(` - Body:\n${changelog.join('\n')}`);
240+
} else {
241+
// eslint-disable-next-line no-await-in-loop
242+
await $({
243+
env: {
244+
...process.env,
245+
GIT_COMMITTER_NAME: 'Code infra',
246+
GIT_COMMITTER_EMAIL: 'code-infra@mui.com',
247+
},
248+
})`git tag -a ${tagName} -m ${`Canary release ${pkg.name}@${version}`}`;
249+
250+
// eslint-disable-next-line no-await-in-loop
251+
await $`git push origin ${tagName}`;
252+
console.log(`✅ Created and pushed git tag: ${tagName}`);
253+
254+
// Create GitHub release
255+
// eslint-disable-next-line no-await-in-loop
256+
const res = await octokit.repos.createRelease({
257+
owner: repoInfo.owner,
258+
repo: repoInfo.repo,
259+
tag_name: tagName,
260+
target_commitish: gitSha,
261+
name: releaseName,
262+
body: changelog.join('\n'),
263+
draft: false,
264+
prerelease: true, // Mark as prerelease since these are canary versions
265+
});
305266

306-
console.log(`✅ Created GitHub release: ${releaseName} at ${res.data.html_url}`);
267+
console.log(`✅ Created GitHub release: ${releaseName} at ${res.data.html_url}`);
268+
}
307269
}
308270

309271
console.log('\n✅ Finished creating GitHub releases');
@@ -424,6 +386,9 @@ async function publishCanaryVersions(
424386
const updateResults = await Promise.all(packageUpdatePromises);
425387

426388
// Prepare changelogs before building and publishing (so it can error out early if there are issues)
389+
/**
390+
* @type {Map<string, string[]>}
391+
*/
427392
let changelogs = new Map();
428393
if (options.githubRelease) {
429394
changelogs = await prepareChangelogsForPackages(packagesToPublish, canaryVersions);

0 commit comments

Comments
 (0)