Make your monorepo CI better! Adjust rush.js changelog!
Tags: javascript, rush, tutorial, programming
Published: 2023-03-06 14:28:26

This article would be useful if you use [rush.js](https://rushjs.io/) as monorepo tool. Rush is a really powerful tool. One of the features is the interactive CLI-friendly changelog generator. If you use rush.js your changelog looks something like this - [CHANGELOG.md](https://github.com/microsoft/rushstack/blob/master/libraries/node-core-library/CHANGELOG.md). 
For more information about `rush change` command, you could follow the [link](https://rushjs.io/pages/commands/rush_change/).

What if you want to add common information for each of your changelog, e.g. link to Jira ticket, link to Merge Request, the Author? This script would help you! You could use it as part of CI or create another one based on this idea.

Pre-requirements:

- Branch name pattern: `<type>/<jira-ticket>-...`

Example: `feature/PT-777-adjust-changelog-script`

If you use something like this script your changelog would become more informative:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nm2ic6rfsfdx9epmztvy.png)

It could help the QA team with their ticket flow (they could easily find how has done some changes, etc.)

FYI, I use this script as a part of our CI/CD in production development.

## Let’s start implementing our script

Firstly, we need some utils to work with the file system.

```javascript
// src/fileUtils.js

import fs from 'fs';
import path from 'path';

export const getNoSuchFileErrorMessage = (dirPath) => `No such file or directory ${dirPath}`;

export const getAllFilesFromDir = (dirPath, arrayOfFiles) => {
  let files;
  try {
    files = fs.readdirSync(dirPath);
  } catch (error) {
    throw new Error(getNoSuchFileErrorMessage(dirPath));
  }

  arrayOfFiles = arrayOfFiles || [];

  files.forEach((file) => {
    if (fs.statSync(`${dirPath}/${file}`).isDirectory()) {
      arrayOfFiles = getAllFilesFromDir(`${dirPath}/${file}`, arrayOfFiles);
    } else {
      arrayOfFiles.push(path.join(dirPath, '/', file));
    }
  });

  return arrayOfFiles;
};
```

I have used the recursive method of getting an array of file from nested directories.

Okay, now we could add stuff for generating addition changelog information. I want to get Jira ticket link, MR link and Author link to adjust my changelog.

```javascript
// src/adjustChangelog.js

const getJiraTicketLink = (jiraDomain, branchName) => {
  const branchInfo = branchName.split('/')[1];
  const jiraProject = branchInfo.split('-')[0];
  const jiraTicket = branchInfo.split('-')[1];
  return `[${jiraProject}-${jiraTicket}](https://${jiraDomain}.atlassian.net/browse/${jiraProject}-${jiraTicket})`;
};
const getMergeRequestLink = (gitlabDomain, gitlabProjectRoute, mergeRequestIID) => {
  return `[!${mergeRequestIID}](https://${gitlabDomain}/${gitlabProjectRoute}/-/merge_requests/${mergeRequestIID})`;
};
const getAuthorLink = (gitlabDomain, authorName, authorLogin) => {
  return `[${authorName}](https://${gitlabDomain}/${authorLogin})`;
};
```
Let’s generate join all the information into one string.

```javascript
// src/adjustChangelog.js

const getChangelogInfo = (
  jiraDomain,
  gitlabDomain,
  gitlabProjectRoute,
  branchName,
  mergeRequestIID,
  authorName,
  authorLogin
) => {
	const jiraTicketLink = getJiraTicketLink(jiraDomain, branchName);
	const mergeRequestLink = getMergeRequestLink(
    gitlabDomain,
    gitlabProjectRoute,
    mergeRequestIID
  );
	const authorLink = getAuthorLink(gitlabDomain, authorName, authorLogin);
  
	return `${jiraTicketLink} / ${mergeRequestLink} / by ${authorLink}`;
};
```
In our main function we should get the array of rush change files using pre-defined `getAllFilesFromDir` method.

```javascript
// src/adjustChangelog.js

const handleIncorrectCommentMessage = () => {
  throw new Error(`Error: Your change file should provide the correct format of changelog info.`);
};

const adjustChangelog = async () => {
  let changeFiles;
  try {
    changeFiles = getAllFilesFromDir(changeFilesDir);
  } catch (error) {
    const noSuchFileError = getNoSuchFileErrorMessage(changeFilesDir);
    if (error.toString().includes(noSuchFileError)) {
      process.exit(0);
    }
    throw new Error(error);
  }
};
```
To get `branchName, mergeRequestIID, authorName, authorLogin` I’m going to use GitLab CI variables, especially the next ones: `CI_MERGE_REQUEST_SOURCE_BRANCH_NAM, ECI_MERGE_REQUEST_IID, GITLAB_USER_NAME, GITLAB_USER_LOGIN` .

After that, we could easily adjust file content. Don’t forget to handle the already adjusted files and method behaviour for special git branches e.g. master.

I have added some loggers to get the info in CI logs.

```javascript
// src/adjustChangelog.js

const CHANGELOG_INFO_REGEX =
  /\[[A-Z]+-[0-9]+\]\(https:\/\/[a-zA-Z0-9_.\-/]+\.atlassian\.net\/browse\/[A-Z]+-[0-9]+\) \/ \[![[0-9]+\]\(https:\/\/[a-zA-Z0-9_.\-/]+\/-\/merge_requests\/[0-9]+\) \/ by \[.+\]\(https:\/\/[a-zA-Z0-9_.\-/]+\/.+\)/;

const rewriteChangeFile = (file, fileContent, textForReplacing) => {
  const newValue = fileContent.replace(/"comment": "/, `"comment": "${textForReplacing} `);
  fs.writeFileSync(file, newValue, 'utf-8');
};

const adjustChangelog = async (jiraDomain, gitlabDomain, gitlabProjectRoute) => {
  const branchName = process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME;
  const mergeRequestIID = process.env.CI_MERGE_REQUEST_IID;
  const authorName = process.env.GITLAB_USER_NAME;
  const authorLogin = process.env.GITLAB_USER_LOGIN;
  const changeFilesDir = 'common/changes';

  if (branchName === 'master') handleIncorrectCommentMessage();

  let changeFiles;
  try {
    changeFiles = getAllFilesFromDir(changeFilesDir);
  } catch (error) {
    const noSuchFileError = getNoSuchFileErrorMessage(changeFilesDir);
    if (error.toString().includes(noSuchFileError)) {
      process.exit(0);
    }
    throw new Error(error);
  }

  changeFiles.forEach((file) => {
    console.log(`Verify ${file}`);
    const fileContent = fs.readFileSync(file, 'utf-8');

    if (!CHANGELOG_INFO_REGEX.test(fileContent)) {
      const textForReplacing = getChangelogInfo(
        jiraDomain,
        gitlabDomain,
        gitlabProjectRoute,
        branchName,
        mergeRequestIID,
        authorName,
        authorLogin
      );
      rewriteChangeFile(file, fileContent, textForReplacing);
      console.log('Changelog info was successfully added.');
    }
  });
};
```
## The full code example

```javascript
// src/fileUtils.js

import fs from 'fs';
import path from 'path';

export const getNoSuchFileErrorMessage = (dirPath) => `No such file or directory ${dirPath}`;

export const getAllFilesFromDir = (dirPath, arrayOfFiles) => {
  let files;
  try {
    files = fs.readdirSync(dirPath);
  } catch (error) {
    throw new Error(getNoSuchFileErrorMessage(dirPath));
  }

  arrayOfFiles = arrayOfFiles || [];

  files.forEach((file) => {
    if (fs.statSync(`${dirPath}/${file}`).isDirectory()) {
      arrayOfFiles = getAllFilesFromDir(`${dirPath}/${file}`, arrayOfFiles);
    } else {
      arrayOfFiles.push(path.join(dirPath, '/', file));
    }
  });

  return arrayOfFiles;
};
```

```javascript
// src/adjustChangelog.js

import fs from 'fs';

import { getAllFilesFromDir, getNoSuchFileErrorMessage } from './fileUtils.js';

const CHANGELOG_INFO_REGEX =
  /\[[A-Z]+-[0-9]+\]\(https:\/\/[a-zA-Z0-9_.\-/]+\.atlassian\.net\/browse\/[A-Z]+-[0-9]+\) \/ \[![[0-9]+\]\(https:\/\/[a-zA-Z0-9_.\-/]+\/-\/merge_requests\/[0-9]+\) \/ by \[.+\]\(https:\/\/[a-zA-Z0-9_.\-/]+\/.+\)/;

const handleIncorrectCommentMessage = () => {
  throw new Error(`Error: Your change file should provide the correct format of changelog info.`);
};

const rewriteChangeFile = (file, fileContent, textForReplacing) => {
  const newValue = fileContent.replace(/"comment": "/, `"comment": "${textForReplacing} `);
  fs.writeFileSync(file, newValue, 'utf-8');
};

const getJiraTicketLink = (jiraDomain, branchName) => {
  const branchInfo = branchName.split('/')[1];
  const jiraProject = branchInfo.split('-')[0];
  const jiraTicket = branchInfo.split('-')[1];
  return `[${jiraProject}-${jiraTicket}](https://${jiraDomain}.atlassian.net/browse/${jiraProject}-${jiraTicket})`;
};
const getMergeRequestLink = (gitlabDomain, gitlabProjectRoute, mergeRequestIID) => {
  return `[!${mergeRequestIID}](https://${gitlabDomain}/${gitlabProjectRoute}/-/merge_requests/${mergeRequestIID})`;
};
const getAuthorLink = (gitlabDomain, authorName, authorLogin) => {
  return `[${authorName}](https://${gitlabDomain}/${authorLogin})`;
};

const getChangelogInfo = (
  jiraDomain,
  gitlabDomain,
  gitlabProjectRoute,
  branchName,
  mergeRequestIID,
  authorName,
  authorLogin
) => {
  const jiraTicketLink = getJiraTicketLink(jiraDomain, branchName);
  const mergeRequestLink = getMergeRequestLink(gitlabDomain, gitlabProjectRoute, mergeRequestIID);
  const authorLink = getAuthorLink(gitlabDomain, authorName, authorLogin);

  return `${jiraTicketLink} / ${mergeRequestLink} / by ${authorLink}`;
};

const adjustChangelog = async (jiraDomain, gitlabDomain, gitlabProjectRoute) => {
  const branchName = process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME;
  const mergeRequestIID = process.env.CI_MERGE_REQUEST_IID;
  const authorName = process.env.GITLAB_USER_NAME;
  const authorLogin = process.env.GITLAB_USER_LOGIN;
  const changeFilesDir = 'common/changes';

  if (branchName === 'master') handleIncorrectCommentMessage();

  let changeFiles;
  try {
    changeFiles = getAllFilesFromDir(changeFilesDir);
  } catch (error) {
    const noSuchFileError = getNoSuchFileErrorMessage(changeFilesDir);
    if (error.toString().includes(noSuchFileError)) {
      process.exit(0);
    }
    throw new Error(error);
  }

  changeFiles.forEach((file) => {
    console.log(`Verify ${file}`);
    const fileContent = fs.readFileSync(file, 'utf-8');

    if (!CHANGELOG_INFO_REGEX.test(fileContent)) {
      const textForReplacing = getChangelogInfo(
        jiraDomain,
        gitlabDomain,
        gitlabProjectRoute,
        branchName,
        mergeRequestIID,
        authorName,
        authorLogin
      );
      rewriteChangeFile(file, fileContent, textForReplacing);
      console.log('Changelog info was successfully added.');
    }
  });
};

export default adjustChangelog;
```

You could contribute to the script repository on [GitHub](https://github.com/BorysShulyak/rush-adjust-changelog). Waiting for discussion and reactions!