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.
- Login to your project’s container registry
docker login registry.gitlab.com
- Build the image
docker build -t registry.gitlab.com/<id>/<project>:/<tag> .
- 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.
- deploy
- test
- 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.
- Runs the docker image
- Mounts the shared directory
- 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