💡 The feature image shows a typical CI/CD pipeline in action partly drawn by OpenAI DALL-E, but in this article, we are going to develop something beneficial
Contents
This tutorial demonstrates how to develop, test, and deploy CI extensions for GitHub Actions, Azure Pipelines, and CircleCI from a single monorepo using TypeScript and Node.js. It covers creating a monorepo, sharing code between actions and tasks, and building and publishing extensions.
Table of Contents
This is a relatively short tutorial on how to develop, test, and deploy your CI extensions for GitHub Actions, Azure Pipelines, and CircleCI from a single monorepo and is based on the experience of creating the Qodana CI extensions.
Start from the official templates
Let’s pick the technology stack for our CI extensions.
OK, I will not pick. I’ll just tell you why I used TypeScript and node.js for the extensions.
Pros for using JS-based actions
- More flexible than bash/Dockerfile-based approaches
- Different libraries (like actions/toolkit and microsoft/azure-pipelines-task-lib) with more accessible and easy-to-use APIs are available out-of-box
- Writing tests is relatively simple
Cons
- JavaScript
So let’s write a TypeScript-based action!
GitHub Actions
I found the GitHub actions documentation easier to read than Azure, so I would recommend starting writing and testing your extensions on GitHub by using the official template actions/typescript-action. The mentioned template provides a good starting point; I won’t repeat the steps here. Play with it, write some simple stuff, and then return here for the next steps.
Azure Pipelines
GitHub Actions are built on Azure infrastructure, so porting your GitHub action to Azure Pipelines should be relatively easy.
So,
- the “action” becomes the “task”
- it’s packed a bit differently, distributed, and installed the other way
And the definition of a task task.json
is the same as the action one action.yml
.
For example, having the following action.yml
:
“Easily” translates to the following Azure task:
From such a simple example, one can see why I suggested starting with GitHub Actions. But let’s continue.
To start developing your new shiny Azure Pipelines task, I suggest just copying the action directory and then implementing steps from the official Azure documentation – it’s pretty straightforward.
- Create
vss-extension.json
- Create
task.json
and place it into yourdist
directory (actually better to name it after the task name) - If you used any methods from
@actions/core
or@actions/github
in your action, you need to replace them with the corresponding methods fromazure-pipelines-task-lib
(e.g.core.getInput
→tl.getInput
)
The API of azure-pipelines-task-lib
is similar to @actions/core
and other @actions/*
libraries.
For example, we have a method for getting the input parameters:
And the same for Azure Pipelines:
For more real cases, feel free to explore our Qodana GitHub Actions codebase utils and Azure Pipelines task utils.
Create the monorepo
We are going to use npm workspaces to manage the monorepo.
Place your action and task code into subdirectories (e.g. github
) of your newly created monorepo. And then create a package.json
file in the root directory.
So the monorepo structure looks like this:
After implementing the workspace setup, you can run tasks and actions from the root directory. For example, to run the build
task from the github
directory, you can use the following command:
Share code between actions and tasks
The most valuable part from using the monorepo approach starts here: you can share the code between your actions and tasks.
We are going to do the following steps:
- Create a
common
directory in the root of the monorepo, a subproject for shared code - Update
tsconfig.json
compiler configurations from all sub-dirs for proper project builds
At first, let’s create the base tsconfig
– tsconfig.base.json
with the base settings that are going to be used in all subprojects:
Then create a simple tsconfig.json
in the project root:
Then common/tsconfig.json
:
And finally, update the tsconfig.json
files in the subprojects (they are basically the same, e.g. github/tsconfig.json
):
Now you can use the shared code from the common
directory in your actions and tasks. For example, we have a qodana.ts
file in the common
directory that contains function getQodanaUrl
that returns the URL to the Qodana CLI tool. And we use it in both actions and tasks.
Build and publish
You already have GitHub workflows from the template configured to publish your actions to your repository releases. For automated releases, we use GH CLI, and we have a simple script that publishes a changelog to the repository releases:
And the GitHub workflow that runs it:
For Azure Pipelines task releases, you can use the official approach from Azure. Still, also you can do the same on GitHub actions infrastructure as their publisher tool can be installed anywhere. So, in our case, it’s solved by a simple GitHub workflow job:
With this setup, each release happens automatically on each tag push.
CircleCI?
Ah, yes, this article mentioned the CircleCI orb also… CircleCI setup is straightforward but does not support TypeScript extensions, so you have to pack your code into a Docker image or a binary and run it there. The only reason it’s included in this post is that we build our orb with the monorepo approach, which works well.
Implement the official orb template and place it in your monorepo, so the structure looks like this:
And remember to commit .circleci/
directory to your repository to make CircleCI lint, test, and publish your orb.