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. 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 and use it as a hosting. You will also learn how to use GitHub 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 to learn how to do it.

TL;DR: Here is a repository with whole process configured. List of deployments can be viewed here and deployment assigned to pull request can be viewed here.

Whole process looks like this:

  • Make a push to repository
  • CircleCI build is triggered
  • GitHub Deployment is created
    Pending Deployment
  • Install dependencies
  • Build storybook
  • Save generated files as CircleCI artifacts
  • If whole process was successful, add success deployment status
    Success Deployment
  • If whole process was not successful, add error deployment status
  • We can see link to generated files on deployments page
  • We can see link to generated files in related pull request

Setting up CircleCI

Go to CircleCI 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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.

1
2
3
- 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.

1
2
3
4
5
6
7
8
- 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#!/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:

1
2
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:

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

On CircleCI add variable:

1
2
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 and to the related pull request.

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.