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 1 commit 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
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
Loading