--- stage: Verify group: Pipeline Authoring info: This page is maintained by Developer Relations, author @dnsmichi, see https://handbook.gitlab.com/handbook/marketing/developer-relations/developer-advocacy/content/#maintained-documentation title: CI/CD component examples --- {{< details >}} - Tier: Free, Premium, Ultimate - Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated {{< /details >}} ## Test a component Depending on a component's functionality, [testing the component](_index.md#test-the-component) might require additional files in the repository. For example, a component which lints, builds, and tests software in a specific programming language requires actual source code samples. You can have source code examples, configuration files, and similar in the same repository. For example, the Code Quality CI/CD component's has several [code samples for testing](https://gitlab.com/components/code-quality/-/tree/main/src). ### Example: Test a Rust language CI/CD component Depending on a component's functionality, [testing the component](_index.md#test-the-component) might require additional files in the repository. The following "hello world" example for the Rust programming language uses the `cargo` tool chain for simplicity: 1. Go to the CI/CD component root directory. 1. Initialize a new Rust project by using the `cargo init` command. ```shell cargo init ``` The command creates all required project files, including a `src/main.rs` "hello world" example. This step is sufficient to build the Rust source code in a component job with `cargo build`. ```plaintext tree . ├── Cargo.toml ├── LICENSE.md ├── README.md ├── src │ └── main.rs └── templates └── build.yml ``` 1. Ensure that the component has a job to build the Rust source code, for example, in `templates/build.yml`: ```yaml spec: inputs: stage: default: build description: 'Defines the build stage' rust_version: default: latest description: 'Specify the Rust version, use values from https://hub.docker.com/_/rust/tags Defaults to latest' --- "build-$[[ inputs.rust_version ]]": stage: $[[ inputs.stage ]] image: rust:$[[ inputs.rust_version ]] script: - cargo build --verbose ``` In this example: - The `stage` and `rust_version` inputs can be modified from their default values. The CI/CD job starts with a `build-` prefix and dynamically creates the name based on the `rust_version` input. The command `cargo build --verbose` compiles the Rust source code. 1. Test the component's `build` template in the project's `.gitlab-ci.yml` configuration file: ```yaml include: # include the component located in the current project from the current SHA - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/build@$CI_COMMIT_SHA inputs: stage: build stages: [build, test, release] ``` 1. For running tests and more, add additional functions and tests into the Rust code, and add a component template and job running `cargo test` in `templates/test.yml`. ```yaml spec: inputs: stage: default: test description: 'Defines the test stage' rust_version: default: latest description: 'Specify the Rust version, use values from https://hub.docker.com/_/rust/tags Defaults to latest' --- "test-$[[ inputs.rust_version ]]": stage: $[[ inputs.stage ]] image: rust:$[[ inputs.rust_version ]] script: - cargo test --verbose ``` 1. Test the additional job in the pipeline by including the `test` component template: ```yaml include: # include the component located in the current project from the current SHA - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/build@$CI_COMMIT_SHA inputs: stage: build - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/test@$CI_COMMIT_SHA inputs: stage: test stages: [build, test, release] ``` ## CI/CD component patterns This section provides practical examples of implementing common patterns in CI/CD components. ### Use boolean inputs to conditionally configure jobs You can compose jobs with two conditionals by combining `boolean` type inputs and [`extends`](../yaml/_index.md#extends) functionality. For example, to configure complex caching behavior with a `boolean` input: ```yaml spec: inputs: enable_special_caching: description: 'If set to `true` configures a complex caching behavior' type: boolean --- .my-component:enable_special_caching:false: extends: null .my-component:enable_special_caching:true: cache: policy: pull-push key: $CI_COMMIT_SHA paths: [...] my-job: extends: '.my-component:enable_special_caching:$[[ inputs.enable_special_caching ]]' script: ... # run some fancy tooling ``` This pattern works by passing the `enable_special_caching` input into the `extends` keyword of the job. Depending on whether `enable_special_caching` is `true` or `false`, the appropriate configuration is selected from the predefined hidden jobs (`.my-component:enable_special_caching:true` or `.my-component:enable_special_caching:false`). ### Use `options` to conditionally configure jobs You can compose jobs with multiple options, for behavior similar to `if` and `elseif` conditionals. Use the [`extends`](../yaml/_index.md#extends) with `string` type and multiple `options` for any number of conditions. For example, to configure complex caching behavior with 3 different options: ```yaml spec: inputs: cache_mode: description: Defines the caching mode to use for this component type: string options: - default - aggressive - relaxed --- .my-component:cache_mode:default: extends: null .my-component:cache_mode:aggressive: cache: policy: push key: $CI_COMMIT_SHA paths: ['*/**'] .my-component:cache_mode:relaxed: cache: policy: pull-push key: $CI_COMMIT_BRANCH paths: ['bin/*'] my-job: extends: '.my-component:cache_mode:$[[ inputs.cache_mode ]]' script: ... # run some fancy tooling ``` In this example, `cache_mode` input offers `default`, `aggressive`, and `relaxed` options, each corresponding to a different hidden job. By extending the component job with `extends: '.my-component:cache_mode:$[[ inputs.cache_mode ]]'`, the job dynamically inherits the correct caching configuration based on the selected option. ## CI/CD component migration examples This section shows practical examples of migrating CI/CD templates and pipeline configuration into reusable CI/CD components. ### CI/CD component migration example: Go A complete pipeline for the software development lifecycle can be composed with multiple jobs and stages. CI/CD templates for programming languages may provide multiple jobs in a single template file. As a practice, the following Go CI/CD template should be migrated. ```yaml default: image: golang:latest stages: - test - build - deploy format: stage: test script: - go fmt $(go list ./... | grep -v /vendor/) - go vet $(go list ./... | grep -v /vendor/) - go test -race $(go list ./... | grep -v /vendor/) compile: stage: build script: - mkdir -p mybinaries - go build -o mybinaries ./... artifacts: paths: - mybinaries ``` {{< alert type="note" >}} For a more incremental approach, migrate one job at a time. Start with the `build` job, then repeat the steps for the `format` and `test` jobs. {{< /alert >}} The CI/CD template migration involves the following steps: 1. Analyze the CI/CD jobs and dependencies, and define migration actions: - The `image` configuration is global, [needs to be moved into the job definitions](_index.md#avoid-using-global-keywords). - The `format` job runs multiple `go` commands in one job. The `go test` command should be moved into a separate job to increase pipeline efficiency. - The `compile` job runs `go build` and should be renamed to `build`. 1. Define optimization strategies for better pipeline efficiency. - The `stage` job attribute should be configurable to allow different CI/CD pipeline consumers. - The `image` key uses a hardcoded image tag `latest`. Add [`golang_version` as input](../inputs/_index.md) with `latest` as default value for more flexible and reusable pipelines. The input must match the Docker Hub image tag values. - The `compile` job builds the binaries into a hard-coded target directory `mybinaries`, which can be enhanced with a dynamic [input](../inputs/_index.md) and default value `mybinaries`. 1. Create a template [directory structure](_index.md#directory-structure) for the new component, based on one template for each job. - The name of the template should follow the `go` command, for example `format.yml`, `build.yml`, and `test.yml`. - Create a new project, initialize a Git repository, add/commit all changes, set a remote origin and push. Modify the URL for your CI/CD component project path. - Create additional files as outlined in the guidance to [write a component](_index.md#write-a-component): `README.md`, `LICENSE.md`, `.gitlab-ci.yml`, `.gitignore`. The following shell commands initialize the Go component structure: ```shell git init mkdir templates touch templates/{format,build,test}.yml touch README.md LICENSE.md .gitlab-ci.yml .gitignore git add -A git commit -avm "Initial component structure" git remote add origin https://gitlab.example.com/components/golang.git git push ``` 1. Create the CI/CD jobs as template. Start with the `build` job. - Define the following inputs in the `spec` section: `stage`, `golang_version` and `binary_directory`. - Add a dynamic job name definition, accessing `inputs.golang_version`. - Use the similar pattern for dynamic Go image versions, accessing `inputs.golang_version`. - Assign the stage to the `inputs.stage` value. - Create the binary director from `inputs.binary_directory` and add it as parameter to `go build`. - Define the artifacts path to `inputs.binary_directory`. ```yaml spec: inputs: stage: default: 'build' description: 'Defines the build stage' golang_version: default: 'latest' description: 'Go image version tag' binary_directory: default: 'mybinaries' description: 'Output directory for created binary artifacts' --- "build-$[[ inputs.golang_version ]]": image: golang:$[[ inputs.golang_version ]] stage: $[[ inputs.stage ]] script: - mkdir -p $[[ inputs.binary_directory ]] - go build -o $[[ inputs.binary_directory ]] ./... artifacts: paths: - $[[ inputs.binary_directory ]] ``` - The `format` job template follows the same patterns, but only requires the `stage` and `golang_version` inputs. ```yaml spec: inputs: stage: default: 'format' description: 'Defines the format stage' golang_version: default: 'latest' description: 'Golang image version tag' --- "format-$[[ inputs.golang_version ]]": image: golang:$[[ inputs.golang_version ]] stage: $[[ inputs.stage ]] script: - go fmt $(go list ./... | grep -v /vendor/) - go vet $(go list ./... | grep -v /vendor/) ``` - The `test` job template follows the same patterns, but only requires the `stage` and `golang_version` inputs. ```yaml spec: inputs: stage: default: 'test' description: 'Defines the format stage' golang_version: default: 'latest' description: 'Golang image version tag' --- "test-$[[ inputs.golang_version ]]": image: golang:$[[ inputs.golang_version ]] stage: $[[ inputs.stage ]] script: - go test -race $(go list ./... | grep -v /vendor/) ``` 1. In order to test the component, modify the `.gitlab-ci.yml` configuration file, and add [tests](_index.md#test-the-component). - Specify a different value for `golang_version` as input for the `build` job. - Modify the URL for your CI/CD component path. ```yaml stages: [format, build, test] include: - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/format@$CI_COMMIT_SHA - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/build@$CI_COMMIT_SHA - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/build@$CI_COMMIT_SHA inputs: golang_version: "1.21" - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/test@$CI_COMMIT_SHA inputs: golang_version: latest ``` 1. Add Go source code to test the CI/CD component. The `go` commands expect a Go project with `go.mod` and `main.go` in the root directory. - Initialize the Go modules. Modify the URL for your CI/CD component path. ```shell go mod init example.gitlab.com/components/golang ``` - Create a `main.go` file with a main function, printing `Hello, CI/CD component` for example. You can use code comments to generate Go code using [GitLab Duo Code Suggestions](../../user/project/repository/code_suggestions/_index.md). ```go // Specify the package, import required packages // Create a main function // Inside the main function, print "Hello, CI/CD Component" package main import "fmt" func main() { fmt.Println("Hello, CI/CD Component") } ``` - The directory tree should look as follows: ```plaintext tree . ├── LICENSE.md ├── README.md ├── go.mod ├── main.go └── templates ├── build.yml ├── format.yml └── test.yml ``` Follow the remaining steps in the [converting a CI/CD template into a component](_index.md#convert-a-cicd-template-to-a-component) section to complete the migration: 1. Commit and push the changes, and verify the CI/CD pipeline results. 1. Follow the guidance on [writing a component](_index.md#write-a-component) to update the `README.md` and `LICENSE.md` files. 1. [Release the component](_index.md#publish-a-new-release) and verify it in the CI/CD catalog. 1. Add the CI/CD component into your staging/production environment. The [GitLab-maintained Go component](https://gitlab.com/components/go) provides an example for a successful migration from a Go CI/CD template, enhanced with inputs and component best practices. You can inspect the Git history to learn more.