CI at NERSC using GitLab
========================
For the majority of cases the self-hosted runners provided by *GitHub* will be
more than sufficient to test and maintain code stability for DESC repositories.
However, there are some pieces of DESC software that would benefit greatly from
having the ability to deploy a CI workflow directly to the *Cori* and
*Perlmutter* machines at `NERSC `__. This allows the
software to be tested more intensely within an HPC environment, gives access to
specific development tools at *NERSC*, and gives the ability to test the code
against the large datasets hosted at the facility.
As there is no way to link CI workflows using *GitHub Actions* to the *NERSC*
facilities directly, we require a bit of a workaround. Starting with our
``source`` repository on *GitHub*, we need to create three additional
supporting repositories at the `NERSC GitLab instance
`__ (where we have direct access to the *Cori* and
*Perlmutter* machines).
#. A ``mirror`` repository, which clones the contents of the *GitHub*
``source`` repository to a ``target`` repository at *GitLab*.
#. A ``target`` repository, which performs the actual CI using *GitLab*'s
builtin CI tools (similar to *GitHub Actions*).
#. A ``status`` repository, which reports the results of the CI workflow
performed at the ``target`` repository back to the *GitHub* ``source``
repository.
This means there is a bit of manual setup required in order to perform CI at
*NERSC* when starting from a *GitHub* repository, however once set up, the
process is fully automated.
.. figure:: ../../images/workflow_diagram.png
Figure 1: Schematic of the three stages of the process described above.
Here we go over the steps required to implement a CI workflow at *NERSC*
starting from a *GitHub* repository. For our `demo repositories'
`__ example workflow,
the goal is the same as before, to trigger the repositories' test suite when
changes to the repositories' codebase are made. The difference now being that
these tests will be performed directly on *Cori*, and not on a *GitHub
Actions*-hosted runner.
Much of this tutorial/example is templated from the `CI at NERSC documentation
`__ and the `CI at NERSC tutorial
`__, which we also recommend looking
at.
.. note:: You will need a *NERSC* account and access to the `NERSC GitLab
instance `__ before moving forward. See `here
`__
for details on how DESC members get an account at *NERSC*.
Getting set up
--------------
To start, you'll need to create three repositories at your *NERSC* *GitLab*
instance (replace ``desc-continuous-integration`` with the name of your
repository below):
#. A blank (*"New Project -> Create Blank Project"*) ``target`` repository
with the same name as the *GitHub* ``source`` repository (e.g,
``desc-continuous-integration``). This can be created in any
namespace/group. The eventual CI workflow is performed here, therefore
those needing access to the CI logs should have this repository visible to
them.
#. An imported (*"New Project -> Import Project -> GitLab Export*") ``mirror``
repository with the name ``mirror-desc-continuous-integration``. This
repository must be created in your private namespace. The *GitLab* export
file to upload is in the `demo repository
`__ under
``./examples/nersc_gitlab/repo_templates/``.
#. An imported (*"New Project -> Import Project -> GitLab Export*") ``status``
repository with the name ``status-desc-continuous-integration``. This
repository must be created in your private namespace. The *GitLab* export
file to upload is in the `demo repository
`__ under
``./examples/nersc_gitlab/repo_templates/``.
.. figure:: ../../images/create_blank_repo.png
:class: with-border
Figure 2: Example of creating a blank repository on *GitLab*.
.. figure:: ../../images/three_repos.png
:class: with-border
Figure 3: At the end you should have three repositories on *GitLab*.
For those interested in the inner workings of the ``mirror`` and ``status``
repositories, have a look at :ref:`nersc-ci-appendix` in the Appendix.
Personal Access tokens
^^^^^^^^^^^^^^^^^^^^^^
Personal access tokens (PATs) are an alternative to using passwords for
authentication to *GitHub* or *GitLab* when using the API or the command line.
We need to set up PATs between our repositories in order for them to
communicate securely through our CI pipeline.
#. In the *GitHub* ``source`` repository, in your user profile, go to *Settings
-> Developer settings -> Personal Access Tokens -> Generate New Token*. Name
the PAT "*NERSC* CI", tick "workflow", and generate the token. Copy the
generated PAT and add it as a CI/CD variable in the ``mirror`` repository
called ``MIRROR_SOURCE_PAT`` (*Settings -> CI/CD -> Variables*). Now add the
same PAT as a CI/CD variable to the ``status`` repository called
``STATUS_TARGET_PAT`` (make sure to tick "masked" when adding both
variables).
#. In the *GitLab* ``target`` repository, create a PAT by going to *Settings ->
Access Tokens*. Name the token "mirror-repo", chose an expiration date,
chose the role "Maintainer", and tick all four checkboxes. Generate the
token, and add it as a CI/CD variable in the ``mirror`` repository named
``MIRROR_TARGET_PAT``. Now add the same PAT as a CI/CD variable to the
``status`` repository called ``STATUS_SOURCE_PAT`` (again, make sure to tick
masked for each).
.. note:: The reason the ``mirror`` and ``status`` repositories should be
created in your private namespace, so only you have access, is to protect
the PATs stored within them. Do not share these tokens with anyone, this is
the equivalent of password sharing.
.. note:: Always mask PATs stored as CI/CD variables. This prevents them from
being displayed within the CI workflow output.
.. note:: PATs can have an expiration date, you may have to periodically create
new PATs.
Trigger tokens
^^^^^^^^^^^^^^
In order for our CI pipeline to work seamlessly behind the scenes, we want the
various CI workflows for each intermediate repository to trigger automatically
when the previous one completes. This is done through "Trigger Tokens", which
allow us to remotely trigger CI workflows within our repositories from an
external source.
#. In the ``mirror`` repository, go to *Settings -> CI/CD -> Pipeline triggers*
and create a trigger token with the description "trigger-from-github". Copy
the created trigger token and add it as a *Secret* called
``MIRROR_TRIGGER_TOKEN`` in the ``source`` repository on GitHub (*Settings
-> Secrets -> Actions*).
#. Go to the ``target`` repository and create a trigger token called
"trigger-from-mirror". Add this one to the ``mirror`` repository as a CI/CD
variable called ``TARGET_TRIGGER_TOKEN``.
#. Go to the ``status`` repository and create a trigger token called
"trigger-from-target". Add this one to the ``target`` repository as a CI/CD
variable called ``STATUS_TRIGGER_TOKEN``.
.. note:: Trigger tokens do not expire, but be sure to keep the variables masked.
Triggering the CI workflow pipeline from GitHub
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The final file to create is a *GitHub Actions* CI workflow to initiate the
pipeline in the ``source`` repository.
The ``ci_nersc_template.yml`` workflow in ``.github/workflows/`` from our our
`demo repository `__
gives a good example of how to do this. Note we need to pass all the
environment variables we will work with in the subsequent CI workflows of the
pipeline, which must be defined in the initial *GitHub Actions* workflow file.
Essentially, all we are doing is kick-starting the pipeline (in this example
manually) by initiating the ``mirror`` repositories CI workflow. You can
trigger the pipeline however you wish, however remember the examples in this
tutorial only work for a single chosen branch of the repository, ``main`` by
default, and that each trigger will run a full CI job at *NERSC*.
You must modify the repository URLs and *GitLab* project numbers (found under
*Settings -> General* in *GitLab*) in the template workflow to your own. You
don't have to modify anything below ``jobs:``.
.. literalinclude:: ../../../.github/workflows/ci_nersc_template.yml
:language: yaml
:linenos:
:caption: ci_nersc_template.yml
Building a GitLab CI workflow for your repository
-------------------------------------------------
Now that we are set up, we can think about how to implement a CI workflow at
*GitLab*. As mentioned previously, the CI workflows for GitLab are placed in a
file called ``.gitlab-ci.yml``, which is located in the root directory of your
``source`` repository. Here we will cover how to build a basic CI workflow
with GitLab, relating back to the GitHub Actions syntax we learned previously.
A more in-depth look at *GitLab*'s CI syntax can be found in `The Complete
GitLab CI Reference Guide `__.
.. literalinclude:: ../../../.gitlab-ci.yml
:language: yaml
:linenos:
:caption: .gitlab-ci.yml
This is the *GitLab* CI workflow for our example repository that will
eventually get run by the ``target`` repository on the *NERSC* *GitLab*
instance. There are many similarities with the *GitHub Actions* CI workflow
syntax. We must tell the workflow how to be triggered (same as ``on:`` in
*GitHub Actions*), which is under ``workflow:`` ``rules:``. In this case we are
saying we only want the workflow to run if it was triggered via a trigger token
(see the reference guide linked above for a full list of workflow options). We
can set environment variables for the job, under ``variables:``. And then we
list our jobs.
As with *GitHub Actions*, we can have multiple jobs, each will run
independently and in parallel unless you deliberately link them. This workflow
only has one job, ``example``. Within each job, ``tags:`` is the same as
``runs-on:`` from *GitHub Actions*. Here we want to run on *Cori*. And
``script:`` contains the sequence of commands to execute for the job, the same
as ``steps:`` from *GitHub Actions*.
When running at *NERSC* you have to set the ``SCHEDULER_PARAMETERS`` environment
variable, which defines your queue preferences for the machine (which queue to
submit to, which project to charge, etc). For more details about this, and
other specifics of running CI at *NERSC*, see `here
`__. Example repositories from *NERSC*
for CI can also be found `here `__.
When everything has finished successfully, you should see the *NERSC* CI status
beside the commit on the *GitHub* repository main page. Clicking its "details"
will take you to the *GitLab* CI report for the job.
.. figure:: ../../images/nersc_success.png
:class: with-border
Figure 4: The *NERSC* tag is the reported status from the *NERSC* CI job.
.. note:: The ``SCHEDULER_PARAMETERS`` in the ``mirror`` and ``status``
repositories' `.gitlab-ci.yml` files are set to ``SCHEDULER_PARAMETERS: "-C
haswell -q debug -N1 -t 00:05:00"``. These jobs are extremely lightweight,
which is why they goto the ``debug`` queue, however you may have to change
this for your needs, for example to charge against a specific *NERSC*
project.
.. note:: If your package uses submodules for some of its dependencies, you
will have to add ``GIT_SUBMODULE_STRATEGY: recursive`` to the variables in
the ``target`` repositories ``.gitlab-ci.yml``.
Things to think about with CI at *NERSC*
----------------------------------------
.. note:: When deploying a CI workflow to *Cori* you are running code in the
same environment, with the same permissions, as if you were working on a
login node. Therefore things like ``$HOME`` refer to your real home
directory, and you need to be careful about what scope your give to your CI
workflows at *NERSC*. As the developer, you are responsible for the code that
is run, and you need to fully understand what is happening in the workflow
that you are implementing.
.. note:: You should not automate the mirroring of code you do not own. If you
must, only clone protected branches of repositories you do not own.
.. note:: Any code you mirror onto the *NERSC* GitLab instance must adhere to the
broader `NERSC user policies `__.