Skip to content

Generate CI pipelines

ContainerHive generates CI pipeline configurations from your project definition, so you don't have to maintain pipeline YAML by hand.

Generating CI pipelines

Use template ci to generate a pipeline for your CI provider:

ch template ci --provider gitlab --output .gitlab-ci.yml
ch template ci --provider github --output .github/workflows/build.yml

Supported providers

Provider Flag value Output
GitLab CI gitlab GitLab CI pipeline YAML
GitHub Actions github GitHub Actions workflow YAML

How it works

  1. ContainerHive discovers your project and resolves image dependencies.
  2. Images are grouped into stages based on their dependency depth — images at the same depth can build in parallel.
  3. For each image, the pipeline includes jobs for building (per-platform), creating multi-arch manifests, and running tests.
  4. If an image has a latest_alias configured, the highest semantic version tag is automatically retagged as the specified alias (e.g., latest, stable).
  5. The generated pipeline references the ContainerHive container image to run all commands.
  6. Before builds, Dockerfiles are linted with hadolint (enabled by default). Lint failures gate the build stage.
  7. After successful builds, an HTML and JSON report is generated and published to GitHub Pages or GitLab Pages.

Options

Flag Description
--provider CI provider (gitlab or github) — required
--output Output file path (default: stdout)
--template-dir Custom template directory to override built-in templates
--artifacts Upload/download build artifacts between jobs
--version ContainerHive CLI version to use (default: current version)
--image-name Container image name for the CLI (default: containerhive/containerhive)

Example with custom image

ch template ci \
  --provider github \
  --output .github/workflows/build.yml \
  --image-name ghcr.io/containerhive/containerhive \
  --version v1.0.0 \
  --artifacts

Custom templates

For use cases beyond CI pipelines, the template custom command renders any Go template with full access to the project context.

ch template custom --template my-template.gotpl --output output.yml

The --output value is itself rendered as a Go template with the same context as the template file, so the destination can be derived from the project:

ch template custom --template image.gotpl --output 'foo/{{ .Image.Name }}.yml'

When the output path references .Image, the template is rendered once per image and one file is written per image — .Image is bound to the current image in both the output path and the template content. Without .Image, a single render writes a single file (and .Image is not available in the content). Missing parent directories in the output path are created automatically.

Template context

Custom templates receive a CIContext with the following fields:

Field Type Description
Images list All images (see below)
Platforms list All unique platforms across all images
Stages list Ordered build stages (build-<image>, test-<image>, manifest-<image>)
Config object Registry and cache configuration (see below)
Artifacts bool Whether build artifacts are passed between jobs (set by template ci --artifacts)
TemplateOptions map Merged template options (defaults + hive.yml), same values the option(key) function returns
HasTagRanges bool Whether any image defines tag_ranges
TagRangeUnits int Total version-cache tag units across all images (0 unless HasTagRanges is true)
Command string The ch template ci command line — only set by template ci, empty in custom templates
Version string CH CLI version used in CI templates — only set by template ci
ImageName string Container image name for the CH CLI — only set by template ci
ProjectPath string --project value when it is not . — only set by template ci

Each image in Images provides:

Field Type Description
Name string Image name
Tags list All tags including variant-suffixed tags (base + variants)
Aliases map Alias name to the exact tag it points at: semantic version series aliases (1.2.3 → 1.2, 1) and latest_alias, including variant-suffixed ones. Same resolution as the registry retagging step
Dependencies list Names of images this image depends on
Depth int Dependency depth (0 = no dependencies)
Platforms list Target platforms
ReadmePath string Path to the image's README.md, empty if it has none. Load the content with read_file
ShardUnits int Number of shard units (base + variant tags) — the useful max
BuildShards int Effective parallel for build jobs (min(ci_build_shards, ShardUnits))
TestShards int Effective parallel for test jobs (min(ci_test_shards, ShardUnits))

Config exposes the project registry and cache configuration:

Field Type Description
Config.Registry.Address string Container registry address
Config.Registry.DockerMediaTypes bool Force Docker-scheme media types (null = auto-detect)
Config.RegistryHost() string Registry hostname without path (e.g. docker.io/timoreymann → docker.io)
Config.Cache.Type string Cache backend: s3 or registry
Config.Cache.Endpoint string S3 endpoint URL
Config.Cache.Bucket string S3 bucket name
Config.Cache.Region string S3 region
Config.Cache.AccessKeyId string S3 access key ID
Config.Cache.SecretAccessKey string S3 secret access key — available, but avoid rendering it into committed files
Config.Cache.UsePathStyle bool Use path-style S3 URLs
Config.Cache.Ref string Registry cache ref (e.g. registry:5000/cache)
Config.Cache.Insecure bool Allow insecure registry connections

Note

Config.Registry and Config.Cache are pointers and render as <no value> when unset — guard with {{ with .Config.Cache }}...{{ end }}. Registry.DockerMediaTypes is a tri-state: unset means auto-detect.

The context also provides helper methods, callable like functions:

Method Description
ChCmd() The ch command prefix, including -p <project> when a project path is set
Dist() The dist directory path, prefixed with the project path when set
VersionCacheDir() The tag_ranges version cache path, prefixed with the project path when set

Template functions

Templates have access to Sprig functions plus:

Function Description
resolve_base(name, tag) Produces an internal image reference (__hive__/name:tag) for inter-image dependencies
option(key) Returns the value of a template option from hive.yml, or empty string if not set
read_file(path) Returns the content of a file, or an empty string for an empty path — e.g. {{ .Image.ReadmePath \| read_file }}. Fails if the file does not exist

Example custom template

{{- range .Images }}
Image: {{ .Name }}
  Tags: {{ range .Tags }}{{ . }}, {{ end }}
  Platforms: {{ range .Platforms }}{{ . }}, {{ end }}
  Dependencies: {{ range .Dependencies }}{{ . }}, {{ end }}
{{ end }}

Overriding built-in CI templates

You can override the built-in CI templates by providing a custom template directory:

ch template ci --provider gitlab --template-dir ./my-templates --output .gitlab-ci.yml

The directory should contain .gotpl files following the same structure as the built-in templates. Files in the custom directory take precedence over the built-in ones.

Built-in template structure

templates/<provider>/
├── pipeline.yml.gotpl   (or workflow.yml.gotpl for GitHub)
├── build-job.yml.gotpl
├── lint-job.yml.gotpl
├── manifest-job.yml.gotpl
└── test-job.yml.gotpl

The entrypoint template includes the job templates as partials using {{ template "build-job.yml.gotpl" . }}.

The generate command

Before building or generating CI configs, run generate to discover the project and render all configurations to dist/:

ch generate

This processes hive.yml and all image.yml definitions, renders Dockerfiles and test templates, and prepares the dist/ directory for subsequent build, test, and sbom commands.

Report generation

By default, the generated CI pipeline includes a report job that runs after all build jobs complete. This job generates:

  • dist/report.html — Interactive HTML report with image details, tags, platforms, and SBOM data
  • dist/report.json — Machine-readable JSON report

GitHub Pages

The HTML report is automatically deployed to GitHub Pages. Your repository must have Pages enabled in settings with the source set to "GitHub Actions".

GitLab Pages

The report is uploaded as a GitLab Pages artifact and exposed as "ContainerHive Report" in the merge request widget. The JSON report is also available as an artifact.

Lint job

By default, the generated CI pipeline includes a lint job that runs after the generate stage and before any build stages. This job lints all plain Dockerfiles in the project using hadolint via ch lint.

GitLab Code Quality report

For GitLab CI, the lint job produces a Code Quality report artifact that GitLab renders inline on merge requests. The report uses the codeclimate output format:

ch lint --format codeclimate=gl-code-quality-report.json

GitHub Actions annotations

For GitHub Actions, use the github-actions output format to emit annotations inline on pull request diffs:

ch lint --format github-actions

Lint failures still cause the job to fail and gate subsequent build jobs. Without the --format flag, findings only appear in the workflow log — using --format github-actions makes them visible on the changed lines in the PR.

Disabling linting

To disable the lint job entirely, set ci_lint to false in your hive.yml:

template_options:
  ci_lint: false

Reducing SBOM size

syft attaches CPEs to every component by default, which is a meaningful contributor to SBOM size on images with many packages. Set ci_sbom_generate_cpes to false to pass --no-cpe to ch sbom in the generated GitLab CI pipeline, trading CPE-based vulnerability matching for a smaller artifact — useful if you're running into GitLab's SBOM artifact size limits:

template_options:
  ci_sbom_generate_cpes: "false"

Cancelling outdated pipelines

By default, when a new pipeline is triggered on the same branch, in-progress pipelines from earlier commits are automatically cancelled to save CI resources. This behaviour is controlled by the ci_cancel_outdated option.

GitHub Actions

The generated workflow uses a concurrency group scoped to the workflow and branch. On a push to the default branch, a new run cancels the previous in-progress run. Pull request runs use a unique group per run, so PR behaviour is unchanged (no cancellation, no queueing).

Requires actions: read permission (included in the generated workflow).

GitLab CI

The generated pipeline sets workflow:auto_cancel:on_new_commit: interruptible (GitLab 16.9+) and marks all jobs as interruptible: true via the default block (GitLab 15.9+). This forces auto-cancel of superseded in-progress pipelines on the same ref, regardless of the project-level "Auto-cancel redundant pipelines" setting.

Note: GitLab's auto-cancel mechanism applies per ref. A new push to a merge request also cancels that MR's superseded pipeline. There is no clean way to restrict cancellation to the default branch only.

To disable, set ci_cancel_outdated to false in your hive.yml:

template_options:
  ci_cancel_outdated: "false"

Sharding build and test jobs

For GitLab CI, ci_build_shards and ci_test_shards set an upper bound on parallel: N for each build/test job. The actual value is capped per image to the number of shard units (base + variant tags) that image has, so images with fewer tags than the configured value never spawn empty jobs that no-op. See Split work across shards for the full picture; the template option is set the same way as ci_lint above:

template_options:
  ci_build_shards: "5"
  ci_test_shards: "5"

Registry retention

Every generated pipeline pushes per-platform intermediate images and, when using --build-id, snapshot manifests and aliases into the registry. These accumulate over time — even for MRs that are never merged. Since ch test and ch finalize need to pull these images, skipping pushes is not an option. Instead, configure a registry-level retention policy to automatically clean up snapshot images after a few days.

See Clean up registry images with retention policies for per-provider setup instructions.