TravisCI

From HerzbubeWiki
Jump to: navigation, search

This page is about Travis CI, the continuous integration (CI) service that is available free of charge to open source projects hosted on GitHub and other platforms.

Although it is very comprehensive, the Travis CI documentation in my opinion does a surprisingly bad job of explaining the structure of the configuration file .travis.yml. The tutorial page is extremely minimal and was not useful for me when I wanted to learn how to build a C++ project with a CMake-based build system. The C++ page on the other hand lost me after a relatively short time because I didn't understand a lot of the stuff written in there.

So after studying the documentation for an hour or two and still failing to cobble together a basic .travis.yml file, I decided that writing a new wiki page (this one) was in order.


References

  • Documentation main entry point: https://docs.travis-ci.com/
  • Travis CI Build Config Reference: https://config.travis-ci.com/
  • Travis CI Build Config Explorer: https://config.travis-ci.com/explore. Here you can copy & paste a config file example and you'll see how it is parsed and how the final list of build jobs looks like. This is an invaluable tool to make sense of the many incomprehensible examples in the documentation, or to quickly test whether a certain config snippet works as expected.


Basic principle

Shamelessly stolen from the Core Concepts for Beginners page:

When you run a build, Travis CI clones your GitHub repository into a brand-new virtual environment, and carries out a series of tasks to build and test your code. If one or more of those tasks fail, the build is considered broken. If none of the tasks fail, the build is considered passed and Travis CI can deploy your code to a web server or application host.

CI builds can also automate other parts of your delivery workflow. This means you can have jobs depend on each other with Build Stages, set up notifications, prepare deployments after builds and many other tasks.

Travis CI provides virtual environments for each of the three major operating systems:

  • Ubuntu Linux (representing all Unices)
  • macOS
  • Windows

Each environment is offered in several versions.


Glossary

Build 
A group of → jobs. Jobs within a build can be subgrouped into → stages.
Job 
An automated process that clones your repository into a virtual environment and then carries out commands in a sequence of predefined → phases.
Matrix 
Also called "job matrix" or "build matrix". This is the list of jobs that Travis generates when it parses the config file.
Phase 
A step within a → job. Travis defines which kind of phases there are and in which sequence they are executed. Examples: Install phase, Script phase, Deploy phase.
Stage 
A subgroup of → jobs within a → build. The jobs withhin a stage run in parallel. Stages within a build run sequentially.


The configuration file

File name and location

Travis CI expects a configuration file with a predefined name to exist in a project's root folder:

.travis.yml

The file format is YAML. Since I'm new to YAML, the next section has an introductory example.


YAML example

This is a generic YAML example that has nothing to do with Travis CI.

# Comments start with a hash ('#') character and continue to the end of the line.

# This begins a new document.
---

# This is a sequence, i.e. a list of values.
# A space must follow after the hyphen ('-') character.
- foo
- bar

# This is also a sequence (format similar to JSON).
[ foo, bar ]

# This is an associative array, i.e. a list of key-value pairs.
# A space must follow after the colon (':') character.
# The key-value pairs are grouped by their indentation. This is also called an "inline block".
# In this example we don't use indentation.
foo1: bar1
foo2: bar2

# This is also an associative array.
# A space must follow after the colon (':') character.
# The space after the comma (',') character is optional.
# The key-value pairs are grouped by the curly braces. This is also called an "inline block".
{ foo1: bar1, foo2: bar2 }

# This is a string that preserves newlines.
# Leading whitespace (= the indentation) of the first line is removed, as well as trailing whitespace.
foo : |
  The quick brown fox
  jumps over the lazy dog.

# This is a string that removes newlines.
# Newlines are converted to spaces, leading whitespace is removed.
foo : >
  The quick brown fox
  jumps over the lazy dog.

# This is a sequence (list) of key-value objects (associate arrays).
- { name: John Smith, age: 33 }
- name: Mary Smith
  age: 27
- [name, age]: [Rae Smith, 4]   # sequences as keys are supported

# This is a single key-value object (associative array) where each value is a sequence (list).
men: [John Smith, Bill Jones]
women:
  - Mary Smith
  - Susan Williams


Build trigger

Travis CI is triggered by a Git commit.

Travis CI uses the .travis.yml file from the branch containing the Git commit that triggered the build.

Although this is not stated explicitly in the docs, it appears that Travis CI has not yet cloned the repository at this point. This is a conclusion I make because of the following observations:

  • You can prevent a build from taking place by specifying which branches to build or not to build (whitelisting/blacklisting) in the .travis.yml.
  • You can prevent cloning by having a specific snippet in .travis.yml. See below for an example.


Prevent a build from taking place

You can specify for which branches a build should take place, or should be prevented, by whitelisting/blacklisting branches in .travis.yml. Travis CI invents new names for this and calls it "safelist a branch" or "blocklist a branch".

# Blacklist / blocklist
branches:
  except:
  - legacy
  - experimental

# Whitelist / safelist
branches:
  only:
  - master
  - stable

Notes:

  • If you use both a safelist and a blocklist, the safelist takes precedence.
  • By default, the gh-pages branch is not built unless you add it to the safelist. The gh-pages branch is a special branch on GitHub that is wholly unrelated to the rest of the Git repository and which consists of files that GitHub uses to populate a website for the project.
  • A branch name that is begins and ends with a slash ('/') is treated as a regular expression (Ruby dialect).


Cloning the repository

When a build is triggered, the first thing Travis CI usually does is clone the Git repository. This can be disabled with

git:
  clone: false

The default is to clone all submodules. This can be disabled with

git:
  submodules: false


The job matrix

Introduction

When Travis CI parses the config file it generates a job matrix from the content that it finds.

The term "matrix" is used because for certain keys their values form combinations of jobs that have to be executed. These keys are called "matrix expansion keys".

Personally I think of this "matrix" as simply a list of combinations, where each list entry is an element in the job matrix.

As an example let's have a look at a few combinations of operating systems and compilers:

Matrix view:
         |  clang                      gcc
---------+--------------------------------------------------
linux    |  Job 1 (linux x clang)      Job 2 (linux x gcc)
osx      |  Job 3 (osx x clang)        Job 4 (osx x gcc)
windows  |  Job 5 (windows x clang)    Job 6 (windows x gcc)

List view:
Job 1 (linux x clang)
Job 2 (linux x gcc)
Job 3 (osx x clang)
Job 4 (osx x gcc)
Job 5 (windows x clang)
Job 6 (windows x gcc)


Matrix expansion

In the following config example Travis CI implicitly combines the 3 values for os with the 2 values for compiler, resulting in a job matrix of 3x2. Travis CI in this case will execute 6 jobs, one for each of the combinations - just as we have seen in the previous introduction section.

This process of automatically generating combinations is called matrix expansion.

language: cpp
os:
  - linux
  - osx
  - windows
compiler:
  - clang
  - gcc


Explicit combinations

In the example in the previous section Travis CI performed the combinations itself, which is very convenient but can lead to more builds than expected or required. It is therefore possible to explicitly specify the combinations that are actually desired, using together the keys jobs and include.

For instance, on macOS we know that there is only one compiler, Clang, so it doesn't make sense to have a job for the macOS/GCC combination. Building on the example from the previous section, here you can see how to explicitly enumerate the remaining 5 combinations.

language: cpp
jobs:
  include:
    - name: Job 1
      os: linux
      compiler: clang
    - name: Job 2
      os: linux
      compiler: gcc
    - name: Job 3
      os: osx
      compiler: clang
    - name: Job 4
      os: windows
      compiler: clang
    - name: Job 5
      os: windows
      compiler: gcc

Notes:

  • We are giving the jobs names that will be displayed by Travis CI. This is optional.
  • Many of the examples in the Travis CI docs use the key matrix. This is just an alias for jobs.


Excluding combinations

Instead of specifying which jobs you want you can also specify which jobs you don't want. This is done by combining the keys jobs and exclude. This example results in exactly the same 5 jobs as the example in the previous section.

language: cpp
os:
  - linux
  - osx
  - windows
compiler:
  - clang
  - gcc
jobs:
  exclude:
    - os: osx
      compiler: gcc


Implicit and explicit combinations

You can have implicit combinations and explicitly specify additional combinations. This gives us a third way how to write a job matrix that does not build the macOS/GCC combination. The result is the same as in the examples in the previous two sections.

language: cpp
os:
  - linux
  - windows
compiler:
  - clang
  - gcc
jobs:
  include:
    - os: osx
      compiler: clang


Build stages

Introduction

A stage is a group of jobs that are allowed to run in parallel. If the job matrix contains multiple stages, the stages run sequentally one after another in the order in which they were defined.

A stage begins only if all jobs in the previous stage have completed successfully. If one job fails in one stage, all other jobs in the same stage will still complete, but all jobs in subsequent stages will be canceled and the build fails.


Basic config example

jobs:
  include:
    - stage: foo
      script: ./foo 1
    # The next job does not specify a stage name, so it will be part of stage "foo"
    - script: ./foo 2
    - stage: bar
      script: ./bar 1
    # The next job explicitly specifies a stage name
    - stage: bar
      script: ./bar 2


The default "test" stage

When Travis CI performs matrix expansion it assigns the generated jobs to a stage with the default name "test".

When you explicitly define a job and you don't specify a stage name, the job is assigned to the same stage as the previous job - this is something we have already seen in the previous section. If there is no previous job then the job is assigned to the default stage "test".

Finally, it's perfectly legal to explicitly assign a jobs to stage "test".

This example shows all 3 use cases.

language: cpp
os: osx
osx_image:
  - xcode11.3                # assign job to stage "test" via matrix expansion
  - xcode10.3                # ditto
jobs:
  include:
    - osx_image: xcode9.3    # assign job to stage "test" because there is no previous job
    - stage: test            # explicitly assign job to stage "test"
      osx_image: xcode8.3


Defining the order of stages

The stages key is used to define the order in which stages will run. Without explicit ordering the stages run in the order in which their jobs are defined.

One particular use case for defining a stage order is if you want a stage to run before the default stage "test".

stages:
  - compile
  - test
  - deploy


Conditional builds, stages and jobs

# Run the entire build only if a condition is met
if: branch = master

# Run a stage only if a condition is met
stages:
  - compile
  - test
  - name: deploy
    if: branch = master

# Generate two jobs via matrix expansion. This is used
# to demonstrate conditional exclude further down.
env:
  - ONE=one
  - TWO=two

jobs:
  include:
    # Run a job only if a condition is met
    - if: branch = master
      script: ./foo
exclude:
    # Exclude a job only if a condition is met
    - if: branch = master
      env: TWO=two          # this defines which job to exclude


Jobs

Phases

The following is a list of all phases in a job. Phases run in the order in which they appear in the list:

  • Install apt addons (optional)
  • Install cache components (optional)
  • before_install
  • install
  • before_script
  • script
  • before_cache (optional) (for cleaning up cache)
  • One of
    • after_success (when the build succeeds)
    • after_failure (when the build fails)
  • before_deploy (optional)
  • deploy (optional)
  • after_deploy (optional)
  • after_script

Notes:

  • Unexpectedly, the various "install" phases are not there to install the built product, but to install dependencies
  • The build breaks, i.e. fails, if any of the commands in the following phases has a non-zero exit code: before_install, install, before_script and script


Installing dependencies

Travis CI defines separate dependency management capabilities for each language. Refer to the language-specific page in the docs for details.

For C++ there is no default dependency management (because there is no dominant convention in the community how thhis should be done). The default install phase does nothing.


Building

The actual build is done in the script phase. Each language has its own default value for script. The default for C++ is:

script: ./configure && make && make test

To override the default you can specify your own build script:

script: ./scripts/build.sh

Or if you prefer it can be done inline, of course:

script:
  - mkdir build
  - cd build
  - cmake -DCMAKE_BUILD_TYPE=Release ..
  - cmake --build .
  - ctest
  - cmake --install . --prefix /path/to/destfolder


Deployment

TODO

One important note, quoting from the docs:

It is important to note that jobs do not share storage, as each job runs in a fresh VM or container. If your jobs need to share files (e.g., using build artifacts from the “Test” stage for deployment in the subsequent “Deploy” stage), you need to use an external storage mechanism.