Simply Wall St

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:

  1. Scaling the team. Breaking down functionality into smaller parts makes it easier for new hires to contribute without being dropped into a monolithic repository.
  2. 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.
  3. 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.
  4. 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

  • The name field in your package.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 to dist using the build script defined in by the package. Type declarations are emitted so that dependents will get full compiler support for IntelliSense etc. The package.json points to the implementation entry-point using main
  • 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

  1. You must create a personal access token, which will be used to authenticate with the registry.
  2. You need to let NPM and/or Yarn know where to look for packages that are scoped to your username or GitLab group.
  3. 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

  • 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:

Passing stages with coverage report

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.

Additional publish stage

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.

The package listed within the GitLab registry

Installing Your Dependencies in External Pipelines

Creating and Deploying a Shared CI/CD Template

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.

Ensure “everyone” is selected

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.

Software developer at Simply Wall St.