--- slug: /1012/ci --- # 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](../learn/1010-dev-cue-package.md) them across all your projects. Or with the world. Dagger ships with [dozens of re-usable components](../reference/README.md) ## 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. ```cue title="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. ```shell # 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): ```shell $ 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](../learn/1009-github-actions.md) 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. ```cue title="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` ```shell $ 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.