How to Implement Semantic Release for Public Non-Scoped Packages

Written by antonkalik | Published 2023/09/25
Tech Story Tags: semantic-release | npm | javascript | npm-package | github-actions | public-non-scoped-packages | automatic-code-versioning | hackernoon-top-story

TLDRSemantic Release is a tool designed to ensure clear and structured versioning. For developers leveraging public non-scoped packages, the process may seem daunting. With the power of GitHub Actions, automation becomes a breeze. This article offers a detailed and comprehensive guide, providing step-by-step instructions to seamlessly integrate Semantic Release into your workflow.via the TL;DR App

Detailed instructions on publishing a non-scoped public package using semantic release leveraging the power of GitHub Actions

In the evolving software development landscape, maintaining version consistency and automating the release process is more important than ever. Enter Semantic Release: a tool designed to ensure clear and structured versioning. For developers leveraging public non-scoped packages, the process may seem daunting. However, with the power of GitHub actions at our fingertips, automation becomes a breeze.

This article offers a detailed and comprehensive guide, providing step-by-step instructions to seamlessly integrate Semantic Release into your workflow, specifically tailored for those using public non-scoped packages. Dive in and discover the streamlined approach to software releases.

When I created my public package, I encountered hurdles in publishing it seamlessly to NPM. It was a challenge to ensure the right configurations.

To nail it, let’s go through a step-by-step experience for proper publishing public packages to NPM.

Content Overview

  • Name the package
  • Create a package
  • Package source
  • Tokens
  • GitHub Actions setup
  • Semantic release
  • Commit format
  • Published package
  • Conclusion

Name the package

Before jumping into implementing our package, it is better to find the proper name for it. To be sure the name is not taken already — check my_package_name and take it for your package. I chose “tokky.” From that point, reserving the package's name is impossible. For the name in npm, you have to publish the package.

Create a package

The objective is to develop a straightforward package that outputs content to the console. We need to make sure that we can install it and run it. For the build process, let’s use simple esbuild.

During this article, I will use the name of the package tokky. Let’s create package.json with the initial data.

mkdir tokky && cd tokky && npm init -y

After executing the command, the system generated a default package.json file for the project, which looks like this:

{
  "name": "tokky",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

In this guide, the package.json file plays a crucial role in ensuring the right configuration. At this juncture, let's specify the node version for our package:

echo "v18" > .nvmrc

and activate the specified version with the following:

nvm use

For the README.md file:

echo "# Tokky\n\nA simple zero dependency logger for node js." > README.md

Finally, install the development dependencies:

npm i -D esbuild eslint prettier

In our initial configuration, we need to address several key points in the package.json:

  • main: This designates the primary entry point for the module.
  • bin: Here, you’ll specify any executables your module provides.
  • files: This should contain an array of file patterns that will be included when the package is packed and subsequently published to the npm registry.
  • private: Ensure this is set to false as our package is intended to be public.
  • publishConfig: The access for this should be set to public.

After these configurations, your package.json should resemble the following:

{
  "name": "tokky",
  "version": "1.0.0",
  "description": "Node js logger package",
  "main": "dist/index.js",
  "scripts": {
    "build": "esbuild src/index.js --bundle --platform=node --format=cjs --minify --outfile=dist/index.js",
  },
  "files": [
    "dist"
  ],
  "bin": {
    "tokky": "./dist/index.js"
  },
  "keywords": [
    "logger",
    "nodejs",
    "tokky"
  ],
  "private": false,
  "author": {
    "name": "Anton Kalik",
    "email": "[email protected]",
    "url": "https://idedy.com"
  },
  "publishConfig": {
    "access": "public"
  },
  "license": "MIT",
  "engines": {
    "node": "18.x.x"
  },
  "devDependencies": {
    "esbuild": "^0.19.2",
    "eslint": "^8.49.0",
    "prettier": "^3.0.3"
  }
}

package.json after initial setup

Additionally, let’s add two ignore files:

.idea
node_modules
dist

.gitignore

and for npm:

.idea
/src/
/node_modules/
/test/
/.nvmrc
.github/

.npmignore

Lastly, I’ll outline my setup for ESLint. However, remember that the configuration may vary based on the specific requirements of your package.

module.exports = {
  env: {
    browser: true,
    commonjs: true,
    es2021: true,
    node: true,
  },
  extends: "eslint:recommended",
  overrides: [
    {
      env: {
        node: true,
      },
      files: ["src/**/*.js", ".eslintrc.{js,cjs}"],
      parserOptions: {
        sourceType: "script",
      },
    },
  ],
  parserOptions: {
    ecmaVersion: "latest",
  },
  rules: {},
};

.eslintrc.js config

Next, head to GitHub and establish a new repository. Name it after your package.

Proceed by executing the subsequent commands:

git init
git add .
git commit -m "first commit"
git branch -M main
git remote add origin [email protected]:<your_github_username>/tokky.git
git push -u origin main

Package source

Next, let’s craft a basic application and set it up for building. Inside the src folder, generate an index.js file and populate it with the following content:

#!/usr/bin/env node

const os = require('os');
const username = os.userInfo().username;

if (process.argv[2] === 'hi') {
  console.log(`Hello ${username}`);
}

Simple script for package example

The concept is straightforward: executing my_package_name hi should display “Hello [username].”

To validate this functionality, execute the command directly from your repository using:

node src/index.js hi

If the output aligns with expectations, it’s time to build the source:

npm run build

Successfully running this command will produce a dist folder containing a minified index.js file.

Tokens

Execute Semantic Release, which will determine version bumps and handle the release process based on commit messages, requires environment variables (GITHUB_TOKENNPM_TOKEN) to operate correctly. The tokens are fetched from GitHub secrets, ensuring they remain confidential.

To set GITHUB_TOKEN, navigate here: https://github.com/settings/tokens

Generate the token using a dropdown. Click on the new personal access token (classic) and set permission as in the picture.

Use your package name as shown below:

Once generated, copy the token value and keep it confidential — it’s crucial not to share this with others. Temporarily store this token securely, as we’ll need it shortly for the Semantic Release CLI.

To generate the NPM_TOKEN, you first need an account on npm's official website. If you haven't registered yet, go through the registration process. After that, navigate to:

https://www.npmjs.com/settings/<your_user_name>/tokens/new

and generate a “classic” token with the “publish” option.

Copy the generated value of the token and navigate to GitHub secrets:

https://github.com/<your_user_name>/<your_repo_name>/settings/secrets/actions/new

and put new secret as NPM_TOKEN to repository secrets:

With our secrets now set up, we can configure GitHub Actions.

GitHub Actions setup

To automate our processes, we are going to use GitHub Actions. This is a CI/CD tool integrated within GitHub. It allows developers to automate workflows directly from their GitHub repositories, such as building, testing, and deploying applications. By defining workflows in YAML files, users can trigger actions based on specific events like push and pull requests or scheduled times, making the software development process more efficient and automated.

To begin, create a .github directory at the root of your project. Within this directory, establish a workflows subfolder.

Here, craft our configuration file named release.yml and populate it with the following content:

name: Release package

on:
  push:
    branches:
      - main

jobs:
  release:
    runs-on: ubuntu-latest
    if: ${{ github.ref == 'refs/heads/main' }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Semantic Release
        run: npm run semantic-release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

This workflow triggers a push event to the main branch. It’s configured to run the job on the latest Ubuntu virtual machine GitHub offers. While it’s not imperative to delve into every job, let’s spotlight some specific ones. Toward the end, note how we invoke npm run semantic-release using the designated tokens.

Semantic release

For the automated release process, we are going to use Semantic Release. This tool handles versioning and package publishing based on commit message semantics. It follows the conventions of Semantic Versioning (SemVer) to determine version bumps (major, minor, or patch). By analyzing commit messages it eliminates the manual steps of versioning, ensures consistent version numbers, and streamlines the release process. Let’s set it up.

For that setup, we will use this GitHub code and run it on your repository:

npx semantic-release-cli setup

And follow the questions:

% npx semantic-release-cli setup
? What is your npm registry? https://registry.npmjs.org/
? What is your npm username? your_user_name
? What is your npm password? [hidden]
? What is your NPM two-factor authentication code? 00000000
? Provide a GitHub Personal Access Token (create a token at https://github.com/s
ettings/tokens/new?scopes=repo) ghp_your_token_here
? What CI are you using? Github Actions

You should already have your Personal Token. Simply input it when prompted. Similarly, the GitHub Actions we’ve set up will utilize the NPM_TOKEN that we've previously established in the repository secrets. If you now check your package.json, the version will display as:

"version": "0.0.0-development",

and new script:

"semantic-release": "semantic-release"

which was auto-generated by the Semantic Release CLI. We’ll need to enhance this script as follows:

"semantic-release": "semantic-release --branches main"

This indicates that releases will only be made from the main branch.

Additionally, Semantic Release generates a description based on the repository field in your package.json. This field offers details about the location of the package's source code.

"repository": {
  "type": "git",
  "url": "https://github.com/<your_github_username>/your_github_repo.git"
}

Now, let’s push all of our changes with:

git add . && git commit -m "semantic release" && git push

Commit format

Semantic Release relies on the convention of structured commit messages to determine the type of version bump (major, minor, or patch) and generate changelogs. This commit convention is often called the “Conventional Commits” format.

For this configuration, we’ll need several plugins. Ensure your package.json contains the following content:

"release": {
    "branches": [
      {
        "name": "main"
      }
    ],
    "plugins": [
      [
        "@semantic-release/commit-analyzer",
        {
          "releaseRules": [
            {
              "type": "feat",
              "release": "minor"
            },
            {
              "type": "fix",
              "release": "patch"
            },
            {
              "type": "refactor",
              "release": "patch"
            },
            {
              "type": "build",
              "release": "patch"
            },
            {
              "type": "chore",
              "release": "patch"
            },
            {
              "type": "minor",
              "release": "patch"
            }
          ]
        }
      ],
      "@semantic-release/release-notes-generator",
      "@semantic-release/npm",
      "@semantic-release/github",
      [
        "@semantic-release/changelog",
        {
          "changelogFile": "CHANGELOG.md"
        }
      ]
    ]
  }

package.json

For the setup commit format tool, we are going to use commitizen. To install it, follow this command:

npx commitizen init cz-conventional-changelog --save-dev --save-exact

This command will take a few minutes. Then update your package.json with a new script:

  "scripts": {
    // ...
    "commit": "cz"
  },

and it’s time to utilize that script. Begin by executing git add ., then run npm run commit and provide the necessary details for your commit.

Here’s what that looks like:

? Select the type of change that you're committing: feat: A new feature
? What is the scope of this change (e.g. component or file name): (press enter 
to skip) commit
? Write a short, imperative tense description of the change (max 86 chars):
 (14) add commitizen
? Provide a longer description of the change: (press enter to skip)
? Are there any breaking changes? No
? Does this change affect any open issues? No

After that, do a git push.

In GitHub actions, you will see that our commit failed because we still have not installed the rest of the packages for the automated commit message process.

npm i -D @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/npm @semantic-release/changelog

A crucial step, often overlooked in most references, is setting the workflow permissions. Navigate to https://github.com/<your_user_name>/tokky/settings/actions and configure the permissions to allow GitHub actions to both read and write.

Next, let’s change things up a bit. Commit with a specific keyword, feat:, followed by your message.

git add . && git commit -m "feat: my feature commit" && git push

Do you recall the releaseRules within the package.json? These rules dictate how we increment the version of our package release. With this in place, you can create a pull request using specific keywords like featfixrefactor, and so on. Once this pull request is approved and subsequently merged into the main branch, it will initiate a trigger. This trigger then activates the GitHub action, automates the release process, and ensures your package is updated seamlessly.

Published package

The package has been successfully published, and the entire process has been automated for efficiency. To confirm the publication, head to your npm settings https://www.npmjs.com/settings/<your_user_name>/packages and look under the packages section; there, you will find your newly published package.

Now, with a simple command like npx your_package_name hi, you can immediately see the results of our development tests. Additionally, the package can be globally installed using the command npm i -g your_package_name.

Conclusion

As we’ve seen throughout this article, while initial setups can be riddled with challenges, the reward lies in establishing a streamlined and consistent release process. Leveraging GitHub Actions simplifies these complexities, ensuring developers can focus on code quality rather than logistical intricacies.

Whether you’re just beginning your journey with public packages or have encountered setbacks in your publishing endeavors, there’s undeniable value in adopting a structured, automated workflow. By integrating Semantic Release, you’re ensuring consistent versioning and championing a future-forward approach to software development.

Here’s to seamless publishing, fewer headaches, and more time spent perfecting the code that drives our digital world forward.

Remember, it’s essential that both NPM_TOKEN and GITHUB_TOKEN are granted the appropriate permissions within GitHub Actions. Additionally, your package.json should be correctly configured with settings for publishConfig access, and ensure that the private config is set to false. If you encounter any issues or have insights, please don't hesitate to comment.

References

Repository: https://github.com/antonkalik/tokky
Semantic Release CLI:https://github.com/semantic-release/cli
Commitizen:https://github.com/commitizen/cz-cli


Also published here.

Thanks to Harper Sunday from Unsplash for the lead image.


Written by antonkalik | Senior Software Engineer @ Amenitiz / Node JS / React
Published by HackerNoon on 2023/09/25