Engineering November 16, 2023 5 min read

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

Bitbucket Pipelines 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 Bitbucket Pipelines 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 Bitbucket Pipelines” 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 to keep in mind we need sometimes 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 bitbucket-pipelines.yml to our repository, resulting in the following structure:

Code
▼ project_name
  ᐅ api
  ᐅ frontend
  - bitbucket-pipelines.yml
  - README.md

The added file bitbucket-pipelines.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
image: ruby:3.1.3

definitions:
  caches:
# Concern 1
    bundler:
      key:
        files:
          - api/Gemfile.lock
      path: /usr/local/bundle
# Concern 2
    node:
      key:
        files:
          - frontend/package-lock.json
      path: frontend/node_modules
  services:
    postgres:
      image: postgres
      environment:
        POSTGRES_DB: postgres
        POSTGRES_USER: postgres
        POSTGRES_PASSWORD: postgres

pipelines:
  branches:
    main:
    - stage:
        name: Prepare and Deploy API
# Concern 3
        condition:
            changesets:
              includePaths:
                - 'api/**'
        steps:
# Concern 4
            - step:
                name: API Prepare
                script:
                  - cd api
                  - tar --exclude='.git' -cvzf /tmp/app.tar.gz .
                  - mv /tmp/app.tar.gz .
                artifacts:
                  - api/app.tar.gz
            
            - step:
                name: API Deploy
                script:
                  - pipe: atlassian/heroku-deploy:2.1.0
                    variables:
                      HEROKU_API_KEY: $HEROKU_API_KEY
                      HEROKU_APP_NAME: $HEROKU_API_APP_NAME
                      ZIP_FILE: api/app.tar.gz
                      WAIT: 'true'
    - stage:
        name: Prepare and Deploy Frontend
# Concern 5
        condition:
            changesets:
              includePaths:
                - 'frontend/**'
        steps:
# Concern 6
            - step:
                name: Frontend Prepare
                script:
                  - cd frontend
                  - tar --exclude='.git' -cvzf /tmp/app.tar.gz .
                  - mv /tmp/app.tar.gz .
                artifacts:
                  - frontend/app.tar.gz
            
            - step:
                name: Frontend Deploy
                script:
                  - pipe: atlassian/heroku-deploy:2.1.0
                    variables:
                      HEROKU_API_KEY: $HEROKU_API_KEY
                      HEROKU_APP_NAME: $HEROKU_FRONTEND_APP_NAME
                      ZIP_FILE: frontend/app.tar.gz
                      WAIT: 'true'
  default:
    - stage:
        name: Setup and Validate API
        condition:
# Concern 7
          changesets:
            includePaths:
              - 'api/**'
        steps:
          - step:
# Concern 8
              name: API Setup
              caches:
                - bundler
              script:
                - cd api
                - bundle install
          
          - step:
# Concern 9
              name: API Tests
              caches:
                - bundler
              services:
                - postgres
              script:
                - cd api
                - bundle exec rake
              artifacts:
                - api/coverage/coverage.json
          
          - step:
# Concern 10
              name: API Lint
              caches:
                - bundler
              script:
                - cd api
                - bundle exec rubocop
          
          - step:
              name: API Coverage
              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
    - stage:
        name: Setup and Validate Frontend
        condition:
# Concern 11
          changesets:
            includePaths:
              - 'frontend/**'
        steps:
          - step:
# Concern 12
              name: Frontend Setup
              image: node:18.16.1-slim
              caches:
                - node
              script:
                - cd frontend
                - npm ci
          
          - step:
# Concern 13
              name: Frontend Tests
              image: node:18.16.1-slim
              caches:
                - node
              script:
                - cd frontend
                - npm test --if-present
          
          - step:
# Concern 14
              name: Frontend Lint
              image: node:18.16.1-slim
              caches:
                - node
              script:
                - cd frontend
                - >
                  if ! [ -f frontend/.eslintrc.js ]; then
                    npm run lint                  
                  else
                    echo "ESLint not configured"
                  fi
                
          - step:
# Concern 15
              name: Frontend Build
              image: node:18.16.1-slim
              caches:
                - node
              script:
                - cd frontend
                - npm run build

Concern 1

In the caches definition for the Ruby on Rails dependencies we use the default folder location bundler uses. And if you want to troubleshoot that location you can add temporally the command bundle config path after the bundle install in the same step.

We don’t change that folder location to a custom folder to simplify the remaining concerns regardless Ruby because it would require us to perform that change\command in each future steps. And what sometimes happens is you forget about that command and steps start failing.

Concern 2

For node, we just need to keep in mind that we are running that project inside of the folder frontend and we need to indicate.

Concerns 3, 5, 7, and 11

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.

Concerns 4, 6, 8, 9, 10, 12, 13, 14, 15

On all these concerns we just need to remember to navigate into the respective folder to perform the pretended commands.

Concerns 4, 6, and 9

In these concerns note that the artifacts' location is always related to where the applications are. Then when we need to read them they will be in that folder level.

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. Bitbucket Pipelines’ 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!