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-alpine
container image, a lightweight image capable of running Node projects. - The
rules
are set to only deploy the Pages site for pipelines on the default branch. - A variable
BUILD_TYPE
is added to denote this is apages
build. 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. Theurl
should be set to the URL of the final site. - The
script
installs all dependencies and runs the npmbuild
script, 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 thepages
job.
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
rules
are 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:name
must be set up. In this case theCI_COMMIT_REF_SLUG
is used, which is the branch name. This allows multiple simultaneous Review Apps, but only one per branch. Theurl
is 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
isReviewApp
should be updated to match thepages_review_app
rules.In this case the
output
directory is set topublic
as noted in the preceding CI jobs. This can be set to other values, but the resulting site output directory must be copied topublic
in 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
extend
ed 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/' "{}" +