Laboratory One Research Blog

Continuous Integration and Deployment for Static Web Apps

December 03, 2017

In this post, we will examine a simple toy Continuous Integration and Deployment system. This system will deploy a static web application to a production server after testing it’s code.

Toy Project Example

Toy Project Repository

As developers, we strive to automate the drudgery out of life. Let us apply this when encountering the process of rolling out code updates. In general, this process involves:

  • Managing code versions
  • Acceptance testing
  • Deploying updates to production

We often have to balance our iteration schedule with the amount of time/effort required to perform the rollout. If the time and effort to perform a rollout exceeds some arbitrary threshold in relation to the value of the new code, the developer will forego the rollout. They will favour a strategy of batching up a number of updates.

We can abstract beyond this balance by completely automating the rollout. These tasks can easily be performed consistently by computers, whereas even attentive humans can’t.

Let us automate beyond these plebeian duties. Let us free ourselves so we can better spend out time on important matters. Problem How can we build a continuous integration and development system for a static web application?

First, let’s break our problem into parts:

  1. Somewhere for the code to live; a single source of truth. What we are describing is known as ‘version control’. We are going to utilize git and Github.
  2. A static web application to deploy. We are going to use ‘create-react-app’ to generate a toy application.
  3. A way to run acceptance tests. These are important! They define what code our Continuous Integration and Deployment system will allow to be pushed to production. We’re going to want to make sure that our code runs, and passes unit tests. We will employ Jest and Standard.js to implement the previously described acceptance tests.
  4. A way to deploy code to a production environment. Create-react-app ships with a ‘build’ script which productionize the code by bundling React in production mode, optimizes the build for the best performance, minifies it, and hashes the filenames. Then we will deploy to Surge.sh which is a static web hosting service.
  5. A way to control the integration and deployment of code. We will use circleCI. This service hooks into Github and spins up environments to run our Continuous Integration and Deployment pipeline.

Topography

Let us visualise the system we are building. It consists of the follow:

Local Machine: a local development environment. Development and testing should happen here before pushing the update to a feature git branch via Github.

A Version Control System with the following branches:

Feature branch

  • A Github branch where we store new features.
  • Each feature should get it’s own branch.
  • Acts a proxy so we don’t accidentally deploy code which is not yet ready.

Production branch

  • A Github branch where production code lives.
  • Reserved for code which has passed acceptance tests.
  • Protected from commits.
  • Features branches merge into this branch.

Production Environment: the environment which the Static Web Application lives. This is where code from the production Github branch is ran. It should be accessed by the open internet, and your domain’s DNS should point here.

Continuous Integration / Deployment system: the system which orchestrates tasks and directs workflows.

Implementation

Now that we understand the problem we’re solving, and have designed a solution, we can consider how it can be implemented. We need a version control system, a toy static web application, acceptance tests, a continuous integration system, and a production environment.

Here’s how I’m going to tackle it:

  1. Create a “toy” static web app
  2. Setup a version control system
  3. Create acceptance tests
  4. Create scripts for the continuous integration system
  5. Implement Continuous Integration
  6. Implement Continuous deployment
  7. Deploy to a production environment

Static Web Application

Let’s set up a static web app with create-react-app. Create-react-app is a React web application generator. It’ll allow us to quickly setup a static web app. It also comes with a number of helpful scripts and development tool which we will explore in the following sections.

The following shell commands will install create-react-app and setup a project. Replace the {project-name}, with your project’s name.

$ yarn global add create-react-app
$ create-react-app {project-name}
$ cd {project-name}

Check that everything is working correctly by starting the development server with:

$ npm start

You should be able to point your browser at http://localhost:3000 to view the static web application locally.

Github

We are going to use Github for version control. Make sure it’s installed on your local machine and setup with your Github account. Bitbucket will work fine for this exercise. Initialise a git repository locally with:

$ git init

Troubleshooting: Make sure you are in your project’s directory. If there is an existing git repository, just remove it with:

$ rm -rf .git

Make an initial commit:

$ git add --a
$ git commit -m "initial commit"

Create a git remote repository on Github. Then add the remote endpoint to your local machine with:

$ git remote add origin {HTTPS||SSH}

*Replace {HTTPS || SSH } with your remote endpoint

Push your commit to the repository master branch with:

$ git push origin master

At this point, you should be able to refresh the Github repository’s website to see your project. While you’re there, configure the master branch to be protected. It should require tests to pass before merging in, not accept commits, and require a pull request to change it. This will ensure that only verified code is deployed to the production environment. Navigate to the repository’s “Settings” and make the following changes:

  • Branches
  • Protected Branches
  • Master
  • Protect this branch
  • Require status checks to pass before merging
  • Save Changes

Protect the Master branch

Finally, create a new branch locally for us to implement testing. Replace {feature_branch} with a name.

$ git checkout -b {feature_branch}

Acceptance Testing

We want to make sure the web application runs and passes unit tests. We’re not going write any unit tests, but Create-react-app ships with Jest, which can run unit tests.

Lets setup a linter. We are going to use StandardJS. It uses a very opinionated style guide which allows us to not configure it much.

$ yarn add standard --dev

Add globals to linter config in: package.json

{
  ...
  "standard": {
    "globals": [
      "fetch",
      "it",
      "URL"
    ]
  }
}

Make sure it’s working correctly:

$ standard

Fix any code errors automatically with our linter

$ standard —fix

Run the test runner to ensure the build is passing.

$ npm test

Commit this your changes, and push it.

$ git add --a
$ git commit -m "implemented linter and tests"
$ git push origin {test branch}

Merge it on the Github website by creating a Pull Request, and approve it. Switch back to your master branch and update it:

$ git checkout master
$ git pull origin master

Then create a new branch for CI development

$ git checkout -b {CI branch}

Scripts

Now we need to setup some scripts to enable our Continuous Integration and Deploy pipeline. We need to be able to run acceptance tests, build the production bundle, and deploy the code.

  • build: build the production bundle
  • deploy: deploys the production bundle to your production environment.
  • lint: lints the app’s code
  • test: runs the linter and unit tests

package.json

{
  ...,
  "scripts": {
    ...
    "build": "react-scripts build",
    "deploy": "./node_modules/.bin/surge --project ./build --domain {your,domain.com}",
    "lint": "standard",
    "start": "react-scripts start",
    "test": "npm run lint && react-scripts test --env=jsdom"
  }
}

circleCI

We will utilize circleCI as our continuous integration and deployment system. Navigate to their website and setup circleCI. Add your github repository to circleCI as a project.

Next, setup circleCI locally. Configure your project for circleCI via config.yml.

$ mkdir .circleci
$ cd .circleci
$ touch config.yml

config.yml

# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2
jobs:
  build-job:
    docker:
      # specify the version you desire here
      - image: circleci/node:latest

      # Specify service dependencies here if necessary
      # CircleCI maintains a library of pre-built images
      # documented at https://circleci.com/docs/2.0/circleci-images/
      # - image: circleci/mongo:3.4.4

    working_directory: ~/repo

    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "package.json" }}
          # fallback to using the latest cache if no exact match is found
          - v1-dependencies-

      - run: yarn install

      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}

      # run tests!
      - run: yarn test

workflows:
  version: 2
  production:
    jobs:
      - build-job

Commit your changes and create a Pull Request.

$ git add --a
$ git commit -m "implement circle ci"
$ git push origin CI

Check circleCI to make sure this build succeeds. Once it does, merge your Pull Request via Github.

Check Build Status

Check Build Details

Merge Pull Request

Switch back to your master branch and update it. Then create a new branch for deployment development.

$ git checkout master
$ git pull origin master
$ git checkout -b {deploy branch}

Deployment

Let’s build our web application.

$ npm run build

surge.sh

Now we need to setup our production environment. We will use surge.sh since we can use their CLI for deployment. Follow the sign up instructions.

Install surge and get a surge token.

$ yarn add surge --dev
$ surge
$ surge token

On your circleCI dashboard, navigate to your project’s settings. Enter your token as an environmental variable. Build Settings Environment Variables Add Variable

Make 2 entries
Name: SURGE_LOGIN
Value: {email}
Name: SURGE_TOKEN
Value: {token}

Setup circleCI's Enviroment Variables

Point your domain DNS to surge.

CNAME @     na-west1.surge.sh
CNAME www   na-west1.surge.sh

Deploy to your application to your domain.

surge --domain {yourdomain.com}

Enable deployment via circleCI’s config.yml.

# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2
jobs:
  build-job:
    docker:
      # specify the version you desire here
      - image: circleci/node:latest

      # Specify service dependencies here if necessary
      # CircleCI maintains a library of pre-built images
      # documented at https://circleci.com/docs/2.0/circleci-images/
      # - image: circleci/mongo:3.4.4

    working_directory: ~/repo

    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "package.json" }}
          # fallback to using the latest cache if no exact match is found
          - v1-dependencies-

      - run: yarn install

      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}

      # run tests!
      - run: yarn test

  deploy-job:
      docker:
        # specify the version you desire here
        - image: circleci/node:7.10

        # Specify service dependencies here if necessary
        # CircleCI maintains a library of pre-built images
        # documented at https://circleci.com/docs/2.0/circleci-images/
        # - image: circleci/mongo:3.4.4

      working_directory: ~/repo

      steps:
        - checkout

        # Download and cache dependencies
        - restore_cache:
            keys:
            - v1-dependencies-{{ checksum "package.json" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

        - run: yarn install

        - save_cache:
            paths:
              - node_modules
            key: v1-dependencies-{{ checksum "package.json" }}

        # build the web app
        - run: yarn run build

        # deploy the build
        - run: yarn run deploy

workflows:
  version: 2
  production:
    jobs:
      - build-job
      - deploy-job:
          requires:
            - build-job
          filters:
            branches:
              only: master

Commit your updates, and make a Pull Request.

$ git add --a
$ git commit -m "implemented deployment"
$ git push origin deploy

You should see your build pass both jobs. If you were to make any changes, they would be deployed via a passing Pull Request.

Merge Deploy Pull Request

Check that both builds succeed

Check details of the builds

Conclusion

We’ve seen how simple a Continuous Integration and Deployment can be. We’ve looked at managing simple testing and deployment. How can we improve upon this? What other kinds of projects could use a Continuous Integration and Deployment?

Consider exploring how one would:

  1. Set up a Continuous Integration and Deployment for multiple environments. Perhaps we want a ‘staging’ environment to test on production-like infrastructure. What if we want to provide a development instances to our developers?
  2. Setting up Continuous Integration and Deployment for APIs. What if we want to support multiple versions in production? How can you migrate data safely?

Peter Chau

Written by Peter Chau, a Canadian Software Engineer building AIs, APIs, UIs, and robots.

peter@labone.tech

laboratoryone

laboratory_one