Skip to content

ci: global changelog generator script #5328

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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"build:types": "wireit",
"build:watch": "wireit",
"changeset-snapshot-publish": "yarn prepublishOnly && yarn changeset version --snapshot && yarn lint:versions --fix && yarn update-version && yarn changeset publish --no-git-tag --tag snapshot",
"changeset-publish": "yarn prepublishOnly && yarn changeset version && yarn install && yarn lint:versions --fix && yarn update-version && yarn changeset publish --no-git-tag && yarn push-to-remote && yarn create-git-tag && yarn postpublish",
"changeset-publish": "yarn prepublishOnly && yarn changeset version && yarn changelog:global && yarn install && yarn lint:versions --fix && yarn update-version && yarn changeset publish --no-git-tag && yarn push-to-remote && yarn create-git-tag && yarn postpublish",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn’t yarn changelog:global be executed before yarn changeset version? This is because yarn changelog:global reads from changeset files located in the .changeset directory. However, after running yarn changeset version, all the changesets are removed/deleted, and the changelogs are populated. Consequently, yarn changelog:global will no longer be able to read the changeset files in the .changeset directory.

"changelog:global": "node ./scripts/add-global-changelog.js",
"update-version": "node ./tasks/update-version.js",
"chromatic": "chromatic --build-script-name storybook:build # note that --project-token must be set in your env variables",
"create-git-tag": "node --no-warnings tasks/create-git-tag.js",
Expand Down
93 changes: 93 additions & 0 deletions scripts/add-global-changelog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.

*/

import fs from 'fs';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
import path from 'path';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoUrl = 'https://github.com/adobe/spectrum-web-components';

const pkg = JSON.parse(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A best practice to keep code scoped is to wrap them in a main function and call the function at the end of the file. I think it's a good idea to maintain that best practice here that we see in our other scripts as well.

fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8')
);
const newVersion = pkg.version;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can skip this abstraction since newVersion is only used in the following line. We also need a check for if the package doesn't load to throw that warning. You're assuming here that pkg.version exists.

const newTag = `v${newVersion}`;
const prevTag = execSync('git tag --sort=-creatordate')
.toString()
.split('\n')
.filter(Boolean)
.find((tag) => tag !== newTag);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs more robust failure captures. I recommend if you're going to use exec for this, separate the command execution (exec is notoriously flaky in node scripts so you need to account for it failing) from the string parsing. Check that exec returned a string and then run the split, etc.


if (!prevTag) {
console.error('No previous tag found.');
process.exit(1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to think about what information I would need to debug this error. At the age of this project, there's no change that there aren't previous tags to be found so maybe we want this error to tell us why the exec command couldn't return a value we were expecting? Maybe this should log the git tag command output?

}

const date = new Date().toISOString().split('T')[0];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const date = new Date().toISOString().split('T')[0];
const date = new Date().toLocaleDateString('en-CA', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}));

This should return the result you're wanting without having to do inline array parsing (which can sometimes lead to invalid results or fail when the array isn't in the format we're expecting).

const compareUrl = `${repoUrl}/compare/${prevTag}...${newTag}`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is prevTag allowed to be a pre-tag or is there a requirement that prevTag must be a semver version? It seems like it must be one of the semver releases (not the betas for example) so maybe we can add a comment to that effect?

const commitLogs = execSync(`git log ${prevTag}..HEAD --pretty=format:"%s|%h"`)
.toString()
.trim();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it's returning the commit logs but not the changelog content. Is that what we're wanting to add to the global changelog? It seems like the commit history is less useful now that we've migrated to changesets.

Copy link
Contributor Author

@Rajdeepc Rajdeepc Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great point where I would want everyone's opinion on. I don't think only a summary of the change is sufficient for the users to check what changes went along. I want to keep the CHANGELOG to follow the same pattern as we were doing during lerna which I feel the users still wants.


const commits = commitLogs.split('\n').map((line) => {
const [message, hash] = line.split('|');
return { message, hash };
});

const features = [];
const fixes = [];

commits.forEach(({ message, hash }) => {
const typeMatch = message.match(/^(feat|fix)\(([^)]+)\):\s*(.+)/i);
if (typeMatch) {
const [, type, scope, description] = typeMatch;
const entry = `- **${scope}**: ${description} ([\`${hash}\`](${repoUrl}/commit/${hash}))`;
if (type === 'feat') {
features.push(entry);
} else if (type === 'fix') {
fixes.push(entry);
}
}
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can serve a temporary fix but we should really be using the changesets tooling to create this content from it's new source (which is not the commit messages): https://github.com/changesets/changesets/blob/main/docs/modifying-changelog-format.md#writing-changelog-formatting-functions

I think the challenge with this as a sustainable approach is that less and less useful data is present in commit messages and the real value for customers now lives in the changesets files.


// Skip if nothing relevant
if (!features.length && !fixes.length) {
console.log('🚫 No new feat() or fix() commits to add.');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
console.log('🚫 No new feat() or fix() commits to add.');
console.log('🚫 No new feat() or fix() commits to add.');

Since no new features or fixes isn't necessarily a failure of the script, should we format this more like a success message that it ran successfully but with no changes?

process.exit(0);
}

// Format new changelog entry
let newEntry = `# [${newVersion}](${compareUrl}) (${date})\n\n`;

if (fixes.length) {
newEntry += `### Bug Fixes\n\n${fixes.join('\n')}\n\n`;
}

if (features.length) {
newEntry += `### Features\n\n${features.join('\n')}\n\n`;
}

// Prepend to existing CHANGELOG.md
const changelogPath = path.resolve(__dirname, '../CHANGELOG.md');
const existingChangelog = fs.existsSync(changelogPath)
? fs.readFileSync(changelogPath, 'utf-8')
: '';

fs.writeFileSync(
changelogPath,
`${newEntry.trim()}\n\n${existingChangelog}`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this push the # Change log heading down to below the latest update?

'utf-8'
);
console.log(`✅ CHANGELOG updated for ${newVersion}`);
Loading