Building a CI/CD Template to Publish TypeScript Libraries to the GitLab Package Registry
As an engineering team that develops a lot of front-end and back-end JavaScript applications grows, it quickly becomes clear that breaking down shared functionality into smaller, well-tested and properly documented packages will be necessary for:
- Scaling the team. Breaking down functionality into smaller parts makes it easier for new hires to contribute without being dropped into a monolithic repository.
- Decentralisation of code ownership. If applications are made up of many smaller libraries that are maintained by many developers, it reduces the risk of a key engineer leaving with a majority share of the domain knowledge, which has obvious implications.
- Implementation consistency. Code sharing between applications means that bugs can be fixed, new features can be rolled out and general improvements can be made across all of your current and future code in one place.
- Properly isolating and abstracting functionality. Separating code into an independent library forces you to make smarter choices about what that library actually does and fully abstract it away from your main application code.
As for how to actually go about doing this, I will cover an implementation that uses GitLab CI/CD to build, test and publish a TypeScript library to the GitLab Package Registry. As a bonus, we’ll create a repository to host a CI/CD template so that we can re-use it across many libraries.
Creating a TypeScript Library
First, we’ll create an example TypeScript library. In my case, I typically go with a project structure that you can check out and clone here. The notable parts are:
- The
name
field in yourpackage.json
must be scoped to match either your username or the name of the topmost group that your repository belongs to. You can read more about package scopes and naming requirements in the GitLab documentation. src
compiles todist
using thebuild
script defined in by the package. Type declarations are emitted so that dependents will get full compiler support for IntelliSense etc. Thepackage.json
points to the implementation entry-point usingmain
- Unit tests are managed with Jest and run with the
test
script. We collect coverage, which I’ll go over integrating with GitLab in the CI/CD setup since that brings some really nice features like displaying coverage change in merge requests.
This template is a good starting point for fleshing out a vanilla JavaScript library with unit tests. Next, we’ll cover some registry pre-requisites.
Getting Setup to Work with the Package Registry
Before we can publish anything to the package registry, we’ll need to cover some preliminary steps. The first thing we need to do is authenticate locally with the registry, otherwise, we won’t actually be able to use our newly published packages in locally running code.
- You must create a personal access token, which will be used to authenticate with the registry.
- You need to let NPM and/or Yarn know where to look for packages that are scoped to your username or GitLab group.
- You need to add credentials (your access token) to authenticate with the registry.
For steps 2 & 3, you can either follow the GitLab documentation or simply append the following to your ~/.npmrc
replacing SCOPE
and TOKEN
as needed.
@SCOPE:registry=https://gitlab.com/api/v4/packages/npm/
//gitlab.com/api/v4/packages/npm/:_authToken=TOKEN
//gitlab.com/api/v4/projects/:_authToken=TOKEN
You’ll see steps very similar to these being repeated as we flesh out our CI/CD since authentication is obviously required there as well.
Building the CI/CD Template
For the GitLab pipeline, we’ll want to have something that can:
- Build the package. This will verify that no compilation errors, dependency issues or other build-related problems have surfaced between pushes.
- Run unit tests and emit coverage metadata. The benefits of unit tests are self-explanatory and test coverage is helpful to highlight degradation in test quality within merge requests.
- Publish the compiled JavaScript to the registry when tags are created. The package version should be automatically bumped to match the semver of the tag.
For the above, we arrive at something like this (replacing MY-SCOPE
with your scope name):
While running within the CI/CD environment, we are able to utilise the CI_JOB_TOKEN
as our authentication credential, rather than having to inject your personal access token created in the earlier stages of this walkthrough.
Saving this .gitlab-ci.yml
into your project root and pushing to master
will run successful build
and test
stages, with the latter including a coverage report column:
When you create new tags to mark a release, you will see an additional publish
stage, which will take your compiled dist
folder from the artifacts directive in the build
stage and publish it to the registry.
At this point, your package will be available through the Packages & Registries tab on GitLab, with a version matching the version you entered when you created the tag.
Installing Your Dependencies in External Pipelines
Projects that depend on your package and have their own CI/CD with a dependency installation step will need to know where your packages are and authenticate with the registry. To do this, you can include the following lines before your script runs:
Creating and Deploying a Shared CI/CD Template
To avoid needing to manually copy and paste this CI/CD YAML around all of your projects, you can leverage GitLab Pages to deploy a template that can be included by all of your projects. This means that any future improvements to the template will be automatically inherited by all of your pipelines across packages that use it, which becomes extremely valuable as you start managing dozens of libraries in your team.
Before you begin, it’s important to understand that templates must be public for your CI/CD pipelines to access them. For this reason, it is extremely important that you do not include private information in templates. Instead, leverage variables to allow the project level CI/CD to inject secret values.
First, create a new repository in GitLab that will store your template YAML. After it is created, go to Settings → General, expand the Visibility section and make sure that Pages is enabled and accessible by everyone. Note that the repository itself does not need to be public, just the template YAML itself will be published in a publicly accessible way.
Once that’s done, you can define a simple CI/CD that will copy any templates you commit to the repository to a public
directory, which is where GitLab pages will publicly host files from. In our case, we’ll make a dedicated templates
directory in the repository root that will contain all of our YAML templates, then create the following .gitlab-ci.yml
Once you push and the pages
stage passes, you’ll be able to see your templates on a URL similar to:
https://my-username.gitlab.io/my-repo/my-template.yml
From this point, all you need to do is use the include
directive in your project level CI/CD to inherit it. You can override stages as needed and declare your own variables
which can be passed down to the template.
Between the example TypeScript project and the ability to define your own shared CI/CD templates, it should be trivial to start breaking down code into smaller, sharable JavaScript packages for your team or even just for personal use.