bereghici@dev ~ %|

Build a scalable front-end with Rush monorepo and React — Github Actions + Netlify

2021-08-198 min read

––– views

Rushjs logo

This is the 4th part of the blog series "Build a scalable front-end with Rush monorepo and React"

  • Part 1: Monorepo setup, import projects with preserving git history, add Prettier

  • Part 2: Create build tools package with Webpack and Jest

  • Part 3: Add shared ESLint configuration and use it with lint-staged

  • Part 4: Setup a deployment workflow with Github Actions and Netlify.

  • Part 5: Add VSCode configurations for a better development experience.


TL;DR

If you're interested in just see the code, you can find it here: https://github.com/abereghici/rush-monorepo-boilerplate

If you want to see an example with Rush used in a real, large project, you can look at ITwin.js, an open-source project developed by Bentley Systems.


Netlify allows you to configure the deployment of your project directly on their dashboard using a build command. This works well when you have a project in a single repository and you don't need to deploy it very often. They give you a free plan which includes only 300 free build minutes. On the other hand, Github Actions is more flexible and they give you 2000 free build minutes. Also, you can run various tasks like "testing", "linting", "deployment", etc.

Create a Netlify site

  • Create an account if you don't have one yet on Netlify and create a new site.
  • Go to the project settings and copy the API ID.
  • Open Github repository and go to the settings of the repository.
  • Click on "Secrets" and add a new secret with the name NETLIFY_SITE_ID and paste the copied API ID from Netlify.
  • Go back to Netlify dashboard and open user settings. https://app.netlify.com/user/applications#personal-access-tokens
  • Click on "Applications" and create a new access token.
  • Open Github "Secrets" and create a new secret with the name NETLIFY_AUTH_TOKEN and paste the new access token created on Netlify.

Create Github Actions workflow

At this point, we have all credentials we need for deployment. Now, we can start writing our configurations.

We need to add two more commands in common/rush/command-line.json: lint and test. We'll trigger them on CI/CD before building the project.

In common/rush/command-line.json add the following:

    {
      "name": "test",
      "commandKind": "bulk",
      "summary": "Run tests on each package",
      "description": "Iterates through each package in the monorepo and runs the 'test' script",
      "enableParallelism": true,
      "ignoreMissingScript": true,
      "ignoreDependencyOrder": true,
      "allowWarningsInSuccessfulBuild": true
    },
    {
      "name": "lint",
      "commandKind": "bulk",
      "summary": "Run linter on each package",
      "description": "Iterates through each package in the monorepo and runs the 'lint' script",
      "enableParallelism": true,
      "ignoreMissingScript": true,
      "ignoreDependencyOrder": true,
      "allowWarningsInSuccessfulBuild": false
    }

In the root of monorepo, create a .github/workflows folder and create a new file named main.yml.

mkdir -p .github/workflows

touch .github/workflows/main.yml

Now, let's write the configurations for Github Actions.

# Name of workflow
name: Main workflow

# When workflow is triggered
on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master
# Jobs to carry out
jobs:
  lint:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [14.x]
    steps:
      # Get code from repo
      - name: Checkout code
        uses: actions/checkout@v1
      # Install NodeJS
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      # Run rush install and build on our code
      - name: Install dependencies
        run: |
          node common/scripts/install-run-rush.js change -v
          node common/scripts/install-run-rush.js install
          node common/scripts/install-run-rush.js build
      # Run eslint to check all packages
      - name: Lint packages
        run: node common/scripts/install-run-rush.js lint
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [14.x]
    env:
      CI: true
    steps:
      # Get code from repo
      - name: Checkout code
        uses: actions/checkout@v1
      # Install NodeJS
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      # Run rush install
      - name: Install dependencies
        run: |
          node common/scripts/install-run-rush.js change -v
          node common/scripts/install-run-rush.js install
          node common/scripts/install-run-rush.js build
      # Run unit tests for all packages
      - name: Run tests
        run: node common/scripts/install-run-rush.js test
  deploy:
    # Operating system to run job on
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [14.x]
        app-name: [react-app]
        include:
          - app-name: react-app
            app: '@monorepo/react-app'
            app-dir: 'apps/react-app'
            app-build: 'apps/react-app/build'
            site-id: NETLIFY_SITE_ID
    needs: [lint, test]
    # Steps in job
    steps:
      # Get code from repo
      - name: Checkout code
        uses: actions/checkout@v1
      # Install NodeJS
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      # Run rush install and build on our code
      - name: Install dependencies
        run: |
          node common/scripts/install-run-rush.js change -v
          node common/scripts/install-run-rush.js install
      - name: Build ${{ matrix.app-name }}
        working-directory: ${{ matrix.app-dir }}
        run: |
          node $GITHUB_WORKSPACE/common/scripts/install-run-rush.js build --verbose --to ${{ matrix.app }}
      - name: Deploy ${{ matrix.app-name }}
        uses: nwtgck/actions-netlify@v1.2
        with:
          publish-dir: ${{ matrix.app-build }}
          production-deploy: ${{ github.event_name != 'pull_request' }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
          enable-pull-request-comment: true
          enable-commit-comment: true
          overwrites-pull-request-comment: true
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets[matrix.site-id] }}

Let's break down the configuration above.

We have 3 jobs: lint, test and deploy. lint and test jobs will run in parallel and deploy job will run after both lint and test jobs are successfully done. We're using matrix to run jobs on different NodeJS versions (Currently we're using only 14.x but can be extended to other versions). Matrix is also used to run the same build steps for multiple projects. At the moment, we have only react-app project, but it can be easily extended.

We're running this workflow when the master branch is modified. For pull requests, Netlify will provide preview urls, but if we push something directly to master branch, it will trigger a production build and the code will be deployed to the main url.

The main workflow we created is mostly suitable for development / staging environments. For production, you probably want to trigger the flow manually and create a git tag. You can create another site in Netlify, create a PRODUCTION_NETLIFY_SITE_ID secret in Github and use the following configuration:

name: React App Production Deployment
on:
  workflow_dispatch:
    inputs:
      version:
        description: Bump Version
        default: v1.0.0
        required: true
      git-ref:
        description: Git Ref (Optional)
        required: false
# Jobs to carry out
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      # Get code from repo
      - name: Clone Repository (Latest)
        uses: actions/checkout@v2
        if: github.event.inputs.git-ref == ''
      - name: Clone Repository (Custom Ref)
        uses: actions/checkout@v2
        if: github.event.inputs.git-ref != ''
        with:
          ref: ${{ github.event.inputs.git-ref }}
      # Install NodeJS
      - name: Use Node.js 14.x
        uses: actions/setup-node@v1
        with:
          node-version: 14.x
      # Run rush install and build on our code
      - name: Install dependencies
        run: |
          node common/scripts/install-run-rush.js change -v
          node common/scripts/install-run-rush.js install
          node common/scripts/install-run-rush.js build
      # Run eslint to check all packages
      - name: Lint packages
        run: node common/scripts/install-run-rush.js lint
  test:
    runs-on: ubuntu-latest
    env:
      CI: true
    steps:
      # Get code from repo
      - name: Clone Repository (Latest)
        uses: actions/checkout@v2
        if: github.event.inputs.git-ref == ''
      - name: Clone Repository (Custom Ref)
        uses: actions/checkout@v2
        if: github.event.inputs.git-ref != ''
        with:
          ref: ${{ github.event.inputs.git-ref }}
      # Install NodeJS
      - name: Use Node.js 14.x
        uses: actions/setup-node@v1
        with:
          node-version: 14.x
      # Run rush install
      - name: Install dependencies
        run: |
          node common/scripts/install-run-rush.js change -v
          node common/scripts/install-run-rush.js install
          node common/scripts/install-run-rush.js build
      # Run unit tests for all packages
      - name: Run tests
        run: node common/scripts/install-run-rush.js test
  deploy:
    # Operating system to run job on
    runs-on: ubuntu-latest
    needs: [lint, test]
    # Steps in job
    steps:
      # Get code from repo
      - name: Clone Repository (Latest)
        uses: actions/checkout@v2
        if: github.event.inputs.git-ref == ''
      - name: Clone Repository (Custom Ref)
        uses: actions/checkout@v2
        if: github.event.inputs.git-ref != ''
        with:
          ref: ${{ github.event.inputs.git-ref }}
      # Install NodeJS
      - name: Use Node.js 14.x
        uses: actions/setup-node@v1
        with:
          node-version: 14.x
      # Run rush install and build on our code
      - name: Install dependencies
        run: |
          node common/scripts/install-run-rush.js change -v
          node common/scripts/install-run-rush.js install
      # Build app
      - name: Build react app
        working-directory: apps/react-app
        run: |
          node  $GITHUB_WORKSPACE/common/scripts/install-run-rush.js build --verbose --to @monorepo/react-app
      - name: Deploy react app
        uses: nwtgck/actions-netlify@v1.2
        with:
          publish-dir: apps/react-app/build
          production-deploy: true
          github-token: ${{ secrets.GITHUB_TOKEN }}
          enable-pull-request-comment: true
          enable-commit-comment: true
          overwrites-pull-request-comment: true
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.PRODUCTION_NETLIFY_SITE_ID }}
      # Create release tag
      - name: Create tag
        run: |
          git tag ${{ github.event.inputs.version }}
          git push origin --tags

Now we can trigger a production deploy manually for react-app project. We can provide the next version number as a version parameter and it will create a tag for us. If we want to revert to a previous version, you can also do it by providing a git-ref.

If you encountered any issues during the process, you can see the code related to this post here.

If you're using VSCode, you might be interested to see some configurations that can enrich your development experience with this monorepo. See the next post.

Discuss on Twitter
Edit on Github