IgnisDa's Blog

Working with NX, Railway and CI/CD

Tutorial Nx Github Actions Railway CI And CD Monorepo

Introduction

This article will show you how to properly deploy an NX project on Railway, an infrastructure platform that handles all the system administration and DevOps tasks for you.

Additionally, we will configure Github Actions such that it deploys only those projects that have been affected by the change pushed.

Premise

Though the builder might have support for NX monorepos, it is often unclear how to integrate your Github Actions perfectly with Railway deployments.

The default strategy of deploying every commit is also rather wasteful since it ends up rebuilding projects that were not even affected by the change.

Example project

We will use this project and deploy it to Railway. We will deploy the remix-pagination-demo project in this tutorial. Let us assume we deploy it to two separate environments - main and dev.

Deployment strategy

Nixpacks, which is Railway’s default builder, supports NX monorepos. To get it working, I just had to set NIXPACKS_NX_APP_NAME environment variable to remix-pagination-demo.

Railway environment variables

NOTE: The deployment strategy does not really matter. You can use whatever you want (Buildpack, Nixpacks or Dockerfile etc). As long as Railway supports it, the below steps will work.

Steps

Create the Railway project and provision the required addons and add the correct environment variables. Also create the required environments.

Disable Github trigger

Once the first deployment is live, we will disable Github triggers. This will instruct Railway to not create a new deployment every time we push a commit to Github.

Disable Trigger

Create project tokens

Next we will need to create two project tokens (one for each environment). For this example, I have created two tokens and named them appropriately.

Project tokens

Add repository secrets

We will use the above tokens we created and add them as repository secrets. They should have the following naming scheme:

RAILWAY_TOKEN__<project_name>_<railway_environment>
Github Secrets

The Github action

This is a workflow that will run on every commit to the main and dev branches, test if a particular project has been affected, and if so, then deploy it to the correct environment on Railway.

name: Deploy affected projects

on:
  push:
    branches:
      - main
      - dev

env:
  NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - uses: ./.github/actions/setup
        name: Setup environment

      - name: Set base commit hash
        run: |
          #!/usr/bin/env bash
          commit=${{ github.event.before }}
          if git branch --contains "$commit"; then
            echo "No force push detected, continuing..."
          else
            # get the commit before this one
            commit=$(git log --format="%H" -n 2 | tail -n 1)
          fi
          echo "BASE_COMMIT=$commit" >> $GITHUB_ENV

      - name: Display base commit
        run: echo "$BASE_COMMIT"

      - name: Lint affected
        run: pnpm nx affected --target=lint --parallel=6 --base=$BASE_COMMIT --head=HEAD

      - name: Build affected
        run: pnpm nx affected --target=build --parallel=6 --base=$BASE_COMMIT --head=HEAD

      - name: Dump access secrets into file
        run: |
          mkdir -p ./dist
          echo '${{ toJSON(secrets) }}' >> ./dist/secrets.json

      - name: Deploy affected
        run: ./deployment/railway-trigger.sh

I am not including the .github/actions/setup/action.yaml file for the sake of brevity, but it is needed for this workflow to run successfully. You can copy it from the example repository.

  • Dump access secrets to file step

    This converts the secrets into json format and dumps them into a file. This can be a potentially exploitable step if used in public repositories, so please make sure you take the appropriate cautions.

  • Deploy affected step

    Let us have a look at the railway-trigger.sh script:

    #!/usr/bin/env bash
    
    set -euo pipefail
    
    RAILWAY_BINARY="/tmp/railway"
    RAILWAY_VERSION="1.8.4"
    
    # Install the Railway CLI
    VERSION="$RAILWAY_VERSION" INSTALL_DIR="$RAILWAY_BINARY" sh -c "$(curl -sSL https://raw.githubusercontent.com/railwayapp/cli/master/install.sh)"
    $RAILWAY_BINARY version
    
    affected_projects=$(pnpm nx print-affected --base="$BASE_COMMIT" --head=HEAD --select=projects --type='app')
    for project in ${affected_projects//,/ }
    do
        echo "Processing: '$project'..."
        # convert hyphens to underscores
        parameterized_name=$(echo $project | tr '-' '_')
        branch_name="$(git branch --show-current)"
        # this is the key by which the required token is present in the secrets file
        env_variable=RAILWAY_TOKEN__"$parameterized_name"_"$branch_name"
        # convert the whole thing to upper case
        final_env_variable=$(echo "$env_variable" | awk '{print toupper($0)}')
        # get the actual value of the token from the secrets file
        railway_token="$(jq -r ."$final_env_variable" ./dist/secrets.json)"
        # trigger the deploy
        RAILWAY_TOKEN="$railway_token" $RAILWAY_BINARY up --detach --verbose
    done

    The above script:

    • Downloads the Railway CLI binary.
    • Finds all the deployable projects that were also affected by the changes pushed.
    • Does some bash trickery to find the correct railway token.
    • Sets the RAILWAY_TOKEN environment variable and then triggers a deploy.

    The --detach flag is used here because building containers might take a lot of time which could eat up free Github Actions quota provided. You can remove it if your project allows it.

Conclusion

That is pretty much it. It is a pretty simple workflow, but somewhat complicated to compose. If you need help, you can ask the community.