Developing a Website with React, Feature Flags, Netlify and GitHub Actions
Tags: react, javascript, webdev, github
Published: 2019-07-19 11:25:57

> **Please note:** this post was written for GitHub Actions v1. GitHub Actions v2 has been released which [deprecates the HCL syntax for workflows](https://github.blog/changelog/2019-09-17-github-actions-will-stop-running-workflows-written-in-hcl/) which renders some parts of this blog post unusable. To learn more about GitHub Actions v2, please refer to the [documentation](https://help.github.com/en/actions/automating-your-workflow-with-github-actions) and the excellent [posts](https://www.edwardthomson.com/blog/github_actions_advent_calendar.html) by Edward Thomson.

In recent weeks I've helped develop a website for a very exciting project at [Awkward](https://awkward.co) called Coffee by Benjamin. Coffee by Benjamin is a coffee roasting kit that allows anyone to roast their coffee at home, this guarantees the freshness of the coffee. The project will launch on Kickstarter soon. If you'd like to keep notified about this project you can follow them on [Instagram](https://www.instagram.com/coffeebybenjamin/) or visit the [website](https://coffeebybenjamin.com/).

This project is my last one at Awkward as I'll be taking up a new challenge at another company soon. Even though I won't be a part of the project going forward, I still want to share something about the way we've been developing and shipping the website by utilizing React, feature flags, Netlify, and GitHub Actions.

## Problem statement

The website will launch in three separate phases outlined below. We're currently in phase 1 but we're nearing completion on phase 2. Meanwhile, we've already started development on phase 3.

- Phase 1: a simple landing page where people can fill in their email address to get notified when the project launches.
- Phase 2: a full-fletched website which contains more information about the project, a FAQ and a support form. This will launch together with the launch of the Kickstarter campaign.
- Phase 3: integrate Shopify into the website to sell the product directly. This will launch after the project has been successfully funded and shipped.

Even though phase 3 won't launch till much later, we wanted to start development on this phase as soon as possible because it's the most complicated part of the website to build. This allows us to start testing the shop functionality long before it's launched and catch costly bugs from creeping into the website.

Now we could build phase 3 in a separate branch, but we'd have to constantly update and solve merge conflicts on this branch when we update the phase 2 website. This is especially hard because there are a lot of overlapping parts that we'll change in phase 3. Furthermore, this would result in having to merge a gigantic pull request when phase 3 launches which comes with the risk of bugs in existing functionality. Instead, we want to gradually merge functionality from phase 3 in the main branch without exposing it to the public. We also want the team to be able to check the progress on both phase 2 and phase 3. Finally, we'd like to completely exclude any code from phase 3 while phase 2 is live so that we don't ship any unnecessary code.

In the rest of the post I'll explain how we used a combination of feature flags, Netlify and GitHub Actions to achieve these goals.

<img src="https://thepracticaldev.s3.amazonaws.com/i/evod8hvfe3zz2lzbe66r.jpg" alt="The tray" width="520">

<figcaption>The tray you'll get as part of the home roasting kit</figcaption>

## Feature flags

The problem statement just screams for feature flags, which is exactly what we'll be using. Feature flags allow us to ship parts of phase 3 but don't actually show them to the public. Let's take a look at a definition of feature flags:

> Release Toggles allow incomplete and un-tested codepaths to be shipped to production as latent code which may never be turned on.
>
> — [Feature Toggles, Pete Hodgson](https://martinfowler.com/articles/feature-toggles.html)

The nice thing about feature flags is that it allows you to switch between new and old functionality with the flip of a switch. Usually you do this by wrapping new functionality in a condition like so:

```js
function Header() {
  if (USE_NEW_FEATURE) {
    return <NewHeader />;
  }

  // feature flag is not enabled
  return <OldHeader />;
}
```

In code that's affected by a feature flag, you'll add new code without replacing the old code. This allows pull requests with new but overlapping functionality to be merged as they won't replace any existing functionality. Later when the feature flag is being phased out you can remove the conditions and remove any old code.

Let's see how we can implement this into our stack.

## Feature Flags in Create React App

We can implement feature flags by using environment variables [which Create React App supports out of the box](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables). The benefits of using environment variables are that they're easy to use and they're compile-time constants, which means that code that is guarded by a condition that checks for the flag being enabled will be completely excluded from a build where the flag was disabled.

Environment variables in Create React App can be supplied in a `.env` file. The `.env` file will contain the default value to use and is checked into Git and will only be changed when phase 3 goes live.

`.env`:

```

REACT_APP_SHOPIFY_INTEGRATION_ENABLED=false

```

Now we can use the feature flag in `App.js` to conditionally render the shop routes. By conditionally rendering the shop routes using a compile-time constant, the code won't end up in the production bundle unless the flag is enabled and users won't be able to route to these pages. The code for the pages will still end up in the production bundle, more on that later.

`src/App.js`:

```jsx
import React, { Suspense } from 'react';
// ... more imports hidden
import Home from 'pages/Home';
import Shop from 'pages/shop';
import Cart from 'pages/cart';
import ProductDetail from 'pages/product-detail';

const App = () => (
  <Router>
    <Switch>
      <Route exact path="/" component={Home} />
      <Route path="/faq" component={Faq} />
      <Route path="/support" component={Support} />
      {process.env.REACT_APP_SHOPIFY_INTEGRATION_ENABLED === 'true' && (
        <>
          <Route path="/shop" component={Shop} />
          <Route path="/cart" component={Cart} />
          <Route path="/product/:productId" component={ProductDetail} />
        </>
      )}
    </Switch>
  </Router>
);

ReactDOM.render(<App />, document.getElementById('root'));
```

Now that we've got the feature flag set up developers can add a `.env.local` (or any of the [other supported `.env` files](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#what-other-env-files-can-be-used)) which won't be checked into git.

`.env.local`:

```
REACT_APP_SHOPIFY_INTEGRATION_ENABLED=true
```

## Configuring Netlify

Now only developers can see the Shopify integration by checking out locally and changing the environment variable in `.env.local`, what about other people that might want to review the site with a simple link? This is where Netlify comes in. Netlify allows developers to configure the build settings per branch and all branches will be deployed with a unique URL (separately from deploy previews), I'll let the Netlify documentation speak for itself:

> Branch deploys are published to a URL which includes the branch name as a prefix. For example, if a branch is called staging, it will deploy to staging--yoursitename.netlify.com.

**NOTE:** You may have to manually set the branch deploys setting to deploy all branches, this is explained in the [Netlify documentation](https://www.netlify.com/docs/continuous-deployment/#branch-deploy-controls).

We can add a branch in Git called `shop-staging` and configure `netlify.toml` to build this branch with the `REACT_APP_SHOPIFY_INTEGRATION_ENABLED` feature flag enabled.

`netlify.toml`:

```toml
[build]
  publish = "build"
  command = "npm run build"

[context."shop-staging"]
  command = "REACT_APP_SHOPIFY_INTEGRATION_ENABLED=true npm run build"
```

Prefixing the build command with `REACT_APP_SHOPIFY_INTEGRATION_ENABLED=true` will override the settings in `.env`. The site with the feature flag enabled will now be automatically deployed to shop-staging--yoursitename.netlify.com. We can now give this URL to testers and they'll be able to check out the progress on phase 3 and they can still check out the progress on phase 2 by visiting develop--yoursitename.netlify.com. You can also use this approach to enable the feature flag for deploy previews for certain pull requests.

There's still one problem though, the `shop-staging` branch will have to be to kept in sync with the main branch (in our case `develop`). Luckily, GitHub provides an extensive API which provides a way to do a [fast-forward update](https://developer.github.com/v3/git/refs/#update-a-reference) for a branch, this allows us to keep the `shop-staging` branch in sync with the `develop` branch. All we have to do is provide it the ref we want to update (`heads/shop-staging`) and a commit SHA of the latest commit on the develop branch and then `shop-staging` will be in sync with the `develop` branch. Furthermore, we can automate this process by using GitHub Actions!

## Creating a GitHub Action to keep branches in sync

GitHub actions, just like shell commands, are extremely composable. There's a lot you can accomplish by composing a few predefined actions. In this case we technically only need the [Filter action](https://github.com/actions/bin/tree/master/filter) and the [`cURL` action](https://github.com/actions/bin/tree/master/curl). But I couldn't get the `cURL` action to accept a JSON body with an interpolated value, so we'll be creating our own.

There are two ways to create GitHub Actions, you can create a separate repository that contains the Action, this way other projects will be able to reuse the Action. But for something small that you won't reuse you can create an Action right inside of the repository where the rest of the code for your project lives.

We first create a folder `.github`, inside of it we create a folder called `branch-sync-action`. We must then create a `Dockerfile`, the contents are copied from the `cURL` action, we just change some of the labels. This `Dockerfile` ensures that we can use `cURL` which we'll use to do the HTTP call.

`.github/branch-sync-action/Dockerfile`

```dockerfile
FROM debian:stable-slim

LABEL "com.github.actions.name"="Branch Sync"
LABEL "com.github.actions.description"=""
LABEL "com.github.actions.icon"="refresh-cw"
LABEL "com.github.actions.color"="white"

COPY entrypoint.sh /entrypoint.sh

RUN apt-get update && \
    apt-get install curl -y && \
    apt-get clean -y

ENTRYPOINT ["/entrypoint.sh"]
```

Next, we create an `entrypoint.sh` which is the script that will be executed when running the action.

`.github/branch-sync-action/entrypoint.sh`

```sh
#!/bin/sh

TARGET_BRANCH=$1

curl \
  -X PATCH \
  -H "Authorization: token $GITHUB_TOKEN" \
  -d "{\"sha\": \"$GITHUB_SHA\"}" \
  "https://api.github.com/repos/$GITHUB_REPOSITORY/git/refs/heads/$TARGET_BRANCH"
```

`$1` stands for the first argument provided to the script. For clarity we give it the name `TARGET_BRANCH`.

Don't forget to provide execution permissions by doing `chmod +x entrypoint.sh`.

That's it for the action itself. Now we have to hook it up in a workflow:

`.github/main.workflow`

```hcl
workflow "Sync shop-staging branch with develop" {
  on = "push"
  resolves = ["Sync Branch"]
}

action "Filter develop branch" {
  uses = "actions/bin/filter@master"
  args = "branch develop"
}

action "Sync Branch" {
  needs = ["Filter develop branch"]
  uses = "./.github/sync-branch-action"
  secrets = ["GITHUB_TOKEN"]
  args = ["shop-staging"]
}
```

In `.github/main.workflow` we define workflows for our project. Workflows decide which actions to run and when. In the `workflow` block we tell it when to run by defining the `on` attribute, in our case the workflow should run for every `push` event, we also define the actions it should execute (in parallel) by defining the `resolves` attribute.

Next, we define the filter action. GitHub will send a `push` event for every push to any branch, we want to add a filter so that we only sync the `shop-staging` branch when someone pushes to the `develop` branch, we're not interested in pushes to any other branch. In the `uses` parameter we point to the slug of the [GitHub repository](https://github.com/actions/bin) that provides this action and in this case the folder within this repository (filter). The `@master` part tells it to use the code that was published on the master branch.

Finally we add the action that syncs the `shop-staging` branch with the `develop` branch. It has the `needs` parameter defined which tells GitHub Actions that it should first run the filter action and only continue with `Sync Branch` if the filter action succeeds. Furthermore we define the `uses` parameter which will point to the folder containing the `Dockerfile` and `entrypoint.sh` which is used by GitHub Actions to run it. We also pass it the `GITHUB_TOKEN` as a secret which we need to make an authenticated HTTP call, `GITHUB_TOKEN` is a uniquely generated token for every project on GitHub. Lastly, we provide the arguments for `entrypoint.sh` which is the target branch it should sync to.

We'll end up with a flow looking like this:

<img src="https://thepracticaldev.s3.amazonaws.com/i/vr4etpfh33kmpjm3fekr.png" width="300" alt="Workflow">

It's important to note that the sync is one-way only. Everything that's pushed to `develop` will be fast-forwarded to `shop-staging`, if you're pushing to `shop-staging` nothing will happen, it will cause problems with future synchronization because updates can't be fast-forwarded anymore. You can solve this by enabling the [`force` parameter](https://developer.github.com/v3/git/refs/#update-a-reference) in the `cURL` request or by resetting the `shop-staging` branch using `git reset`.

## Lazy loading shop routes

A last problem we still have to tackle is excluding phase 3 related code from the bundle while phase 2 is live. We can tackle this by utilizing some new features released in React just last year: [`React.lazy`](https://reactjs.org/docs/code-splitting.html#reactlazy) and `Suspense`. The changes we have to make to our code are quite minimal, we have to change the way we're importing the shop pages by utilizing `React.lazy` and dynamic imports:

`src/App.js`:

```jsx
import React, { Suspense } from 'react';
// ... more imports hidden
import Home from 'pages/Home';
const Shop = React.lazy(() => import('pages/shop'));
const Cart = React.lazy(() => import('pages/cart'));
const ProductDetail = React.lazy(() => import('pages/product-detail'));

const App = () => (
  <Suspense fallback={<div>Loading...</div>}>
    <Router>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/faq" component={Faq} />
        <Route path="/support" component={Support} />
        {process.env.REACT_APP_SHOPIFY_INTEGRATION_ENABLED === 'true' && (
          <>
            <Route path="/shop" component={Shop} />
            <Route path="/cart" component={Cart} />
            <Route path="/product/:productId" component={ProductDetail} />
          </>
        )}
      </Switch>
    </Router>
  </Suspense>
);

ReactDOM.render(<App />, document.getElementById('root'));
```

Now the shop pages will not end up in the main bundle, they will instead be lazily loaded when a user hits one of the shop routes. Which is impossible when the flag is not enabled. All of the routes are wrapped in a `Suspense` component which is responsible for showing a fallback state when visiting one of the lazily loaded routes as it still takes some time to download the bundle. If you'd like to know more about code-splitting (in React) I can recommend the excellent [React documentation](https://reactjs.org/docs/code-splitting.html).

## Demo

I created a simplified example of the code in this post which you can check out here: https://github.com/TimonVS/sync-branch-demo. You can clone it and push a commit to the master branch to see that the shop-staging branch will automatically be kept in sync.

## Conclusion

We're quite happy with this approach. GitHub Actions deem to be very flexible. It'd have been even simpler if Netlify would support this use case out of the box, but since that's not the case syncing two branches isn't too bad either.

The approach described in this post can also be used when using split-testing which is built into Netlify and allows you to test two (or more) variants of a website. It's not something we're using ourselves but with split-testing come the same problems as described in the problem statement.

Finally, I have to note that we're currently only using one feature flag. This approach might not scale well if you want to use lots of feature flags because you might want to deploy separate staging sites for all combination of flags.

Happy roasting!