Setup GitLab Review Apps with Eleventy
GitLab Pages provide an easy means of deploying a site hosted on GitLab, but GitLab does not provide support for creating Review Apps for a Pages site. This post outlines a reusable technique to work around that and setup Review Apps with Eleventy to enable creation of a unique, browsable instance of a site with the changes in a merge request.
Overview of GitLab review apps #
GitLab Review Apps allow the creation of temporary environments deploying the code changes in a merge request. Once configured, a new environment is created for each merge request, allowing review of a running version of the application. A link to that application is provided in the merge request widget to simplify access. That temporary environment is destroyed after a specified time period, or when the merge request is merged.
Currently, GitLab only allows a single Pages site and doesn't support the deployment of Review Apps for Pages, although there is an open issue to enable that capability in the future. Until that is implemented, this technique allows adding that capability for Eleventy sites.
GitLab pages setup #
To start with a common foundation, a job to deploy the site to GitLab Pages is
configured. The requirements to deploy a Pages site are a job in the pipeline
named pages that saves the site as artifacts in the public/ directory. This
job builds on that with a few additional capabilities.
- The jobs uses the
node:lts-alpinecontainer image, a lightweight image capable of running Node projects. - The
rulesare set to only deploy the Pages site for pipelines on the default branch. - A variable
BUILD_TYPEis added to denote this is apagesbuild. This is used in the eleventy configuration to control aspects of the Eleventy build. - The job sets up a production
environment, which enables the built-in GitLab features for tracking Pages deployments. Theurlshould be set to the URL of the final site. - The
scriptinstalls all dependencies and runs the npmbuildscript, assuming this builds the Eleventy site (for examplenpx @11ty/eleventy). These steps are included here to have a complete job definition, but depending on the pipeline those actions could be done in another job and its artifacts used in thepagesjob.
The complete pages job is shown below.
pages:
image: node:lts-alpine
stage: deploy
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
variables:
BUILD_TYPE: pages
environment:
name: production
deployment_tier: production
url: https://<site_url>/
script:
- npm ci
- npm run build
artifacts:
paths:
- public
Review apps setup #
There are several steps to configure Review Apps, including enabling Review Apps for the project, setting up the CI job, and some Eleventy configuration updates.
Enabling review apps #
To configure Review Apps they must be enabled for the project, which can be done per the instructions in the GitLab documentation.
The job artifacts workaround for review apps #
Without a dedicated means of deploying a Review App for a GitLab Pages site, the
workaround used here relies on the fact that CI job artifacts are actually
served by the GitLab Pages server with a URL following the format
https://<namespace>.<pages_domain>/-/<project>/-/jobs/<job_id>/artifacts/<file path>.
As an example, for this site's
GitLab project that URL
would be something like
https://aarongoldenthal.gitlab.io/-/aarongoldenthal/-/jobs/1234567890/artifacts/public/index.html
(based on the job ID).
For an Eleventy site, the initial URL should be the index.html file in the
project root directory. For consistency with the pages job, it's assumed that
the project is in the public/ directory, so the <file path> should be
public/index.html. The other parameters - <namespace>, <pages_domain>,
<project>, and <job_id> - can all be taken from
GitLab predefined variables,
which provides the most generic job definition allowing it to be re-used in
multiple projects on either gitlab.com or a self-hosted GitLab instance.
The differences from the pages job are:
- The
rulesare set to only run on merge request pipelines. If merge request pipelines are not being used, details are also provided below for using branch pipelines instead. - For a Review App, a unique
environment:namemust be set up. In this case theCI_COMMIT_REF_SLUGis used, which is the branch name. This allows multiple simultaneous Review Apps, but only one per branch. Theurlis set to the format described previously.
The complete pages_review_app job is shown below.
pages_review_app:
image: node:lts-alpine
stage: deploy
rules:
# If not using MR pipelines this can be set to run on branch pipelines
# that are not the default branch with:
# if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
- if: $CI_MERGE_REQUEST_ID
variables:
BUILD_TYPE: pages
environment:
name: review/$CI_COMMIT_REF_SLUG
deployment_tier: testing
url: https://$CI_PROJECT_ROOT_NAMESPACE.$CI_PAGES_DOMAIN/-/$CI_PROJECT_NAME/-/jobs/$CI_JOB_ID/artifacts/public/index.html
script:
- npm ci
- npm run build
artifacts:
paths:
- public
At this point running a merge request pipeline exposes a Review App, which opens the home page, but there are two issues that still need to be addressed:
- The home page opens properly with the Review App, but any root-relative path
references are incorrect. They are pointing to locations relative to
/, but with this URL format the/directory is actually thepublic/directory, which is nested 7 directories deep. This affects any root-relative paths in<a>,<img>,<link>,<script>or other tags. - Links between pages point to directories, expecting the web server to serve default pages. In the case of artifacts this does not occur, so a 404 response is returned.
Both of these issues are addressed in the following sections.
Fixing path references with eleventy configuration #
To resolve the root-relative path issues, the Eleventy configuration needs to be
updated to dynamically set the pathPrefix property. This updates all of the
URLs to reflect the site being deployed in a sub-directory. For test or
production (i.e GitLab Pages) builds, the default / path is used, but it's set
to the appropriate directory for Review Apps.
To simplify working with
GitLab predefined variables
the gitlab-ci-env package is
used in this example. This package returns an object with all GitLab predefined
variable values, hierarchically organized by context, which can simplify cases
where a lot of predefined variables are used. If this is not desired, see the
package documentation for a complete mapping of properties to the actual
predefined environment variable names.
The buildType uses the previously specified BUILD_TYPE variable. If this is
not specified, it defaults to test, intended to be a configuration used for
other testing. The isReviewApp value is set to true if the build is run in CI
in a merge request pipeline, which matches the preceding CI job definition. In
this case these values are only used to set the pathPrefix, but could be
exposed for use in other places (for example excluding analytics code in Review
Apps).
The pathPrefix is finally set using these two values. If the buildType is
not pages, or if isReviewApp is false, the default path is used. Otherwise,
this is a build for a Review App and the path prefix as specified in the
pages_review_app job is used. The pathPrefix finally needs to be specified
in the object returned from the Eleventy configuration file.
As noted previously, feature branch pipelines could be used instead of merge request pipelines. In that case the logic for
isReviewAppshould be updated to match thepages_review_apprules.In this case the
outputdirectory is set topublicas noted in the preceding CI jobs. This can be set to other values, but the resulting site output directory must be copied topublicin both Pages CI jobs to be collected as CI artifacts.
The complete Eleventy configuration updates are shown below.
const gitlabEnv = require('gitlab-ci-env');
const defaultBuildType = 'test';
const defaultPathPrefix = '/';
const buildType = process.env.BUILD_TYPE || defaultBuildType;
const isReviewApp = gitlabEnv.ci.isCI && gitlabEnv.ci.mergeRequest.id;
const pathPrefix =
buildType === 'pages' && isReviewApp
? `/-/${gitlabEnv.ci.project.name}/-/jobs/${gitlabEnv.ci.job.id}/artifacts/public/`
: defaultPathPrefix;
module.exports = function (eleventyConfig) {
...
return {
pathPrefix,
...
dir: {
...
output: 'public'
}
};
};
Fixing links within the site #
The last issue to resolve is that links within the site point to directories,
and GitLab does not serve the default index.html files. To resolve this, the
output HTML files from the Eleventy build are post-processed to update the
links.
The find command is used
to search the ./public directory (find ./public) for any files (-type f)
whose name matches *.html (-name "*.html"). Then a command is executed on
each file (-exec <command> "{}" +).
The sed command is used to
update the links. The -i argument makes the changes to the files in place. The
-E argument allows the use of extended regular expressions, which provides
more consistent behavior of ? and + (in this case they must be escaped to be
literal values, otherwise they behave as special characters). The regular
expression finds any <a> element with an href value with a root-relative
path (that is, starting with /), which includes the public directory, and an
optional subdirectory. It also captures any other attributes preceding or
following the href attribute. The sed command then inserts index.html
before the href closing ".
For a more detailed explanation of the complete regular expression a great tool is regex101, which breaks it down in detail and allows interactive testing. See this link for this specific regular expression and some examples.
This command is run in after_script so that the same script can be re-used
for all Pages jobs.
pages_review_app:
...
after_script:
- >
find ./public -type f -name "*.html" -exec
sed -i -E 's/(<a[^>]*href="\/.+\/public\/(.*\/)?)("[^>]*>)/\1index.html\3/' "{}" +
Refactoring the pages jobs #
Looking at both complete jobs there are a lot of common elements, so the final
jobs below extract those common components to a template .pages which is then
extended for both the pages and pages_review_app jobs.
.pages:
image: node:lts-alpine
stage: deploy
variables:
BUILD_TYPE: pages
script:
- npm ci
- npm run build
artifacts:
paths:
- public
pages:
extends: .pages
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
environment:
name: production
deployment_tier: production
url: https://<site_url>/
pages_review_app:
extends: .pages
rules:
- if: $CI_MERGE_REQUEST_ID
environment:
name: review/$CI_COMMIT_REF_SLUG
deployment_tier: testing
url: https://$CI_PROJECT_ROOT_NAMESPACE.$CI_PAGES_DOMAIN/-/$CI_PROJECT_NAME/-/jobs/$CI_JOB_ID/artifacts/public/index.html
after_script:
- >
find ./public -type f -name "*.html" -exec sed -i -E
's/(<a[^>]*href="\/.+\/public\/(.*\/)?)("[^>]*>)/\1index.html\3/' "{}" +
Aaron Goldenthal