Continuous Integration / Continuous Development (CI/CD) - Github Edition

Introduction

Overview

Teaching: 5 min
Exercises: 0 min
Questions
  • What is continuous integration / continuous deployment?

Objectives
  • Understand why CI/CD is important

  • Learn what can be possible with CI/CD

  • Find resources to explore in more depthg

What is CI/CD?

Continuous Integration (CI) is the concept of literal continuous integration of code changes. That is, every time a contributor (student, colleague, random bystander) provides new changes to your codebase, those changes are tested to make sure they don’t “break” anything. Continuous Deployment (CD), similarly, is the literal continuous deployment of code changes. That means that, assuming the CI passes, you’d like to automatically deploy those changes.

Catch and Release

This is just like a fishing practice for conservation preservation!

via GIPHY

Breaking Changes

What does it even mean to “break” something? The idea of “breaking” something is pretty contextual. If you’re working on C++ code, then you probably want to make sure things compile and run without segfaulting at the bare minimum. If it’s python code, maybe you have some tests with pytest that you want to make sure pass (“exit successfully”). Or if you’re working on a paper draft, you might check for grammar, misspellings, and that the document compiles from LaTeX. Whatever the use-case is, integration is about catching breaking changes.

Deployment

Similarly, “deployment” can mean a lot of things. Perhaps you have a Curriculum Vitae (CV) that is automatically built from LaTeX and uploaded to your website. Another case is to release docker images of your framework that others depend on. Maybe it’s just uploading documentation. Or to even upload a new tag of your python package on pypi. Whatever the use-case is, deployment is about releasing changes.

Workflow Automation

CI/CD is the first step to automating your entire workflow. Imagine everything you do in order to run an analysis, or make some changes. Can you make a computer do it automatically? If so, do it! The less human work you do, the less risk of making human mistakes.

Anything you can do, a computer can do better

Any command you run on your computer can be equivalently run in a CI job.

Don’t just limit yourself to thinking of CI/CD as primarily for testing changes, but as one part of automating an entire development cycle. You can trigger notifications to your cellphone, fetch/download new data, execute cron jobs, and so much more. However, for the lessons you’ll be going through today and that you’ve just recently learned about python testing with pytest, we’ll focus primarily on setting up CI/CD with tests for code that you’ve written already.

CI/CD Solutions

Now, obviously, we’re not going to make our own fully-fledged CI/CD solution. Plenty exist in the wild today, and below are just a popular few:

For today’s lesson, we’ll only focus on GitHub’s solution (GitHub Actions). However, be aware that all the concepts you’ll be taught today: including pipelines, jobs, artifacts; all exist in other solutions by similar/different names. For example, GitLab supports two features known as caching and artifacts; but Travis doesn’t quite implement the same thing for caching and has no native support for artifacts. Therefore, while we don’t discourage you from trying out other solutions, there’s no “one size fits all” when designing your own CI/CD workflow.

Key Points

  • CI/CD is crucial for any reproducibility and testing

  • Take advantage of automation to reduce your workload


Exit (light) Codes

Overview

Teaching: 10 min
Exercises: 10 min
Questions
  • What is an exit code?

Objectives
  • Understand exit codes

  • How to print exit codes

  • How to set exit codes in a script

  • How to ignore exit codes

  • Create a script that terminates in success/error

As we enter the first episode of the Continuous Integration / Continuous Deployment (CI/CD) session, we learn how to exit.

Start by Exiting

How does a general task know whether or not a script finished correctly or not? You could parse (grep) the output:

> ls nonexistent-file
ls: cannot access 'nonexistent-file': No such file or directory

But every command outputs something differently. Instead, scripts also have an (invisible) exit code:

> ls nonexistent-file
> echo $?
ls: cannot access 'nonexistent-file': No such file or directory
2

The exit code is 2 indicating failure. What about on success? The exit code is 0 like so:

> echo
> echo $?

0

But this works for any command you run on the command line! For example, if I mistyped git status:

> git stauts
> echo $?
git: 'stauts' is not a git command. See 'git --help'.

The most similar command is
  status
1

and there, the exit code is non-zero – a failure.

Exit Code is not a Boolean

You’ve probably trained your intuition to think of 0 as falsy. However, exit code of 0 means there was no error. If you feel queasy about remembering this, imagine that the question asked is “Was there an error in executing the command?” 0 means “no” and non-zero (1, 2, …) means “yes”.

Try out some other commands on your system, and see what things look like.

Printing Exit Codes

As you’ve seen above, the exit code from the last executed command is stored in the $? environment variable. Accessing from a shell is easy echo $?. What about from python? There are many different ways depending on which library you use. Using similar examples above, we can use the getstatusoutput() call:

Snake Charming

To enter the Python interpreter, simply type python in your command line.

Once inside the Python interpreter, simply type exit() then press enter, to exit.

>>> from subprocess import getstatusoutput # for python2: from commands import getstatusoutput
>>> status,output=getstatusoutput('ls')
>>> status
0
>>> status,output=getstatusoutput('ls nonexistent-file')
>>> status
2

It may happen that this returns a different exit code than from the command line (indicating there’s some internal implementation in Python). All you need to be concerned with is that the exit code was non-zero (there was an error).

Setting Exit Codes

So now that we can get those exit codes, how can we set them? Let’s explore this in shell and in python.

Shell

Create a file called bash_exit.sh with the following content:

#!/usr/bin/env bash

if [ $1 == "hello" ]
then
  exit 0
else
  exit 59
fi

and then make it executable chmod +x bash_exit.sh. Now, try running it with ./bash_exit.sh hello and ./bash_exit.sh goodbye and see what those exit codes are.

Python

Create a file called python_exit.py with the following content:

#!/usr/bin/env python

import sys
if sys.argv[1] == "hello":
  sys.exit(0)
else:
  sys.exit(59)

and then make it executable chmod +x python_exit.py. Now, try running it with ./python_exit.py hello and ./python_exit.py goodbye and see what those exit codes are. Déjà vu?

Ignoring Exit Codes

To finish up this section, one thing you’ll notice sometimes (in ATLAS or CMS) is that a script you run doesn’t seem to respect exit codes. A notable example in ATLAS is the use of setupATLAS which returns non-zero exit status codes even though it runs successfully! This can be very annoying when you start development with the assumption that exit status codes are meaningful (such as with CI). In these cases, you’ll need to ignore the exit code. An easy way to do this is to execute a second command that always gives exit 0 if the first command doesn’t, like so:

> :(){ return 1; };: || echo ignore failure

The command_1 || command_2 operator means to execute command_2 only if command_1 has failed (non-zero exit code). Similarly, the command_1 && command_2 operator means to execute command_2 only if command_1 has succeeded. Try this out using one of scripts you made in the previous session:

> ./python_exit.py goodbye || echo ignore

What does that give you?

Overriding Exit Codes

It’s not really recommended to ‘hack’ the exit codes like this, but this example is provided so that you are aware of how to do it, if you ever run into this situation. Assume that scripts respect exit codes, until you run into one that does not.

Key Points

  • Exit codes are used to identify if a command or script executed with errors or not

  • Not everyone respects exit codes


Being Assertive

Overview

Teaching: 5 min
Exercises: 5 min
Questions
  • What happens with assertions in python?

Objectives
  • Understand how assertions in python correspond to exit codes

  • Figure out how pytest fits in

This is a relatively short section, but we need to connect some things you’ve learned from testing in python with exit codes.

Assert Your Tests

An assertion is a sanity-check carried out by the assert statement, useful when debugging code.

Let’s create a file called python_assert.py with the following content:

x = "hello"

assert x == "hello"

assert x == "goodbye"

and then run it with python python_assert.py.

What happens when an assertion fails in python?

Traceback (most recent call last):
  File "./python_assert.py", line 5, in <module>
    assert x == "goodbye"
AssertionError

An exception is raised, AssertionError. The nice thing about python is that all unhandled exceptions return a non-zero exit code. If an exit code is not set, this defaults to 1.

> echo $?
1

Ignoring what would cause the assertion to be True or False, we can see that assertions automatically indicate failure in a script.

What about pytest?

Pytest, thankfully, handles these assertion failures intuitively. To try this out quickly, go ahead and modify python_assert.py as follows:

x = "hello"

def test_assert_success():
    assert x == "hello"

def test_assert_failure():
    assert x == "goodbye"

Running pytest python_assert.py will produce an expected exit code depending on whether the test passed or failed. You should be able to confirm that the exit codes are useful here.

Key Points

  • You can do whatever you like in a test, as long as you return the right exit code

  • Pytest, and other test utilities, will propagate the exit codes correctly


Understanding Yet Another Markup Language

Overview

Teaching: 5 min
Exercises: 0 min
Questions
  • What is YAML?

Objectives
  • Learn about YAML

YAML

YAML is Yet Another Markup Language is a human-readable data-serialization language. It is commonly used for configuration files and in applications where data is being stored or transmitted. CI systems’ modus operandi typically rely on YAML for configuration. We’ll cover, briefly, some of the native types involved and what the structure looks like.

Tabs or Spaces?

We strongly suggest you use spaces for a YAML document. Indentation is done with one or more spaces, however two spaces is the unofficial standard commonly used.

Scalars

number-value: 42
floating-point-value: 3.141592
boolean-value: true # on, yes -- also work
# strings can be both 'single-quoted` and "double-quoted"
string-value: 'Bonjour'
unquoted-string: Hello World
hexadecimal: 0x12d4
scientific: 12.3015e+05
infinity: .inf
not-a-number: .NAN
null: ~
another-null: null
key with spaces: value
datetime: 2001-12-15T02:59:43.1Z
datetime_with_spaces: 2001-12-14 21:59:43.10 -5
date: 2002-12-14

Give your colons some breathing room

Notice that in the above list, all colons have a space afterwards, : . This is important for YAML parsing and is a common mistake.

Lists and Dictionaries

jedis:
  - Yoda
  - Qui-Gon Jinn
  - Obi-Wan Kenobi
  - Luke Skywalker

jedi:
  name: Obi-Wan Kenobi
  home-planet: Stewjon
  species: human
  master: Qui-Gon Jinn
  height: 1.82m

Inline-Syntax

Since YAML is a superset of JSON, you can also write JSON-style maps and sequences.

episodes: [1, 2, 3, 4, 5, 6, 7]
best-jedi: {name: Obi-Wan, side: light}

Multiline Strings

In YAML, there are two different ways to handle multiline strings. This is useful, for example, when you have a long code block that you want to format in a pretty way, but don’t want to impact the functionality of the underlying CI script. In these cases, multiline strings can help. For an interactive demonstration, you can visit https://yaml-multiline.info/.

Put simply, you have two operators you can use to determine whether to keep newlines (|, exactly how you wrote it) or to remove newlines (>, fold them in). Similarly, you can also choose whether you want a single newline at the end of the multiline string, multiple newlines at the end (+), or no newlines at the end (-). The below is a summary of some variations:

folded_no_ending_newline:
  script:
    - >-
      echo "foo" &&
      echo "bar" &&
      echo "baz"


    - echo "do something else"

unfolded_ending_single_newline:
  script:
    - |
      echo "foo" && \
      echo "bar" && \
      echo "baz"


    - echo "do something else"

Nested

requests:
  # first item of `requests` list is just a string
  - http://example.com/

  # second item of `requests` list is a dictionary
  - url: http://example.com/
    method: GET

Comments

Comments begin with a pound sign (#) and continue for the rest of the line:

# This is a full line comment
foo: bar # this is a comment, too

Key Points

  • YAML is a plain-text format, similar to JSON, useful for configuration

  • YAML is a superset of JSON, so it contains additional features like comments, while still supporting JSON.


YAML and GitHub Actions

Overview

Teaching: 5 min
Exercises: 0 min
Questions
  • What is the GitHub Actions specification?

Objectives
  • Learn where to find more details about everything for the GitHub Actions.

  • Understand the components of GitHub Actions YAML file.

GitHub Actions YAML

The GitHub Actions configurations are specified using YAML files stored in the.github/workflows/ directory. Here is an example of a YAML file:

name: example

on: push

jobs:
  job_1:
    runs-on: ubuntu-latest
    steps:
      - name: My first step
        run: echo This is the first step of my first job.

name: GitHub displays the names of your workflows on your repository’s actions page. If you omit name, GitHub sets it to the YAML file name.

on: Required. Specify the event that automatically triggers a workflow run. This example uses the push event, so that the jobs run every time someone pushes a change to the repository.
For more details, check this link.

jobs

Specify the job(s) to be run. Jobs run in parallel by default. To run jobs sequentially, you have to define dependencies on other jobs. We’ll cover this in a later section.

<job_id>: Each job must have an id to associate with the job, job_1 in the above example. The key job_id is a string that is unique to the jobs object. It must start with a letter or _ and contain only alphanumeric characters, -, or _. Its value is a map of the job’s configuration data.

  • runs-on: Required. Each job runs in an virtual environment (runner) specified by the key runs-on. Available environments can be found here.

  • steps: Specify sequence of tasks. A step is an individual task that can run commands (known as actions). Each step in a job executes on the same runner, allowing the actions in that job to share data with each other. If you do not provide a name, the step name will default to the text specified in the run command.

Further reading on jobs.

Overall Structure

Every single parameter we consider for all configurations are keys under jobs. The YAML is structured using job names. For example, we can define three jobs that run in parallel (more on parallel/serial later) with different sets of parameters.

name: <name of your workflow>

on: <event or list of events>

jobs:
  job_1:
    name: <name of the first job>
    runs-on: <type of machine to run the job on>
    steps:
      - name: <step 1>
        run: |
          <commands>
      - name: <step 2>
        run: |
          <commands>
  job_2:
    name: <name of the second job>
    runs-on: <type of machine to run the job on>
    steps:
      - name: <step 1>
        run: |
          <commands>
      - name: <step 2>
        run: |
          <commands>

Reference

The reference guide for all GitHub Actions pipeline configurations is found at workflow-syntax-for-github-actions. This contains all the different parameters you can assign to a job.

Key Points

  • You should bookmark the GitHub Actions reference. You’ll visit that page often.

  • Actions are standalone commands that are combined into steps to create a job.

  • Workflows are made up of one or more jobs and can be scheduled or triggered.


Coffee break!

Overview

Teaching: 0 min
Exercises: 15 min
Questions
  • Get up, stretch out, take a short break.

Objectives
  • Refresh your mind.

Key Points

  • You’ll be back.

  • They’re the jedi of the sea.


Hello CI World

Overview

Teaching: 5 min
Exercises: 10 min
Questions
  • How do I run a simple GitHub Actions job?

Objectives
  • Add CI/CD to your project.

Adding CI/CD to a project

The first thing we’ll do is create a .github/workflows/main.yml file in the project.

cd virtual-pipelines-eventselection/
mkdir -p .github/workflows

Open .github/workflows/main.yml with your favorite editor and add the following

name: example
on: push
jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
      -run: echo hello world

Run GitHub Actions

Run on your computer

At this point we need the nektos/act tool.

You can run

act -l  # -l stands for list (list workflows)

What happened??? We can see that this failed because the YAML was invalid… (Error: yaml: line …)

We should fix this accondingly to the Error message. Note that act is not a YAML Validator but can help. Some YAML Validators: https://yamllint.readthedocs.io, https://codebeautify.org/yaml-validator, http://www.yamllint.com/, …

Let’s rerun:

act -l

This should print out the job name, i.e greeting:

ID          Stage  Name       
greeting    0      greeting

To run greeting do

act -j greeting  # -l stands for job (run job)
[example/greeting] 🚀  Start image=catthehacker/ubuntu:act-latest
[example/greeting]   🐳  docker run image=catthehacker/ubuntu:act-latest entrypoint=["/usr/bin/tail" "-f" "/dev/null"] cmd=[]
[example/greeting] ⭐  Run echo hello world
| hello world
[example/greeting]   ✅  Success - echo hello world

Run on GitHub

We’ve created the .github/workflows/main.yml file but it’s not yet on GitHub. Next step we’ll push these changes to GitHub so that it can run our job. Since we’re adding a new feature (Actions) to our project, we’ll work in a feature branch. This is just a human-friendly named branch to indicate that it’s adding a new feature.

cd virtual-pipelines-eventselection/
git checkout -b feature/add-actions
git add .github/workflows/main.yml
git commit -m "my first actions"
git push -u origin feature/add-actions

And that’s it! You’ve successfully run your CI/CD job and you can view the output. You just have to navigate to the GitHub webpage for the virtual-pipelines-eventselection project and hit Actions button, you will find details of your job (status, output,…).

GitHub Actions Page

From this page, click through until you can find the output for the successful job run which should look like the following CI/CD Hello World Success Output

Pull Request

Lastly, we’ll open up a pull request for this branch, since we plan to merge this back into master when we’re happy with the first iteration of the Actions.

Work In Progress?

If you expect to be working on a branch for a bit of time while you have a pull request open, it’s good etiquette to mark it as a Work-In-Progress (WIP). However this is not a built-in feature of GitHub, it requires to install it from GitHub Marketplace. Free for personal account.

Work In Progress

Key Points

  • Creating .github/workflows/main.yml is the first step to salvation.

  • Pipelines are made of jobs with steps.

  • ACT is especially useful to check and run GitHub Actions jobs (locally) before pushing changes.


Adding CI to Your Existing Code

Overview

Teaching: 5 min
Exercises: 10 min
Questions
  • I have code already in GitHub, how can I add CI to it?

Objectives
  • Learn how to get your CI/CD Runners to build your code

  • Try and see if the CI/CD can catch problems with our code.

Time To Skim

The Naive Attempt

As of right now, your .github/workflows/main.yml should look like

name: example
on: push
jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
      - run: echo hello world

Let’s go ahead and teach our CI to build our code. Let’s add another job (named build_skim) that runs in parallel for right now, and runs the compiler ROOT uses.
Note: ROOT is a open source framework uses in High Energy Physics.

Let’s give a try.

COMPILER=$(root-config --cxx)
$COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx

The compilation will result in an output binary called skim.

Adding a new job

How do we change the CI in order to add a new job that compiles our code?

Solution

jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
      - run: echo hello world

  build_skim:
    runs-on: ubuntu-latest
    steps:
      - name: build
        run: |
          COMPILER=$(root-config --cxx)
          $COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx

Check jobs

act -l
ID          Stage  Name        
build_skim  0      build_skim  
greeting    0      greeting

We can see 2 parallel jobs. Let’s commit the changes we made.

git add .github/workflows/main.yml
git commit -m "add build skim job"
git push -u origin feature/add-actions

Job failure root-config Job failure root-config details

No root-config?

Ok, so maybe we were a little naive here. Let’s start debugging locally.

We can reproduce this error:

act -j build_skim

You got this error when you tried to build

[example/build_skim] 🚀  Start image=catthehacker/ubuntu:act-latest
[example/build_skim]   🐳  docker run image=catthehacker/ubuntu:act-latest entrypoint=["/usr/bin/tail" "-f" "/dev/null"] cmd=[]
[example/build_skim] ⭐  Run COMPILER=$(root-config --cxx)
$COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx
| /github/workflow/0: line 1: root-config: command not found
[example/build_skim]   ❌  Failure - COMPILER=$(root-config --cxx)

Broken Build

What happened?

Answer

It turns out we had the wrong docker image for our build. If you look at the log, you’ll see

🚀  Start image=catthehacker/ubuntu:act-latest
  🐳  docker run image=catthehacker/ubuntu:act-latest entrypoint=["/usr/bin/tail" "-f" "/dev/null"] cmd=[]

How do we fix it? We need to define the image with ROOT installed.

Docker???

Don’t panic. You do not need to understand docker to be able to use it.

Let’s go ahead and update our .github/workflow/main.yml and fix it to use a versioned docker image that has root: rootproject/root-conda:6.18.04 from the rootproject/root-conda docker hub page.

runs-on: ubuntu-latest
container: rootproject/root-conda:6.18.04

If you run again act -j build_skim, you see

[example/build_skim] 🚀  Start image=rootproject/root-conda:6.18.04

Failed again???

What’s that?
error: skim.cxx: No such file or directory

Answer

It seems the job cannot access the repository. We need to instruct GitHub actions to checkout the repository.

steps:
   - name: checkout repository
     uses: actions/checkout@v2

Let’s go ahead and tell our CI to checkout the repository.

Still failed??? What the hell.

What happened?

Answer

It turns out we just forgot the include flags needed for compilation. If you look at the log, you’ll see

 | skim.cxx:11:10: fatal error: ROOT/RDataFrame.hxx: No such file or directory
 |  #include "ROOT/RDataFrame.hxx"
 |           ^~~~~~~~~~~~~~~~~~~~~
  #include "ROOT/RDataFrame.hxx"
           ^~~~~~~~~~~~~~~~~~~~~
 | compilation terminated.
 Error: exit with `FAILURE`: 1

How do we fix it? We just need to add another variable to add the flags at the end via $FLAGS defined as FLAGS=$(root-config --cflags --libs).

Ok, let’s go ahead and update our .github/workflow/main.yml again, and it better be fixed or so help me…

Building multiple versions

Great, so we finally got it working… CI/CD isn’t obviously powerful when you’re only building one thing. Let’s build both the version of the code we’re testing and also test that the latest ROOT image (rootproject/root-conda:latest) works with our code. Call this new job build_skim_latest.

Adding the build_skim_latest job

What does the .github/workflow/main.yml look like now?

Solution

jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
      - run: echo hello world

  build_skim:
    runs-on: ubuntu-latest
    container: rootproject/root-conda:6.18.04
    steps:
      - name: checkout repository
        uses: actions/checkout@v2

      - name: build
        run: |
          COMPILER=$(root-config --cxx)
          FLAGS=$(root-config --cflags --libs)
          $COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS

  build_skim_latest:
    runs-on: ubuntu-latest
    container: rootproject/root-conda:latest
    steps:
      - name: checkout repository
        uses: actions/checkout@v2

      - name: latest
        run: |
          COMPILER=$(root-config --cxx)
          FLAGS=$(root-config --cflags --libs)
          $COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS

We’re ready for a coffee break.

Key Points

  • Setting up CI/CD shouldn’t be mind-numbing

  • All defined jobs run in parallel by default


Matrix

Overview

Teaching: 5 min
Exercises: 10 min
Questions
  • How can we make job templates?

Objectives
  • Don’t Repeat Yourself (DRY)

  • Use a single job for multiple jobs

Matrix

From the previous lesson, we tried to build the code against two different ROOT images by adding an extra job:

jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
      - run: echo hello world

  build_skim:
    runs-on: ubuntu-latest
    container: rootproject/root-conda:6.18.04
    steps:
      - name: checkout repository
        uses: actions/checkout@v2
      - name: build
        run: |
          COMPILER=$(root-config --cxx)
          FLAGS=$(root-config --cflags --libs)
          $COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS

  build_skim_latest:
    runs-on: ubuntu-latest
    container: rootproject/root-conda:latest
    steps:
      - name: checkout repository
        uses: actions/checkout@v2
      - name: latest
        run: |
          COMPILER=$(root-config --cxx)
          FLAGS=$(root-config --cflags --libs)
          $COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS

Building a matrix across different versions

We could do better using matrix. The latter allows us to test the code against a combination of versions in a single job.

jobs:
  greeting:
   runs-on: ubuntu-latest
   steps:
     - run: echo hello world

 build_skim:
   runs-on: ubuntu-latest
   container: rootproject/root-conda:${{ matrix.version }}
   strategy:
     matrix:
       version: [6.18.04, latest]
   steps:
     - name: checkout repository
       uses: actions/checkout@v2

     - name: build
        run: |
          COMPILER=$(root-config --cxx)
          FLAGS=$(root-config --cflags --libs)
          $COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS

More details on matrix: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.

Let’s update our .github/workflow/main.yml and use act to run the job with matrix.

act -j build_skim
[example/build_skim-1] 🧪  Matrix: map[version:6.18.04]
[example/build_skim-1] 🚀  Start image=rootproject/root-conda:6.18.04
[example/build_skim-2] 🧪  Matrix: map[version:latest]
[example/build_skim-2] 🚀  Start image=rootproject/root-conda:latest
[example/build_skim-1]   🐳  docker run image=rootproject/root-conda:6.18.04 entrypoint=["/usr/bin/tail" "-f" "/dev/null"] cmd=[]
[example/build_skim-1]   🐳  docker cp src=/tmp/eventselection/. dst=/github/workspace
[example/build_skim-1] ⭐  Run checkout repository
[example/build_skim-1]   ✅  Success - checkout repository
[example/build_skim-1] ⭐  Run COMPILER=$(root-config --cxx)
FLAGS=$(root-config --cflags --libs)
$COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS
[example/build_skim-1]   ✅  Success - COMPILER=$(root-config --cxx)
FLAGS=$(root-config --cflags --libs)
$COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS
[example/build_skim-2]   🐳  docker run image=rootproject/root-conda:latest entrypoint=["/usr/bin/tail" "-f" "/dev/null"] cmd=[]
[example/build_skim-2]   🐳  docker cp src=/tmp/eventselection/. dst=/github/workspace
[example/build_skim-2] ⭐  Run checkout repository
[example/build_skim-2]   ✅  Success - checkout repository
[example/build_skim-2] ⭐  Run COMPILER=$(root-config --cxx)
FLAGS=$(root-config --cflags --libs)
$COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS
[example/build_skim-2]   ✅  Success - COMPILER=$(root-config --cxx)
FLAGS=$(root-config --cflags --libs)
$COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS

We can push the changes to GitHub and see how it will look like.

git add .github/workflows/main.yml
git commit -m "add multi jobs"
git push -u origin feature/add-ci

While the jobs are running, let’s imagine we don’t want our CI/CD to crash if that happens. We have to add continue-on-error: true to a job

runs-on: ubuntu-latest
continue-on-error: true

For the matrix case, Github Actions fails the entire workflow and stops all the running jobs if any of the jobs in the matrix fails. This can be prevented by using fail-fast: false key:value.

strategy:
  fail-fast: false

More details: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.

Allow a specific matrix job to fail

But what if we want to only allow the job with version set to latest to fail without failing the workflow run?

Solution

runs-on: ubuntu-latest
container: rootproject/root-conda:${{ matrix.version }}
continue-on-error: ${{ matrix.allow_failure }}
strategy:
  matrix:
    version: [6.18.04]
    allow_failure: [false]
    include:
      - version: latest
        allow_failure: true

Look how much cleaner you’ve made the code. You should now see that it’s pretty easy to start adding more build jobs for other images in a relatively clean way, as you’ve now abstracted the actual building from the definitions.

Key Points

  • Using matrix allows to test the code against a combination of versions.


Coffee break!

Overview

Teaching: 0 min
Exercises: 15 min
Questions
  • Get up, stretch out, take a short break.

Objectives
  • Refresh your mind.

Key Points

  • Stupid mistakes happen, but telling a computer to do what you mean versus what you say is hard


Dependent Jobs

Overview

Teaching: 5 min
Exercises: 5 min
Questions
  • How do you make some jobs run after other jobs?

Objectives
  • Run some jobs in serial.

Defining dependencies

From the last session, we’re starting with

jobs:
  greeting:
   runs-on: ubuntu-latest
   steps:
     - run: echo hello world

 build_skim:
   runs-on: ubuntu-latest
   container: rootproject/root-conda:${{ matrix.version }}
   strategy:
     matrix:
       version: [6.18.04, latest]
   steps:
     - name: checkout repository
       uses: actions/checkout@v2

     - name: build
       run: |
        COMPILER=$(root-config --cxx)
        FLAGS=$(root-config --cflags --libs)
        $COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS

We’re going to talk about another useful parameter needs.

Specify dependencies between jobs

The key-value needs: jobs or list of jobs allows you to specify dependencies between jobs in the order you define.
Example:

job2:
  needs: job1

job2 waits until job1 completes successfully. Further reading.

Dependent jobs

How to make build_skim job to run after greeting?

Solution

jobs:
 greeting:
   runs-on: ubuntu-latest
   steps:
     - run: echo hello world

build_skim:
  needs: greeting
  runs-on: ubuntu-latest
  container: rootproject/root-conda:${{ matrix.version }}
  strategy:
    matrix:
      version: [6.18.04, latest]
  steps:
    - name: checkout repository
      uses: actions/checkout@v2

    - name: build
      run: |
        COMPILER=$(root-config --cxx)
        FLAGS=$(root-config --cflags --libs)
        $COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS

If you do

act -l

you should see stages: build_skim will run after greeting

ID          Stage  Name        
greeting    0      greeting    
build_skim  1      build_skim

Let’s go ahead and add those changes and look at GitHub.

git add .github/workflows/main.yml
git commit -m "add dependent jobs"
git push -u origin feature/add-actions

CI/CD Pipeline Two Stages

Key Points

  • needs allows to specify dependencies between jobs in order to run them in serial.


A Skimmer Higgs

Overview

Teaching: 5 min
Exercises: 10 min
Questions
  • How can I run my skimming code in the GitHub Actions?

Objectives
  • Learn how to skim code and set up artifacts.

The First Naive Attempt

Let’s just attempt to try and get the code working as it is. Since it worked for us already locally, surely the CI/CD must be able to run it???

As a reminder of what we’ve ended with from the last session:

jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
      - run: echo hello world
 
  build_skim:
    needs: greeting
    runs-on: ubuntu-latest
    container: rootproject/root-conda:${{ matrix.version }}
    strategy:
      matrix:
        version: [6.18.04, latest]
    steps:
      - name: checkout repository
        uses: actions/checkout@v2

      - name: build
        run: |
          COMPILER=$(root-config --cxx)
          FLAGS=$(root-config --cflags --libs)
          $COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS

Since the skim binary is built, let’s see if we can run it. We need to add a job , with name skim.

skim is meant to process data (skimming) we are going to run on.

Let’s go ahead and figure out how to define a run job. Seems too easy to be true?

skim:
  needs: build_skim
  runs-on: ubuntu-latest
  container: rootproject/root-conda:6.18.04
  steps:
      - name: checkout repository
        uses: actions/checkout@v2

      - name: skim
        run: ./skim

Let’s do a quick check

act -l
ID          Stage  Name        
greeting    0      greeting    
build_skim  1      build_skim  
skim        2      skim

We now have 3 stages.

After you’ve added the skim job you can push your changes to GitHub:

git add .github/workflows/main.yml
git commit -m "add skim job"
git push -u origin feature/add-actions

Skim waiting

Skim waiting

Failed???

Let’s have a look at the log message

./skim: not found

We’re too naive

Ok, fine. That was way too easy. It seems we have a few issues to deal with. The skim binary in the build_skim job isn’t in the skim job by default. We need to use GitHub artifacts to copy over this from the right job.

Artifacts

Artifacts are used to upload (upload-artifact) and download (download-artifact) files and directories which should be attached to the job after this one has completed. That way it can share those files with another job in the same workflow.

More Reading

Passing data between two jobs in a workflow

job_1:
  - uses: actions/upload-artifact@v2
    with:
      name: <name>
      path: <file>
job_2:
  - uses: actions/download-artifact@v2
    with:
      name: <name>

Note that the artifact name should not contain any of the following characters ",:,<,>,|,*,?,\,/.

In order to take advantage of passing data between two jobs, one combines download-artifact with needs.

Combining download-artifact with needs

Let’s do it.

Solution

...
...
build_skim:
  needs: greeting
  runs-on: ubuntu-latest
  container: rootproject/root-conda:${{ matrix.version }}
  strategy:
    matrix:
      version: [6.18.04, latest]
  steps:
    - name: checkout repository
      uses: actions/checkout@v2

    - name: build
      run: |
        COMPILER=$(root-config --cxx)
        FLAGS=$(root-config --cflags --libs)
        $COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS

    - uses: actions/upload-artifact@v2
      with:
        name: skim${{ matrix.version }}
        path: skim

skim:
  needs: build_skim
  runs-on: ubuntu-latest
  container: rootproject/root-conda:6.18.04
  steps:
    - name: checkout repository
      uses: actions/checkout@v2

    - uses: actions/download-artifact@v2
      with:
        name: skim6.18.04

    - name: skim
      run: ./skim

What happened?

./skim: Permission denied

Permissions can be changed using the chmod command.

Solution

run: |
  chmod +x ./skim
  ./skim

From the log message, one can see that we are missing arguments.

Use executable with following arguments: ./skim input output cross_section integrated_luminosity scale

We will deal with that in the next lesson.

Key Points

  • Making jobs aware of each other is pretty easy.

  • Artifacts are pretty neat.

  • We’re too naive.


Getting into the Spy Game

Overview

Teaching: 5 min
Exercises: 10 min
Questions
  • How can I give my GitHub actions private information?

Objectives
  • Add custom environment variables

  • Learn how to give your CI/CD Runners access to private information

Recall

build_skim:
  needs: greeting
  runs-on: ubuntu-latest
  container: rootproject/root-conda:${{ matrix.version }}
  strategy:
    matrix:
      version: [6.18.04, latest]
  steps:
    - name: checkout repository
      uses: actions/checkout@v2

    - name: build
      run: |
        COMPILER=$(root-config --cxx)
        FLAGS=$(root-config --cflags --libs)
        $COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS

    - uses: actions/upload-artifact@v2
      with:
        name: skim${{ matrix.version }}
        path: skim
skim:
  needs: build_skim
  runs-on: ubuntu-latest
  container: rootproject/root-conda:6.18.04
  steps:
    - name: checkout repository
      uses: actions/checkout@v2

    - uses: actions/download-artifact@v2
      with:
        name: skim6.18.04

    - name: skim
      run: |
        chmod +x ./skim
       ./skim

In the previous lesson, we saw that the executable skim takes 5 arguments: input (remote data), output (processed data), cross-section, integrated luminosity, and scale.

Let’s consider the following values

input: root://eosuser.cern.ch//eos/user/g/gstark/AwesomeWorkshopFeb2020/GluGluToHToTauTau.root
output: skim_ggH.root
cross_section: 19.6
integrated_luminosity: 11467.0
scale: 0.1

Our YAML file should look like

...
 skim:
   needs: build_skim
   runs-on: ubuntu-latest
   container: rootproject/root-conda:6.18.04
   steps:
     - name: checkout repository
       uses: actions/checkout@v2

     - uses: actions/download-artifact@v2
       with:
         name: skim6.18.04

     - name: skim
       run: |
         chmod +x ./skim
         ./skim root://eosuser.cern.ch//eos/user/g/gstark/AwesomeWorkshopFeb2020/GluGluToHToTauTau.root skim_ggH.root 19.6 11467.0 0.1

What about the output?

>>> Process input: root://eosuser.cern.ch//eos/user/g/gstark/AwesomeWorkshopFeb2020/GluGluToHToTauTau.root
Error: n <TNetXNGFile::Open>: [ERROR] Server responded with an error: [3010] Unable to give access - user access restricted - unauthorized identity used ; Permission denied

Access Control

The data we’re using are on CERN User Storage (EOS). As a general rule, access to protected data should be authenticated, CERN can’t just grab it!. It means we need to give our GitHub Actions access to our data. CERN uses kinit for access control.

Anyhow, this is pretty much done by executing printf $USER_PASS | base64 -d | kinit $USER_NAME@CERN.CH assuming that we’ve set the corresponding environment variables by safely encoding them (printf "hunter42" | base64).

Running example

Sometimes you’ll run into a code example here that you might want to run locally but relies on variables you might not have set? Sure, simply do the following

USER_PASS=hunter42 USER_NAME=GoodWill printf $USER_PASS | base64 -d | kinit $USER_NAME

Base-64 encoding?

Sometimes you have a string that contains certain characters that would be interpreted incorrectly by GitHub Actions runners. In order to protect against that, you can safely base-64 encode the string, store it, and then decode it as part of the Actions job. This is entirely safe and recommended.

Encrypted secrets

We first have to store our sensitive information in GitHub:

  1. navigate to the main page of the repository.
  2. select Settings.
  3. in the left sidebar, go to Secrets and then New repository secret.
  4. type USER_NAME in the Name input box and add the secret value.
  5. similarly add USER_PASS as well.
  6. Click to save the variables.

DON’T PEEK

DON’T PEEK AT YOUR FRIEND’S SCREEN WHILE DOING THIS.

Naming your secrets

Note that there are some rules applied to secret names:

Access encrypted secrets

The secrets you’ve created are available to use in GitHub Actions workflows. GitHub allows to access them using secrets context: ${{ secrets.<secret name> }}.

e.g:

printf ${{ secrets.USER_PASS }} | base64 -d | kinit ${{ secrets.USER_NAME }}@CERN.CH

Further Reading

Adding Artifacts on Success

As it seems like we have a complete CI/CD that does physics - we should see what came out. We just need to add artifacts for the skim job. This is left as an exercise to you.

Adding Artifacts

Let’s add artifacts to our skim job to save the skim_ggH.root file. Let’s have the artifacts expire in a week instead.

Solution

...
skim:
   needs: build_skim
   runs-on: ubuntu-latest
   container: rootproject/root-conda:6.18.04
   steps:
     - name: checkout repository
       uses: actions/checkout@v2

     - uses: actions/download-artifact@v2
       with:
         name: skim6.18.04

     - name: access control
       run: printf ${{ secrets.USER_PASS }} | base64 -d | kinit ${{ secrets.USER_NAME }}@CERN.CH

     - name: skim
       run: |
         chmod +x ./skim
         ./skim root://eosuser.cern.ch//eos/user/g/gstark/AwesomeWorkshopFeb2020/GluGluToHToTauTau.root skim_ggH.root 19.6 11467.0 0.1

     - uses: actions/upload-artifact@v2
       with:
         name: skim_ggH
         path: skim_ggH.root

If you are not a CERN user, don’t worry. We have a backup solution for you!
CERN public storage: root://eospublic.cern.ch//eos/root-eos/HiggsTauTauReduced/GluGluToHToTauTau.root.

And this allows us to download artifacts from the successfully run job.

Key Points

  • Encrypted secrets in GitHub actions allow you to hide protected information from others who can see your code


Making Plots to Take Over The World

Overview

Teaching: 5 min
Exercises: 10 min
Questions
  • How do we make plots?

Objectives
  • Use everything you learned to make plots!

On Your Own

So in order to make plots, we just need to take the skimmed file skim_ggH.root and pass it through the histograms.py code that exists. This can be run with the following code

python histograms.py skim_ggH.root ggH hist_ggH.root

This needs to be added to your .github/workflows/main.yml which should look like the following:

jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
      - run: echo hello world
 
  build_skim:
    needs: greeting
    runs-on: ubuntu-latest
    container: rootproject/root-conda:${{ matrix.version }}
    strategy:
      matrix:
        version: [6.18.04, latest]
    steps:
      - name: checkout repository
        uses: actions/checkout@v2

      - name: build
        run: |
          COMPILER=$(root-config --cxx)
          FLAGS=$(root-config --cflags --libs)
          $COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS
	
      - uses: actions/upload-artifact@v2
        with:
          name: skim${{ matrix.version }}
          path: skim

  skim:
    needs: build_skim
    runs-on: ubuntu-latest
    container: rootproject/root-conda:6.18.04
    steps:
      - name: checkout repository
        uses: actions/checkout@v2

      - uses: actions/download-artifact@v2
        with:
          name: skim6.18.04

      - name: skim
        run: |
          chmod +x ./skim
         ./skim root://eospublic.cern.ch//eos/root-eos/HiggsTauTauReduced/GluGluToHToTauTau.root skim_ggH.root 19.6 11467.0 0.1

      - uses: actions/upload-artifact@v2
        with:
          name: skim_ggH
          path: skim_ggH.root

Adding Artifacts

So we need to do a two things:

  1. add a plot job
  2. save the output hist_ggH.root as an artifact

You know what? While you’re at it, why not delete the greeting job and multi versions job as well? There’s no need for it anymore 🙂.

Solution

...
...
...
 skim:
   needs: build_skim
   runs-on: ubuntu-latest
   container: rootproject/root-conda:6.18.04
   steps:
     - name: checkout repository
       uses: actions/checkout@v2

    - uses: actions/download-artifact@v2
      with:
        name: skim6.18.04

    - name: skim
      run: |
        chmod +x ./skim
        ./skim root://eospublic.cern.ch//eos/root-eos/HiggsTauTauReduced/GluGluToHToTauTau.root skim_ggH.root 19.6 11467.0 0.1

    - uses: actions/upload-artifact@v2
      with:
        name: skim_ggH
        path: skim_ggH.root

 plot:
   needs: skim
   runs-on: ubuntu-latest
   container: rootproject/root-conda:6.18.04
   steps:
     - name: checkout repository
       uses: actions/checkout@v2

    - uses: actions/download-artifact@v2
      with:
        name: skim_ggH

    - name: plot
      run: python histograms.py skim_ggH.root ggH hist_ggH.root

    - uses: actions/upload-artifact@v2
      with:
        name: histograms
        path: hist_ggH.root

Once we’re done, we should probably start thinking about how to test some of these outputs we’ve made. We now have a skimmed ggH ROOT file and a file of histograms of the skimmed ggH.

Are we testing anything?

Integration testing is actually testing that the scripts we have still run. So we are constantly testing as we go here which is nice. Additionally, there’s also continuous deployment because we’ve been making artifacts that are passed to other jobs. There are many ways to deploy the results of the code base, such as pushing to a web server and so on. Artifacts are one way to deploy.

Key Points

  • Another action, another job, another artifact.


Let's Actually Make A Test (For Real)

Overview

Teaching: 5 min
Exercises: 20 min
Questions
  • I’m out of questions.

Objectives
  • Actually add a test on the output of running physics

So at this point, I’m going to be very hands-off, and just explain what you will be doing. Here’s where you should be starting from:

...
...
...
 skim:
   needs: build_skim
   runs-on: ubuntu-latest
   container: rootproject/root-conda:6.18.04
   steps:
     - name: checkout repository
       uses: actions/checkout@v2

    - uses: actions/download-artifact@v2
      with:
        name: skim6.18.04

    - name: skim
      run: |
        chmod +x ./skim
        ./skim root://eospublic.cern.ch//eos/root-eos/HiggsTauTauReduced/GluGluToHToTauTau.root skim_ggH.root 19.6 11467.0 0.1

    - uses: actions/upload-artifact@v2
      with:
        name: skim_ggH
        path: skim_ggH.root

 plot:
   needs: skim
   runs-on: ubuntu-latest
   container: rootproject/root-conda:6.18.04
   steps:
     - name: checkout repository
       uses: actions/checkout@v2

    - uses: actions/download-artifact@v2
      with:
        name: skim_ggH

    - name: plot
      run: python histograms.py skim_ggH.root ggH hist_ggH.root

    - uses: actions/upload-artifact@v2
      with:
        name: histograms
        path: hist_ggH.root

Adding a regression test

  1. Add a test job, test_ggH that has the right dependencies
    • Note: ./skim needs to be updated to produce a skim_ggH.log (hint: ./skim .... > skim_ggH.log)
    • We also need the hist_ggH.root file produced by the plot job
  2. Create a directory called tests/ and make two python files in it named test_cutflow_ggH.py and test_plot_ggH.py that uses PyROOT and python3
    • you might find the following lines (below) helpful to set up the tests
  3. Write a few different tests of your choosing that tests (and asserts) something about hist_ggH.root. Some ideas are:
    • check the structure (does ggH_pt_1 exist?)
    • check that the integral of a histogram matches a value you expect
    • check that the bins of a histogram matches the values you expect
  4. Update your test_ggH job to execute the regression tests
  5. Try causing your CI/CD to fail on the test_ggH job

Done?

Once you’re happy with setting up the regression test, mark your merge request as ready by clicking the Resolve WIP Status button, and then merge it in to master.

Template for test_cutflow_ggH.py

import sys

logfile = open('skim_ggH.log', 'r')
lines = [line.rstrip() for line in logfile]

required_lines = [
   'Number of events: 47696',
   'Cross-section: 19.6',
   'Integrated luminosity: 11467',
   'Global scaling: 0.1',
   'Passes trigger: pass=3402       all=47696      -- eff=7.13 % cumulative eff=7.13 %',
   'nMuon > 0 : pass=3402       all=3402       -- eff=100.00 % cumulative eff=7.13 %',
   'nTau > 0  : pass=3401       all=3402       -- eff=99.97 % cumulative eff=7.13 %',
   'Event has good taus: pass=846        all=3401       -- eff=24.88 % cumulative eff=1.77 %',
   'Event has good muons: pass=813        all=846        -- eff=96.10 % cumulative eff=1.70 %',
   'Valid muon in selected pair: pass=813        all=813        -- eff=100.00 % cumulative eff=1.70 %',
   'Valid tau in selected pair: pass=813        all=813        -- eff=100.00 % cumulative eff=1.70 %',
]

print('\n'.join(lines))
for required_line in required_lines:
    if not required_line in lines:
        print(f'Did not find line in log file. {required_line}')
        sys.exit(1)

Template for test_plot_ggH.py

import sys
import ROOT

f = ROOT.TFile.Open('hist_ggH.root')
keys = [k.GetName() for k in f.GetListOfKeys()]

required_keys = ['ggH_pt_1', 'ggH_pt_2']

print('\n'.join(keys))
for required_key in required_keys:
    if not required_key in keys:
        print(f'Required key not found. {required_key}')
        sys.exit(1)

integral = f.ggH_pt_1.Integral()
if abs(integral - 222.88716647028923) > 0.0001:
    print(f'Integral of ggH_pt_1 is different: {integral}')
    sys.exit(1)

Key Points

  • This kind of test is a regression test, as we’re testing assuming the code up to this point was correct.

  • This is not a unit test. Unit tests would be testing individual pieces of the Framework code-base, or specific functionality you wrote into your algorithms.


Homework

Overview

Teaching: 0 min
Exercises: 30 min
Questions
  • If you have any, ask on Mattermost channel!

Objectives
  • Add more testing, perhaps to statistics.

Like the last section, I will simply explain what you need to do. After the previous section, you should have the following in .github/workflows/main.yml:

jobs:
  build_skim:
    runs-on: ubuntu-latest
    container: rootproject/root-conda:6.18.04
    steps:
      - name: checkout repository
        uses: actions/checkout@v2

      - name: build      
        run: |
          COMPILER=$(root-config --cxx)
          FLAGS=$(root-config --cflags --libs)
          $COMPILER -g -O3 -Wall -Wextra -Wpedantic -o skim skim.cxx $FLAGS        

      - uses: actions/upload-artifact@v2
        with:
          name: skim6.18.04
          path: skim

  skim:
    needs: build_skim
    runs-on: ubuntu-latest
    container: rootproject/root-conda:6.18.04
    steps:
      - name: checkout repository
        uses: actions/checkout@v2

     - uses: actions/download-artifact@v2
       with:
         name: skim6.18.04

     - name: skim
       run: |
         chmod +x ./skim
         ./skim root://eospublic.cern.ch//eos/root-eos/HiggsTauTauReduced/GluGluToHToTauTau.root skim_ggH.root 19.6 11467.0 0.1 > skim_ggH.log

      - uses: actions/upload-artifact@v2
        with:
          name: skim_ggH
          path: |
            skim_ggH.root
            skim_ggH.log
  
  plot:
    needs: build_skim
    runs-on: ubuntu-latest
    container: rootproject/root-conda:6.18.04
    steps:
      - name: checkout repository
        uses: actions/checkout@v2

     - uses: actions/download-artifact@v2
       with:
         name: skim_ggH

     - name: plot
       run: python histograms.py skim_ggH.root ggH hist_ggH.root

      - uses: actions/upload-artifact@v2
        with:
          name: histograms
          path: hist_ggH.root

  test:
    needs: plot
    runs-on: ubuntu-latest
    container: rootproject/root-conda:6.18.04
    steps:
      - name: checkout repository
        uses: actions/checkout@v2

      - name: Download from skim
        uses: actions/download-artifact@v2
        with:
          name: skim_ggH

      - name: Download from plot
        uses: actions/download-artifact@v2
        with:
          name: histograms

      - name: cutflow test
        run: python tests/test_cutflow_ggH.py

      - name: plot test
        run: python tests/test_plot_ggH.py

In your virtual-pipelines-eventselection repository, you need to:

  1. Add more tests
  2. Go wild!

Key Points

  • Use everything you’ve learned to write your own CI/CD!