Building a CI/CD Template to Publish TypeScript Libraries to the GitLab Package Registry
- 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:
namefield in your
package.jsonmust 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.
buildscript defined in by the package. Type declarations are emitted so that dependents will get full compiler support for IntelliSense etc. The
package.jsonpoints to the implementation entry-point using
- Unit tests are managed with Jest and run with the
testscript. 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.
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
TOKEN as needed.
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.
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.
.gitlab-ci.yml into your project root and pushing to
master will run successful
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
Once you push and the
pages stage passes, you’ll be able to see your templates on a URL similar to:
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.