Engineering November 23, 2023 5 min read

GitLab CI\CD concerns for a Ruby on Rails API and React Frontend in subfolders same repo

GitLab CI\CD concerns for a Ruby on Rails API and React Frontend in subfolders same repo

The goal of this blog post is to quickly show how can you define your GitLab steps if your Ruby on Rails application is inside of a subfolder and not accessible from the root of a repository.

This structure is something here at Runtime Revolution we don’t recommend. We always advise keeping only one application per repository. But sometimes this can be a reality or requirement for some reason and that’s why we are writing about how to solve some concerns with this state.

In this blog post, we won’t enter into details. We will focus on Concerns. For details, I advise you to check “Ruby on Rails CI\CD with GitLab” which tries to explain all the CI\CD steps for a repository with only one application.

Just one note, this CI\CD will respect the same Git workflow from the mentioned blog post above. Any change in all branches that aren’t the production branch will trigger the CI, and all changes in the production branch main will trigger CD.

Now let’s imagine the following repository structure:

Code
▼ project_name
  ᐅ api
  ᐅ frontend
  - README.md

Inside the api folder, we have an API in Ruby on Rails to be consumed by the React application inside of the folder frontend.

Some concerns with this structure.

Both API and Frontend have their unit tests, if changes only occur on one side there is no reason to consume time running tests from both. The Continuous Integration (CI) should be smart to only validate what has changed.

Since each application is inside of a folder it needs us to keep in mind we sometimes need to navigate into that folder before running the required commands.

And for the Continuous Delivery (CD) part the same way, we don’t want to redistribute both applications if only one has changed. Also, you don’t want to push\send code that doesn’t make sense to exist on the cloud platform that makes your applications available on the Web.

Let’s add the required .gitlab-ci.yml to our repository, resulting in the following structure:

Code
▼ project_name
  ᐅ api
  ᐅ frontend
  - .gitlab-ci.yml
  - README.md

The added file .gitlab-ci.yml will have all logic for both applications, and both Continuous Integration and Delivery. Here’s the content with some Concern comments that we will discuss after:

Code
stages:
  - setup
  - test
  - coverage
  - deploy

##################################
# API

.api_common:
  image: ruby:3.1.3
  rules:
# Concern 1
    - if: $CI_COMMIT_BRANCH != "main"
      changes:
        - api/**
        - api/**/**
  before_script:
# Concern 2
    - cd api
    - bundle config set --local path './vendor/ruby'
  cache:
# Concern 3
    key:
        files:
          - api/Gemfile.lock
    paths:
      - ./api/vendor/ruby

api_setup:
  stage: setup
  extends:
    - .api_common
  script:
    - bundle install

api_test:
  stage: test
  extends:
    - .api_common
  script:
    - bundle exec rake
  artifacts:
    paths:
      - api/coverage/coverage.json

api_lint:
  stage: test
  extends:
    - .api_common
  script:
    - bundle exec rubocop

api_coverage:
  stage: coverage
  extends:
    - .api_common
  dependencies:
    - api_test
  script:
    - >
      if ! command -v jq &> /dev/null; then
        apt-get update && apt-get install -y jq
      fi
    - >
      covered_percent=$(cat api/coverage/coverage.json | jq -r '.metrics.covered_percent');
      echo "Coverage ($covered_percent%)";
      required_coverage=80;
      echo "Minimum coverage ($required_coverage%)";
      if (( $(echo "$covered_percent < $required_coverage" | bc -l) )); then
        echo "Coverage ($covered_percent%) is below the required threshold of $required_coverage%."
        exit 1
      else
        echo "Coverage ($covered_percent%) passed the required threshold of $required_coverage%."
      fi


.api_cd_common:
  image: ruby:3.1.3
  rules:
# Concern 4
    - if: $CI_COMMIT_BRANCH == "main"
      changes:
        - api/**/*

api_deploy:
  stage: deploy
  extends:
    - .api_cd_common
  script:
# Concern 5
    - git remote add heroku https://heroku:[email protected]/$HEROKU_API_APP_NAME.git
    - git push --force heroku `git subtree split --prefix api HEAD`:master

##################################
# FRONTEND

.frontend_common:
  image: node:18.16.1-slim
  rules:
# Concern 6
    - if: $CI_COMMIT_BRANCH != "main"
      changes:
        - frontend/**
        - frontend/**/**
  before_script:
# Concern 7
    - cd frontend
  cache:
# Concern 8
    key:
        files:
          - frontend/package-lock.json
    paths:
      - ./frontend/node_modules

frontend_setup:
  stage: setup
  extends:
    - .frontend_common
  script:
    - npm ci

frontend_test:
  stage: test
  extends:
    - .frontend_common
  script:
    - npm test --if-present

frontend_lint:
  stage: test
  extends:
    - .frontend_common
  script:
    - >
      if ! [ -f frontend/.eslintrc.js ]; then
        npm run lint                  
      else
        echo "ESLint not configured"
      fi

frontend_build:
  stage: test
  extends:
    - .frontend_common
  script:
    - npm run build

##################################
# MARK - FRONTEND - CD

.frontend_cd_common:
  image: node:18.16.1
  rules:
# Concern 9
    - if: $CI_COMMIT_BRANCH == "main"
      changes:
        - frontend/**
        - frontend/**/**

frontend_deploy:
  stage: deploy
  extends:
    - .frontend_cd_common
  script:
# Concern 10
    - git remote add heroku https://heroku:[email protected]/$HEROKU_FRONTEND_APP_NAME.git
    - git push --force heroku `git subtree split --prefix frontend HEAD`:main

Concerns 1, 4, 6, and 9

In these concerns, we want to make sure the CI\CD only runs if did occur changes for the respective application. If no changes occur on the API side there is no reason to waste CI time on that. The same for distribution we don’t want to re-distribute an application that doesn’t have any changes compared to what is already distributed.

Concern 2

In this concern inside of the hidden job, .api_common we navigate into the api folder. This allows us to write the running steps simply. Also, it prevents us from forgetting the navigation. Then since we are drying the commands it is easy to change the bundler dependencies path without the risk of forgetting about that.

Concerns 3 and 8

When we define the caches locations we need to indicate their respective subfolders.

Concerns 5 and 10

When we push our code to Heroku we don’t want to push code that doesn’t make sense to exist there. To do that we split our repository and create a new git subtree from the respective application folder API or Frontend. Which in practice means that the folder will be the root of Heroku’s repository.

Concern 7

The same way we have for the API we do the same for the Frontend. We have a hidden job that each Frontend step extends. This hidden job will navigate into the Frontend folder. This allows us to write the running steps simply. Also, it prevents us from forgetting the navigation.

Conclusion

In this blog post, we explored and learned the concepts of Continuous Integration and Continuous Delivery for a repository with multiple applications in different environments. By automating the testing and deployment processes, you can ensure your application is always in a reliable state and ready for release. GitLab’s flexibility and integration with your existing repositories make it an ideal choice for streamlining your development workflow.

Remember, CI/CD is not just a one-time setup; it’s an ongoing process. As your application evolves, keep iterating and refining your workflows to accommodate new requirements and improve overall efficiency. Happy coding!