Automatically publish your private NPM packages from monorepo to Gitlab Package Registry

A solution to publish private npm packages and have most of the jobs done automatically so that you only need to care about good code.

Thai Nguyen
Legalstart
4 min readDec 5, 2022

--

Before you read: This article focuses on the automation process, assuming that you have base knowledge on:

  • npm package management
  • Gitlab CI
  • multi-package repo (monorepo) concept and you are already using Lerna to manage it (If you are not, let’s check this doc first!)

🤔 Question

How do I publish only the sub-packages that have been changed when my merge request is merged into the main branch?

💡 Solution

In short, we will use Gitlab CI to automate the process, including:

  • Use Lerna's version and publish commands (they are built on top of schematic-release) to (1) determine the next version number, (2) generate the release notes, and (3) publish the package.
  • Use Gitlab Package Registry to store the packages (4)

Let’s go into detail!

Configure Lerna

(1) Versioning: determine the next version number

Config lerna version command to increase the package’s version number by adding this into Lerna’s configs:

{
"command": {
"version": {
"conventionalCommits": true,
"createRelease": "gitlab",
"message": "chore(release): publish packages"
}
},
"version": "independent"
}

With that config, Lerna will detect the current package, identifies the current version, and propose the next one based on conventional-commits rules.

"version": "independent" allows us to increment package versions independently of each other.

(2) Generate release notes

By using "conventionalCommits": true, Lerna will collect the changes log from your commit messages and add them to package’s CHANGELOG.md file which you can use as release notes.

(3) Publish the package

As we want to publish the packages onto our private registry, add registry to publish command config, so that forwarded npm commands will use the specified registry for your package(s).

{
"command": {
"publish": {
"registry": "https://<YOUR_GITLAB_DOMAIN>/api/v4/projects/<PROJECT_ID>/packages/npm/"
}
}
}

After having the new version number and the release notes, Lerna will update the changed package’s package.json with that new version number, commit the changes with predefinedmessage then push the commit to the remote repo. It also creates an official Gitlab release based on the changed packages.

In final lerna.jon, you can add some other configs to match your own project. Here, I use yarn as npm client and put my sub-packges in packages/*.

lerna.jon

{
"command": {
"publish": {
"registry": "https://<YOUR_GITLAB_DOMAIN>/api/v4/projects/<PROJECT_ID>/packages/npm/"
},
"version": {
"conventionalCommits": true,
"createRelease": "gitlab",
"message": "chore(release): publish packages"
}
},
"npmClient": "yarn",
"packages": [
"packages/*"
],
"version": "independent"
}

Configure Gitlab

Now let’s create a Gitlab CI job to automate the above process whenever a merge request is merged into the main branch.

We will put them into 2 stages: build then release the packages to Gitlab Private Registry. Don’t forget to install the dependencies before each script.

cache:
key: $CI_COMMIT_REF_SLUG
paths:
- .yarn

before_script:
- yarn install --cache-folder .yarn

stages:
- build
- release

build stage

Here, I will simply use node:latest image to build the packages with yarn workspaces run build command and then store the artifacts in packages/* path when the changes are merged into the main branch master.

build_packages:
image: node:latest
stage: build
script:
- yarn workspaces run build
artifacts:
paths:
- packages/*
expire_in: 1 hour
only:
- master

release state

Wait for the build_packages to be done

dependencies:
- build_packages

Prepare the variable to authenticate the commands with Gitlab

variables:
GL_TOKEN: $GITLAB_TOKEN
GL_API_URL: $CI_API_V4_URL
  • GL_TOKEN (required) - Your GitLab authentication token (under User Settings > Access Tokens).
  • GL_API_URL - An absolute URL to the API, including the version. (Default: https://gitlab.com/api/v4). When using self-hosted Gitlab, let’s take it from Gitlab's predefined variable $CI_API_V4_URL.

Now is the important part, make a script to prepare the needed resources and then run the Lerna publish command in the end. I put a comment to explain what’s needed for each block below:

  script:
# If no .npmrc if included in the repo, generate a temporary one that is configured to publish to GitLab's NPM registry
- |
if [[ ! -f .npmrc ]]; then
echo 'No .npmrc found! Creating one now. Please review the following link for more information: https://docs.gitlab.com/ee/user/packages/npm_registry/index.html#project-level-npm-endpoint-1'
{
echo "@${CI_PROJECT_ROOT_NAMESPACE}:registry=${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/"
echo "${CI_API_V4_URL#http*:}/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=\${CI_JOB_TOKEN}"
} >> .npmrc
fi
- echo "Created the following .npmrc:"; cat .npmrc

# put SSH key in `.ssh` and make it accessible, in order to push release changes
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_DEPLOY_KEY" > ~/.ssh/id_rsa; chmod 0600 ~/.ssh/id_rsa
- echo "StrictHostKeyChecking no " > /root/.ssh/config

# Config git instance
- git config --global user.name "$GITLAB_USER_NAME"
- git config --global user.email "$GITLAB_USER_EMAIL"

- echo "setting origin remote to 'git@$CI_SERVER_HOST:$CI_PROJECT_PATH.git'"
- git remote set-url origin "git@$CI_SERVER_HOST:$CI_PROJECT_PATH.git"
- git checkout -B "$CI_COMMIT_REF_NAME" "$CI_COMMIT_SHA"

# Run publish command
- yarn lerna publish -y

The Lerna publish will only take into account the packages that have been changed and push them to Gitlab, the others unchanged will be ignored.

🎉 Voilà! Until now whenever you merge a merge request to the main branch, Gitlab CI will build and publish your changed packages to Gitlab Package Registry!

In your Gitlab repo, you can find the published packages under Packages & Registries > Package Registry like this:

Enjoy coding! 🤓

--

--