DEV Community

Kasper Mikiewicz
Kasper Mikiewicz

Posted on

Host Storybook for each pull request with CircleCI and GitHub Deployments

Originally posted at kasper.io

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:

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
Enter fullscreen mode Exit fullscreen mode

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.

  - run:
      name: Create GitHub Deployment
      command: ./tasks/deployment/start.sh > deployment
Enter fullscreen mode Exit fullscreen mode

Only one of other two commands will execute. Execution is based on the status of whole build.

  - 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
Enter fullscreen mode Exit fullscreen mode

Create deployment scripts

Now create 2 files:
tasks/deployment/start.sh - this will create a GitHub Deployment.

  #!/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
Enter fullscreen mode Exit fullscreen mode

tasks/deployment/end.sh - this will update Deployment status to success or error.

#!/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
Enter fullscreen mode Exit fullscreen mode

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

git update-index --add --chmod=+x ./tasks/deployment/start.sh
git update-index --add --chmod=+x ./tasks/deployment/end.sh
Enter fullscreen mode Exit fullscreen mode

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:

https://circleci.com/gh/GITHUB_USERNAME/REPOSITORY_NAME/edit#env-vars
Enter fullscreen mode Exit fullscreen mode

On CircleCI add variable:

name: GITHUB_DEPLOYMENTS_TOKEN
value: xxxx-xxxx-xxxx-your-github-token
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)