Skip to content
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

Feat(ci): Introduce posting changelog into the Slack channel #1797

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,8 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

- name: Post Changelog
run: yarn zx scripts/post-changelog.mjs
env:
SLACK_CHANGELOG_WEBHOOK_URL: ${{ secrets.SLACK_CHANGELOG_WEBHOOK_URL }}
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,14 @@
"babel-loader": "9.2.1",
"core-js": "3.39.0",
"css-loader": "7.1.2",
"dotenv-safe": "^9.1.0",
"eslint": "8.57.1",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-prettier": "5.2.1",
"eslint-plugin-react": "7.37.2",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-storybook": "0.11.1",
"gitdiff-parser": "^0.3.1",
"husky": "9.1.7",
"is-ci": "3.0.1",
"lerna": "8.1.8",
Expand All @@ -96,11 +98,14 @@
"remark-frontmatter": "5.0.0",
"remark-lint-heading-capitalization": "1.2.0",
"sass-loader": "14.2.1",
"simple-git": "^3.27.0",
"slackify-markdown": "^4.4.0",
"style-loader": "4.0.0",
"typescript": "4.7.4",
"vite-raw-plugin": "1.0.2",
"webpack": "5.96.1",
"webpack-cli": "5.1.4"
"webpack-cli": "5.1.4",
"zx": "^8.2.4"
},
"resolutions": {
"typescript": "4.7.4"
Expand Down
210 changes: 210 additions & 0 deletions scripts/post-changelog.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable no-console */
import dotenv from 'dotenv-safe';
import gitDiffParser from 'gitdiff-parser';
import { simpleGit } from 'simple-git';
import slackifyMarkdown from 'slackify-markdown';
import { $, fetch, argv } from 'zx';

const COLOR_CORE = '#00A58E';
const PACKAGES = ['web', 'web-react', 'web-twig', 'design-tokens', 'icons', 'codemods', 'analytics'];
const SLACK_CHANGELOG_WEBHOOK_URL = process.env.SLACK_CHANGELOG_WEBHOOK_URL ?? '';

/**
* Generates a title for the given package.
*
* @param {string} pkg - The package name.
* @returns {string} The generated title.
*/
function getTitle(pkg) {
return `🚀 New ${pkg
.split('-')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ')} package release`;
}

/**
* Sends the content to the specified webhook URL.
*
* @param {object} params - The parameters.
* @param {object} params.content - The content to send.
* @param {string} params.webhookUrl - The webhook URL to send the content to.
*/
async function sendToWebhook({ content, webhookUrl }) {
await fetch(webhookUrl, {
method: 'POST',
body: JSON.stringify(content),
})
.then((res) => {
if (res.status !== 200) {
throw new Error(`${res.status} ${res.statusText}`);
}
})
.catch((err) => {
console.error(err);
process.exit(1);
});
}

/**
* Formats the changelog string with the given package name and prefix.
*
* @param {string} str - The changelog string to format.
* @param {string} packageName - The name of the package.
* @param {string} prefix - The prefix to use for the package.
*
* @returns {string} The formatted changelog string.
*/
function format(str, packageName, prefix = '@lmc-eu') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Screenshot 2024-12-16 at 12 51 49

Ideas

  • Can we get rid of the package name in individual list items?

  • To reduce visual clutter, I'd consider:

    • merging the two lines with "new release" and package name into one,
    • maybe removing the date since it's part of the message meta data,
    • for example:
    **🚀 New `@lmc-eu/spirit-web` package release [3.1.0][#]**

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I think can do many things. Primarily, this should help us with fabricating release notes. I know that just copy&paste will not be enough but it can save a lot of time. So let's modify this to our liking. :-)

const output = str
.replace(
/^#+ /,
`# 📦 ${packageName
.split('-')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ')} \`${prefix}/spirit-${packageName}\` `,
)
.replace('Bug Fixes', '🐛 Bug Fixes')
.replace('Features', '⚡ Features')
.replace('BREAKING CHANGES', '🚨 BREAKING CHANGES')
.replace('Dependencies', '📦 Dependencies')
.replace('Documentation', '📜 Documentation')
.replace('Tests', '🧪 Tests')
.replace('Code Refactoring', '🛠️ Code Refactoring')
.replace('Chores', '🔨 Chores')
.replace('Styles', '💅 Styles')
.replaceAll('https://github.com/lmc-eu/spirit-design-system/issues/', 'https://jira.almacareer.tech/browse/');

return output;
}

/**
* Extracts the changelog content from the given diff files.
*
* @param {Array} files - The diff files to extract the changelog from.
* @returns {string} The extracted changelog content.
*/
function getChangelogFromDiff(files) {
// Only one file as we're only looking at the changelog
const versionPattern = /<a name=".*"><\/a>/;
const [changelogFile] = files;
const changelog = changelogFile.hunks
.flatMap((hunk) => hunk.changes.filter(({ isInsert }) => isInsert).map(({ content }) => content))
.filter((line) => !versionPattern.test(line))
.join('\n')
.trim();

return changelog;
}

/**
* Gets the diff for the given tag and path.
*
* @param {string} tag - The tag to get the diff for.
* @param {string} path - The path to get the diff for.
* @returns {Promise<string>} The diff output.
*/
function getDiff(tag, path) {
return simpleGit().show([tag, path]);
}

/**
* Returns the changelog path for the given package name.
*
* @param {string} packageName - The name of the package.
* @returns {string} The changelog path.
*/
function changelogPath(packageName) {
return `packages/${packageName}/CHANGELOG.md`;
}

/**
* Posts a Slack notification with the given changelog.
*
* @param {string} changelog - The changelog content to post.
* @param {string} packageName - The name of the package.
*
* @returns {Promise<void>}
*/
async function postSlackNotification(changelog, packageName) {
try {
$.verbose = false;
const res = await sendToWebhook({
webhookUrl: SLACK_CHANGELOG_WEBHOOK_URL,
content: {
attachments: [
{
title: getTitle(packageName),
text: changelog,
color: COLOR_CORE,
},
],
},
});

return res;
} catch (err) {
console.log('Error posting to Slack');
console.error(err);
}

return null;
}

/**
* Configures the webhook URL from the environment variables.
*/
async function configureWebhookURL() {
try {
dotenv.config({
allowEmptyValues: true,
example: '.env.example',
});
} catch (err) {
if (/SLACK_CHANGELOG_WEBHOOK_URL/g.test(err.message)) {
throw new Error('SLACK_CHANGELOG_WEBHOOK_URL is not set');
}
}
}

/**
* Publish the changelog for the given npm package.
*
* @param {string} npmPackage - The name of the npm package.
*/
async function publishChangelog(npmPackage) {
try {
await simpleGit().fetch(['origin', 'main', '--tags']);
const tags = await simpleGit().tags({ '--sort': '-taggerdate' });
const diff = await getDiff(tags.latest ?? '', changelogPath(npmPackage));
const files = gitDiffParser.parse(diff);
if (files.length === 0) {
console.log(`No changes in ${npmPackage}`);

return;
}
const changelog = getChangelogFromDiff(files);
const formattedChangelog = format(changelog, npmPackage);
const slackifiedChangelog = slackifyMarkdown(formattedChangelog);

if (argv.dry) {
console.info(formattedChangelog);
} else {
await configureWebhookURL();
await postSlackNotification(slackifiedChangelog, npmPackage);
}
} catch (err) {
console.error(err);
process.exit(1);
}
}

(async () => {
await Promise.all(
PACKAGES.map(async (npmPackage) => {
await publishChangelog(npmPackage);
}),
);
process.exit(0);
})();
Loading
Loading