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
- ContainerHive discovers your project and resolves image dependencies.
- Jobs run as soon as the jobs they need have finished: an image's build waits for the images it depends on, its tests wait for its build. On GitLab, each image gets one stage, named after the image and ordered by dependency depth, that groups its build, test and manifest jobs.
- For each image, the pipeline includes jobs for building (per-platform), creating multi-arch manifests, and running tests.
- If an image has a
latest_aliasconfigured, the highest semantic version tag is automatically retagged as the specified alias (e.g.,latest,stable). - The generated pipeline references the ContainerHive container image to run all commands.
- Before builds, Dockerfiles are linted with hadolint (enabled by default). Lint failures gate the build stage.
- 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: ghcr.io/containerhive/containerhive for github, registry.gitlab.com/containerhive/containerhive for gitlab) |
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.
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:
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 | One stage per image, named after the image, in dependency order |
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: union of the base tag and variant platforms. Plain linux platforms are shortened to the architecture (amd64), others keep their full value (linux/arm/v7); use platform_id for identifiers |
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 |
platform_id(platform) |
Turns a platform into a value safe for job IDs and artifact names: amd64 stays amd64, linux/arm/v7 becomes linux-arm-v7 |
platform_dir(platform) |
Returns the dist/ directory name of a platform, e.g. linux-amd64 for amd64 |
platform_arch(platform) |
Returns the architecture of a platform, e.g. arm64 for linux/arm64/v8 |
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:
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/:
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 datadist/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:
GitHub Actions annotations
For GitHub Actions, use the github-actions output format to emit annotations inline on pull request diffs:
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:
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:
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:
Change detection
Merge and pull request pipelines only build, test and finalize the images a change affects, see Build only changed images for the rules and the provider differences. Configure which files count in change_detection.
To build every image in every pipeline, set ci_change_detection to false in your hive.yml:
Docker-in-Docker for test jobs
ch test needs a Docker daemon. Both providers start the privileged
dind image from the
ContainerHive helper images:
- GitHub Actions runs it as a container on the runner VM, with
--privileged. - GitLab CI runs it as a
dindservice. This needs a runner that allows privileged services, which GitLab.com hosted runners and most self-managed Docker executors do.
Kubernetes runners without privileged pods
On a GitLab Kubernetes executor that forbids privileged pods, the privileged daemon does not start. Switch to the rootless
dind-rootless image with
ci_dind_image:
The rootless daemon needs a user mapping that fits the pod (hostUsers: false), which is a cluster-side setting.
It does not start on GitHub-hosted runners, because they restrict unprivileged user namespaces.
To use the upstream image or your own mirror instead, set ci_dind_image to it, for example docker:dind.
The BuildKit daemon for the build jobs comes from the
buildkit helper image. Use
ci_buildkit_image and ci_buildkit_version to pin another one, for example the upstream moby/buildkit.
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:
Generated build and test jobs are split by platform and pass --platform to ch, which skips tags and variants that do
not support that platform (see Selecting platforms).
Generated test jobs run on matching architecture runners. In GitLab, each job extends a shared
.test-<platform> (and .build-<platform>) base, which can be extended in your root .gitlab-ci.yml to add runner
tags. <platform> is the platform with linux/ dropped for plain architectures and / replaced by - otherwise:
amd64, arm64, linux-arm-v7, windows-amd64. Job IDs and artifact names use the same form.
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.