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:
▼ project_name
ᐅ api
ᐅ frontend
- README.mdInside 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:
▼ project_name
ᐅ api
ᐅ frontend
- .gitlab-ci.yml
- README.mdThe 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:
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`:mainConcerns 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!