Skip to main content

Make your CI workflow portable

Problems with existing CIs​

  • The CI code is sticky to the underlying CI infrastructure.
  • It's hard to migrate the CI from a runner to another.
  • The CI syntax is different with each CI runner.
  • Most CI workflows are represented using YAML.

Implement the CI workflow using dagger solves all of those problems.

Benefits of defining your CI workflow in Dagger​

  • Your CI only runs dagger. It moves all the CI logic from the non-portable CI syntax, to a dagger plan, that runs the same way everywhere.
  • Develop and run your CI workflow locally. No need to create a Pull Request to trigger CI, you can run the workflow locally. Since dagger workflows are containerized, you can expect the same results no matter where the workflow is executed.
  • Write once, Run anywhere. The same workflow can run in any CI, goodbye vendor lock in.
  • Blazing Fast: Dagger will automatically build an optimized execution graph, completing the job as fast as possible. Spend less time staring at CI to complete and reduce costs.
  • Effortless Cache Optimizations. Dagger will automatically re-use cached execution results if no changes are detected. Made a change to the frontend? The backend won't be built again.
  • Composable & Reusable. Define re-usable steps and share them across all your projects. Or with the world. Dagger ships with dozens of re-usable components

Example​

This example illustrates how to use Dagger to test and lint a Go project. The use of Go is irrelevant and the example can be easily adapted to other languages.

From the project repository, create a file named ci/main.cue and add the following configuration to it.

ci/main.cue
package main

import (
"alpha.dagger.io/dagger"
"alpha.dagger.io/os"
"alpha.dagger.io/docker"
)

// Source directory of the repository. We'll connect this from the CLI using `dagger input`
source: dagger.#Artifact

// Here we define a test phase.
// We're using `os.#Container`, a built-in package that will spawn a container
// We're also using `docker.#Pull` to execute the container from a Docker image
// coming from a registry.
test: os.#Container & {
image: docker.#Pull & {
from: "golang:1.16-alpine"
}
mount: "/app": from: source
command: "go test -v ./..."
dir: "/app"
}

// Likewise, here we define a lint phase.
lint: os.#Container & {
image: docker.#Pull & {
from: "golangci/golangci-lint:v1.39.0"
}
mount: "/app": from: source
command: "golangci-lint run -v"
dir: "/app"
}

The configuration above defines:

  • source code of the project. More on this later.
  • test task which executes go test inside the source artifact using the golang Docker image
  • lint task which executes golangci-lint inside the source artifact using the golangci-lint Docker image.

Before we can execute the configuration, we need to set up the Dagger project and environment.

# Initialize a dagger project at the root of your project
dagger init

# Create a CI environment using the CUE files in the `./ci` directory
dagger new ci -p ./ci

# Link the `source` artifact defined in the configuration with the project
# source code.
dagger input dir source .

Next, bring up the CI environment (e.g. execute the CI configuration):

$ dagger up
# ...
7:15PM INF test | computing environment=ci
7:15PM INF lint | computing environment=ci
# ...

Since test and lint do not depend on each other, they are executed in parallel.

Running dagger up a second time will return almost immediately: since no changes were made to source, Dagger will re-use the cached results for both test nor lint.

Integrating with CI​

All it takes to execute a Dagger workflow in CI is to run dagger up.

We provide a GitHub Actions to make integration with GitHub easier.

Monorepos​

Dagger workflows scale really well with CI complexity.

The following example illustrates how to test two different projects in the same repository: a Node frontend (located in ./frontend) along with a Go backend (located in ./backend).

From the project repository, update the file named ci/main.cue with the following configuration.

ci/main.cue
package main

import (
"alpha.dagger.io/dagger"
"alpha.dagger.io/os"
"alpha.dagger.io/docker"
)

// Source directory of the repository. We'll connect this from the CLI using `dagger input`
source: dagger.#Artifact

backend: {
// We use `os.#Dir` to grab a sub-directory of `source`
code: os.#Dir & {
from: source
path: "./backend"
}

test: os.#Container & {
image: docker.#Pull & {
from: "golang:1.16-alpine"
}
mount: "/app": from: code
command: "go test -v ./..."
dir: "/app"
}
}

frontend: {
// We use `os.#Dir` to grab a sub-directory of `source`
code: os.#Dir & {
from: source
path: "./frontend"
}

test: os.#Container & {
image: docker.#Pull & {
from: "node:16-alpine"
}
mount: "/app": from: code
command: """
# Install Dependencies
yarn

# Run the test script
yarn test
"""
dir: "/app"
}
}
tip

Larger configurations can be split into multiple files, for example backend.cue and frontend.cue

The configuration above defines a frontend and backend structure, each containing:

  • A code directory, defined as a subdirectory of source
  • A language specific test task using either yarn or go
$ dagger up
7:15PM INF frontend.test | computing environment=ci
7:15PM INF backend.test | computing environment=ci
7:15PM INF frontend.test | #8 0.370 yarn install v1.22.5 environment=ci
7:15PM INF frontend.test | #8 0.689 [1/4] Resolving packages... environment=ci
7:15PM INF frontend.test | #8 1.626 [2/4] Fetching packages... environment=ci
...

frontend.test and backend.test are running in parallel since there are no dependencies between each other.

If you were to make changes to the ./frontend directory, only frontend.test will be executed.