<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Naveed Ausaf's blog]]></title><description><![CDATA[Naveed Ausaf's blog]]></description><link>https://www.naveedausaf.com</link><generator>RSS for Node</generator><lastBuildDate>Wed, 27 May 2026 15:20:35 GMT</lastBuildDate><atom:link href="https://www.naveedausaf.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Automate version numbers and release notes with "semantic-release" [IN POGRESS]]]></title><description><![CDATA[Introduction
semantic-release is an NPM package that you can use to automatically increment version number and generate release notes whenever you release your repo's code to production.
If your repo ]]></description><link>https://www.naveedausaf.com/automate-version-numbers-and-release-notes-with-semantic-release-in-pogress</link><guid isPermaLink="true">https://www.naveedausaf.com/automate-version-numbers-and-release-notes-with-semantic-release-in-pogress</guid><dc:creator><![CDATA[Naveed Ausaf]]></dc:creator><pubDate>Mon, 20 Apr 2026 19:40:23 GMT</pubDate><content:encoded><![CDATA[<h2>Introduction</h2>
<p><a href="https://github.com/semantic-release/semantic-release/blob/master/docs/usage/getting-started.md#getting-started">semantic-release</a> is an NPM package that you can use to automatically <strong>increment version number</strong> and <strong>generate release notes</strong> whenever you release your repo's code to production.</p>
<p>If your repo is on GitHub, the version number and release notes can be published to a <strong>release</strong> in the <strong>Releases</strong> section of the repo:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728903490743/fb4e5415-fe86-437b-847d-6813a24ef2bd.png" alt="" style="display:block;margin:0 auto" />

<h3>How semantic-release works</h3>
<p>Every time there is a commit to <code>main</code>, a release to prod pipeline (a GitHub Actions workflow) springs into action and runs semantic-release which would analyze commit messages on <code>main</code>.</p>
<p>semantic-release then decides whether any of the changes are significant enough that the version number should to be incremented and therefore, details about those changes should be published in release notes.</p>
<p>If so, then two things would happen:</p>
<ul>
<li><p>semantic-release would bump the version number, generate release notes, and publish these two items as a GitHub <strong>release</strong> of the sort shown above.</p>
</li>
<li><p>the deployment logic checks if version number was indeed incremented and release notes generated, and if so, it would deploy the deployment artifacts (binaries, contianer etc.) of this new version to production.</p>
</li>
</ul>
<h2>What this guide covers</h2>
<p><strong>In this post I will show you how to set up semantic-release in a GitHub repo</strong>.</p>
<p>The example I use is particularly simple: deploying an NPM package to the public NPM registry.</p>
<p>This would allow me to explain in detail how to setup semantic-release in your repo, without getting bogged down in the details of the deployment logic or Infrastructure-as-Code (IaC).</p>
<p>In part 2 of this post, I’ll show how to deploy a more complex monorepo, with a Next.js frontend app, a .NET Core API and a PostgreSQL database.</p>
<p>The developer workflow I use is built around pull requests: I assume that you do not push directly to <code>main</code> and instead push first to a feature branch and open a pull request for merge of the feature branch to <code>main</code>. Once the changes in the feature branch have been reviewed, the pull request is merged.</p>
<p>However, there’s no reason why you can’t set up semantic-release if you do trunk-based development (i.e. push directly to <code>main</code>, without feature branches). Just follow the details below and ignore anything to do with pull requests or GitHub workflows that have a <code>pull_request</code> trigger.</p>
<h2>Prerequisites</h2>
<p>To follow along, you would need to know the basics of Git and GitHub Actions.</p>
<p>In addition you would need a passing familiarity with installing NPM packages in a folder (although you don’t need to be a JavaScript or Node.js developer).</p>
<h2>Key Concepts</h2>
<p>Before we dive into semantic-release setup, I would like to explain a few concepts.</p>
<h3>Semantic Versioning</h3>
<p><a href="https://semver.org/">Semantic Versioning specification</a> (or <em>SemVer</em> for short) specifies version numbers of the form <strong>..</strong> e.g. <code>1.0.12</code>.</p>
<p>When releasing a new version of a project, exactly one of the three components of the project's version number is incremented, and the ones to the right reset to <code>0</code>, according to the following rules:</p>
<ul>
<li><p>is incremented if the new release contains neither new functionality nor a breaking change, for example if it only contains bug fixes or documentation changes.</p>
<p>So in the above version, we would go from <code>1.0.12</code> to <code>1.0.13</code>.</p>
</li>
<li><p>is incremented if there are new features in the release but clients of the previous version can switch to the new one without breaking.</p>
<p>From <code>1.0.12</code>, we would go to <code>1.1.0</code>.</p>
</li>
<li><p>should be incremented if the new release contains a change that would break clients of the previous release. If those clients want to use the new release, they would have to update their code or other artifacts.</p>
<p>Incrementing the major version would take us from <code>1.0.12</code> to <code>2.0.0</code>.</p>
<p>For libraries it is obvious when the major version of a library would need to be updated: when it would not be compatible with client code that uses a previous version of the library.</p>
<p>However, for user-facing projects such as web or mobile apps, the situation is less clear-cut. We should never break anything for our users with a new release. Therefore if they have investments in things like data or workflows that they have created, these should be automatically migrated to work with the new release if necessary. Thus ideally, no existing clients or their artifacts would ever break. For such projects, incrementing the major version number may be a marketing decision, such as when there are big-bang changes like a new or a revamped UI or the addition of generative AI features to an app.</p>
</li>
</ul>
<p><strong>Are there changes that should not lead to the version number being incremented? YES!</strong></p>
<p>The version number is only incremented if there are <strong>changes to functionality</strong>.</p>
<p>This means if you update READMEs or comments, the version number wouldn’t be incremented as there is not new functionality. Hence no release notes would be generated and there wouldn’t be another deployment to prod.</p>
<p>Similarly if you refactor your code, then although your release pipeline should run the whole test suite on your commits to verify these changes, this doesn’t mean you need to do a code deployment also: deployments take resources and are always risky. Why would you want to deploy if there are no changes in customer-observable behaviour or functionality?</p>
<p>Thus, generally, any commits which only contain refactorings or code cleanup should not cause the version number to be incremented. These would also not go into the release notes: customers do not need to know about them. And they wouldn’t trigger a deploy to prod.</p>
<h3>How does <code>semantic-release</code> know which commits should trigger a release?</h3>
<p>When <code>semantic-release</code> is run in the root folder of the NPM package in which it is installed (using command <code>npx semantic-release</code>), it scans the commit graph of the currently checked out branch, looking for certain keywords in commit messages. These keywords are defined by the configured <a href="https://github.com/semantic-release/semantic-release#commit-message-format">commit message format</a>.</p>
<p>There are many different commit message formats (aka <em>commit message conventions</em>). The one used in this tutorial is defined by the Angular project and is referred to as the <a href="https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit">Angular commit message format</a>. This is the default in semantic-release.</p>
<p>Another popular commit message format is <a href="https://www.conventionalcommits.org/en/v1.0.0/"><strong>Conventional Commits</strong></a><strong>.</strong> It is similar to the Angular format.</p>
<p>Some of the keywords that the Angular format defines are as follows:</p>
<ul>
<li><p><code>fix:</code> as the prefix of a commit message indicates that the commit contains a change that is neither new functionality nor a breaking change.</p>
</li>
<li><p><code>feat:</code> as the prefix of a commit message indicates the commit contains new functionality but not a breaking change</p>
</li>
<li><p><code>BREAKING CHANGE</code> or <code>BREAKING CHANGES</code> in <em>message footer</em> (i.e. in the last line of commit message) indicates the commit contains a breaking change.</p>
</li>
<li><p><code>ci:</code> or <code>build:</code> as commit message prefix indicates that changes in the commit pertain to CI pipeline or build logic respectively.</p>
</li>
</ul>
<p>Based on the semantics of above keywords (described above and fully defined in <a href="https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit">Angular commit message format</a>) and the semver specification (described in previous section), semantic-release takes <code>fix</code> to indicate that the patch component of the version number needs to be incremented, <code>feat</code> to indicate an increment in the minor component, <code>BREAKING CHANGE</code> and <code>BREAKING CHANGES</code> to indicate an increment in the major component of the version number.</p>
<p>Commit message containing prefixes <code>ci</code> and <code>build</code> are ignored by semantic-release and do not contribute to an increment in any part of the version number. This is obviously an opinionated choice and you can override it in configuration.</p>
<p>When run in the root of the project, semantic-release scans all commits in the commit graph, starting from the most recent commit that has a semver tag up to <code>HEAD</code> (i.e. the latest commit), looking for these keywords. Based on the keywords encountered in commit messages, it decides which one of the , or components should be incremented (or none at all, if all keywords encountered were those that do not indicate an increment in any part of the version number, such as <code>ci</code> and <code>build</code> above).</p>
<p>In the picture below the most recent commit on <code>main</code> with a semver tag has the tag <code>v1.0.1</code>. This is the version number of the last release from the branch. So semantic-release would look at the commit message of each of the four commits forward of this.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729042442927/9adae282-d743-4714-9d8c-1d8adb637c82.png" alt="" style="display:block;margin:0 auto" />

<p>The first two of the scanned commits indicate that and components respectively should be incremented. If these were the only two commits at the tip of <code>main</code>, the new version number would be <code>v1.1.0</code>: only one component, not both, would be incremented by semantic-release and is the more significant one of the two.</p>
<p>However, the third commit encountered has <code>BREAKING CHANGE</code> in its commit message footer. Its complete commit message is as follows:</p>
<pre><code class="language-yaml">feat: user account API change

Migrated REST API to GraphQL.
Existing clients will break.

BREAKING CHANGE
</code></pre>
<p>Therefore, even though it has the <code>feat</code> prefix in its commit message which indicates an increment in the minor component, this commit is taken to be a breaking change and therefore indicates an increment in the major component of the version number.</p>
<p>Therefore it is the major component in the last version, <code>v1.0.1</code>, that would be incremented to produce the new version number, <code>v2.0.0</code>.</p>
<p><strong>By the time semantic-release has finished running, it would have</strong>:</p>
<ul>
<li><p>tagged <code>v2.0.0</code> on the last commit in the graph</p>
</li>
<li><p>generated release notes for the new version.</p>
</li>
<li><p>Published a release to GitHub Releases section of the repo.</p>
</li>
</ul>
<p>Since a new deployment is only supposed to happen if semantic-release published a new release, and in this case it did, therefore if a Continuous Deployment pipeline were set up on this repo, the tagged commit would be deployed to production also.</p>
<h2>Initialize a Git repo to follow along</h2>
<p>To follow along, you can either fork the <a href="https://github.com/naveedausaf/semantic-release-demo">starter repo for this article</a> - this is a Next.js React app with a single page - or set up semantic-release in your own NPM package.</p>
<h3>To Use the Starter Project</h3>
<ol>
<li><p>Fork the <a href="https://github.com/naveedausaf/semantic-release-demohttps://github.com/naveedausaf/semantic-release-demo">repo</a>:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727138596207/f7a099e1-18b8-4aae-8870-86aa8b33a05f.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Clone your fork to your local machine:</p>
<pre><code class="language-bash">git clone &lt;your forked GitHub repo URL&gt;
</code></pre>
</li>
</ol>
<p>Now skip to the section <a href="#heading-set-up-husky-and-commitlint">Set up Husky and commitlint</a> below.</p>
<h3>To Use Your Own Project</h3>
<p>You can also follow along by installing semantic-release in an NPM package you already have, such as a Node.js package, a React app or an Angular app.</p>
<p>In order to do this, please go through the following requirements:</p>
<ul>
<li><p>Make sure a local Git repo is initialized in the package.</p>
<p>If your project does not already have a local Git repo initialised, git commands such as <code>git remote -v</code> would raise an error:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727128676365/68e972c3-cc90-4927-a61f-b094af2391aa.png" alt="" style="display:block;margin:0 auto" />

<p>In this case, you can initialise a git repo by running <code>git init</code> in project folder.</p>
</li>
<li><p>You have a GitHub repo for the package. If not, create it in GitHub.</p>
</li>
<li><p>The GitHub repo is added as a remote named <code>origin</code> in the local Git repo in your package.</p>
<p>You can view the remotes with command <code>git remote -v</code></p>
<p>You can add a remote named <code>origin</code> with command <code>git remote add origin &lt;URL of your GitHub repo&gt;</code></p>
</li>
<li><p>I am assuming that code would be released into production from a branch named <code>main</code> and that this is the branch on which semantic-release would run.</p>
<p>If you release from some other branch (e.g. <code>release</code> or <code>master</code>) then substitute the name of that branch for <code>main</code> when going through the rest of this tutorial.</p>
<p>In particular, in most of the GitHub actions workflows (<code>.yml</code> files) shown here, you might need to <a href="https://github.com/actions/checkout?tab=readme-ov-file#usage">set <code>ref</code> parameter</a> in action <code>actions/checkout@v3</code> to the name of the branch from which you release and/or substitute <code>main</code> with the name of your branch in workflow triggers.</p>
<p>Also, in the various config files created in section <a href="#heading-local-setup">Local Setup</a> below, you would likely need to substitute <code>main</code> with the name of the branch you release from.</p>
</li>
<li><p>OPTIONAL: I recommend that you have a <code>.gitattributes</code> file in your project root which contains the following line (for an explanation, see my post <a href="https://nausaf.hashnode.dev/lf-vs-crlf-configure-git-and-vs-code-to-use-unix-line-endings">LF vs CRLF - Configure Git and VS Code to use Unix line endings</a>):</p>
<pre><code class="language-plaintext">* text=auto eol=lf
</code></pre>
<p>If this file doesn’t exist, you can create it with command <code>echo "* text=auto eol=lf" &gt;&gt; .gitattributes</code></p>
</li>
</ul>
<h2>Local Setup</h2>
<p>I install the following packages in the project (i.e. in the NPM package in which I wish to set up semantic-release):</p>
<ul>
<li><p><a href="https://github.com/conventional-changelog/commitlint"><strong>commitlint</strong></a> lints commit messages. I configure it so that it checks that commit messages comply with <a href="https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit">Angular commit message conventions</a>. This ensures that semantic-release, which is also set up by default to use the Angular conventions, would be able to parse them.</p>
</li>
<li><p><a href="https://github.com/typicode/husky"><strong>Husky</strong></a> is used for registering commands to run in Git hooks. I register commitlint with Husky to run in the <code>commit-msg</code> hook. This means that commitlint would automatically run whenever a commit is made to the local repo and if it fails - i.e. the message of the commit does not comply with Angular commit message conventions - then <code>git commit</code> command would fail.</p>
<p>Thus the combination of commitlint and Husky helps prevent commit messages which semantic-release cannot parse from getting into the repo.</p>
</li>
<li><p><a href="https://semantic-release.gitbook.io/semantic-release"><strong>semantic-release</strong></a> also needs to be installed and configured in the project, although it will be invoked from the deployment pipeline rather than locally from command line.</p>
</li>
</ul>
<p>Set up these three tools in your project by following the steps below.</p>
<h3>Set up Husky and commitlint</h3>
<ol>
<li><p>Open the NPM package in which you wish to set up semantic-release in your code editor</p>
</li>
<li><p>On the terminal, in your project’s root folder, checkout main and install dependencies of yuor NPM package:</p>
<pre><code class="language-bash">git checkout main
git pull
npm install
</code></pre>
</li>
<li><p>On <code>main</code>, check out a new branch named <code>semantic-release-setup</code> on which we will set up semantic-release:</p>
<pre><code class="language-bash">git checkout -b semantic-release-setup
</code></pre>
</li>
<li><p>Install <code>commitlint</code>:</p>
<pre><code class="language-bash">npm install --save-dev @commitlint/config-angular @commitlint/cli conventional-changelog-angular
</code></pre>
</li>
<li><p><strong>Configure commit message rules for commitlint:</strong> in the root of your project, create a file named <code>commitlint.config.js</code> with the following content.</p>
<p><strong>If your package.json has</strong> <code>”type”: “module”</code> attribute (this lets you use ES6 modules without the the need for a bundler such as Webpack or Rollup), then replace the top line <code>module.exports = {</code> with <code>export default {</code> in the snippet shown below:</p>
<pre><code class="language-javascript">/* eslint-env node */
module.exports = {
  extends: ['@commitlint/config-angular'],
  parserPreset: 'conventional-changelog-angular',
  rules: {
    'header-max-length': [2, 'always', 72],
    'body-max-line-length': [2, 'always', 72],
    'header-full-stop': [2, 'never', '.'],
    'body-leading-blank': [2, 'always'],
  },
};
</code></pre>
<p><strong>This is what the lines of this config file do:</strong></p>
<ul>
<li><p><code>extends: ['@commitlint/config-angular'],</code> says that rules specified in package <code>@commitlint/config-angular</code>, which provides commitlint linting rules for the <a href="https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit">Angular commit message format</a> (as JS/TS functions, à la ESLint), should be used to lint commit messages. Commitlint rules packages exist for <a href="https://github.com/conventional-changelog/commitlint#shared-configuration">other conventions</a> also.</p>
</li>
<li><p><code>parserPreset: ['conventional-changelog-angular'],</code> I’ll explain in a second what this does. First allow me to explain what <code>conventional-changelog</code> is.</p>
<p><a href="https://github.com/conventional-changelog">conventional-changelog</a> is a sprawling ecosystem of tools and packages whose unifying theme seems to be parsing of commit messages in a repo according to a specified set of commit message conventions.<br />If commit messages are written according to a well-defined format or set of commit message <strong>conventions</strong>, then they describe, in a (quasi-)structured manner, the change that each commit contains. Therefore the commit messages, in order, form a <strong>changelog</strong> for the repo.</p>
<p>Hence the name <strong>conventional-changelog</strong> for the monorepo of tools and packages that help with parsing and processing such commit messages.<br />Commitlint itself and the various <code>@commitlint/*</code> packages that it uses or can use all sit <a href="https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint">within the conventional-changelog monorepo</a>.</p>
<p>Commitline uses the parser <a href="https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser#conventionalcommitsparseroptions"><code>conventional-commit-parser</code></a> to parse commit messages. The parsed messages are then passed to <code>@commitlint/config-angular</code> (configured in previous line) so that linting rules specific to Angular conventions can be applied. Both of these pacakges also sti within conventional-changelog monorepo.</p>
</li>
<li><p>What the line <code>parserPreset: ['conventional-changelog-angular'],</code> does is it configures package <a href="https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular#readme"><code>conventional-changelog-angular</code></a> as the <em>parser preset.</em></p>
<p>A parser preset package provide settings for <a href="https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser#conventionalcommitsparseroptions"><code>conventional-commit-parser</code></a>.</p>
<p>Note that parser preset for angular, <code>conventional-changelog-angular</code>, <a href="https://github.com/conventional-changelog/commitlint/blob/65b219e0a6992f4441046aad9fd874e73a30b8e8/%40commitlint/parse/src/index.ts#L5">is the default</a> in commitlint. However, since you need to explicitly supply the pacakge of linting rules specific to Angular format, <code>@commitlint/config-angular</code>, I feel the parser preset package for the same conventions should also be explicitly configured. This is what I have done.</p>
</li>
<li><p>Note that semantic-release uses the same parser, <a href="https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser#conventionalcommitsparseroptions"><code>conventional-commit-parser</code></a>, and the same parser preset, <a href="https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular#readme"><code>conventional-changelog-angular</code></a>, and numerous other packages from conventional-changelog monorepo, but itself sits ouside conventional-changelog.</p>
<p>For more details on parser and parser preset, see <a href="#heading-appendix-details-of-semantic-release-configuration-and-internal-workflow">Appendix: Details of semantic-release Configuration and Internal Workflow</a> at the end of this article.</p>
</li>
<li><p><code>rules</code> section declares custom rules, in addition to those given by the Angular rules package <code>@commitlint/config-angular</code> that we have configured. These additional help me write clean, readable commit messages.</p>
<p>For example <code>'header-max-length': [2, 'always', 72]</code> and <code>'body-max-line-length': [2, 'always', 72],</code> together ensure that lines of a commit message are not wider than 72 characters which is good for readability, not only in the terminal but also in GitHub UI.  </p>
<p>For example you wouldn't get the following <em>wide</em> render of a commit message if every line in the body had been limited to 72 characters:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66df9b90154f69c2562f9372/396c7fcb-7f54-4cb8-9361-a6a0647c5bf1.png" alt="" style="display:block;margin:0 auto" />

<p>Also, GitHub truncates the header (first/top line) of a commit message on the commit history page to 72-80 characters.  </p>
<p>Therefore it is worth having the 72 characters limit for both the header and body lines of a commit message as configured above.</p>
<p>See <a href="https://commitlint.js.org/reference/rules">commitlint rules reference</a> for more details.</p>
</li>
</ul>
</li>
<li><p><strong>If you use VS Code as your code editor:</strong> then add the following snippet to your <code>settings.json</code> file:  </p>
<pre><code class="language-json">"[scminput]": {
    // 52 just under 72 chars in the commit message window in Source Control Sidebar. A vertical line would be shown at this width.
    // I need this to ensure my commit message lines are not wider than 72 characters, which is a rule I enforce through commitlint (see commitline.config.js).
    "editor.rulers": [52],
    "editor.wordWrap": "off",
    "editor.fontFamily": "monospace"
  }
</code></pre>
<p>This gives you a faint vertical line in the commit message box of the Source Control sidebar to indicate (approximately) the width of 72 characters so that you can press Enter to move to the next line and avoid a commitlint error on commit:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66df9b90154f69c2562f9372/e7b7fbf0-d4cd-44a7-95a9-4ebdacd67105.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Install Husky:</p>
<pre><code class="language-bash">npm install --save-dev husky
</code></pre>
</li>
<li><p><strong>If you do not already have a</strong> <code>.husky</code> <strong>folder in your project root,</strong> then initialize Husky:</p>
<pre><code class="language-bash">npx husky init
</code></pre>
<p>One of the things this command does is it creates a <code>"prepare"</code> script in package.json.</p>
<p>The <code>"prepare"</code> script is a <a href="https://docs.npmjs.com/cli/v9/using-npm/scripts#life-cycle-scripts">lifecycle script</a> and one of the conditions under which it runs automatically is when <code>npm install</code> is run without arguments in the project folder.</p>
<p>The command we just ran set up the <code>prepare</code> script as <code>"prepare": "husky"</code>. This ensures that if you or a teammate fetches the repo and then runs <code>npm install</code> to install project’s NPM dependencies, then Husky would also run.</p>
<p>This would register Husky with Git as a handler for Git hooks. Thus if a command has been registered with Husky to run in one of these hooks - we will do this in the next step - Husky would run it whenever Git invokes it to run in that hook.</p>
</li>
<li><p>Register commitlint with Husky to run in Git <code>commit-msg</code> hook</p>
<pre><code class="language-bash">echo "npx --no -- commitlint --edit \$1" &gt;&gt; .husky/commit-msg
</code></pre>
<p>You should now find a file <code>.husky/commit-msg</code> in your project folder with line <code>npx --no -- commitlint --edit \(1</code> a the end of it (the <code>\</code> in the command shown above was only used to escape the <code>\)</code> character).</p>
<p><strong>In this command:</strong></p>
<p><code>--no</code> <a href="https://docs.npmjs.com/cli/v10/commands/npx">suppresses any promps</a> that npx might show</p>
<p><code>--</code> indicates to npx that subsequent arguments (<code>commitlint</code> which is the name of the package we want to run, <code>--edit</code> and <code>\(1</code>) <a href="https://stackoverflow.com/questions/43046885/what-does-do-when-running-an-npm-command">are all <em>positional arguments</em></a>, not named options or flags to <code>npx</code> itself. This prevents <code>--edit</code> and <code>\)1</code> from being interpreted as arguments to <code>npx</code>. Instead, the positional arguments are concatenated and executed by npx. Thus the command that <code>npx</code> executes is:</p>
<pre><code class="language-yaml">commitlint --edit $1
</code></pre>
<p>Here:</p>
<p><code>--edit</code> is a <a href="https://commitlint.js.org/reference/cli.html">commitlint option</a> that reads the commit message from a specified file. This file is specified as <code>$1</code>.</p>
<p><code>$1</code> is <a href="https://github.com/typicode/husky/discussions/1108">evaluated by Git to be <code>./.git/COMMIT_EDITMSG</code></a>. This file, in the hidden folder <code>.git</code> within the project folder, <a href="https://git-scm.com/docs/git-commit/2.2.3#_files">contains the commit message of the commit in progress</a>.</p>
</li>
<li><p>Inspect your <code>.husky/pre-commit</code> file and delete it if necessary:</p>
<pre><code class="language-bash">rm .husky/pre-commit
</code></pre>
<p>This file is autogenerated by <code>npx husky init</code> that we executed above.</p>
<p>By default it runs <code>npm run test</code> which I do not like to run in a Git hook as it would drastically increase the amount of time a <code>git commit</code> takes. Also, if you do not have a “test” script defined in package.json, there would be an error at <code>git commit</code> when <code>npm tun test</code> is executed.</p>
<p>If you do not have code in <code>.husky/pre-commit</code> that you definitely want to run in Git pre-commit hook, then delete the file.</p>
</li>
<li><p>Make sure you have an appropriate <code>.gitignore</code> in project root, as we are about to make our first commit of the tutorial.</p>
<p>I usually copy and paste an appropriate one from <a href="https://github.com/github/gitignore">GitHub’s gitignore collection</a> at the start of my projects.</p>
</li>
<li><p>Now try to execute the following on the terminal in your project’s root folder:</p>
<pre><code class="language-bash">
git add .
git commit -m "finished installing commitlint"
</code></pre>
<p>The command <code>npx --no -- commitlint --edit $1</code> we configured to run in Git <code>commit-msg</code> hook woud run.</p>
<p>This means that commitlint would run and lint the commit message.</p>
<p>Since the message does not comply with Angular commit message conventions - it doesn’t have <a href="https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit">one of the prescribed prefixes</a> such as <code>fix:</code>, <code>feat:</code>, or <code>ci:</code> - commilint would fail.</p>
<p>This in turn would make the <code>git commit</code> operation fail:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725987252690/63c84073-d898-4bdc-8e9e-4a74451f50b2.png" alt="" style="display:block;margin:0 auto" />

<p>We will fix this error in the next step.</p>
<p>Another error I sometimes get at this point is <code>ReferenceError: module is not defined in ES module scope</code> referring to <code>commitlint.config.js</code>. This happens if I didn’t follow step 5 above properly: my package.json has <code>”type”: “module”</code>, i.e. I have explicitly set the project to use ES6 modules, but the <code>commitlint.config.js</code> is still using CommonJS-style default export syntax.</p>
<p>To fix this, replace <code>module.exports = {</code> at the top of <code>commitlint.config.js</code>, with <code>export default {</code>. Then run <code>git add .</code> and <code>git commit</code> again.</p>
</li>
<li><p>Now execute the following command that has a valid commit message:</p>
<pre><code class="language-bash">git commit -m "build: install commitlint"
</code></pre>
<p>This should go through because the commit message header now contains the prefix <code>build</code> which is one of the prefixes defined by <a href="https://github.com/angular/angular/blob/main/CONTRIBUTING.md#-commit-message-format">Angular commit message conventions</a> that we configured commitlint with.</p>
</li>
</ol>
<h3>Set up semantic-release</h3>
<ol>
<li><p>On the terminal, in app root folder, run command:</p>
<pre><code class="language-bash">npm install --save-dev semantic-release
</code></pre>
</li>
<li><p>Install additional plugins:</p>
<pre><code class="language-plaintext">npm install --save-dev @semantic-release/changelog @semantic-release/exec
</code></pre>
<p>Each of the plugins installed above performs a specific job in semantic-release's internal workflow. <a href="https://semantic-release.gitbook.io/semantic-release/usage/plugins#default-plugins">Some plugins</a> get installed by default. However, we need a few additional ones from <a href="https://semantic-release.gitbook.io/semantic-release/usage/plugins">this list</a>, which are what we just installed.</p>
<p>See <a href="#heading-appendix-details-of-semantic-release-configuration-and-internal-workflow">Appendix: Details of semantic-release Configuration and Internal Workflow</a> at the end of this article for details.</p>
</li>
<li><p>To the <code>"scripts"</code> section of your <code>package.json</code> file, add:</p>
<pre><code class="language-plaintext">"release": "semantic-release"
</code></pre>
<p>This would be invoked as <code>npm run release</code> in the release pipeline.</p>
</li>
<li><p>Create file <code>release.config.js</code> in app’s root folder, with the following content.</p>
<p><strong>As with</strong> <code>commitlint.config.js</code><strong>,</strong> if your package.json has <code>”type”: “module”</code>, i.e. you are using ES6 modules, then replace the line <code>module.exports = {</code> with <code>export default {</code> in the config file:</p>
<pre><code class="language-javascript">/* eslint-env node */

module.exports = {
  branches: ["main"],
  plugins: [
    "@semantic-release/commit-analyzer",

    "@semantic-release/release-notes-generator",

    [
      "@semantic-release/npm",
      {
        npmPublish: false,
      },
    ],
    [
      "@semantic-release/changelog",
      {
        changelogFile: "docs/CHANGELOG.md",
      },
    ],
    [
      "@semantic-release/github",
      {
        assets: ["docs/CHANGELOG.md"],
      },
    ],
    [
      "@semantic-release/exec",
      {
        successCmd:
          "echo 'RELEASED=1' &gt;&gt; \(GITHUB_ENV &amp;&amp; echo 'NEW_VERSION=\){nextRelease.version}' &gt;&gt; $GITHUB_ENV",
      },
    ],
  ],
};
</code></pre>
<p>semantic-release uses plugins for the various steps in its internal workflow.</p>
<p><a href="#heading-appendix-details-of-semantic-release-configuration-and-internal-workflow">Appendix: Details of semantic-release Configuration and Internal Workflow</a> at the end of this article provides detail on semantic-release configuration and several plugins.</p>
<p>However, here I want to quickly explain what the plugins declared in the file above do and how they have been configured:</p>
<ul>
<li><p><code>@semantic-release/commit-analyzer</code> plugin analyzes commits according to <a href="https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit">Angular commit message conventions</a> (this is the default commit message format but can be changed in configuration of the plugin).</p>
</li>
<li><p><code>@semantic-release/release-notes-generator</code> plugin generates release notes.</p>
</li>
<li><p><code>@semantic-release/changelog</code> plugin writes out the generated release notes to file <code>docs/CHANGELOG.md</code>.</p>
</li>
<li><p><code>@semantic-release/github</code> plugin publishes a release to GitHub Releases together with the text of release notes generated by <code>@semantic-release/release-notes-generator</code> plugin. It also attached the release notes file, <code>docs/CHANGELOG.md</code> that was written out by <code>@semantic-release/changelog</code> plugin.</p>
</li>
<li><p><code>@semantic-release/npm</code> plugin has been configured NOT to publish the package to NPM as we have set <code>npmPublish: false</code>. Instead, we would use a deployment job in the release pipeline, a GitHub Actions workflow, to publish the package.</p>
<p>Later on I will show you an alternate way of publishing a package: publishing directly to NPM from within semantic-release by setting <code>npmPublish: true</code>.</p>
</li>
<li><p>The <code>@semantic-release/exec</code> plugin has been configured to set two environment variables if there is a release: <code>RELEASED</code> is set to <code>1</code> and <code>NEW_VERSION</code> is set to the version number of the new release that has been computed by semantic release.</p>
<p>These variables are set using the <a href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-environment-variable">GitHub Actions workflow command syntax</a> by writing them to the <a href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#environment-files">environment file</a> <code>$GITHUB_ENV</code>. This makes them available to all steps and commands that run subsequently in the same GitHub Actions job.</p>
<p>We will use these two variables in the next section.</p>
</li>
</ul>
</li>
</ol>
<h3>Set <code>”version”</code> in package.json</h3>
<p>In <code>package.json</code>, set <code>"version": "0.0.0-managed.by.semantic.release",</code></p>
<p>Reasons for doing this are given in the Appendix in the <a href="#heading-semantic-releasegit-plugin">section for @semantic-release/git plugin</a>.</p>
<h2>GitHub Setup</h2>
<p>In this section we shall</p>
<p>setup GitHub Actions workflows for the Continuous Integration (CI) and Continuous Deployment (CD) pipelines and configure pull requests and permissions.</p>
<p><strong>To set these up, proceed as follows):</strong></p>
<h3>Configure a Personal Access Token</h3>
<p>In order to tag <code>main</code> (or other configured branch) and publish a release to the GitHub repo's Releases, semantic-release needs a <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens">GitHub Personal Access Token (PAT)</a> with write persmissions.</p>
<p><strong>The</strong> <code>secrets.GITHUB_TOKEN</code> <strong>that is injected by default into every running GitHub Actions workflow has the required permissions</strong> (<code>read-write</code> on each of <strong>Contents</strong>, <strong>Issues</strong> and <strong>Pull Requests</strong>). But if you want to create a custom token to use, store in as a secret in a new Production environment in GitHub repo, use the environment using an <code>environment</code> block in the job (not shown below, need to update from flowmazondotnet repo) to access it as <code>secrets.GH_TOKEN</code> (the secret name cannot start with <code>GITHUB</code> as this is a reserved prefix).</p>
<p>As per <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#types-of-personal-access-tokens">GitHub's recommendation</a>, create a <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#fine-grained-personal-access-tokens">fine-grained PAT</a> (as opposed to a classic PAT) as follows:</p>
<ol>
<li><p>Create a fine-grained Personal Access Token named <code>semantic-release-demo</code> (or any other name of your choice) by following instructions given in <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token">”Creating a fine-grained personal access token” in GitHub Docs</a>.</p>
<p><strong>BUT, before pressing the “Generate Token” button,</strong> note the following:</p>
<ul>
<li><p>A fine-grained token is created at the account/organization level, so it is not repo-specific.</p>
</li>
<li><p>Under <strong>Repository Access</strong>, choose <strong>Only selected repositories</strong> and select your repo from the dropdown.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726674626183/9fedc0d3-530f-4c8d-aff3-28b968504aed.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Under <strong>Repository Permissions</strong>, select <strong>Read and Write</strong> access for <strong>Contents</strong>, <strong>Issues</strong> and <strong>Pull Requests</strong>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726675338333/1e84419e-8967-4d66-a1c3-9ce0d7561cdf.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726675364393/ed0c09b3-eed4-497b-9194-c458293770fd.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726675390179/4d61b031-da5d-4687-b76f-3a431f9f2e31.png" alt="" style="display:block;margin:0 auto" /></li>
</ul>
</li>
<li><p>Once the PAT is created, <strong>copy the token to clipboard:</strong></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726675687824/275bdd3b-b1f3-4485-8cbe-d34b21d15f18.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p><strong>Go to your repo</strong> and click <strong>Settings</strong> in the top right hand corner of the repo page (these are repo settings and NOT account settings):</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726678337837/83b93b18-5a49-4fb8-bd9e-254f9e7c4a1e.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>In the menu on the left, click <strong>Secrets and Variables</strong>, then <strong>Actions</strong>:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726678371815/30c2f49a-a941-4976-9e26-ef140096d9d4.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Click the <strong>New repository secret</strong> button:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726678413986/1bcb9471-2ffa-4e83-8098-43010cb99e5d.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Name the secret <code>GH_TOKEN</code> and paste the token value you copied to clipboard earlier and click <strong>Add Secret</strong>:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726679327999/c3662b55-077e-434d-8df5-75a7ebb36042.png" alt="" style="display:block;margin:0 auto" /></li>
</ol>
<h3>Create Commitlint Check on Pull Requests</h3>
<p>In folder <code>.github/workflows</code> in the project root, create a GitHub Actions workflow file named <code>ci-commitlint.yml</code> with the following content:</p>
<pre><code class="language-yaml">name: Commitlint Workflow
concurrency:
  group: ci-${{ github.ref }}-commitlint
  cancel-in-progress: true

on:
  pull_request:
    types: [edited, synchronize, opened, reopened]
    branches:
      - main

jobs:
  lint-commit-messages:
    name: Run commitlint
    runs-on: ubuntu-24.04

    steps:
      - uses: actions/checkout@v3
        with:
          # this is needed to get one of the
          # commitlint invocations below (that
          # analyzes commits from --from to --to)
          # to work
          fetch-depth: 0

      - name: Install dependencies
        # If we don't do this, commitlint throws
        # errors aboutnot not finding rules package
        # for commit message conventions
        run: npm ci

      # Using env in steps below below to pass
      # required values from github context rather
      # than accessing properties from github context
      # directly. GitHub Docs recommend
      # that this is better for security as it
      # mitigates against script injection attacks
      - name: Run commitlint on PR source branch commit messages
        env:
          basesha: ${{ github.event.pull_request.base.sha }}
          headsha: ${{ github.event.pull_request.head.sha }}
        run: npx commitlint --from \(basesha --to \)headsha --verbose

      - name: Run commitlint on PR Title and Description
        env:
          prtitle: ${{ github.event.pull_request.title }}
          prdescription: ${{ github.event.pull_request.body }}
        run: printf "\(prtitle\n\n\)prdescription" | npx commitlint
</code></pre>
<p><strong>REST OF THIS SECTION EXPLAINS THIS WORKFLOW.</strong></p>
<p>The trigger for this workflow is <code>pull_request</code>. Based on the activity types that have been declared in <code>types</code> attribute of the trigger: <code>synchronize</code> (explained <a href="https://github.com/orgs/community/discussions/24567">here</a>) and <code>edited</code>, <code>opened</code> and <code>reopened</code> (explained <a href="https://frontside.com/blog/2020-05-26-github-actions-pull_request/#how-do-workflows-trigger-on-pull_request">here</a>), this workflow runs when:</p>
<ul>
<li><p>a pull request is opened/reopened (activity types <code>opened</code> and <code>reopened</code>)</p>
</li>
<li><p>source branch is updated e.g. it gets new commits or a force push (<code>synchronize</code>)</p>
</li>
<li><p>PR title, PR description (aka PR body) or target branch (aka base branch) are modified</p>
</li>
</ul>
<p>When triggered, the workflow runs commitlint to lint messages of the commits on the source branch, as well as the concatenation of PR title and PR description.</p>
<p><strong>Step</strong> <code>actions/checkout@v3</code> checks out the repo so that commitlint can analyze the source branch of the pull request.</p>
<p><code>fetch-depth</code> parameter specifies the <a href="https://github.com/actions/checkout?tab=readme-ov-file#usage">number of commits to fetch</a>, with <code>0</code> meaning fetch all commits on all branches.</p>
<p>commitlint needs <code>fetch-depth</code> to be <code>0</code> if <code>—from</code> and <code>—to</code> parameters are specified as is the case in the workflow file above, and would throw an error if this were not set on the <code>actions/checkout@v3</code> action that checked out the code.</p>
<p>A <a href="https://commitlint.js.org/guides/ci-setup.html#github-actions">sample GitHub Actions workflow</a> given in commitlint docs also also uses <code>fetch-depth: 0</code>.</p>
<p><strong>Step “Install dependencies”</strong> installs dependencies declared in package.json of the package by running <code>npm ci</code> which is like <code>npm install</code> but is more suitable for use in a build/release pipeline environment, <a href="https://docs.npmjs.com/cli/v10/commands/npm-ci">as described here</a>.</p>
<p><strong>Step “Run commitlint on PR source branch commit messages”</strong>, adapted from <a href="https://commitlint.js.org/guides/ci-setup.html#github-actions">commitlint documentation</a>, runs commitlint on commit messages on source branch of a pull request.</p>
<p>If you set up commitlint to run in Husky earlier, then you would lint these commit messages locally before committing and pushing to GitHub. However, running commitlint on these commits again, as done in the workflow above, would flag up any commit messages that did not comply with your commtlint configuration: there are ways that such messages can creep into the repo even if commitlint is set up to run locally via Husky.</p>
<p>When scanning pull request source branch, commitlint parses commit messages from <code>—from</code> commit to the <code>—to</code> commit. In doing so, it excludes the <code>—from</code> commit, parsing only commits that are ahead of it rather than including it, up until the commit specified in <code>—to</code> parameter (which <em>is</em> included).</p>
<p>I provide <code>\({{ github.event.pull_request.base.sha }}</code> as value of <code>—from</code>, which evaluates to (tip of) <code>main</code>. The value of <code>—to</code> is <code>\){{ github.event.pull_request.head.sha }}</code> which is (tip of) the source branch of pull request.</p>
<p>The behaviour I get is that if the feature branch is directly ahead of <code>main</code>, commitlint parses commits from tip of <code>main</code> (exclusive) to tip of the feature branch (inclusive). For me this is exactly the desired behaviour as I always rebase my feature banch on <code>main</code> before pushing it to my GitHub repo.</p>
<p>However, sometimes it does happen that the feature branch diverges from <code>main</code>. In this case, even though <code>—from</code> is (the tip of) <code>main</code>, commitlint lints finds the common ancestor of the two branches, then parses commits from this up to the <code>—to</code> commit which is the (tip of the) feature branch.</p>
<p>For example in the commit graph shown below where the source branch of pull request <code>final-commitlint3</code> has diverged from the target branch <code>main</code>:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729422938312/2459160d-cabc-4242-b1da-7aa22d60cff5.png" alt="" style="display:block;margin:0 auto" />

<p>For this commit graph, commitlint would find the common ancestor, the commit <code>82d7c9</code> shown with a red frame, and take this as the starting commit instead of <code>main</code>, the commit <code>d57d42</code>, that was specified as value of <code>—from</code>. Then it would parse, excluding this commit, the three commits in a straight line up to, and including, the tip of the feature branch <code>final-commitlint3</code>.</p>
<p>Again, this is exactly the behaviour I want on the odd occasion I forgot to rebase my feature branch on to <code>main</code> before pushing and so there was divergence.</p>
<p>Finally, note that I am passing properties of <code>github</code> context as environment variables (using an <code>env</code> block in a step) in multiple steps in this workflow, as well as in other workflows (see below). I do this, instead of referencing them directly within Bash script in a step, because <a href="https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable">GitHub advises that this mitigates against script injection attacks</a>.</p>
<p><strong>ASIDE:</strong></p>
<p>Previously in this step, I used expression <code>\({{ github.event.pull_request.head.sha }}~\){{ github.event.pull_request.commits }}</code> as value of <code>—-from</code> parameter of <code>commitlint</code>. I took this from the following command taken from <a href="https://commitlint.js.org/guides/ci-setup.html#github-actions">sample of using commitlint as a pull request check</a> in commitlint documentation:</p>
<pre><code class="language-yaml">npx commitlint --from \({{ github.event.pull_request.head.sha }}~\){{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }}
</code></pre>
<p>Here <code>github.event.pull_request.commits</code> appears to be the number of new commits in the source branch of pull request compared to the target branch.</p>
<p>This led to problems if I had created a merge commit from <code>main</code> directly in the pull request on GitHub.</p>
<p>For example, consider again the commit graph given above:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729384396597/a2399ed8-31ca-4702-bb3b-f34fd659d7c1.png" alt="" style="display:block;margin:0 auto" />

<p>When I open a pull request to merge <code>final-commitlint3</code> into <code>main</code>, GitHub alerts me that the source branch of the PR has conflicts:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729383883986/2b453933-889c-444e-8ceb-6a3c49bba33b.png" alt="" style="display:block;margin:0 auto" />

<p>When I resolve the conflict, a merge commit will be created which points back to both <code>main</code> and the PR source branch <code>final-commitlint3</code>:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729381490105/0a266fef-4dcc-4269-b6ee-51b89b6f4a24.png" alt="" style="display:block;margin:0 auto" />

<p>The number of new commits on the feature branch is now 4, including the merge commit just created. So expression <code>${{ github.event.pull_request.commits }}</code> evaluates to 4. However to walk back these 4 commits from the tip of <code>final-commitlint3</code>, there are now two possible paths.</p>
<p>What I have seen happen is that instead of ending up at the common ancestor of <code>main</code> and <code>final-commitlint3</code>, the commit <code>82d7c9</code>, the expression passed as value of commitlint’s <code>—from</code> parameter, <code>\({{ github.event.pull_request.head.sha }}~\){{ github.event.pull_request.commits }}</code> takes the other path and ends up 4 commits back at commit <code>ee93ba</code> that has tag <code>v1.0.7</code>. This become the <code>—from</code> commit instead of the common ancestor of the two branches as would have been the desired behavior.</p>
<p>The <code>—from</code> value I now use instead, i.e. <code>main</code>, has the behaviour of parsing forward from common ancestor of <code>main</code> and feature branch up to the (tip) of the feature branch. This is exactly what I want.</p>
<p><strong>ANOTHER ASIDE:</strong></p>
<p>If you create a merge commit in a pull request in GitHub like I did above, it always has the message <code>Merge branch ‘&lt;PR target branch&gt;’ into &lt;PR source branch&gt;</code>. You cannot alter it and it does not comply with Angular commit conventions configured in out <code>commitlint.config.js</code>.</p>
<p>Yet commitlint does not raise an error when it encounters this commit message.</p>
<p>This is because this particular commit message is <a href="https://github.com/conventional-changelog/commitlint/blob/65b219e0a6992f4441046aad9fd874e73a30b8e8/%40commitlint/is-ignored/src/defaults.ts#L18-L20">one of the default commit message patterns</a> that commitlint <a href="https://github.com/conventional-changelog/commitlint/blob/65b219e0a6992f4441046aad9fd874e73a30b8e8/%40commitlint/is-ignored/src/is-ignored.ts#L26-L27">ignores</a>. If you need to, you can add to what commitlint ignores by default by providing one or more test functions in <a href="https://commitlint.js.org/reference/configuration.html#configuration-object-example"><code>ignores</code> key in commitlint configuration file,</a> <code>commitlint.config.js</code>.</p>
<p><strong>Step “Run commitlint on PR Title and Description”</strong> ensures that the concatenation of pull request title and description complies with our commitlint configuration.</p>
<p>PR description, aka <em>PR body</em>, is the topmost comment in the Conversation tab of the pull request:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729550415341/8dca0cd8-2d13-45da-b15f-9aa5f3d0e5eb.png" alt="" style="display:block;margin:0 auto" />

<p>Under the setup given in a later section, <a href="#heading-github-configuration-for-pull-requests">GitHub Configuration for Pull Requests</a>, the default message for the Squash Commit that would be created when you merge the pull request would be the concatenation of “Pull request title and description”. Therefore I lint this also.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729557580358/d6efb07c-1703-46c1-a484-8612be57432a.png" alt="" style="display:block;margin:0 auto" />

<p>The same check would also make sense if you allow Merge Commits (see screenshot above) as in that case too, you can default the commit message to “Pull request title and description”.</p>
<p><strong>However, if you only allow rebase merging</strong> (see screenshot above)<strong>,</strong> and disallow both squash meging and merge commits, then this check would be redundant. This is because in rebase merging, when yo merge the PR, the new commits in source branch of PR are rebased on <code>main</code>, then placed in front of <code>main</code>. The commit messages of rebased commits remain the same and these we already lint in the previous step in the workflow, <strong>Validate PR source branch commits with commitlint</strong>.</p>
<p>Therefore if you only allow rebase merging, then comment out or delete this step (“Run commitlint on PR Title and Description”).</p>
<p>For more details, see my post <a href="https://nausaf.hashnode.dev/types-of-merges-in-a-github-pull-request">The Three Types of Pull Request Merge in GitHub</a>.</p>
<h3>Create Linked Issue Check on Pull Requests</h3>
<p>Create a GitHub Actions workflow file <code>.github/workflows/ci-verifylinkedissue.yml</code>, with the following content:</p>
<pre><code class="language-yml"># Based on example given in nearform repo:
# https://github.com/nearform/actions-toolkit/blob/master/.github/workflows/check-linked-issues.yml
#
name: Verify Linked Issue in PR
concurrency:
  group: ci-${{ github.ref }}-verifylinkedissue
  cancel-in-progress: true

on:
  pull_request:
    types: [edited, synchronize, opened, reopened]
    branches:
      - main

jobs:
  verify-linked-issue:
    name: Check for Linked Issue
    runs-on: ubuntu-24.04
    permissions:
      issues: read
      pull-requests: write
    steps:
      # PROS: Actually checks that the issue exists in repo
      # CONS: This needs issue ref to be in description of PR, not title
      - uses: nearform-actions/github-action-check-linked-issues@v1
        id: check-linked-issues
        with:
          comment: false
</code></pre>
<p><strong>REST OF THIS SECTION EXPLAINS THIS WORKFLOW.</strong></p>
<p>This workflow checks that an issue from GitHub Issues is referenced in the pull request description (PR description is the topmost comment on the PR’s page) using a string of the form <code>&lt;special keyword&gt; #&lt;Issue Number&gt;</code>. An example would be <code>Fix #23</code>.</p>
<p>Keywords other than <code>Fix</code> can be used to link an issue to a pull request and the full list is given <a href="https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword">here in GitHub Docs</a>.</p>
<p>Because of the setup we shall do shortly, the pull request description would become the body and footer of the commit message (i.e. the content after the first line, aka <em>header</em>, of the commit message) of the new Squash Commit that will be created on <code>main</code> (this behaviour would be the same if you instead did a Merge Commit when merging the pull request). Any issue references of form <code>Fix #&lt;Issue Number&gt;</code> in this commit message would be automatically closed when the pull request is merged.</p>
<p>Another advantage of attaching issues like this is that when semantic-release runs on <code>main</code>, it would output any referenced issues in commit messages in release notes as URLs.</p>
<p>I like to have this check in my repos because I always make sure that all work I push to <code>main</code> references some user story (which would be an issue if you store user stories in GitHub Issues in the repo).</p>
<p>You can reference more than one issue e.g. <code>Fix #32, Fix #34</code>. You can also reference issues without closing them e.g. <code>#32, #34</code>. These will not be closed when the pull request is closed but will go into the commit message and from there into release notes as URLs to the respective issues.</p>
<p>For the workflow above to run successfully, you need at least one issue that will be closed when the pull request is merged, i.e. which is referenced in PR description using <a href="https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword">one of the special keywords</a> e.g. <code>Fix #32</code> or <code>closes #60</code> rather than simply as <code>#&lt;issue number&gt;</code>.</p>
<h3>Create Deployment Pipeline</h3>
<p>In <code>.github/workflows</code> folder in the project, create a GitHub Actions workflow file named <code>release.yml</code>, with the following contents:</p>
<pre><code class="language-yaml">name: Release to Production
concurrency: release-to-prod-pipeline
on:
  push:
    branches:
      - main

jobs:
  create-release:
    permissions:
      contents: write # to be able to publish a GitHub release
      issues: write # to be able to comment on released issues
      pull-requests: write # to be able to comment on released pull requests
    runs-on: ubuntu-24.04
    name: Create Release
    outputs:
      released: ${{ env.RELEASED }}
      newVersion: ${{ env.NEW_VERSION }}
    steps:
      - uses: actions/checkout@v3
        name: Checkout code
        id: checkout
        with:
          # This is needed for semantic-release to work
          fetch-depth: 0

      - name: Install Package Dependencies
        # needed for both commitlint and semantic-release to work
        run: npm ci

      - name: Lint commits on main since last version tag
        run: |

          # Find latest version tag 
          # Starts with 'v' followed by a digit.
          # Prevent non-zero exit code from git describe
          # if a version tag is not found by appending an
          # `|| echo "..."` to the git describe statement.
          lasttag=$(git describe --tags --abbrev=0 --match="v[0-9]*" 2&gt;/dev/null) \
            || echo "no version tag found, will only lint commit message of HEAD commit"
            

          # Compute arguments to commitlint
          if [ "$lasttag" == "" ]; then

          # A version tag was not found (i.e. semantic-release has yet
          # to run successfully for the first time on current branch).
          # So only parse the last/latest commit.
            clargs="--last"

          else

          # latest version tag (that was found) should be mapped to
          # SHA of the commit bearing the tag. This should be --from
          # argument to commitlint (this is excluded when commitlint
          # run) and HEAD should be the --to argument (this would
          # be included when commitlint runs)
            echo "latest version tag is $lasttag, will lint messages of all commits forward of this up to HEAD..."
            clargs="--from=\((git rev-parse \)lasttag) --to=HEAD"
            
          fi

          # Run commitlint with computed arguments
          npx -- commitlint --verbose $clargs
      - name: Create GitHub release
        id: semanticrelease
        env:
          GH_TOKEN: ${{ secrets.GH_TOKEN }}
        run: |
          echo "RELEASED=0" &gt;&gt; $GITHUB_ENV
          npm audit signatures
          npm run release

      # Instead of a separate job, you can have steps to
      # deploy in the same job.
      # However, each would need to be made conditional on
      # env.RELEASED variable (set during npm run release)
      # being 1. For example:
      #
      # - name: Deploy Step 1
      #   id: deploystep1
      #   if: ${{ env.RELEASED == 1 }}
</code></pre>
<p><strong>REST OF THIS SECTION EXPLAINS THIS WORKFLOW.</strong></p>
<p>The <code>release.yml</code> file we created above is meant to be the release pipeline, i.e. the workflow which releases your project into production.</p>
<p>This workflow is triggered on a <code>push</code> to <code>main</code>. When there is a push to <code>main</code>, semantic-release will run in a job named <code>create-release</code> in the workflow.</p>
<p>Steps and properties of this job - those that haven’t already been explained for earlier workflows - are described below:</p>
<p><strong>Step</strong> “<strong>Lint commits on main since last version tag”</strong> lints the commit messages of all commits on <code>main</code> since the last version tag.</p>
<p>Normally, only the HEAD commit on <code>main</code> after pull request has been merged should need to be linted. This is because:</p>
<ul>
<li><p>Under Squash Merging, there is only ever a single commit created on <code>main</code>.</p>
</li>
<li><p>Under Merge Commit, one or more commits from feature branch are added to <code>main</code>, but the messages of all of these have already been linted in the pull request check <strong>Run commitlint</strong> described above, except that of the new merge commit.</p>
<p>So in Merge Commit also, only the commit message of HEAD needs to be linted.</p>
</li>
<li><p>In Rebase Merge, all commit messages are from the pull request source branch and their messages have already been checked in the pull request.</p>
<p>Still, verifying the tip commit’s message doesn’t do any harm even if it is not useful.</p>
</li>
</ul>
<p>Indeed, the <a href="https://commitlint.js.org/guides/ci-setup.html#github-actions">sample GitHub Actions workflow given in commitlint documentation</a> only lints commit message of the HEAD commit on <code>push</code> event.</p>
<p><strong>Why does the commit message of tip of</strong> <code>main</code> <strong>need to be linted at all?</strong> As described under <code>ci-commitlint.yml</code> above, I set up the PR Title and Description to be the default for commit message of the new Squash Merge commit that would be created on <code>main</code>, and the same default can be used if opting for a Merge Commit.</p>
<p>When I initiate a pull request merge using either Merge Commit or Squash Merge, PR title and description would be filled in as the commit message for the Merge Commit or the Squash Commit that would be created:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728863855137/cb126340-9501-4ece-b8b4-d6c2cb379492.png" alt="" style="display:block;margin:0 auto" />

<p>I already lint the concatenation of PR title and description in <code>ci-commitlint.yml</code> (described above) before PR merge. So the message shown above complies with my commitilnt configuration.</p>
<p>However, the problem is that just before pressing <strong>Confirm squash and merge</strong> I can modify it so that it is no longer compliant and therefore semantic-release would no longer be able to parse it when it runs on <code>main</code>.</p>
<p>For example I can delete the <strong>fix:</strong> prefix in the picture above.</p>
<p>When semantic-release runs on <code>main</code> and parses this commit message, then instead of throwing an error like commitlint would, it would simply ignore the commit. If this is the only new commit on <code>main</code> that semantic-release had to parse, as would happen under Squash Merge which is what I use, there there would be no release. If there are deployment jobs that depend upon semantic-release actually making a release, those wouldn’t run either.</p>
<p>Yet there would be no notification of there NOT being a release.</p>
<p>Parsing the commit message of tip of <code>main</code> with commitlint before running semantic-release protects against this situation as a unlike semantic-release, commitlint would fail if it comes across a non-compliant commit message. This means whole deployment workflow would fail and we would get notified.</p>
<p><strong>The reason why I lint all commits since last version tag instead of only HEAD</strong> is that in the unlikely scenario where two or more pull request get merged at around the same time, there could be multiple commits on <code>main</code>, one from each merged pull request, that need to be linted.</p>
<p>Secondly, doing so protects against two further unlikely events: pushes being made directly to <code>main</code> without pull requests and checks on the pull request not preventing merg in event of a check failure.</p>
<p>Either of these situations can occur because appropriate branch protections on <code>main</code> were not in place or were not operational. The latter case can arise if you changed visibility of your repo from public, where branch protections work, to private where branch protections only work if you have a paid GitHub account.</p>
<p>For more details, you can read my post <a href="https://nausaf.hashnode.dev/types-of-merges-in-a-github-pull-request">The Three Types of Pull Request Merge in GitHub</a>.</p>
<p><strong>In the Bash command for finding latest version tag,</strong></p>
<pre><code class="language-yaml">lasttag=$(git describe --tags --abbrev=0 --match="v[0-9]*" 2&gt;/dev/null) \
            || echo "no version tag found, will only lint commit message of HEAD commit"
</code></pre>
<p>note the following:</p>
<ul>
<li><p>I append <code>2&gt;/dev/null</code> to <code>git describe</code> that finds the latest version tag (i.e. a tag that begins with <code>v</code> followed by a digit).</p>
<p>I do this because if there are no version tags on <code>main</code>, <code>git describe</code> fails with error message <code>fatal: No names found, cannot describe anything.</code> which be a bit misleading on the console as in this case, the subsequent commitlint command in this step would still lint the HEAD commit.</p>
<p>Therefore I hide this error message from console by redirecting it to <code>/dev/null</code> by appending <code>2&gt;/dev/null</code> to <code>git describe</code>, as <a href="https://stackoverflow.com/questions/25931510/how-to-suppress-error-from-a-single-line-of-script">explained here</a>.</p>
</li>
<li><p>I append <code>|| echo "no version tag found, will only lint commit message of HEAD commit"</code> to <code>git describe</code> because otherwise if no version tag can be found and <code>git describe</code> exits with a non-zero exit code, the whole step and therefore the job fails. This happens because GitHub Actions runner launches Bash <a href="https://stackoverflow.com/questions/73066461/github-actions-why-an-intermediate-command-failure-in-shell-script-would-cause">with <code>-e</code> option</a>.</p>
<p>However, failure of <code>git describe</code> is not an error condition for the step which would still want to lint the HEAD commit in this case.</p>
<p><a href="https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin">Appending an <code>|| echo “…”</code> ensures</a> that an error is not reported to Bash if the <code>git describe</code> command preceding the <code>||</code> fails.</p>
</li>
</ul>
<p><strong>Step</strong> <strong>“Create GitHub release“</strong> runs semantic-release on <code>main</code>, which is the branch that is checked out by action <code>actions/checkout@v3</code> in an earlier step as it is the default branch of the repo.</p>
<p>This step is a bash script that contains three commands:</p>
<ul>
<li><p><code>echo "RELEASED=0" &gt;&gt; $GITHUB_ENV</code> sets environment variable <code>RELEASED</code> to <code>0</code>.</p>
<p>The environment variable is set using the <a href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-environment-variable">GitHub Actions workflow command syntax</a> by writing it to the special <a href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#environment-files">environment file</a> <code>$GITHUB_ENV</code>.</p>
<p>We configured <code>release.config.js</code> in the previous section so that when semantic-release runs, if it computes that a release needs to be made (i.e. the version number needs to be incremented and release notes published), then it would set this variable to <code>1</code>. We would use this variable in deployment job(s) shown later.</p>
</li>
<li><p><code>npm audit signatures</code> scans all dependencies of your package and verifies provenance attestations where these are available. Since provenance is a new feature, not all pacakges in the dependency graph would have provenance attestations.</p>
<p>Secondly, it verifies the signature of every package.</p>
<p>Every package that is published to NPM <a href="https://docs.npmjs.com/about-registry-signatures">is signed</a> by NPM. If a copy of the package stored on a mirror of the registry or in a proxy such as GitHub Packages or an Azure Artifacts feed is compromised, then the signature verification would fail and threfore <code>npm run audit signatures</code> would fail.</p>
</li>
<li><p><code>npm run release</code> runs the script <code>”release”</code> in package.json which we defined earlier as <code>semantc-release</code>. Therefore this command would execute semantic-release which would run according to its configuration in <code>release.config.js</code> that we created earlier in <a href="#heading-set-up-semantic-release">Local Setup</a>.</p>
</li>
</ul>
<p><strong>The</strong> <code>env</code> <strong>block of the step:</strong></p>
<pre><code class="language-yaml">- name: Create GitHub release
  id: semanticrelease
  env:
    GH_TOKEN: ${{ secrets.GH_TOKEN }}
</code></pre>
<p>sets an environment variable <code>GH_TOKEN</code> to a GitHub Personal Access Token (PAT) read from repo secret also named <code>GH_TOKEN</code> that we created earlier.</p>
<p>This would be used by semantic-release to publish a release to GitHub Releases and comment on pull request and issues and annotate pull requests with a <strong>released</strong> badge.</p>
<p><strong>Besides the steps described above, the job</strong> <code>seamntic-release</code> <strong>also defines two</strong> <a href="https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idoutputs"><strong>job output variables</strong></a><strong>,</strong> <code>released</code> and <code>newVersion</code>:</p>
<pre><code class="language-bash">outputs:
  released: ${{ env.RELEASED }}
  newVersion: ${{ env.NEW_VERSION }}
steps:
  ...
</code></pre>
<p>Environment variables <code>RELEASED</code> (a 0 or 1 value that indicates whether or not there was a new release) and <code>NEW_VERSION</code> (the version number computed by semantic-release if it did create a new release) are set by semantic-release when it runs in the step “Create GitHub release“ in the job. See discussion of semantic-release configuration file in section <a href="#heading-set-up-semantic-release">Set up semantic-release</a> for details.</p>
<p>When the job has completed, it evaluates the output variables defined in the the <code>outputs</code> object. Thus <code>released</code> and <code>newVersion</code> get set to environment variables <code>RELEASED</code> and <code>NEW_VERSION</code> respectively, which in turn would have been set when semantic-release ran.</p>
<p>While the runner would be torn down and therefore the environment variables would not longer be available after the job has finished, the output varaibles associated with the job would still be available to any subsequent job. A deployment job would be able to read these and take appropriate deployment decisions.</p>
<p>Examples of deployment jobs that use these job output variables appear in <a href="#heading-deploy-to-a-non-npm-target">Deployment sections</a> later.</p>
<p><strong>The job also has a</strong> <code>permissions</code> <strong>block:</strong></p>
<pre><code class="language-yaml">jobs:
  create-release:
    permissions:
      contents: write # to be able to publish a GitHub release
      issues: write # to be able to comment on released issues
      pull-requests: write # to be able to comment on released pull requests
</code></pre>
<p>This block of permissions wasn't needed in earlier versions of semantic-release (I have a project currently using an earlier version and it works fine without this).</p>
<p><a href="https://github.com/semantic-release/semantic-release/issues/2481#issuecomment-1421429306">This comment in a GitHub issue</a> explains that a <code>contents: write</code> permission is now needed. I have verified that that with just this permission, the workflow is able to create a release in GitHub Releases and create comments on pull requests and on issues linked to pull requests like this:</p>
<p>However, a <a href="https://semantic-release.gitbook.io/semantic-release/recipes/ci-configurations/github-actions">GitHub Actions recipe in semantic-release documentation</a> states that <code>issues: write</code> and <code>pull-requests: write</code> permissions should be declared to allow semantic-release to comment on issues and pull requests respectively. Therefore I have included them in the workflow above for future-proofing.</p>
<p><strong>I am using</strong> <code>ubuntu-24.04</code> <strong>as the runner for the job</strong> instead of <code>ubuntu-latest</code> that I would have preferred to use in a tutorial. At the time of this writing, <code>ubuntu-latest</code> maps to the older version of the runner (<code>ubuntu-22.04</code>) which installs Node 18 LTS version. However, several of the dependencies required by semantic-release, such as <code>@semantic-release/github</code>, require Node v20. This threw errors when <strong>Create GitHub release</strong> job ran on an <code>ubuntu-latest</code> runner:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726843905717/b7a78e3e-dfdc-4bc6-8ad0-c540695a1313.png" alt="" style="display:block;margin:0 auto" />

<p>The later version of the runner that I have used, <code>ubuntu-24.04</code>, installs Node 20 LTS instead, which means the job runs fine.</p>
<p>If you need to use an earlier version of the runner, you can either use <a href="https://github.com/actions/setup-node"><code>setup-node</code> action</a> in the workflow to install the version of Node you want (e.g. that current version of semantic-release needs), or install a version of <code>semantic-release</code> which, along with its dependencies, is compatible with the version of Node that is preinstalled on your runner.</p>
<h3>Commit and Push</h3>
<ol>
<li><p>OPTIONAL STEP: If you are not following along with the sample repo and already have build or test jobs in your release workflow, then delcare these as dependencies of the job by uncommenting <code>needs</code> property of the <code>create-release</code> job:</p>
<pre><code class="language-yaml">jobs:
  create-release:
    # needs: [build, test]
</code></pre>
<p>I only run semantic-release after a clean build has been done and all tests pass. <a href="https://semantic-release.gitbook.io/semantic-release/usage/ci-configuration#run-semantic-release-only-after-all-tests-succeeded">semantic-release documentation</a> advises the same. This is what uncommenting <code>needs</code> in the snippet above would do.</p>
</li>
<li><p>Commit your work on the feature branch <code>semantic-release-setup</code> and push it to the remote GitHub repo:</p>
<pre><code class="language-bash">git add .
git commit -m "ci: set up semantic-release"
git push -u origin semantic-release-setup
</code></pre>
</li>
</ol>
<h3>GitHub Configuration for Pull Requests</h3>
<p>In this section we shall configure some useful branch protections on <code>main</code> and pull request status checks.</p>
<ol>
<li><p>Go to the <strong>Pull Requests</strong> tab on the GitHub repo and press the button <strong>Compare &amp; pull request</strong>:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726765960692/ed09d1c5-6172-4fb7-bc5f-d7dae8d9cbae.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>On the <strong>Open a Pull Request</strong> page that opens, enter title:</p>
<pre><code class="language-plaintext">ci: set up semantic-release
</code></pre>
<p>Then press <strong>Create pull request</strong> button:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727190767018/00ef40b5-2ebf-4f95-8fd9-844386a31f80.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>On the page for the new pull request, wait for the two jobs in the workflows we created above to complete:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726767589786/146e73ba-a37c-45e8-816a-10dc33a29819.png" alt="" style="display:block;margin:0 auto" />

<p>We will address the failing check shortly.</p>
</li>
<li><p>On <strong>Settings</strong> tab of the repo in GitHub, under <strong>Branches</strong>, click <strong>Add classic branch protection rule</strong> if you do not already have a branch protection rule on <code>main</code>:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727192241021/b6f6cd54-15a4-4a04-850a-9eb5f89d316e.png" alt="" style="display:block;margin:0 auto" />

<p>Otherwise, click on the <strong>branch protection rule on</strong> <code>main</code> that you already have.</p>
</li>
<li><p>On the page for the <strong>Branch protection rule</strong>, fill out the various fields as shown below.<br />Then press <strong>Create</strong> or <strong>Save Changes</strong>:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727220187534/42e4506f-2907-4ffc-aac1-27ccdd133211.png" alt="" style="display:block;margin:0 auto" />

<p>What we have just done is configure the two jobs in the workflows created above as pull request status checks. These need to pass - i.e. the workflows need to run successfully - before a pull request would be allowed to merge.</p>
<p>On this page, I also check <strong>Require Linear History</strong> and <strong>Do not allow bypassing above settings</strong>. However, this is not required for this tutorial.</p>
</li>
<li><p>OPTIONAL STEP: On the <strong>General</strong> tab of repo <strong>Settings</strong>, scroll down to the <strong>Pull Requests</strong> section.</p>
<p>Configure it as follows (this change gets saved automatically, there is no <strong>Save Changes</strong> button to press at the end):</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729608101409/f30091e9-2b11-4af3-8d19-0b294c9bcd99.png" alt="" style="display:block;margin:0 auto" />

<p>On this page, I also check <strong>Automatically delete head branches</strong> checkbox, though this is OPTIONAL for this tutorial.</p>
<p>There isn’t a <strong>Save Changes</strong> button to press as changes get autosaved as you make them on this particular page.</p>
<p>Of the three possible ways of merging the source branch (aka “head branch”) of a pull request into the target branch (aka “base branch”), I enable only one: <strong>Allow squash merging</strong>.</p>
<p>If you are not familiar with three types of merges, you can find more detail <a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges">here in GitHub documentation</a> or in <a href="https://nausaf.hashnode.dev/types-of-merges-in-a-github-pull-request">my blog post on the topic</a>.</p>
<p>Which of the three merge methods you use, or allow, is up to you. The setup given in this post would work fine with any of the three methods.</p>
<p>However, I personally prefer the Squash Merge method and disallow the other two because:</p>
<ul>
<li><p>I prefer to keep my <code>main</code> linear as it’s much easier to understand (e.g. to <code>git bisect</code>) than the sort of non-linear history you get when you have merge commits. Therefore I select option <strong>Require Linear History</strong> in <strong>Branch protection rule</strong> for <code>main</code> (in step 5 above).<br />This precludes the use of merge commits: the <strong>Merge Commit</strong> option would not be available on a pull request if you have the require Linear History option checked in branch protection rules for the target branch of the pull request:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729609059235/15e26ea2-37d9-451f-af29-1801d72732cc.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Of the remaining two available merge methods - Squash merge and Rebase merge - I prefer squashing.</p>
<p>I like to merge smaller pull request frequently rather than attempt to merge into <code>main</code> several days or weeks of work done on the feature branch.</p>
<p>For small merges, I find that having a single meaningful squash commit on <code>main</code> works a lot better than having lot of tiny commits on <code>main</code> that were obtained by rebasing the original commits in the feature branch onto <code>main</code>.</p>
</li>
</ul>
<p>Thus with squash merging, I get a linear <code>main</code> on which individual commits are chunky and meaningful rather than a <code>main</code> that is potentially non-linear or has lots of tiny commits. Such a <code>main</code> is easier to understand and reason about.</p>
</li>
<li><p>Go to <strong>Issues</strong> tab on the repo and create a new issue. with title “<strong>Set up semantic-release</strong>”.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726780757464/ec79f90a-7bdf-43a5-8fc8-9d03f422bb15.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Go back to the pull request you opened earlier and edit the topmost comment. Enter <code>Fix #&lt;number of issue created above&gt;</code>, e.g. <code>Fix #2</code>, then press <strong>Update Comment</strong>, as shown below:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727221146180/6b48164a-f874-43ad-80b4-73ed2f477921.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>The <strong>Verify Linked Issue in PR</strong> status check on the pull request should now pass.</p>
<p>Go to <strong>Checks</strong> tab on the open pull request and make sure there are no failing checks. You may need to wait until the checks have finished running:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727221756225/0b6dac95-9eb4-4afa-a8c7-579bb886b6b1.png" alt="" style="display:block;margin:0 auto" />

<p>Both the status checks ran again after we modified the pull request description (i.e. the topmost comment on the pull request) and both should now pass.</p>
</li>
<li><p>Go back to the <strong>Conversation</strong> tab on the pull request. Then scroll down to the page and select <strong>Squash and Merge:</strong></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726971555657/3f789a61-120b-4cfe-be08-a9d324b5f0ce.png" alt="" style="display:block;margin:0 auto" />

<p>Accept the default commit message (taken from the PR title and description) by pressing <strong>Confirm squash and merge:</strong></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729609641450/4f7789a2-2591-4b36-8e67-1b64025a5323.png" alt="" style="display:block;margin:0 auto" /></li>
</ol>
<p><strong>Semantic release setup is now complete.</strong></p>
<p>After PR is merged, <code>release.yml</code> would run and semantic-release release would run as part of it.</p>
<p>However, there will NOT be a release.</p>
<p>This is because we have used prefixes <code>ci</code> and <code>build</code> in the commits made so far. These prefixes are not mapped to an increment in version number by the the default configuration of semantic-release’s commit analyzer plugin, as explained in the <a href="#heading-how-semantic-release-works">Key Concepts</a> section as well as the <a href="#heading-appendix-details-of-semantic-release-configuration-and-internal-workflow">Appendix</a>.</p>
<p><strong>To actually do a release,</strong> you can make another change and commit it with message <code>fix: …</code> or <code>feat: …</code> or that contains the keywords <code>BREAKING CHANGE</code> or <code>BREAKING CHANGES</code> in the commit message body. When this change is merged to <code>main</code> via a pull request, a release will be published to GitHub Releases by semantic-release executing within <code>release.yml</code>.</p>
<p><strong>To see a release in action on the sample app</strong>, continue with <a href="#heading-deploy-to-a-non-npm-target">Deploy to a non-NPM target</a> section below where we deploy the sample web app to Vercel, or go to <a href="#heading-deploy-to-npm">Deploy to NPM</a> section which shows how to deploy an NPM package to NPM registry (for which another sample repo is provided).</p>
<h2>Deploy to a non-NPM target</h2>
<p>In this section, you will deploy the Next.js sample app as a web app on Vercel.</p>
<p><strong>If you are following along with your own project</strong> then, as long as you deploy to a target other than the NPM registry, you should be able to adapt the deployment logic given here.</p>
<p>In my release pipelines, I only run deployment logic after semantic-release has run and has generated a new release.</p>
<p>If a release was made - i.e. version number was incremented and a release was published with release notes to GitHub Releases - then the job output variables <code>released</code> and <code>newVersion</code> would have been set by the <code>create-release</code> job. I make deployment job(s) conditional on <code>released</code> boolean variable and on the <code>create-release</code> job:</p>
<pre><code class="language-yaml">jobs:
  deploy:
    needs: create-release
    if: ${{ needs.create-release.outputs.released == 1}}
</code></pre>
<p>semantic-release (running in <code>create-release</code>job) may compute that no release should be made. This would happen when the commit message does not indicate a type of change that requires the version number to be incremented (e.g. the <code>ci:</code> and <code>build:</code> prefixes we used above). In this case, its <code>release</code> output variable would be <code>0</code> and nothing would be deployed.</p>
<p>This is also the built-in behaviour of semantic-release if you configure it to directly publish the package to NPM registry (as shown in section <a href="#heading-deploy-to-npm">Deploy to NPM</a> below): it would publish nothing - neither the package nor release notes - if it did not increment the version number.</p>
<p><strong>To deploy the Next.js sample app to Vercel,</strong> follow the steps in the subsections below.</p>
<h3>Create Deployment Workflows</h3>
<ol>
<li><p>Create a new branch:</p>
<pre><code class="language-bash">git checkout main
git pull
git checkout -b setup-deployment
</code></pre>
</li>
<li><p>Create an account with <a href="https://vercel.com/">Vercel</a> if you don not already have one (ideally using your GitHub login).</p>
</li>
<li><p>Follow the steps below to setup your repo as a project on Vercel and obtain Vercel token and other bits from it for use in the deployment job in <code>release.yml</code> (from <a href="https://vercel.com/guides/how-can-i-use-github-actions-with-vercel#configuring-github-actions-for-vercel">this Vercel page</a>).</p>
<p>In these steps, create <strong>Repository secrets</strong> and not Environment secrets or Variables in your repo:</p>
<ul>
<li><p>Install the <a href="https://vercel.com/docs/cli">Vercel CLI</a> and run <code>vercel login</code></p>
</li>
<li><p>Inside your folder, run <code>vercel link</code> to create a new Vercel project</p>
</li>
<li><p>Inside the generated <code>.vercel</code> folder, retrieve the <code>projectId</code> and <code>orgId</code> from the <code>project.json</code></p>
</li>
<li><p>In you GitHub repo, add <code>VERCEL_PROJECT_ID</code> and <code>VERCEL_ORG_ID</code> as <a href="https://docs.github.com/en/actions/security-guides/encrypted-secrets">repo secrets</a> with values of <code>projectId</code> and <code>orgId</code>.</p>
</li>
<li><p>Retrieve a <a href="https://vercel.com/guides/how-do-i-use-a-vercel-api-access-token">Vercel Access Token</a>.</p>
</li>
<li><p>Save your Vercel Access Token as repo secret named <code>VERCEL_TOKEN</code>.</p>
</li>
</ul>
</li>
<li><p>Copy and paste the following deployment job in <code>.github/workflows/release.yml</code>, under <code>jobs:</code> key:</p>
<pre><code class="language-yaml">#jobs:
  # OTHER JOBS HERE...

  deploy:
    name: Deploy to Vercel
    runs-on: ubuntu-24.04
    env:
      VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
      VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
    needs: create-release
    if: ${{ needs.create-release.outputs.released == 1}}
    steps:
      - name: Print version to console
        run: |
          echo "New Version Number is: ${{ needs.create-release.outputs.newVersion }}"
      - uses: actions/checkout@v2
      - name: Update Version Number in package.json
        run: npm --no-git-tag-version version ${{ needs.create-release.outputs.newVersion }}
      - name: Install Vercel CLI
        run: npm install --global vercel@latest
      - name: Pull Vercel Environment Information
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
      - name: Build Project Artifacts
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
      - name: Deploy Project Artifacts to Vercel
        run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
</code></pre>
<p>This uses all three of the repo secrets we created above and deploys the sample Next.js app from <code>main</code> to Vercel.</p>
</li>
<li><p>Create <code>.github/workflows/ci.yml</code> in your project folder, with the following contents:</p>
<pre><code class="language-yaml">name: CI
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true
permissions:
  checks: write
  pull-requests: write
on:
  pull_request:
    branches:
      - main
env:
  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

jobs:
  audit-signatures:
    name: Audit signatures of NPM dependencies
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v2
      - name: Audit Provenance Attestations and Signatures
        run: |
          npm ci
          npm audit signatures
  deploy-to-vercel-preview-env:
    name: Deploy to Vercel Preview environment
    runs-on: ubuntu-24.04
    steps:
      - name: Show in-progress message in sticky comment
        id: show-in-progress-message
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          message: |
            # Vercel Preview Environment
            Deployment in progress...
      - uses: actions/checkout@v2
      - name: Update Version Number in package.json
        run: npm --no-git-tag-version version 0.0.0-pr${{ github.event.number }}
      - name: Install Vercel CLI
        run: npm install --global vercel@latest
      - name: Pull Vercel Environment Information
        run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
      - name: Build Project Artifacts
        run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
      - name: Deploy Project Artifacts to Vercel
        id: deploy-artifacts
        run: |
          previewUrl=\((vercel deploy --prebuilt --token=\){{ secrets.VERCEL_TOKEN }})
          echo "previewUrl=\(previewUrl" &gt;&gt; "\)GITHUB_OUTPUT"
      - name: Show URL of PR-specific deployment in Sticky Comment
        id: show-url-in-pr
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          message: |
            # Vercel Preview Environment
            Released \({{ github.sha }} to &lt;\){{ steps.deploy-artifacts.outputs.previewUrl }}&gt;
      - name: Show deployment failure in sticky comment
        id: show-deplolyment-failure-in-pr
        if: failure() || cancelled()
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          message: |
            # Vercel Preview Environment
            Deployment failed.
</code></pre>
<p>This deploys the source branch of an open pull request to the Preview environment in your Vercel project, every time there is a commit to the source branch.</p>
<p><strong>In this file:</strong></p>
<ul>
<li><p><code>audit-signatures</code> job audits signatures and provenance attestations of dependencies</p>
</li>
<li><p><code>deploy-to-vercel-preview-env</code> deploys to Preview environment of your project in Vercel</p>
</li>
</ul>
</li>
</ol>
<p>The cool thing about the Preview environment in a Vercel project is that you can make multiple deployments to it and they would all be active at the same time (and can be accessed at their respective URLs).</p>
<p>So if you have multiple active pull requests in the repo, then the above workflow would deploy the source branch of each of them to the Preview environment but each would get a separate, autogenerated URL. For each open PR, the workflow wuld post the URL of the its source branch’s deployment in a sticky comment in the pull request (using <a href="https://github.com/marocchino/sticky-pull-request-comment">action <code>marocchino/sticky-pull-request-comment@v2</code></a>).</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728407171035/580284f3-8bcf-4df9-a5d3-e9b25c66c982.png" alt="" style="display:block;margin:0 auto" />

<p>So effectively, you have PR-specific preview/UAT deployments and multiple of these can be active at any given time.</p>
<p>Preview environment deployments are only accesible by someone who has a Vercel acccount and to whom you have granted access to your project. So the fact that there are many Preview deployments active at the same time, and that they may hang around even after the pull request has been merged, is not really a problem.</p>
<p>There are of course limits to how many Preview deployments may be active at a time but these are very generous, and <a href="https://vercel.com/docs/limits/overview">given here in Vercel Docs</a>.</p>
<p>Details of how long a deployment stays active are <a href="https://vercel.com/docs/security/deployment-retention">given here</a>.</p>
<h3>OPTIONAL: Use GitHub Environments</h3>
<p>If your repo is public, or if it is private but you have a paid GitHub account, then you can take advantage of the GitHub Environments feature. These provide a nicer DX/UX than a pull request sticky comment of the kind I used above.</p>
<p>For more details and sample CI and release workflows for deploying to Vecel, see <a href="https://dev.to/nausaf/environments-in-github-with-example-of-nextjs-deployment-to-vercel-3hmm">my post on GitHub Environments</a>.</p>
<p><strong>If you want to use GitHub environments for deployment jobs</strong>, make the following changes to the work you did above:</p>
<ol>
<li><p>Define two GitHub environments named <strong>Production</strong> and <strong>Preview</strong> in your GitHub repo (go to repo Settings, then click Environments in the nav on the left).</p>
</li>
<li><p>Replace job <code>deploy-to-vercel-preview-env</code> in <code>ci.yml</code> with the job of the same name in CI workflow given in the <a href="https://dev.to/nausaf/environments-in-github-with-example-of-nextjs-deployment-to-vercel-3hmm">abovementioned post</a>.</p>
</li>
<li><p>Replace job <code>deploy</code> in <code>release.yml</code> with the job of the same name in the Release-to-Production workflow given in the <a href="https://dev.to/nausaf/environments-in-github-with-example-of-nextjs-deployment-to-vercel-3hmm">abovementioned post</a>.</p>
</li>
<li><p>In workflow <code>ci.yml</code>, after step <code>uses: actions/checkout@v2</code> in job <code>deploy-to-vercel-preview-env</code>, add the following step:</p>
<pre><code class="language-yaml">- name: Update Version Number in package.json
  run: npm --no-git-tag-version version 0.0.0-pr${{ github.event.number }}
</code></pre>
</li>
<li><p>In workflow <code>release.yml</code>, before <code>steps</code> attribute in the <code>deploy</code> job, add the following:</p>
<pre><code class="language-yaml">needs: create-release
if: ${{ needs.create-release.outputs.released == 1}}
</code></pre>
</li>
<li><p>In workflow <code>release.yml</code>, after step <code>uses: actions/checkout@v2</code> in <code>deploy</code> job, add the following two steps:</p>
<pre><code class="language-yaml">- name: Print version to console
  run: |
    echo "New Version Number is: ${{ needs.create-release.outputs.newVersion }}"
- name: Update Version Number in package.json
  run: npm --no-git-tag-version version ${{ needs.create-release.outputs.newVersion }}
</code></pre>
</li>
</ol>
<h3>Commit and Create a Release</h3>
<p>Commit your work and create a release as follows:</p>
<ol>
<li><p>On the terminal, in project root, run:</p>
<pre><code class="language-bash">git add .
git commit -m "fix: finish semantic release setup"
git push -u origin setup-deployment
</code></pre>
</li>
<li><p>Go to your repo in GitHub and in Issues tab, create an issue with a title like “Add Next.js Vercel deployment”.</p>
<p>Take note of the issue number.</p>
</li>
<li><p>Create a pull request from <code>setup-deployment</code> to <code>main</code>.</p>
<p>In description, enter <code>Fix #&lt;number of issue just created&gt;</code>:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729686273871/95b6df00-f136-40c2-8fd0-f303e19858f3.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>On the page of the pull request just created, you would see the three checks running:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728594537780/4a38eb74-c59c-4a23-b2fd-ebf5b0a43f54.png" alt="" style="display:block;margin:0 auto" />

<p>Once the check <strong>CI/Deploy to Vercel Preview environment</strong> has completed, you should see a sticky comment on the pull request showing URL of the Vercel deployment:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728407171035/580284f3-8bcf-4df9-a5d3-e9b25c66c982.png" alt="" style="display:block;margin:0 auto" />

<p>If you set up GitHub Environments using the optional section above, then instead of the sticky comment above, you would see a slightly different one:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728668429872/5f5eaa74-e896-4104-9fb7-9d37d9953fbc.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Go to branch protections rule for <code>main</code> by navigating to <strong>repo Settings</strong>, then <strong>Branches</strong> from nav on the left, then <strong>edit.</strong><br />If you do not already have a branch protection rule, create one as described in section <a href="#heading-github-configuration-for-pull-requests">GitHub Configuration for Pull Requests</a> above.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728585357619/02d0f8e6-d11c-4a80-9bed-9fec53c3c55c.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Add the two jobs in <code>ci.yml</code>, <strong>Audit signatures of NPM dependencies</strong> and <strong>Deploy to Vercel Preview evironment</strong> as status checks on the branch protection rule page.<br />Remember to press SAVE at the end:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729687582214/1f223d65-bbaf-4e18-909c-e8935ed42792.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Once all checks have passed, merge the pull request.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728407524603/d3c02ce4-6e59-46d4-8d2f-01d7429dbc14.png" alt="" style="display:block;margin:0 auto" /></li>
</ol>
<p><strong>Once</strong> <code>release.yml</code> <strong>has finished running</strong> (you can check on Actions tab of your repo), you should see latest commit in <code>main</code> tagged with the version number of the new release :</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728410567866/1c048e50-78c7-45ef-91fd-f0b64c727ac8.png" alt="" style="display:block;margin:0 auto" />

<p>and a new release in the Releases section on the right hand side of the repo home page:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728410514439/ce9cc584-2e85-426e-806b-a073cded57e8.png" alt="" style="display:block;margin:0 auto" />

<p>You can navigate to this release from here to see release notes:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728412904661/1857a3e7-ea72-40a5-a6dc-97532480c607.png" alt="" style="display:block;margin:0 auto" />

<p>After a while, you should see <code>main</code> deployed to Production environment of your Vercel target.<br />Go to your project in Vercel to navigate to this deployment:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728412244489/1387f70e-4bca-4fd1-a51e-2976ebb52743.png" alt="" style="display:block;margin:0 auto" />

<p>If you set up GitHub Environments using the optional section above, then click <strong>Production</strong> in the <strong>Deployments</strong> section on your repo’s home page:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728668705003/46ad2820-67c8-4509-941d-a9367abafc23.png" alt="" style="display:block;margin:0 auto" />

<p>This would take you to the repo’s Production environment where you would find the URL to the deployment made to Vercel project’s Production environment.</p>
<h1>Example 2: Deploy to NPM Registry</h1>
<p>If you want to publish the package to NPM registry, you can modify the <code>deploy</code> job in <code>release.yml</code> given in section <a href="#heading-deploy-to-a-non-npm-target">Deploy to non-NPM target</a> above and use command <code>npm publish</code> instead of <code>vercel deploy</code>. See <a href="https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-nodejs-packages">GitHub Docs</a> for more details on using <code>npm publish</code> in a GitHub Actions workflow.</p>
<p>The other method of deploying to NPM would be to configure plugin <code>@semantic-release/npm</code> in semantic-release configuration file <code>release.config.js</code>. This publishes to NPM within the internal workflow of semantic-release. With this method we do not need to add a <code>deploy</code> job to <code>release.yml</code>.</p>
<p><strong>In this section, I describe the second method in detail,</strong> i.e. how to deploy a package to NPM registry from within semantic-release.</p>
<p><strong>Whichever method you choose</strong>, there are essentially three bits of configuration that you need to do:</p>
<ul>
<li><p>Generate a token with “Read and Write” permissions from your NPM account and make it available as an environment variable so that it can be accessed by the tool - semantic-release or <code>npm publish</code> - that you use to publish the package to NPM.</p>
</li>
<li><p>To allow NPM to compute a provenance attestation, place the following in your package.json:</p>
<pre><code class="language-yaml">"repository": {
  "url": "&lt;URL of your GitHub repo (i.e. of repo home page)&gt;"
},
"publishConfig": {
  "provenance": true

}
</code></pre>
</li>
<li><p>To create a provenance attestation (see below), NPM needs to verify the identity of the workflow from which the package is being published to NPM. To acheive this, GitHub Actions creates an Open ID Connect (OIDC) ID token that authenticates the workflow file as belonging to a certain repo and having a certain name and path within its containing repo. GitHub Actions in this case is the OIDC Identity Provider that NPM registry trusts.</p>
<p>The mechanism behind authenticating workloads (jobs in a GitHub Actions workflow would be classified as <em>workloads</em>) using OIDC as if they were users is called <a href="https://cloud.google.com/iam/docs/workload-identity-federation">Workload Identity Federation</a> (WIF).</p>
<p><strong>To allow an OIDC ID Token to be generated for the job</strong> <code>create-release</code> in which semantic-release or <code>npm publish</code> runs, we need to declare an <code>id-token: write</code> permission on the job, <a href="https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings">as described in GitHub Docs</a>:</p>
<pre><code class="language-yaml">permissions:
  # ...OTHER PERMISSIONS...
  id-token: write # to enable use of OIDC for npm provenance
</code></pre>
</li>
</ul>
<h2>Backgrounder: NPM Provenance Attestations</h2>
<p>NPM introduced a <a href="https://github.blog/security/supply-chain-security/introducing-npm-package-provenance/"><strong>provenance attestations</strong> feature</a> in 2022 as a measure for improving supply chain security.</p>
<p>Under this feature, if you publish a package to NPM registry from a compatible CI/CD platform (as of September 2024, only GitHub Actions and GitLab CI/CD are supported) then NPM generates an <strong>attestation</strong>: this is proof that the package came from a specific workflow file or release pipeline in a specific commit in a specified repository.</p>
<p>For example, if I publish the sample package of section <a href="#heading-deploy-to-npm">Deploy to NPM</a> in this article to NPM, this is what the provenance attestation looks like on the page of the package in NPM:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728506930436/1fc23211-a0d2-472c-8ecc-5156429f49d4.png" alt="" style="display:block;margin:0 auto" />

<p>Thus a provenance attestation provides assurance that the package was published to NPM from an authentic source (the source being a specific workflow in a specific commit of a repo identified by its URL).</p>
<p>I’ll show you how to check provenance attestations of dependencies of your package. I’ll also show you how to ensure that NPM is able to create and publish a provenance attestation when you publish your own package.</p>
<h2>Set up semantic-release in an NPM package</h2>
<p><strong>To follow along</strong>, you can use a GitHub repo for an NPM package that you already have.</p>
<p><strong>Alternatively,</strong> you can fork <a href="https://github.com/naveedausaf/show-version-number?tab=readme-ov-file">show-version-number</a> repo and implement the steps given below on that. This is a package that simply prints its version number (the value of <code>”version”</code> key in its package.json) to console when it is run with <code>npx</code>. I have already deployed it to npm and you can try it out by running <code>npx show-version-number</code> on the command line.</p>
<p><strong>Follow the steps below</strong> to deploy your package to NPM from within semantic-release:</p>
<ol>
<li><p>Make sure that in your repo, you have followed steps for setting up semantic-release from beginning of this article up until the end of the previous section (<strong>GitHub Setup</strong>).</p>
</li>
<li><p>Create a new branch:</p>
<pre><code class="language-bash">git checkout main
git pull
git checkout -b setup-deployment
</code></pre>
</li>
<li><p>In <code>relase.config.js</code> in the project root, set <code>npmPublish: true</code> (previously we set it to <code>false</code>)</p>
</li>
<li><p>In <code>.github/workflows/release.yml</code>, add <code>NPM_TOKEN</code> environment variable to <code>env</code> block of the <code>semanticrelease</code> step:</p>
<pre><code class="language-bash">env:
  GH_TOKEN: ${{ secrets.GH_TOKEN }}
  NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
</code></pre>
<p>In the above, snippet, <code>GH_TOKEN</code> envrionment variable for the job was already part of <code>release.yml</code>. We have now added <code>NPM_TOKEN</code>. We now need to store its value as a repo secret which we shall do shortly.</p>
</li>
<li><p>In <code>.github/workflows/release.yml</code>, add permission <code>id-token: write</code> to the <code>permissions</code> block of job <code>create-release</code> (reasons for this were given earlier):</p>
<pre><code class="language-yaml">jobs:
  create-release:
    permissions:
      # ...OTHER PERMISSIONS...
      id-token: write # to enable use of OIDC for npm provenance
</code></pre>
</li>
<li><p>Since we would be deploying to NPM registry, <a href="https://remarkablemark.org/npm-package-name-checker/">check here</a> to see if the name of your pacakge - in <code>”name”</code> key in your package.json - is available.</p>
<p>If not, find a name that <em>is</em> available and set it as value of <code>”name”</code> key in package.json. If you do this ,then consider changing the name of your repo as we as of the name of the local project folder to the same value.</p>
</li>
<li><p>Add the following in package.json in project folder:</p>
<pre><code class="language-yaml">"repository": {
  "url": "https://github.com/naveedausaf/semantic-release-npm"
},
"publishConfig": {
  "provenance": true
}
</code></pre>
</li>
<li><p>Commit and push:</p>
<pre><code class="language-bash">git add .
git commit -m "fix: set up NPM deployment"
git push -u origin setup-deployment
</code></pre>
</li>
<li><p><strong>Create an NPM token:</strong></p>
<ul>
<li><p>Sign in to your <a href="https://www.npmjs.com/">NPM account</a> (or create an NPM account if you don’t have one already)</p>
</li>
<li><p>On the account menu, accessible on the top right hand side of your NPM home page, select <strong>Access Tokens:</strong></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728490857363/1c475a4e-37c1-4e18-ae58-076ad11c9dd9.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Generate a new <strong>Granular Access Token</strong> with <strong>Read and Write</strong> permissions.</p>
</li>
</ul>
</li>
<li><p>Store the value of the token as a <a href="https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions"><strong>repo secret</strong></a> named <code>NPM_TOKEN</code> in your GitHub repo.<br />Make sure to create a <strong>Repository Secret</strong> and not an Environment Secret or a Variable (unless you know what you’re doing).</p>
</li>
<li><p>In your GitHub repo, create new issue in the repo (for its title, you could use a string like “Set up deployment to NPM”).<br />Take note of the issue number.</p>
</li>
<li><p>Open a pull request to merge <code>setup-deployment</code> into <code>main</code>.<br />In the pull request Description enter <code>Fix #&lt;your issue number&gt;</code></p>
</li>
<li><p>Once checks have completed, merge pull request.</p>
</li>
</ol>
<p>Once <code>release.yml</code> has finished, your package should be deplolyed to NPM. To navigate to it select <strong>Packages</strong> options from the menu on the left on your NPM home page.</p>
<p>On your package’s page in NPM, you would find this lovely provenance attestation:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728506930436/1fc23211-a0d2-472c-8ecc-5156429f49d4.png" alt="" style="display:block;margin:0 auto" />

<p>Note that in this section I have only shown how to setup deployment in the release (to production) pipeline. Unlike deployment setup shown for the Next.js sample app in the section <a href="#heading-deploy-to-a-non-npm-target">Deploy to a non-NPM Target</a> above:</p>
<ul>
<li><p>I have not shown how to write a CI pipeline to deploy the package to a pull request-specific Preview/UAT environment.</p>
</li>
<li><p>I have not hown how to use GitHub Environments for a better DX/UX</p>
</li>
</ul>
<p>You can add these if you wish as I sometimes do when I deploy packages to NPM registry from my release pipelines.</p>
<p>In particular, a PR-specific UAT environment can be achieved by deploying to GitHub Packages from a CI workflow and showing the name of the package with version number (e.g. <code>show-version-number@0.0.0-pr14</code>), and instructions on how to add GitHub Packages of the repo as a package registry in your local NPM installation, in a sticky comment on the pull request.</p>
<h2>Relaxing Commitlint Setup</h2>
<p>In the setup given above, you have to provide a commit message that is compliant with the Angular commit message conventions in two places: when you create commits on the source branch and in the pull request title + description.</p>
<p>However, if you only use Squash Merge as I have done in this article, it is enough to lint only the pull request title + description to ensure that semantic-release would be able to parse commit messages on <code>main</code>.</p>
<p>I still lint commits on source branch of pull request, both locally by running commitlint in Husky and in a GitHub Actions workflow when there are pushes to the source branch of an open pull request. The reason this is that it forces me to identify the type of change - <code>fix</code>, <code>feat</code>, <code>BREAKING CHANGE</code> etc. - that I am putting into every commit. This helps me in code review to determine the final type of the merged pull request, which would be put into the PR title or commit message.</p>
<p>For instance, suppose all commit messages on the source branch have prefix <code>fix:</code> except one which contains <code>BREAKING CHANGE</code> in message footer. In this case I would ensure, when merging the pull request after PR review, that the pull request description contains <code>BREAKING CHANGE</code> in the footer.</p>
<p>However, if you only do Squash Merging and find that linting commit messages in source branch of a pull request is too onerous in addition to also linting PR title + description, then you can remove linting of source branch commit messages as follows:</p>
<ul>
<li><p>In <code>.husky/commit-msg</code>, delete the line <code>npx --no -- commitlint --edit $1</code></p>
</li>
<li><p>In <code>.github/workflows/ci-commitlint.yml</code>, delete step <code>Run commitlint on PR source branch commit messages</code></p>
</li>
</ul>
<p>If you do not use Squash Merging but do use either Merge Commits or Rebase Merging methods when merging a pull request, then source branch commit messages are replicated in commit created in in <code>main</code> on merge. Therefore they do need to be linted.</p>
<p>If you use only Rebase Merging, then PR title + description are not used as message of a commit in <code>main</code>. In this case, to stop commitilnt unnecessarily liting this, delete step <code>Run commitlint on PR Title and Description</code> in <code>.github/workflows/ci-commitilnt.yml</code></p>
<h2>Appendix: Details of semantic-release Configuration and Internal Workflow</h2>
<p>Consider the semantic-release config <code>release.config.js</code> given in section <a href="#heading-set-up-semantic-release">Set up semantic-release</a> above:</p>
<pre><code class="language-javascript">/* eslint-env node */
module.exports = {
  branches: ['main'],
  plugins: [
    '@semantic-release/commit-analyzer',

    '@semantic-release/release-notes-generator',

    [
      '@semantic-release/npm',
      {
        npmPublish: false,
      },
    ],
    [
      '@semantic-release/changelog',
      {
        changelogFile: 'docs/CHANGELOG.md',
      },
    ],
    [
      '@semantic-release/github',
      {
        assets: ['docs/CHANGELOG.md'],
      },
    ],
    [
      '@semantic-release/exec',
      {
        successCmd:
          "echo 'RELEASED=1' &gt;&gt; \(GITHUB_ENV &amp;&amp; echo 'NEW_VERSION=\){nextRelease.version}' &gt;&gt; $GITHUB_ENV",
      },
    ],
  ],
};
</code></pre>
<p><code>branches: [main]</code> says when run, semantic-release would scan commit history of <code>main</code>.</p>
<p><strong>The rest of the file is configuration of semantic-release plugins.</strong></p>
<p>When semantic-release runs on a branch of a Git repo, it goes through a number of steps that constitute its internal workflow. These steps, in order of execution, are as follows (details on these can be found on <a href="https://semantic-release.gitbook.io/semantic-release/usage/plugins">Plugins page of semantic-release docs</a>):</p>
<ol>
<li><p><code>verifyConditions</code></p>
</li>
<li><p><code>analyzeCommits</code>: At this step, the <strong>type of release</strong> - i.e which one of the patch, minor and major components of the previous version number should be incremented - is determined.</p>
</li>
<li><p><code>verifyRelease</code></p>
</li>
<li><p><code>generateNotes</code>: The text of the release notes is generated</p>
</li>
<li><p><code>prepare</code>: At this step, files such as <code>package.json</code> and <a href="http://CHANGELOG.md"><code>CHANGELOG.md</code></a> are created and/or updated.</p>
</li>
<li><p><code>publish</code></p>
</li>
<li><p><code>addChannel</code></p>
</li>
<li><p><code>success</code>: At this step, notifications for the new release are issued</p>
</li>
<li><p><code>fail</code> At this step, notifications of a failed release are issued</p>
</li>
</ol>
<p><strong>It is important to note that:</strong></p>
<ul>
<li><p><strong>semantic-release uses <em>plugins</em> to execute the steps</strong> above.</p>
<p>The plugins included by default are:</p>
<pre><code class="language-json">"@semantic-release/commit-analyzer"
"@semantic-release/release-notes-generator"
"@semantic-release/npm"
"@semantic-release/github"
</code></pre>
<p>In addition, we installed the NPM packages for, and configured, plugins <code>@semantic-release/changelog</code> and <code>@semantic-release/exec</code> in the config file above.</p>
<p><em>Official</em> plugins are <a href="https://semantic-release.gitbook.io/semantic-release/extending/plugins-list">listed here in semantic-release docs</a>.</p>
</li>
<li><p><strong>There is NOT a one to one mapping between steps and plugins.</strong></p>
<p>semantic-release calls every plugin at every step.</p>
<p>However, most plugins respond at one or only a few of the steps (see plugin's documentation for details). <strong>For example:</strong></p>
<ul>
<li><p><code>@semantic-release/commit-analyzer</code> only responds at the <code>analyzeCommits</code> step.</p>
</li>
<li><p><code>@semantic-release/release-notes-generator</code> only responds at the <code>generateNotes</code> step.</p>
</li>
<li><p><code>@semantic-release/github</code> plugin, which is used to publish a release to a GitHub repo using GitHub's built-in Releases mechanism, acts during each of the <code>verifyConditions</code>, <code>publish</code>, <code>success</code> and <code>fail</code> steps.</p>
</li>
</ul>
<p>The <a href="https://semantic-release.gitbook.io/semantic-release/extending/plugins-list">official plugins list</a> states during which steps each of the plugins used in the config file above operates in.</p>
</li>
</ul>
<p><strong>In the subsections below I describe what the various plugins do.</strong></p>
<h3><code>@semantic-release/commit-analyzer</code> plugin</h3>
<p>This plugin analyses commits in the commit graph of the configured branch (<code>main</code> in the config shown) and determines the type of next release, i.e. which one of the <code>major</code>, <code>minor</code> and <code>patch</code> component of the version number should be incremented.</p>
<p>To explain how it works, I am going to take the configuration of the plugin in the config file above where all I did was declare the name of the plugin:</p>
<pre><code class="language-javascript">module.exports = {
  branches: ['main'],
  plugins: [
    '@semantic-release/commit-analyzer',
</code></pre>
<p>and expand it out to fill in the effective default values for the plugin’s configuration settings:</p>
<pre><code class="language-javascript">module.exports = {
  branches: ['main'],
  plugins: [
    [
      '@semantic-release/commit-analyzer',
      {
        preset: 'angular',
        releaseRules: [
          { breaking: true, release: "major" },
          { revert: true, release: "patch" },
          // Angular
          { type: "feat", release: "minor" },
          { type: "fix", release: "patch" },
          { type: "perf", release: "patch" },
        ],
      }
</code></pre>
<p>The <code>releaseRules</code> object above contains the <a href="https://github.com/semantic-release/commit-analyzer/blob/3ee80835d872eed9810e1a4bdea961bc75d8916b/lib/default-release-rules.js#L7-L12">default release rules</a> used by the plugin to map various bits of information extracted from the commit message to the component of the semver version number that should be incremented. The items of information that are mapped are:</p>
<ul>
<li><p><code>type</code> of a commit message is the prefix of the message header (first line of the commit message). For example in commit message header <code>fix: Change parsing logic</code>, <code>type</code> would be <code>fix</code>.<br />Mapped value of <code>type</code> attribute are <code>feat</code>, <code>fix</code> and <code>perf</code>. Other types such as <code>ci</code> and <code>build</code> are allowed by Angular commit message conventions. However, since they are not mapped in the default <code>releaseRules</code> to a release type (<code>major</code>, <code>minor</code> or <code>patch</code>) using <code>type</code> and <code>release</code> attributes, a commit containing such a prefix in its commit message would be ignored by semantic-release unless it indicates a revert commit or a breaking changes (see below).</p>
</li>
<li><p>If the commit message indicates that its purpose is to revert to a previous commit (e.g. the commit was created using <a href="https://www.atlassian.com/git/tutorials/undoing-changes/git-revert"><code>git revert</code></a>), then the <code>patch</code> component of the version number would be incremented.<br />Under the Angular commit message conventions (which are the default), a revert commit’s commit message should not only begin with the prefix <code>revert:</code> , it should also meet additional requirements, <a href="https://github.com/angular/angular/blob/68a6a07/CONTRIBUTING.md#revert-commits">as explained here</a>. This is probably why it is not enough to say <code>{ type: “revert", release: "patch"}</code> and the first part of the rule instead says <code>{ revert: “true", ...</code>.</p>
</li>
<li><p>If the commit message indicates a breaking change then the <code>major</code> component of the version number needs to be incremented.</p>
<p>Under Angular message conventions (which are the default), a breaking change is indicated by including the phrase <code>BREAKING CHANGE</code> in the message footer.</p>
<p>Footer is the part of the commit message that comes after body and is separated from it by a blank line:</p>
<pre><code class="language-javascript">&lt;header&gt;
&lt;BLANK LINE&gt;
&lt;body&gt;
&lt;BLANK LINE&gt;
&lt;footer&gt;
</code></pre>
<p>In <a href="https://www.conventionalcommits.org/en/v1.0.0/">Conventional Commits</a>, which is another set of commit message conventions, a breaking change may instead be indicated by <code>feat!</code> or <code>fix!</code> appearing as <code>type</code> in commit message header.</p>
</li>
</ul>
<p>To extract the various bits of information from a commit message, such as <code>type</code> from message header and whether or not <code>BREAKING CHANGE</code> was found in message footer, the plugin uses another package, <a href="https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser"><code>conventional-commits-parser</code></a>.</p>
<p>The parser needs to be configured with several properties that describe the message structure and allow various bits of information to be extracted from it. These property values are usually provided by a <em>preset</em> which is yet another NPM package. It is specific to the commit message conventions you have decided to use.</p>
<p>The default <code>preset</code>, as shown in expanded config above, is <code>"angular"</code>. Based on naming conventions, this translates into the preset package <a href="https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular"><code>conventional-changelog-angular</code></a>. Other possible values for <code>preset</code> are <a href="https://github.com/semantic-release/commit-analyzer?tab=readme-ov-file#options">given here</a>.</p>
<p>Note that a preset in semantic-release configuration is the counterpart to a <em>config</em> in commitlint configuration. Since we are using the default <code>angular</code> preset in semantic release configuration, we <a href="#heading-set-up-husky-and-commitlint">set up commitlint</a> to use config <a href="https://www.conventionalcommits.org/en/v1.0.0/"><code>config-angular</code></a> earlier.</p>
<p>All options for the parser are given in the <a href="https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser">GitHub repo of <code>conventional-commits-parser</code></a>. The default preset <code>conventional-changelog-angular</code> <a href="https://github.com/conventional-changelog/conventional-changelog/blob/d3b8aaa16337993bbad4d91db1ffac5a7568756b/packages/conventional-changelog-angular/src/index.js#L8">provides</a> only <a href="https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-angular/src/parser.js">some of these</a> options:</p>
<pre><code class="language-javascript">{
    headerPattern: /^(\w*)(?:\((.*)\))?: (.*)$/,
    headerCorrespondence: [
      'type',
      'scope',
      'subject'
    ],
    revertPattern: /^(?:Revert|revert:)\s"?([\s\S]+?)"?\s*This reverts commit (\w{7,40})\b/i,
    revertCorrespondence: ['header', 'hash'],
    noteKeywords: ['BREAKING CHANGE'],
  }
}
</code></pre>
<p>The plugin <a href="https://github.com/semantic-release/commit-analyzer/blob/master/lib/load-parser-config.js">loads the parser options</a> above that are exported by the preset, and <a href="https://github.com/semantic-release/commit-analyzer/blob/3ee80835d872eed9810e1a4bdea961bc75d8916b/index.js#L31-L34">passes these on to the parser</a>.</p>
<p>If you want to override these, or set other parser options that are not provided by the preset, you can do that in <a href="https://github.com/semantic-release/commit-analyzer?tab=readme-ov-file#options"><code>parserOpts</code> object</a> in the plugin’s configuration. All the options you can provide in this object are <a href="https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser#parseroptions">lised on <code>conventional-commits-parser</code>'s README</a> and include the five options, <code>headerPattern</code>, <code>headerCorrespondence</code> and others shown in the snippet above (the snippet shown above would be a valid value for the <code>parserOpts</code> property in the plugin’s configuartion, although by default <code>parserOpts</code> is not set in the plugin’s configuration).</p>
<p>In the parser options shown above, <code>headerPattern</code> is a regular expression that contains three capturing groups: <code>(\w*)</code>, <code>(.*)</code> and <code>(.*)</code>. These match parts of the commit message as shown below in a screenshot from <a href="https://regex101.com/r/u4Y38C/1">this example I have created on regex101</a>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726404511472/f5128542-69fc-4dfc-af16-f4954e57146e.png" alt="" style="display:block;margin:0 auto" />

<p>The parser applies the given <code>headerPattern</code> to commit message header, then using <code>headerCorrespondence</code>, it maps the matches of the three capturing groups in order from left to right to <code>type</code>, <code>scope</code> and <code>subject</code> attributes respectively. So for the commit message in the screenshot above, the matches <code>fix</code>, <code>db</code> and <code>change database read logic</code>, become <code>type</code>, <code>scope</code> and <code>subject</code> respectively. The parser returns these attribute-value pairs to the plugin.</p>
<p><code>scope</code> is an optional part of the commit message in both <a href="https://www.conventionalcommits.org/en/v1.0.0/">Conventional Commits</a> and <a href="https://github.com/angular/angular/blob/68a6a07/CONTRIBUTING.md#commit">Angular</a> message conventions. It indicates the scope of the change contained in the commit and is usually the name of a component of the system, e.g. <code>db</code>, <code>webapp</code>, <code>paymentsprocessor</code>.</p>
<p>Out of the three items of information that may be extracted from commit message header, the plugin, as per the <code>releaseRules</code> object in its default configuration shown above, only uses <code>type</code> to compute the type of next release. For complete details on how the plugin uses or can use the three properties - <code>type</code>, <code>scope</code> and <code>subject</code> - and how to configure them in <code>releaseRules</code>, see the <a href="https://github.com/semantic-release/commit-analyzer?tab=readme-ov-file#releaserules">plugin’s README on Github</a>.</p>
<p>The <a href="https://github.com/conventional-changelog/conventional-changelog/blob/d3b8aaa16337993bbad4d91db1ffac5a7568756b/packages/conventional-commits-parser/src/CommitParser.ts#L434"><code>conventional-changelog-parser</code> matches the header again, this time against <code>revertPattern</code> regex</a> for the <a href="https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-angular/README.md#revert">Angular commit message conventions</a>. Using the value of <code>revertCorrespondence</code> option, it maps the two capturing groups in the pattern shown to properties <code>revert</code> and <code>hash</code>. These are also passed back to the plugin along with the other properties - <code>type</code>, <code>scope</code>, <code>subject</code> etc. - that were parsed from the header.</p>
<p><code>revert</code> tells the plugin if the commit indicates a revert to an earlier commit, whose hash is in the extracted <code>hash</code> property’s value.</p>
<p>Since the default <code>releaseRules</code> above match <code>revert</code> to <code>patch</code>, therefore the plugin would take a commit message that matches <code>revertPattern</code> - i.e. from which <code>revert</code> and <code>hash</code> proeprty values were extracted by the parser - to mean that the <code>patch</code> component of the version number should be incremented.</p>
<p>Finally, <code>noteKeywords</code> in parser options simply declares keywords that may appear anywhere in the commit message and not necessarily in the prefix of the commit message header. The parser simply picks these out and returns them to the plugin alongside other parsed information. The plugin doesn’t care what these keywords are. It only cares that parser found at least one occurence of any of the keywords specified in the parser’s <code>noteKeywords</code> option (the default, from Angular preset, is <code>noteKeywords: ['BREAKING CHANGE'],</code>). If this is the case, the plugin deduces there is a breaking change (from <a href="https://github.com/semantic-release/commit-analyzer/blob/3ee80835d872eed9810e1a4bdea961bc75d8916b/lib/analyze-commit.js">code of <code>analyze-commit.js</code></a> in the plugin’s repo; <a href="https://github.com/semantic-release/commit-analyzer/blob/3ee80835d872eed9810e1a4bdea961bc75d8916b/index.js#L58-L67">this file’s default export is called</a> from the plugin’s default export, the function <code>analyzeCommits</code>).</p>
<p>Thus if any of the keywords specified in <code>noteKeywords</code> parser option is found anywhere in the commit message by the parser, given the default <code>releaseRules</code> for the plugin which contain the mapping <code>{ breaking: true, release: "major" }</code>, the plugin increments the major version number.</p>
<p>Further details on the plugin’s configuration, including the <code>preset</code>, <code>parserOpts</code> and <code>releaseRules</code> properties described above, <a href="https://github.com/semantic-release/commit-analyzer/tree/master?tab=readme-ov-file#options">are given in its documentation</a>.</p>
<h3><code>@semantic-release/release-notes-generator</code> plugin</h3>
<p>This plugin generates the text of the release notes.</p>
<p><strong>Like the commit analyzer plugin</strong>, it uses <code>conventional-changelog-parser</code> to parse the commit message and obtain various parts of the message from it, and by default obtains parser options to pass to the parser from the Angular preset (the package <code>conventional-changelog-angular</code>).</p>
<p>As with commit analyzer, you can override the parser options provided by the preset by providing a <code>parserOpts</code> object in this plugin’s configuration.</p>
<p>In order to generate the text of the release notes, this plugin internally uses the package <a href="https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-writer"><code>conventional-changelog-writer</code></a>. This generates the release notes using Handlebars templates. The default templates it uses are <a href="https://github.com/conventional-changelog/conventional-changelog/tree/d3b8aaa16337993bbad4d91db1ffac5a7568756b/packages/conventional-changelog-writer/templates">here in its repo</a>.</p>
<p><a href="https://handlebarsjs.com/">Handlebars.js</a> is a templating toolkit frequently used in web apps. It consists of a templating language using which you create templates, and a library that you can use to substitute text into a template to generate final text output at run time.</p>
<p>Options need to be provided to <code>conventional-changelog-writer</code> and a big part of these are locations of the handlebars templates that the writer needs to generate text of the release notes. The preset typically provides these just as it provides parser options: in the default export of the Angular preset, not only is there a <code>parserOpts</code> object, <a href="https://github.com/conventional-changelog/conventional-changelog/blob/d3b8aaa16337993bbad4d91db1ffac5a7568756b/packages/conventional-changelog-angular/src/index.js#L6-L10">there is also a <code>writerOpts</code></a>:</p>
<pre><code class="language-javascript">export default async function createPreset () {
  return {
    parser: createParserOpts(),
    writer: await createWriterOpts(),
  }
}
</code></pre>
<p>release-notes-generator plugin passes the <code>writer</code> object from the return value of preset-provided <code>createPreset()</code> function to the writer.</p>
<p>You can override writer options by providing a <code>writerOpts</code> object in this plugin’s configuration in <code>semantic-release.config.js</code>. All of the options are documented <a href="https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-writer#options">starting here in the writer’s repo README</a> and include <code>transform</code> which is “A function to transform commits” and <code>mainTemplate</code> which is the main handlebars template.</p>
<p>All configuration settings for release-notes-generator, including <code>preset</code>, <code>parserOpts</code> and <code>writerOpts</code> discussed above are <a href="https://github.com/semantic-release/release-notes-generator?tab=readme-ov-file#configuration">given in the README in plugin’s repo</a>.</p>
<h3><a href="https://github.com/semantic-release/changelog"><code>@semantic-release/changelog</code></a> plugin</h3>
<p>This plugin actually writes out the changelog contents (generated by <code>release-notes-generator</code> plugin) to a specified file.</p>
<p>We have configured it to write to file <code>docs/</code><a href="http://CHANGELOG.md"><code>CHANGELOG.md</code></a>. In GitHub setup this file would be picked up and attached as an asset to a release published by the <code>github</code> plugin. In an Azure DevOps setup it could be written out to the project wiki.</p>
<pre><code class="language-javascript"> [
  '@semantic-release/changelog',
  {
    changelogFile: 'docs/CHANGELOG.md',
  },
],
</code></pre>
<h3><a href="https://github.com/semantic-release/npm"><code>@semantic-release/npm</code></a> plugin</h3>
<p>This plugin is responsible for publishing the versioned Node package to NPM.</p>
<p>I have configured it <em>not</em> to publish to NPM, as described in <a href="https://dev.to/darraghor/semantic-versioning-javascript-projects-with-no-npm-publish-49kl">Darragh o' Riordan's post</a>:</p>
<pre><code class="language-javascript"> [
  '@semantic-release/npm',
  {
    npmPublish: false,
  },
],
</code></pre>
<p>For many Node packages such as React or Angular web apps or API projects it doesn't really make sense to publish them to NPM.</p>
<p>Where it does make sense to publish your pacakge to NPM registry, you can set <code>npmPublish: true,</code> in configuration of this plugin. This is described in detail in <a href="#heading-deploy-to-npm">Deploy to NPM</a> section above and further details is provided in the <a href="https://github.com/semantic-release/npm">plugin’s README</a>.</p>
<h3><code>@semantic-release/github</code> plugin</h3>
<p>This plugin creates a GitHub release with the version number generated by commit analyzer plugin and release notes generated by release notes generator plugin.</p>
<p>I have configured it to attach file <a href="http://CHANGELOD.md"><code>docs/CHANGELOG.md</code></a> generated by the changelog plugin to the GitHub release as a (downloadable) asset as shown below:</p>
<pre><code class="language-javascript">[
  '@semantic-release/github',
  {
    assets: ['docs/CHANGELOG.md'],
  },
],
</code></pre>
<h3><code>@semantic-release/git</code> plugin</h3>
<p>The plugin creates a new commit in the repo. This could contain things like the file of release notes (<code>CHANGELOG.md</code>) and a pacakge.json in which <code>”version”</code> key has been updated to the new version number.</p>
<p>However, semantic-release <a href="https://semantic-release.gitbook.io/semantic-release/support/faq#making-commits-during-the-release-process-adds-significant-complexity">advises against the use of this plugin</a> and it is not used or installed by semantic-release by default. Also, I have not used it in the setup shown in this article.</p>
<p>By default semantic-release only tags the last commit on the branch on which <code>semantic-release</code> was run with the new (auto-incremented) version number. A new commit is not created. In particular, package.json with updated <code>”version”</code> is not checked in. <strong>I find that this is perfectly good behaviour.</strong></p>
<p>In the past I have used this plugin, and configured it to get the following behaviour:</p>
<ul>
<li><p>Store the new version number the semantic-release had generated in <code>"version"</code> key in <code>package.json</code>.<br />The bash script I used to run in my release pipelines to update <code>package.json</code> with the new version number is as follows. It may have been unnecessary as <em>I think</em> that <code>”version”</code> in <code>package.json</code> is updated by semantic-release, it’s just not committed to the repo:</p>
<pre><code class="language-javascript">relout=$(npm run release-dry-run)
vnumber=\((echo \)relout | grep -o  -E 'The next release version is (([0-9]+).([0-9]+).([0-9]+))' | grep -o -E '[0-9]+.[0-9]+.[0-9]+')

echo "next version (parsed from semantic-release dry tun output): $vnumber"
npm pkg set version="$vnumber"
</code></pre>
</li>
<li><p>Given that the <a href="https://github.com/semantic-release/changelog"><code>@semantic-release/changelog</code></a> plugin would already have stored the generated release notes in <code>docs</code> folder in project root, I configure this plugin to take both the release notes and the updated <code>package.json</code> with the new version number, and create a new commit with these two files.<br />The commit message would be have the special prefix <code>[skip ci]</code> in the commit message so that release pipeline does not run again (otherwise we would end up in an infinite loop as the release pipeline would run semantic-release to create another release).</p>
<pre><code class="language-javascript">[
    '@semantic-release/git',
    {
        message:
        'fix(release): \({nextRelease.version} [skip ci]\n\n\){nextRelease.notes}',
        assets: ['docs/CHANGELOG.md', 'package.json'],
    },
],
</code></pre>
</li>
</ul>
<p>The supposed gain from this setup was that:</p>
<ul>
<li><p><code>package.json</code> always contained the latest version number</p>
</li>
<li><p>release notes got checked into the repo</p>
</li>
</ul>
<p>However this notional gain did not translate into an advantage in my day to day work:</p>
<ul>
<li><p>As a developer I never look at <code>package.json</code> of a package I am working on to find out what <code>”version”</code> it is. If ever I need to find out the current version number of the package, I can always see it on the repo’s web page on GitHub or Azure DevOps.</p>
</li>
<li><p>Release notes are always available to view wherever semantic-release published them to (a Release on GitHub, or the project wiki in Azure DevOps). I would never need to look at release notes in my code editor while working on the containing project.</p>
</li>
</ul>
<p>So there was practically nothing to be gained by creating the additional commit.</p>
<p>However, the additional commit was quite cumbersome in practice. It polluted <code>main</code> with (almost) redundant commits. This did not match up well with the fact that I always try to keep the history of my <code>main</code> linear with chunky, meaningful commits and descriptive commit messages.</p>
<p>Therefore the setup I now have is as shown in this article above, where:</p>
<ul>
<li><p>I do not use <code>@semantic-release/git</code> plugin to create an aditional commit with the modified <code>package.json</code> and <code>CHANGELOG.md</code>.</p>
</li>
<li><p>In <code>package.json</code>, I set <code>"version": "0.0.0-managed.by.semantic.release",</code> .</p>
<p>I do this because a package.json with an updated version number is not checked in by the release process. At the same time, the actual version number, as tagged on commits on <code>main</code> and as stated in release notes and in GitHub Releases, and visible on the repo’s home page in GitHub, is increasing.</p>
<p>Thus the version in <code>package.json</code>, such as the initial and never-changing <code>1.0.0</code>, can be misleading to someone who looks at the code of the package. Therefore I replace it with <code>"version": "0.0.0-managed.by.semantic.release",</code> to indicate that it is a dummy value.</p>
<p>semantic-release itself has its <a href="https://github.com/semantic-release/semantic-release/blob/b95ca315bca443a0fdf0d6145f9a9e1b88339773/package.json#L4">version number set to <code>0.0.0-development</code></a> in its package.json. However, I prefer the suffix given above as I feel that it more clearly indicates what is going on.</p>
<p>Either suffix, <code>-development</code> or <code>-managed.by.semantic.release</code> is a valid <em>pre-release identifier</em> as per the <a href="https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions">EBNF grammar for for a valid SemVer version.</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[The Three Types of Pull Request Merge in GitHub]]></title><description><![CDATA[When you go to merge a pull request on GitHub, you would see three choices:

The behaviour of each of the merge methods is as follows (assuming for the sake of this post that the target branch of the pull request is main, i.e. you are merging into ma...]]></description><link>https://www.naveedausaf.com/types-of-merges-in-a-github-pull-request</link><guid isPermaLink="true">https://www.naveedausaf.com/types-of-merges-in-a-github-pull-request</guid><category><![CDATA[Git]]></category><category><![CDATA[GitHub]]></category><dc:creator><![CDATA[Naveed Ausaf]]></dc:creator><pubDate>Sun, 22 Sep 2024 02:09:53 GMT</pubDate><content:encoded><![CDATA[<p>When you go to merge a pull request on GitHub, you would see three choices:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726970585142/6c4cfc29-84df-4512-bb23-926699b629ca.png" alt class="image--center mx-auto" /></p>
<p>The behaviour of each of the merge methods is as follows (assuming for the sake of this post that the target branch of the pull request is <code>main</code>, i.e. you are merging into <code>main</code>):</p>
<p><strong>Merge commits</strong> keep all of the commits in the pull request’s source branch and simply add a single new merge commit that points back both to the last commit in the source banch and to the last commit in <code>main</code>.</p>
<p>In the commit graph shown below, the tip of <code>main</code> is the merge commit.</p>
<p>The commit message of the merge commit defaults to the combined pull request and description (topmost comment in the pull request) if this is what you have selected under <strong>Allow merge commits</strong> in Settings:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726938037341/f4a8cf7b-20e9-4610-bcf5-44aa571be3d4.png" alt class="image--center mx-auto" /></p>
<p><strong>Squash merging</strong> does not carry over source branch’s commits into <code>main</code>. Instead it squashes all changes in the source branch commits and creates a single commit from these and places it in front of the last commit in <code>main</code>.</p>
<p>In the commit graph below, the tip of <code>main</code> is the squash commit. As you can see, unlike a merge commit, it doesn’t point back to the branch <code>add-hello-world</code> that was merged. Instead it squashes the two commits on that branch ahead of <code>main</code> into a single commit. This becomes the new tip of <code>main</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726961169930/69cd017f-a9c2-4676-8abe-938b1855b51a.png" alt class="image--center mx-auto" /></p>
<p>Again, the message of this commit is the combined pull request title and description if this is what you have selected under Pull Requests in Settings:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726939494453/97d3a7da-5b9e-4624-8fe7-8262e86a62e8.png" alt class="image--center mx-auto" /></p>
<p><strong>Rebase merging</strong> rebases the source branch onto main (without altering the source branch) and places the new rebased commits in front of <code>main</code>. Hence there is one new commit on <code>main</code> for every commit in the source branch that is ahead of the original tip of <code>main</code>. The last rebased commit becomes the new tip of <code>main</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726967177618/de48001e-2869-43d7-9aff-c8320312f2a8.png" alt class="image--center mx-auto" /></p>
<p>Each rebased commit retains the commit message of the original commit. Therefore the pull request title or description do not get used in any commit message and there is also no option to use them under <strong>Allow rebase merging</strong> in <strong>Settings</strong>.</p>
<h2 id="heading-my-personal-preference-squash-merge">My personal preference: Squash Merge</h2>
<p>I personally prefer the Squash merge method and disallow the other two because:</p>
<ul>
<li><p>I prefer to keep my <code>main</code> linear as it’s much easier to understand (e.g. to <code>git bisect</code>) than the sort of non-linearhistory yo uget when you have merge commits. Therefore I select option <strong>Require Linear History</strong> in <strong>Branch protection rule</strong> for <code>main</code> (see step XX above).<br />  This precludes the use of merge commits: the merge commit options would not be available on a pull request if you have the require Linear History option checked in the branch protection rule for the target branch (<code>main</code> in this case).</p>
</li>
<li><p>Of the remaining two available merge methods - Squash merge and Rebase merge - I prefer squashing. I like to merge smaller pull request frequently rather than attempt to merge several days or weeks of work on the feature branch into <code>main</code>. For smaller merges, I find that having a single meaningful squash commit on <code>main</code> works a lot better than having lot of tiny commits on <code>main</code> that were obtained by rebasing lots commits on feature branch onto <code>main</code>.</p>
</li>
</ul>
<p>Thus with squash merging, I get a linear <code>main</code> on which individual commits are chunky and meaning rather than a <code>main</code> that is littered with lots of tiny commits.</p>
]]></content:encoded></item><item><title><![CDATA[LF vs CRLF - Configure Git and VS Code to use Unix line endings]]></title><description><![CDATA[Set LF as end of line character in VS Code and Git
To set the default for line endings in VS Code to LF:

go to Command Palette (Ctrl + Shift + P)

type Settings. Choose Preferences: Open Settings (UI) from the list of options that are displayed

In ...]]></description><link>https://www.naveedausaf.com/lf-vs-crlf-configure-git-and-vs-code-to-use-unix-line-endings</link><guid isPermaLink="true">https://www.naveedausaf.com/lf-vs-crlf-configure-git-and-vs-code-to-use-unix-line-endings</guid><category><![CDATA[Git]]></category><category><![CDATA[vscode]]></category><category><![CDATA[Windows]]></category><category><![CDATA[EOL]]></category><category><![CDATA[#LF, CRLF]]></category><dc:creator><![CDATA[Naveed Ausaf]]></dc:creator><pubDate>Mon, 16 Sep 2024 11:39:06 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-set-lf-as-end-of-line-character-in-vs-code-and-git">Set LF as end of line character in VS Code and Git</h2>
<p><strong>To set the default for line endings in VS Code to LF:</strong></p>
<ul>
<li><p>go to Command Palette (<strong>Ctrl + Shift + P</strong>)</p>
</li>
<li><p>type Settings. Choose <strong>Preferences: Open Settings (UI)</strong> from the list of options that are displayed</p>
</li>
<li><p>In the <strong>Search settings</strong> textbox on the <strong>Settings tab</strong>, type <strong>eol</strong> This would bring up the end of line setting:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726824655833/2465aaf5-4f73-4fb6-bec7-a4b51f8b4a7d.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>From the dropdown, select <code>\n</code> (as shown above). This would default VS Code to always use LF line endings in new files.</p>
</li>
<li><p>Close <strong>Settings</strong> tab.</p>
</li>
</ul>
<p>Every new file created in VS Code on Windows would now use LF as the end of line character.</p>
<p><strong>To configure Git on Windows to use LF line endings when committing staged files or when fetching from a remote repo:</strong></p>
<ul>
<li><p>If it does not already exist, create a <code>.gitattributes</code> file in root of the repo's working directory.</p>
</li>
<li><p>Add the following to the <code>.gitattributes</code> file:</p>
<pre><code class="lang-javascript">  * text=auto eol=lf
</code></pre>
</li>
<li><p>Commit your changes:</p>
<pre><code class="lang-javascript">  git add .
  git commit -m <span class="hljs-string">"Configured .gitattributes with LF line endings"</span>
</code></pre>
</li>
</ul>
<p><strong>The</strong> <code>git add .</code> command and fetch from remote should now happen smoothly without warnings about LF and CRLF.</p>
<h2 id="heading-why-do-this">Why do this</h2>
<p>There are two different character sequences that are used in text files to indicate a line break, i.e. end of the current line and beginning of the next one:</p>
<ul>
<li><p>Text editors (and code editors) on Unix-based operating systems, such as Linux and Mac OS, embed a character known as <strong>Line Feed</strong> (ASCII Code 10, Unicode character code also the same, written as <code>0x000A</code> in hexadecimal) in the text to indicate a line break. The name of this character is usually abbreviated to <strong>LF</strong>.</p>
</li>
<li><p>Text- and code editors on Windows typically embed a sequence of two characters at the location of a line break in text: a <strong>Carriage Return</strong> character (<strong>CR</strong> for short; ASCI code 13, in Unicode it is the same and written in hexadecimal as <code>0x000D</code>) followed by a <strong>Line Feed</strong>. This character sequence is usually referred to as <strong>CRLF</strong>.</p>
</li>
</ul>
<p>Characters/character sequences to indicate a line breaks are also referred to as <strong>End of Line (EOL) characters</strong> or as <strong>Line Endings</strong>.</p>
<p>In programming languages (such as JavaScript, C#) the Line Feed character is escaped as <code>\n</code> whereas Carriage Return Character is escaped as <code>\r</code>. In order to see the effect of LF, open F12 Developer Toolbar in the browser. In this go to Console. Type <code>console.log("first line\nsecond line\nthis is the third line")</code> and press Enter:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726825209655/4da8aa96-4302-4e98-b950-5c296444ed3d.png" alt class="image--center mx-auto" /></p>
<p>You can see that the LF character (escaped using <code>\n</code> in the line of JavaScript code shown) translates into a line break.</p>
<p>Now type the same line with <code>\r\n</code> in places where there was <code>\n</code>, i.e. type the following line in Console and press Enter:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">console</span>.log(<span class="hljs-string">"first line\r\nsecond line\r\nthis is the third line"</span>)
</code></pre>
<p>This too displays as three separately lines, i.e. the character sequence CRLF was printed as a line break.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726825263272/8a66971b-2912-41aa-b776-88a622afb6da.png" alt class="image--center mx-auto" /></p>
<p>Both LF and CRLF work as line break when you print text from a programming language, regardless of the operating system on which the code executes. So the two lines of code given above should work on both Windows and on Linux/Unix/MacOS, in Node.js as well in the browser.</p>
<p><strong>However, when editors and text tools on Unix-based systems load files which were authored in Windows and use CRLF line endings, they can have trouble displaying or processing them</strong>. On the other hand, all modern word processors and code editors on Windows can handle the Unix LF line endings perfectly well.</p>
<p>Since teams members may use different operating systems and since open source projects can accept contributions from developers using a variety of operating systems, <strong>it is best practice to ensure that all text files (including code files) in a Git repo use Unix line endings (i.e. LF, and not CRLF)</strong>.</p>
<p>Code scaffolders such as <code>create-next-app</code> typically generate files with LF line endings, whether they are run on Windows or on other operating systems. If you open a code file generated by such a scaffolder and look in the status bar in your VS Code, you should see <strong>LF</strong>, indicating that the line endings used in the file would be LF.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726825372966/b49aab82-18fb-4884-9e53-e28ff9e9189b.png" alt class="image--center mx-auto" /></p>
<p>However, if you create a new (empty) file, this would default to CRLF on Windows. <strong>Since we want line endings in all files in a Git repo to be LF, VS Code needs to be configured to use LF for all new files</strong>.</p>
<p><strong>Also, Git on Windows also uses CRLF line ending by default</strong>. What this means is that it would replace all standalone LF characters (those not prefixed with the CR character) with CRLF when committing your changes or when fetching a remote repo. So even if all of the files in the local Git working directory had LF endings, when you run <code>git add.</code>, you would get a warning for every file that was added to the local repo's staging area which says that <strong>LF will be replaced by CRLF the next time Git touches</strong> that file:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725930986122/b5630966-e8dd-4f47-bb58-eed6eb023ea6.png" alt="Warnings shown by git add command when some of the files being added contain LF but the repo's line ending default is CRLF" /></p>
<p>Therefore, <strong>Git on Windows also needs to be configured to use LF as the EOL character</strong> instead of CRLF.</p>
]]></content:encoded></item></channel></rss>