Engineering
Release

In this documentation, we'll outline our process for managing releases in our monorepo, which hosts both our Next.js-based web application and our NestJS-based backend. Our process leverages Git for versioning, Turbo for building, GitLab/GitHub pipelines for continuous integration/continuous deployment (CI/CD).

Project Structure

Our project structure is as follows:

/my-monorepo
	├── .git
	├── .gitignore
	├── .gitlab-ci.yml
	├── README.md
	├── package.json
	├── turbo.json
	├── yarn.lock
	└── apps
		├── api
		│   ├── src
		│   ├── test
		│   ├── package.json
		│   └── tsconfig.json
		└── web
			├── pages
			├── public
			├── styles
			├── package.json
			└── next.config.js

This is a basic representation of our structure it can variate depending on the project.

As you can see we have a monorepo with two applications, api and web. The api application is our backend, which is built with NestJS. The web application is our frontend, which is built with Next.js. Both applications are built with TypeScript.

To generate the build artifacts for each application, we use Turbo. Turbo is a tool that allows us to build our applications independently of each other, and it also allows us to build our applications in parallel. This is important because it allows us to build our applications faster. Turbo also allows us to build our applications in a way that is compatible with GitLab/GitHub pipelines.

As you can see we have another important file in our project structure, turbo.json. This file is used to configure Turbo, and it is also used to configure our GitLab/GitHub pipelines. We'll discuss this file in more detail later. But basically, this file tells Turbo how to build our applications, and it also tells GitLab/GitHub how to build our applications.

Versioning Process

A release process often involves creating a release branch from the main branch when you are ready to prepare a new version of your project. This release branch allows you to isolate the release preparation, which might involve final testing, bug fixes, and documentation updates.

git checkout -b release/vX.X.X

Once the release is ready, a Git tag should be created to mark the specific commit that represents this release. Tags create points in the project history that can be easily referred back to. In this case, the tag represents a specific release version of the project.

git tag -a v1.0.0 -m "Release vX.X.X"

The release branch can then be merged back into the main branch, and any necessary updates can also be merged into any other branches that need to include the changes made for the release.

git checkout main
git merge release/vX.X.X

At this point, the new release is complete, and you have a tagged version of your codebase that you can always refer back to. You can also use the tag to trigger specific actions in your CI/CD pipeline, such as deploying the release to a production environment.

git push origin --tags

The process can be repeated for each new release. Over time, you will accumulate a history of tags representing each of your project's releases.

This versioning process with branches and tags allows managing releases in a systematic and controlled manner. You can isolate changes, manage features and fixes, prepare releases, and maintain a clear history of your project's releases.

Building with Turbo

Turbo (opens in a new tab) is a high-performance build system for JavaScript and TypeScript codebases. It leverages the monorepo structure to optimize build times and only rebuild what's necessary based on the changes made in the repository.

Turbo Configuration

The first step is to set up Turbo correctly for your monorepo. At the root of your project, there should be a turbo.json file, which is the Turbo configuration file. It should specify the tasks and pipeline to be run for your packages.

{
	"pipeline": {
		"build": {
			"dependsOn": ["^build"]
		},
		"test": {
			"dependsOn": ["build"]
		}
	}
}

The dependsOn property specifies the tasks that must be run before the current task. The ^ prefix indicates that the task should be run for all packages in the monorepo. If you want to run a task for a specific package, you can specify the package name instead of the ^ prefix.

Building with Turbo

To build your packages with Turbo, you should run the following command in your terminal:

turbo run build

This command will execute the build task in your turbo.json configuration, building all packages.

Building Changed Packages with Turbo

One of the advantages of Turbo is that it can detect which packages have changed and only build those. To build only the packages that have changed since the main branch, you can run:

turbo run build --since main

Or you can use the --filter changed flag:

turbo run build --no-cache --filter changed

This is particularly useful in CI/CD pipelines, where you often want to avoid unnecessary builds. By only building what's necessary, Turbo can significantly reduce build times.

Deploying with Turbo

Like building, Turbo can deploy only the packages that have changed. To do so, you need to specify a deploy task in your turbo.json configuration and then run:

turbo run deploy --no-cache --filter changed

This command will deploy only the packages that have changed.

Deploying with GitHub/GitLab CI/CD

CI/CD is a powerful tool that you can use to automate your software workflows. It is integrated with GitHub/GitLab and makes it easy to automate all your software workflows, including deployment.

Workflow Configuration

We will use github actions as an example but the same can be applied to gitlab.

The first step is to set up a GitHub Actions workflow for your project. You can do this by creating a YAML file under the .github/workflows directory in your repository.

Here's an example of a workflow configuration for a monorepo that uses Turbo to build and deploy changed applications:

name: Build and Deploy
 
on:
  push:
    branches:
      - 'release/**'
    tags:
      - 'v*'
 
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout Repository
      uses: actions/checkout@v2
 
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: 18
 
    - name: Install Dependencies
      run: yarn install --frozen-lockfile
 
    - name: Run Turbo
      run: yarn turbo run build --no-cache --filter changed
 
    - name: Deploy API
      if: steps.turbo.outputs.apps == 'apps/api'
      run: echo "Deploying API..."
 
    - name: Deploy Web
      if: steps.turbo.outputs.apps == 'apps/web'
      run: echo "Deploying Web..."

This workflow runs whenever there is a push to any branch that matches the pattern 'release/**', or a new tag that starts with 'v' is created. It sets up the necessary environment, checks out your code, installs dependencies, and uses Turbo to build only the applications that have changed.

Then, based on which applications have changed, it runs the deploy steps for those applications. In this example, it uses an if condition to check the output of the Turbo step and only runs the corresponding deploy step if that application has changed.

Note: The echo "Deploying API/Web..." commands are placeholders and should be replaced with your actual deployment commands.

By using CI/CD in conjunction with Turbo, you can automate your deployment process and ensure that you are only deploying the applications that have changed. This can lead to faster and more efficient deployments, as well as easier management of your monorepo.

Building and Testing in the Pipeline

Below is an example of a workflow configuration for running linting and tests in a monorepo, using Turbo to target only the applications that have changed:

name: Lint and Test
 
on: [push]
 
jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout Repository
      uses: actions/checkout@v2
 
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: 14
 
    - name: Install Dependencies
      run: yarn install --frozen-lockfile
 
    - name: Lint changed applications
      run: yarn turbo run lint --no-cache --filter changed
 
    - name: Test changed applications
      run: yarn turbo run test --no-cache --filter changed

This workflow runs on every push to your repository. After setting up the environment and installing dependencies, it runs the linting and testing tasks on the applications that have changed.

Note: You will need to specify lint and test tasks in your turbo.json configuration for Turbo to execute these tasks. For example:

{
	"pipeline": {
		"lint": {},
		"test": {
			"dependsOn": ["lint"]
		},
		"build": {
			"dependsOn": ["test"]
		}
	}
}

In this configuration, the lint task runs first, then the test task runs after lint, and finally the build task runs after test.

Conclusion

Our release process streamlines the development and deployment of new versions of our web and backend applications. By automating as much as possible with tools like Turbo, GitLab/GitHub pipelines, we can ensure that our release process is as smooth and error-free as possible.Always remember to thoroughly test new features before merging them into main to maintain the stability of our applications.