Host Storybook for each pull request with CircleCI and GitHub Deployments
Tags: webdev, javascript, storybook
Published: 2019-05-23 20:46:47

---
title: Host Storybook for each pull request with CircleCI and GitHub Deployments
published: true
description: 
tags: webdev, javascript, storybook
---

Originally posted at [kasper.io](https://kasper.io/host-storybook-with-circleci-and-github-deployments/)

Lately at company, we had a lot of work with developing UI components using React. It went smooth thanks to [Storybook](https://storybook.js.org). We hosted files generated by Storybook for each push to branch which was really helpful for quality assurance process.

Storybook is a tool for developing UI components in isolation. While this tool is useful for local development, it's also possible to build a static version of Storybook and host it. I'll show how to configure a deploy for each push made to repository.

You will learn how to build Storybook on [CircleCI](https://circleci.com/) and use it as a hosting. You will also learn how to use [GitHub Deployments](https://developer.github.com/v3/repos/deployments/). Deployments are requests to deploy a specific branch, commit, tag. External services can listen for those requests and act.

**This guide assumes that you have initialized Storybook using `@storybook/cli`. If not, go [here](https://storybook.js.org/docs/guides/quick-start-guide/) to learn how to do it.**

> **TL;DR**: Here is a [repository](https://github.com/Idered/storybook-circleci-github-deployments) with whole process configured. List of deployments can be viewed [here](https://github.com/Idered/storybook-circleci-github-deployments/deployments) and deployment assigned to pull request can be viewed [here](https://github.com/Idered/storybook-circleci-github-deployments/pull/1).

Whole process looks like this:
- Make a push to repository
- CircleCI build is triggered
- [GitHub Deployment](https://developer.github.com/v3/repos/deployments/) is created
  ![Pending Deployment](https://i.imgur.com/UokKkza.png)
- Install dependencies
- Build storybook
- Save generated files as CircleCI artifacts
- If whole process was successful, add success deployment status
  ![Success Deployment](https://i.imgur.com/hH4zImy.png)
- If whole process was not successful, add error deployment status
- We can see link to generated files on [deployments page](https://github.com/Idered/storybook-circleci-github-deployments/deployments)
  ![](https://i.imgur.com/C8aqPZ5.png)
- We can see link to generated files in related [pull request](https://github.com/Idered/storybook-circleci-github-deployments/pull/1)
  ![](https://i.imgur.com/XLXY2fs.png)

## Setting up CircleCI

Go to [CircleCI Dashboard](https://circleci.com/dashboard) and add your project. Start the build process - it will fail at first but we will fix it in next steps.

### Create CircleCI config file

In your git repository, create `.circleci/config.yml`:

```yml
version: 2.1

jobs:
  build-storybook:
    working_directory: ~/repo
    docker:
      - image: circleci/node:lts
    steps:
      - checkout
      - run:
          name: Create GitHub Deployment
          command: ./tasks/deployment/start.sh > deployment
      - restore_cache:
          keys:
            - cache-{{ checksum "package.json" }}
            # fallback to using the latest cache if no exact match is found
            - cache-
      - run:
          name: Installing Dependencies
          command: npm install
      - run:
          name: Build Storybook
          command: npm run build-storybook
      - store_artifacts:
          path: storybook-static
      - run:
          name: Add GitHub Deployment success status
          command: ./tasks/deployment/end.sh success
          when: on_success
      - run:
          name: Add GitHub Deployment error status
          command: ./tasks/deployment/end.sh error
          when: on_fail
      - save_cache:
          paths:
            - node_modules
          key: cache-{{ checksum "package.json" }}

workflows:
  deploy:
    jobs:
      - build-storybook
```

There are 3 parts that are related to creating and adding status updates of GitHub Deployments. This command will create a Deployment and save it's id to `deployment` file. Deployment will be visible in related pull request as pending.
  ```yml
  - run:
      name: Create GitHub Deployment
      command: ./tasks/deployment/start.sh > deployment
  ```
Only one of other two commands will execute. Execution is based on the status of whole build.
  ```yml
  - run:
      name: Add GitHub Deployment success status
      command: ./tasks/deployment/end.sh success
      when: on_success
  - run:
      name: Add GitHub Deployment error status
      command: ./tasks/deployment/end.sh error
      when: on_fail
  ```

### Create deployment scripts

Now create 2 files:
`tasks/deployment/start.sh` - this will create a GitHub Deployment.
```bash
  #!/bin/sh

  set -eu

  token=${GITHUB_DEPLOYMENTS_TOKEN:?"Missing GITHUB_TOKEN environment variable"}

  if ! deployment=$(curl -s \
                    -X POST \
                    -H "Authorization: bearer ${token}" \
                    -d "{ \"ref\": \"${CIRCLE_SHA1}\", \"environment\": \"storybook\", \"description\": \"Storybook\", \"transient_environment\": true, \"auto_merge\": false, \"required_contexts\": []}" \
                    -H "Content-Type: application/json" \
                    "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/deployments"); then
    echo "POSTing deployment status failed, exiting (not failing build)" 1>&2
    exit 1
  fi

  if ! deployment_id=$(echo "${deployment}" | python -c 'import sys, json; print json.load(sys.stdin)["id"]'); then
    echo "Could not extract deployment ID from API response" 1>&2
    exit 3
  fi

  echo ${deployment_id} > deployment
```

`tasks/deployment/end.sh` - this will update Deployment status to success or error.
```bash
#!/bin/sh

set -eu

token=${GITHUB_DEPLOYMENTS_TOKEN:?"Missing GITHUB_TOKEN environment variable"}

if ! deployment_id=$(cat deployment); then
  echo "Deployment ID was not found" 1>&2
  exit 3
fi

if [ "$1" = "error" ]; then
  curl -s \
    -X POST \
    -H "Authorization: bearer ${token}" \
    -d "{\"state\": \"error\", \"environment\": \"storybook\"" \
    -H "Content-Type: application/json" \
    "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/deployments/${deployment_id}/statuses"
  exit 1
fi

if ! repository=$(curl -s \
                  -X GET \
                  -H "Authorization: bearer ${token}" \
                  -d "{}" \
                  -H "Content-Type: application/json" \
                  "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}"); then
  echo "Could not fetch repository data" 1>&2
  exit 1
fi

if ! repository_id=$(echo "${repository}" | python -c 'import sys, json; print json.load(sys.stdin)["id"]'); then
  echo "Could not extract repository ID from API response" 1>&2
  exit 3
fi

path_to_repo=$(echo "$CIRCLE_WORKING_DIRECTORY" | sed -e "s:~:$HOME:g")
url="https://${CIRCLE_BUILD_NUM}-${repository_id}-gh.circle-artifacts.com/0${path_to_repo}/storybook-static/index.html"

if ! deployment=$(curl -s \
                  -X POST \
                  -H "Authorization: bearer ${token}" \
                  -d "{\"state\": \"success\", \"environment\": \"storybook\", \"environment_url\": \"${url}\", \"target_url\": \"${url}\", \"log_url\": \"${url}\"}" \
                  -H "Content-Type: application/json" \
                  "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/deployments/${deployment_id}/statuses"); then
  echo "POSTing deployment status failed, exiting (not failing build)" 1>&2
  exit 1
fi
```

> It might be necessary to update scripts file mode to executable:

```sh
git update-index --add --chmod=+x ./tasks/deployment/start.sh
git update-index --add --chmod=+x ./tasks/deployment/end.sh
```

### Configure GitHub access token

Go to https://github.com/settings/tokens and create a new access token. Required scopes:
- `repo:status`
- `repo_deployment`
- `public_repo` 

Copy new token and go to `Environment Variables` configuration section in CircleCI project. If you can't find it, use this url, just replace GITHUB_USERNAME and REPOSITORY_NAME with valid values:

```sh
https://circleci.com/gh/GITHUB_USERNAME/REPOSITORY_NAME/edit#env-vars
```

On CircleCI add variable:
```yaml
name: GITHUB_DEPLOYMENTS_TOKEN
value: xxxx-xxxx-xxxx-your-github-token
```

### Result

Now whenever you push a new commits to your repository, you will get a storybook hosted on CircleCI. The link to storybook will be added to repository [deployments page](https://github.com/Idered/storybook-circleci-github-deployments/deployments) and to the [related pull request](https://github.com/Idered/storybook-circleci-github-deployments/pull/1).

### Bonus

Are you working in company? Create a company github bot account and use it's personal access token to deploy. Customize it's name and avatar.