-
Couldn't load subscription status.
- Fork 235
chore: make changelog generation work for multiple packages and update changeset docs #5795
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
📚 Branch Preview🔍 Visual Regression Test ResultsWhen a visual regression test fails (or has previously failed while working on this branch), its results can be found in the following URLs:
Deployed to Azure Blob Storage: If the changes are expected, update the |
Tachometer resultsCurrently, no packages are changed by this PR... |
|
Is it a good idea to create a approach document before we add these changes or are you planning to add it in the description root of this PR? |
56bf0c9 to
c522607
Compare
This is being tackled in SWC-1234. We can wait for those docs! |
Update: we agreed to not keep this one blocked, separate both concerns and revisit documentation post-barebones (e.g., how to surface this and other docs in the unified place). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! The changes look fairly structured and robust. Just few nits on some performance improvement and abstraction.
| function calculateNextVersion(currentVersion, majorChanges, minorChanges) { | ||
| const currentVersionParts = currentVersion | ||
| .split('.') | ||
| .map((part) => parseInt(part, 10)); | ||
| let nextVersion; | ||
|
|
||
| // Calculate next version based on changes | ||
| if (majorChanges.length > 0) { | ||
| // Major version bump | ||
| nextVersion = `${currentVersionParts[0] + 1}.0.0`; | ||
| return `${currentVersionParts[0] + 1}.0.0`; | ||
| } else if (minorChanges.length > 0) { | ||
| // Minor version bump | ||
| nextVersion = `${currentVersionParts[0]}.${currentVersionParts[1] + 1}.0`; | ||
| return `${currentVersionParts[0]}.${currentVersionParts[1] + 1}.0`; | ||
| } else { | ||
| // Patch version bump | ||
| nextVersion = `${currentVersionParts[0]}.${currentVersionParts[1]}.${currentVersionParts[2] + 1}`; | ||
| return `${currentVersionParts[0]}.${currentVersionParts[1]}.${currentVersionParts[2] + 1}`; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: use can use semver package to do this.
function calculateNextVersion(currentVersion, majorChanges, minorChanges) {
if (majorChanges.length > 0) return semver.inc(currentVersion, 'major');
if (minorChanges.length > 0) return semver.inc(currentVersion, 'minor');
return semver.inc(currentVersion, 'patch');
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea! Updated it.
| fs.writeFileSync( | ||
| changelogPath, | ||
| `${headerText}\n\n${newEntry.trim()}\n\n${existingChangelog.trim()}`, | ||
| `${headerText}\n\n${newEntry.trim()}\n\n${remainingContent.trim()}`, | ||
| 'utf-8' | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These sync writes can block the event loop and will slow down execution since we are trying to read through the component readme. With growing component directory this can impact. You can use non blocking fsPromises instead.
await fsPromises.writeFile(
changelogPath,
`${headerText}\n\n${newEntry.trim()}\n\n${remainingContent.trim()}`,
'utf8'
);There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated!
| async function createGlobalChangelog() { | ||
| const currentTag = validateCurrentVersion(); | ||
| const { firstGen, core } = processChangesets(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can skip here early if no changes detected just to add that extra performance optimisation.
if (
!firstGen.majorChanges.length && !firstGen.minorChanges.length && !firstGen.patchChanges.length &&
!core.majorChanges.length && !core.minorChanges.length && !core.patchChanges.length
) {
console.log('No new changesets detected. Skipping changelog generation.');
return;
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| createGlobalChangelog().catch((error) => { | ||
| console.error('Error updating changelog:', error); | ||
| process.exit(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very hard exit. Can you add a global try/catch?
(async () => {
try {
await createGlobalChangelog();
} catch (error) {
console.error('Error updating changelog:', error);
process.exit(1);
}
})();There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update it!
| const changesetDir = path.resolve(__dirname, '../.changeset'); | ||
| /** | ||
| * Processes changeset files and categorizes changes by type and target | ||
| * @returns {Object} Object containing categorized changes for both first-gen and core | ||
| */ | ||
| function processChangesets() { | ||
| const changesetDir = path.resolve(__dirname, '../../.changeset'); | ||
| const changesetFiles = fs | ||
| .readdirSync(changesetDir) | ||
| .filter((file) => file.endsWith('.md') && file !== 'README.md'); | ||
|
|
||
| const majorChanges = []; | ||
| const minorChanges = []; | ||
| const patchChanges = []; | ||
| const coreMajorChanges = []; | ||
| const coreMinorChanges = []; | ||
| const corePatchChanges = []; | ||
|
|
||
| // Process each changeset file to extract change information | ||
| for (const file of changesetFiles) { | ||
| const filePath = path.join(changesetDir, file); | ||
| const content = fs.readFileSync(filePath, 'utf-8'); | ||
|
|
||
| // Extract the frontmatter from the changeset | ||
| const frontmatterMatch = content.match( | ||
| /---\n([\s\S]*?)\n---\n([\s\S]*)/ | ||
| ); | ||
|
|
||
| if (frontmatterMatch) { | ||
| const [, frontmatter, description] = frontmatterMatch; | ||
|
|
||
| // Parse the frontmatter to determine the change type | ||
| const isMajor = frontmatter.includes('major'); | ||
| const isMinor = frontmatter.includes('minor'); | ||
| // If not major or minor, it's a patch | ||
|
|
||
| // Extract the package scope from the frontmatter | ||
| const packageMatch = frontmatter.match( | ||
| /'@spectrum-web-components\/([^']+)':|"@spectrum-web-components\/([^"]+)":/ | ||
| ); | ||
| // Extract component name from package name and prefix with "sp-" | ||
| const match = packageMatch?.[1] || packageMatch?.[2]; | ||
| const scope = match ? `sp-${match}` : 'core'; | ||
| // Clean up the description text | ||
| const cleanDescription = description.trim(); | ||
|
|
||
| // Create the entry (without commit hash since we're using changesets) | ||
| const entry = `**${scope}**: ${cleanDescription}\n\n`; | ||
| // "@spectrum-web-components" (first-gen components) | ||
| // go to first-gen global changelog | ||
| for (const match of frontmatter.matchAll( | ||
| /['"]@spectrum-web-components\/([^'"]+)['"]:\s*(major|minor|patch)/g | ||
| )) { | ||
| const componentName = match[1]; | ||
| const changeType = match[2]; | ||
| const scope = `sp-${componentName}`; | ||
| const entry = `**${scope}**: ${cleanDescription}\n\n`; | ||
|
|
||
| if (changeType === 'major') { | ||
| majorChanges.push(entry); | ||
| } else if (changeType === 'minor') { | ||
| minorChanges.push(entry); | ||
| } else { | ||
| patchChanges.push(entry); | ||
| } | ||
| } | ||
|
|
||
| // Categorize based on semver bump type | ||
| if (isMajor) { | ||
| majorChanges.push(entry); | ||
| } else if (isMinor) { | ||
| minorChanges.push(entry); | ||
| } else { | ||
| patchChanges.push(entry); | ||
| // @swc/core changes go to core changelog | ||
| for (const match of frontmatter.matchAll( | ||
| /['"]@swc\/core['"]:\s*(major|minor|patch)/g | ||
| )) { | ||
| const changeType = match[1]; | ||
| const entry = `${cleanDescription}\n\n`; | ||
|
|
||
| if (changeType === 'major') { | ||
| coreMajorChanges.push(entry); | ||
| } else if (changeType === 'minor') { | ||
| coreMinorChanges.push(entry); | ||
| } else { | ||
| corePatchChanges.push(entry); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Parse version into number array for potential version calculations | ||
| return { | ||
| firstGen: { majorChanges, minorChanges, patchChanges }, | ||
| core: { | ||
| majorChanges: coreMajorChanges, | ||
| minorChanges: coreMinorChanges, | ||
| patchChanges: corePatchChanges, | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel this is a little noisy since you are repeating nearly identical parsing logic for @spectrum-web-components/* and @swc/core.
This is not a blocker but just a suggestion. I just abstracted the parsing logic into a helper extractChanges just to reduce the cognitive load.
function extractChanges(frontmatter, description, pattern, prefix = '') {
const changes = { major: [], minor: [], patch: [] };
for (const match of frontmatter.matchAll(pattern)) {
const [, name, type] = match;
const entry = prefix
? `**${prefix}${name}**: ${description.trim()}\n\n`
: `${description.trim()}\n\n`;
changes[type].push(entry);
}
return changes;
}
async function processChangesets() {
const changesetDir = path.resolve(__dirname, '../../.changeset');
// 💡 Use non-blocking I/O for directory read
const files = await fsPromises.readdir(changesetDir);
const markdownFiles = files.filter(
(f) => f.endsWith('.md') && f !== 'README.md'
);
// 💡 Read all files concurrently
const fileContents = await Promise.all(
markdownFiles.map((file) =>
fsPromises.readFile(path.join(changesetDir, file), 'utf8')
)
);
// Prepare change containers
const firstGen = { majorChanges: [], minorChanges: [], patchChanges: [] };
const core = { majorChanges: [], minorChanges: [], patchChanges: [] };
for (const content of fileContents) {
const frontmatterMatch = content.match(/---\n([\s\S]*?)\n---\n([\s\S]*)/);
if (!frontmatterMatch) continue;
const [, frontmatter, description] = frontmatterMatch;
const cleanDescription = description.trim();
// 💡 Extract first-gen (@spectrum-web-components/*) changes
const swcChanges = extractChanges(
frontmatter,
cleanDescription,
/['"]@spectrum-web-components\/([^'"]+)['"]:\s*(major|minor|patch)/g,
'sp-'
);
// 💡 Extract @swc/core changes
const coreChanges = extractChanges(
frontmatter,
cleanDescription,
/['"]@swc\/core['"]:\s*(major|minor|patch)/g
);
// 💡 Merge results into categorized buckets
firstGen.majorChanges.push(...swcChanges.major);
firstGen.minorChanges.push(...swcChanges.minor);
firstGen.patchChanges.push(...swcChanges.patch);
core.majorChanges.push(...coreChanges.major);
core.minorChanges.push(...coreChanges.minor);
core.patchChanges.push(...coreChanges.patch);
}
return { firstGen, core };
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing all the changes. Looks solid.
Description
Refactored and updated the current global changelog generation script (
first-gen/scripts/update-global-changelog.js) to allow generating a 2nd-gen global changelog.Updated the changesets README documentation to provide guidance for creating changesets in the new monorepo structure considering both first-gen and second-gen packages.
Motivation and context
The monorepo structure now includes both first-gen Spectrum Web Components and second-gen @swc/core packages, but the changeset documentation didn't provide clear guidance on how to create changesets for this new structure nor did the global changelog script support it.
Related issue(s)
Screenshots (if appropriate)
N/A - Internal tooling improvement
Author's checklist
Reviewer's checklist
patch,minor, ormajorfeaturesManual review test cases
Test changelog generation for first-gen components
yarn changesetand create a changeset with@spectrum-web-components/button: minornode first-gen/scripts/update-global-changelog.jsTest changelog generation for @swc/core packages
yarn changesetand create a changeset with@swc/core: patchnode first-gen/scripts/update-global-changelog.jsTest changeset documentation clarity
.changeset/README.mdTest script error handling