Photo by Scott Graham on Unsplash
Deploy React apps for review on every PR in Gitlab CI/CD
Creating a pipeline for previewing new frontend versions on pull requests
Introduction
Review is an essential part of the software development process. However, looking at the code is not sufficient to determine if everything works as expected. What I like to do when reviewing frontend apps is to have an option to click through some of the changes.
Many teams use UAT or other dev environments to be able to do that. However, these have to be set up and often are not maintenance free. For years now we've been able to deploy our static frontend apps to Github Pages or Gitlab Pages. It would be great to be able to deploy to them on all pull requests, wouldn't it?
Currently, Pages don't offer hosting of multiple versions of the app. However, Gitlab has so-called browsable artifacts that in principle work in the same way. I'm going to show you how to use them in order to be able to deploy your frontend app every time a pull request is created. We are going to integrate it with the review apps feature of GitLab.
Additionally, our main branch is going to be deployed to GitLab pages. You can remove this part if it's not what you're looking for.
Create a new react app
Skip this step if you already have one. We're going to generate an app via CRA.
First, create a directory for our project.
mkdir frontend-review-apps
Then generate the app boilerplate in the client folder. I am assuming you may want to store some other parts of the app like the backend or serverless functions in the project folder as well.
npx create-react-app client
After the app is generated, we're going to add React Router, so the example is a bit more realistic. Version 6 is the latest at the time of writing. The configuration in the latter parts of the article should be similar for future and past versions.
npm install --save react-router-dom@6
Then we have to make our App.js
actually use router's features.
import React from "react";
import {
BrowserRouter as Router,
Routes,
Route,
Link
} from "react-router-dom";
export default function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Learn React</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/users">Users</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/about" element={ <About /> } />
<Route path="/users" element={ <Users /> } />
<Route path="/" element={ <Home /> } />
</Routes>
</div>
</Router>
);
}
function Home() {
return <h2>Home</h2>;
}
function About() {
return <h2>About</h2>;
}
function Users() {
return <h2>Users</h2>;
}
Let's run it locally and confirm it works!
npm start
Preparing the app
We need to make one adjustment in order to be able to deploy our app under a subdirectory (example.com/subdirectory
) in GitLab pages as our app URL will look like https://horosin.gitlab.io/frontend-review-apps/
. By default the router is not prepared for such case.
Sadly, using browsable artifacts this way won't work as they don't behave completely the same as a full-on static file web server. You will be able to access the app only starting from the root URL. If anyone works around this, please let me know!
To do that just alter the line 11 of App.jsx
to look like the following.
<Router basename={process.env.PUBLIC_URL}>
It won't break anything if the variable is empty and will allow us to add a path prefix that we need.
Scripting the CI/CD pipeline
I like to structure and modularize my Gitlab CI config. Here's my main .gitlab-ci.yml
file. It makes sure that on pull requests only one pipeline is run. The file also imports CI config for the client.
image: node:16
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
when: never
- if: '$CI_COMMIT_BRANCH'
stages:
- build
- test
- deploy
include:
- local: .gitlab-ci/client.yml
Client specific config is stored in .gitlab-ci/client.yml
. I am using a few tricks to avoid repetition: .cache
, .client-job
and .pages_deploy_job
are reused in this config.
Most of the pipeline is pretty standard. This yaml defines build and test steps that are going to run before the deployment job.
In the .pages_deploy_job
we are altering the package.json
file.
sed -i '5i\ "homepage": "'"$HOMEPAGE"'",' ./package.json
This line sets PUBLIC_URL
environment variable and makes sure react imports js and other files using appropriate paths.
We are also copying index.html
to 404.html
in order to make routing work when sharing a particular subpage. The page for review is set to be live for a week. It is visible only to people that are able to view the project. We are also describing the environment, so that its visible in the project's Deployments > Environments section and in pull requests.
stop review app:
job is there to allow GitLab to remove the environment from the list after merging the pull request.
.cache: &client_cache
key: $CI_COMMIT_REF_SLUG
paths:
- client/.npm
.client-job:
variables:
NODE_ENV: production
before_script:
- cd client && npm i --production=false --cache .npm --prefer-offline
cache:
<<: *client_cache
policy: pull
build-client-job:
stage: build
extends: .client-job
script:
- CI=true npm run build
cache:
<<: *client_cache
test-client-unit-job:
stage: test
extends: .client-job
script:
- CI=true npm test
.pages_deploy_job:
extends: .client-job
stage: deploy
script:
- |
sed -i '5i\ "homepage": "'"$HOMEPAGE"'",' ./package.json
- CI=true npm run build
- rm -rf public
- mv build public
- cp public/index.html public/404.html
- mv public ../public
artifacts:
expire_in: 1 week
paths:
- public
variables:
HOMEPAGE: ""
review app:
extends: .pages_deploy_job
variables:
HOMEPAGE: "/-/$CI_PROJECT_NAME/-/jobs/$CI_JOB_ID/artifacts/public"
except:
- main
environment:
name: review/$CI_COMMIT_REF_SLUG
url: "https://$CI_PROJECT_NAMESPACE.gitlab.io/-/$CI_PROJECT_NAME/-/jobs/$CI_JOB_ID/artifacts/public/index.html"
auto_stop_in: 1 week
stop review app:
stage: deploy
image: alpine
script: echo "stop review app for $CI_COMMIT_REF_NAME"
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
rules:
- when: never
pages:
extends: .pages_deploy_job
variables:
HOMEPAGE: "/$CI_PROJECT_NAME"
only:
- main
environment:
name: pages
url: $CI_PAGES_URL
Test it!
Let's see it in action. Firstly, create a new branch.
git checkout -b home-page-content
Change something in the home page, e. g.:
function Home() {
return (<Fragment>
<h2>Home</h2>
<p>Welcome.</p>
</Fragment>);
}
Let's upload it to gitlab.
git add .
git commit -m "Update homepage content"
git push --set-upstream origin home-page-content
And create a merge request. You should see the pipeline running.
After it finishes, it should change to this.
Click the "View app" button to test it! Our review app should be up and running.
Summary
I hope this was a useful tutorial for you. It took me some time to figure out how to do this properly and I couldn't find an end to end example. Making apps available to reviewers in this way was very useful to me and my coworkers.
This article didn't focus on the backend of the app. To make it available to review you may need to do a standard deployment in your environment. Otherwise you need to use mocks or if your API is changing rarely, you can probably connect to an already deployed version.
If you decide to use it in your project and it's public, make sure to share it in the comments.
Full code: gitlab.com/horosin/frontend-review-apps