DEV Community

Boris Shulyak
Boris Shulyak

Posted on

Make your monorepo CI better! Adjust rush.js changelog!

This article would be useful if you use rush.js 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.
For more information about rush change command, you could follow the link.

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

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.

// 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;
};
Enter fullscreen mode Exit fullscreen mode

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.

// 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})`;
};
Enter fullscreen mode Exit fullscreen mode

Let’s generate join all the information into one string.

// 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}`;
};
Enter fullscreen mode Exit fullscreen mode

In our main function we should get the array of rush change files using pre-defined getAllFilesFromDir method.

// 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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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.

// 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.');
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

The full code example

// 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;
};
Enter fullscreen mode Exit fullscreen mode
// 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;
Enter fullscreen mode Exit fullscreen mode

You could contribute to the script repository on GitHub. Waiting for discussion and reactions!

Top comments (0)