Deploying a Hugo site hosted on Firebase using GitLab CI

Introduction

In this post I shall demonstrate how to setup a pipeline to automate deployments of a static Hugo site.

Here is an overview of the overall CI process.

Step 1

  • make changes to the site code and test locally

  • test changes locally by using the command

hugo serve

Step 2

Once all changes are reviewed and are approved

  • run hugo to update files to use the correct base url defined in your config.toml file

  • commit changes to master (in this example there is only 1 branch)

  • git commit triggers a CI pipeline setup on GitLab, which starts the deployment process of uploading artifacts over to the live instance of this site on Firebase

GitLab CI

Pipeline Prerequisites

Firebase authentication token

Before setting up the deployment automation using the Firebase CLI.

You first need to generate a Firebase authentication token.

This is run locally.

firebase login:ci

Save this authentication token since you will need this later on.

A custom docker image

GitLab allows you to store your own custom docker images inside your projects container registry.

This is accessible via - https://gitlab.com/<id>/<project>/container_registry

The docker image I prebuilt just has the Firebase CLI installed.

This is used to automate Firebase deployments.

Here is the rather basic Dockerfile used to build the docker image.

FROM alpine:latest
RUN apk add --no-cache --update npm bash ca-certificates && npm install firebase-tools -g

To save the docker image to your projects container registry follow these steps.

  1. Login to your project’s container registry
docker login registry.gitlab.com
  1. Build the image
docker build -t registry.gitlab.com/<id>/<project>:/<tag> .
  1. Push the image to the registry
docker push registry.gitlab.com/<id>/<project>:<tag>

Once pushed you should the built image uploaded to your container registry.

A custom wrapper shell script

The shell script is to call the Firebase CLI via the command line.

This sets the environment variable to the Firebase authentication token.

This authenticates any subsequent firebase CLI calls.

export FIREBASE_TOKEN="$TOKEN"

The script calls the following Firebase CLI commands.

This tells the Firebase CLI to use a specific project

firebase use --add "$FIREBASE_PROJECT"

Displays a list of Firebase apps

firebase apps:list

Sets the hosting target (if you have more than 1 app hosted in Firebase this locks the deployment to that specific target)

firebase target:apply hosting app "$FIREBASE_APP"

Finally attempts the deployment (using a message called ‘GitLab CI Deployment’ which will be displayed in your Firebase console).

firebase deploy -m "GitLab CI Deployment"

When you run the script with 0 arguments.

It prints the following.

./firebase-ci.sh

──────────────────────────────────────────
 Firebase deployment
──────────────────────────────────────────
✔ Firebase version = 8.2.0
Usage: ./firebase-ci.sh [path to site directory] [token] [project] [app]

More details on those arguments are mentioned later on in this post.

Pipeline base image

image: docker:latest

We specify the base docker image here to ensure we can use docker in docker.

As each stage of the pipeline runs in a docker container.

We need a way to call the command docker ...

Pipeline variables

variables:
  DOCKER_DRIVER: overlay
  IMAGE_NAME: <name of the custom docker image>
  IMAGE_VERSION: <version of the custom docker image>
  GROUP: <your gitlab id, typically your username>
  PROJECT: <the name of the project>

These variables are used throughout the rest of the pipeline. These are not secret variables and can be hardcoded inside the pipeline.

Note. you can also define these variables through the GitLab CI/CD settings too.

Services definition

services:
  - name: docker:dind
    alias: docker

Services allows you to use another Docker image that is run during your job and is linked to the Docker image that the image keyword defines.

Without explicitly defining the alias docker you would not be able to use the command else where in any of the stages in the pipeline.

Pipeline stages

The CI pipeline goes through 3 basic stages.

  1. deploy
  2. test
  3. post

Stage 1 - Deploy

We go straight to the deployment, since we are not building anything.

deploy_hosting:
  stage: deploy
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

  script:
    - 'export SHARED_PATH="$(dirname ${CI_PROJECT_DIR})/shared"'
    - mkdir -p ${SHARED_PATH}
    - cp -r $(pwd)/debugthis ${SHARED_PATH}
    - cp -r $(pwd)/bin ${SHARED_PATH}
    - docker pull $CI_REGISTRY/$GROUP/$PROJECT:$IMAGE_VERSION
    - docker run -v ${SHARED_PATH}:/firebase --rm $CI_REGISTRY/$GROUP/$PROJECT:$IMAGE_VERSION /firebase/bin/firebase-ci.sh "firebase/debugthis" "$FIREBASE_TOKEN" "$FIREBASE_PROJECT" "$FIREBASE_APP"

Defining the keyword before_script ensures this is run before the code in the script section. Here, we want to login to the docker registry which stores our custom docker image we upload earlier on

Once the registry is logged into.

The script section of code runs.

export SHARED_PATH="$(dirname ${CI_PROJECT_DIR})/shared"
mkdir -p ${SHARED_PATH}

The git repository also stores all the code (html, css, javascript e.t.c) for the site So we need a way to mount this inside our custom image.

Note. You cannot simply mount a directory directly from the repository as it results in a blank directory being listed during the docker run command.

This may be as designed by GitLab.

So having a shared directory is a workaround.

A shared directory is created which acts as a staging directory which is later mounted inside the docker image during the docker run command.

cp -r $(pwd)/debugthis ${SHARED_PATH}
cp -r $(pwd)/bin ${SHARED_PATH}

In this example the site code is stored in a directory called ‘debugthis’ and the wrapper script for firebase is stored in ‘bin’

Copy these 2 directory’s into the shared directory.

docker pull $CI_REGISTRY/$GROUP/$PROJECT:$IMAGE_VERSION

Pull the docker image from your project container registry

docker run -v ${SHARED_PATH}:/firebase --rm $CI_REGISTRY/$GROUP/$PROJECT:$IMAGE_VERSION /firebase/bin/firebase-ci.sh "firebase/debugthis" "$FIREBASE_TOKEN" "$FIREBASE_PROJECT" "$FIREBASE_APP"

This part does the actual deployment.

  1. Runs the docker image
  2. Mounts the shared directory
  3. Calls the firebase-ci.sh script passing in the following arguments:
    • “firebase/debugthis” > This is the directory where your Hugo site code is stored
    • FIREBASE_TOKEN > Your firebase authentication token
    • FIREBASE_PROJECT > The name of the firebase project
    • FIREBASE_APP > The name of the firebase app

Stage 2 - Test

Once deployment is completed the test stage runs.

stage: test
  before_script:
    - apk add curl
  script:
    - 'export STATUS_CODE=`curl -o /dev/null -s -w "%{http_code}\n" https://${FIREBASE_PROJECT}.web.app`'
    - if [[ "${STATUS_CODE}" == 200 ]]; then echo "Firebase hosted app status = 200 (OK)"; exit 0; else echo "Firebase hosted app status = ${STATUS_CODE}"; exit 1; fi

All this does is install curl, then run curl to check the http status code.

200 = OK anything else fail the deployment pipeline.

Stage 3 - .post

This is guaranteed to run at as the last stage of the pipeline.

docker logout $CI_REGISTRY

Additional pipeline variables

The values of these variables (CI_REGISTRY_PASSWORD, FIREBASE_APP, FIREBASE_PROJECT, FIREBASE_TOKEN) are deemed secret and hence added in via the GitLab CI/Settings page.

This page allows you to mask the values of the variables which you shouldn’t commit into any SCM system.

Pipeline when run

Jobs can be monitored under your GitLab Project > CI/CD > Jobs

This is accessible under your GitLab Project > CI/CD > Pipelines

Skipping the Pipeline

If you want to skip the Pipeline from running.

Add ci skip or skip ci, using any capitalization, into the git commit message.

The Pipeline will then be skipped.

Custom shell script in GitLab

The script is available - here

Last updated on 21 May 2020
Published on 21 May 2020