Skip to content

Commit

Permalink
Feat(ci): Introduce posting changelog into the Slack channel
Browse files Browse the repository at this point in the history
refs #1781
  • Loading branch information
literat committed Jan 10, 2025
1 parent 591b1f5 commit 140ad56
Show file tree
Hide file tree
Showing 8 changed files with 769 additions and 8 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SLACK_CHANGELOG_WEBHOOK_URL=
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 post-changelog
env:
SLACK_CHANGELOG_WEBHOOK_URL: ${{ secrets.SLACK_CHANGELOG_WEBHOOK_URL }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ nx-cloud.env
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# Dotenv
.env*
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
"node": ">=18"
},
"workspaces": [
"apps/demo",
"apps/storybook",
"configs/*",
"packages/*",
"examples/*",
"exporters/*",
"apps/demo",
"apps/storybook"
"packages/*",
"scripts/*"
],
"scripts": {
"start": "yarn packages:start --ignore '@almacareer/spirit-example*'",
Expand Down Expand Up @@ -54,6 +55,7 @@
"packages:diff": "lerna diff",
"packages:changed": "lerna changed",
"packages:list": "lerna ls",
"post-changelog": "yarn workspace @lmc-eu/spirit-post-changelog post-changelog",
"release": "npm-run-all --serial packages:build && packages:publish",
"version": "yarn format:fix:changelog"
},
Expand Down
26 changes: 26 additions & 0 deletions scripts/post-changelog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# `post-changelog`

> Post Changelog to Slack
Parses the latest changelog entry when new package version is release and posts it to our Slack notifications channel.

Dry run:

```shell
yarn zx scripts/post-changelog.mjs --dry
```

Production run:

```shell
yarn zx scripts/post-changelog.mjs
```

## Development & Testing

Set up a local `.env` file with the content based on the `.env.example` file.

See the [Slack Block Kit documentation][slack-block-kit-docs] and [chat.postMessage API method documentation][slack-post-message-docs] for more information.

[slack-block-kit-docs]: https://api.slack.com/reference/block-kit/
[slack-post-message-docs]: https://api.slack.com/methods/chat.postMessage
28 changes: 28 additions & 0 deletions scripts/post-changelog/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@lmc-eu/spirit-post-changelog",
"version": "1.0.0",
"description": "Post changelog to Slack of the Spirit Design System",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/lmc-eu/spirit-design-system.git",
"directory": "scripts/post-changelog"
},
"license": "MIT",
"keywords": [
"post",
"changelog",
"slack"
],
"scripts": {
"post-changelog": "yarn zx ./post-changelog.mjs"
},
"dependencies": {
"dotenv": "^16.4.7",
"dotenv-safe": "^9.1.0",
"gitdiff-parser": "^0.3.1",
"simple-git": "^3.27.0",
"slackify-markdown": "^4.4.0",
"zx": "^8.3.0"
}
}
232 changes: 232 additions & 0 deletions scripts/post-changelog/post-changelog.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// This script is based on @see { @link https://github.com/kiwicom/orbit/blob/master/scripts/post-changelog.mjs }
/* eslint-disable import/no-unresolved */
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable no-console */
import { fileURLToPath } from 'url';
import dotenv from 'dotenv-safe';
import gitDiffParser from 'gitdiff-parser';
import { simpleGit } from 'simple-git';
import slackifyMarkdown from 'slackify-markdown';
import { $, fetch, argv, path } from 'zx';

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

/**
* Generates a title for the given package.
*
* @returns {string} The generated title.
*/
function getTitle() {
return `🚀 New release published`;
}

/**
* 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.log('Error posting to Slack');
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') {
const output = str
.replace(
/^(#+ )(.+)/,
`# 📦 ${packageName
.split('-')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ')} _$2_ \`${prefix}/spirit-${packageName}\``,
)
.replace(/\(\d{4}-\d{2}-\d{2}\)/, '') // Remove release date
.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) {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

return path.resolve(__dirname, `../../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: [
{
color: COLOR_CORE,
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: getTitle(packageName),
emoji: true,
},
},
],
},
{
// The `text` field is used as fallback, but it has higher character limit (4000) then section block
// @see { @link https://api.slack.com/methods/chat.postMessage#text_usage}
// @see { @link https://api.slack.com/reference/block-kit/blocks#section_fields }
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',
});
SLACK_CHANGELOG_WEBHOOK_URL = process.env.SLACK_CHANGELOG_WEBHOOK_URL;
} 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(
argv.dry ? '@lmc-eu/[email protected]' : (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(slackifiedChangelog);
} 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

0 comments on commit 140ad56

Please sign in to comment.