Skip to content

Publishing generators as OCI artifacts

Kapitan's OCI dependency type lets you pull generator bundles from any OCI-compliant registry. This page walks through the other half of that workflow: how to package your own generators and publish them so teams or projects can consume them by reference.

Publishing uses the oras CLI, a small, standalone binary purpose-built for pushing and pulling arbitrary files to OCI registries.


What is a generator bundle?

A generator bundle is an ordinary directory that Kapitan will copy into a target's input path before compilation. For a Kadet generator it typically looks like this:

my-generator/
├── __init__.py        # entry point must define main()
└── lib/               # any helper modules
    └── utils.py

The contents are entirely up to you. Kapitan will copy the directory (or the declared subpath within it) verbatim, so the bundle can also contain Jinja2 templates, static YAML, or any other input type.


Prerequisites

Install the oras CLI by following the official installation guide.

You also need write access to an OCI registry (GHCR, Docker Hub, a private Harbor instance, etc.) and your credentials available via docker login or environment variables.


Pushing a generator

Authenticate with your registry, then push the directory in a single command:

# Authenticate (GHCR example)
echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin

# Push version 1.2.0 — run from INSIDE the generator directory
cd my-generator
oras push ghcr.io/kapicorp/generators:1.2.0 \
  --config /dev/null:application/vnd.oci.image.config.v1+json \
  .

oras automatically compresses the directory to .tar.gz before upload.

Run oras push from inside the generator directory

oras preserves the exact path you specify at push time. If you push system/generators/mygenerator/ from a parent directory, oras recreates that full path on pull — your files land at output_path/system/generators/mygenerator/ instead of output_path/.

Always cd into the directory you want to publish and push ., or specify subpath: <the-pushed-path> in the consumer inventory to tell Kapitan which subdirectory to extract.

Kapitan logs the artifact root contents at INFO level on every pull so you can immediately see what paths are inside the image.

Registry compatibility: always supply --config

Without --config, oras defaults to the deprecated application/vnd.oci.artifact.manifest.v1+json format. Several registries — including GitLab Container Registry — reject that format. Passing --config /dev/null:application/vnd.oci.image.config.v1+json instructs oras to use the standard OCI image manifest, which is accepted by all compliant registries.

Filtering by media type

If you publish multiple different artifact types under the same image (e.g. a generator bundle alongside a schema file) you can annotate each layer with a custom media type:

cd my-generator
oras push ghcr.io/kapicorp/generators:1.2.0 \
  --config /dev/null:application/vnd.oci.image.config.v1+json \
  .:application/vnd.oci.image.layer.v1.tar+gzip

Consumers can then set media_type: application/vnd.oci.image.layer.v1.tar+gzip in their inventory to pull only that layer. For single-artifact images this is unnecessary.

After a successful push, record the immutable digest shown in the output for reproducible deployments:

Pushed [registry] ghcr.io/kapicorp/generators:1.2.0
Digest: sha256:abc123...

Tagging strategy

Scenario Recommended source value
Latest of a moving branch ghcr.io/org/generators:latest
Pinned release ghcr.io/org/generators:1.2.0
Fully reproducible (immutable) ghcr.io/org/generators@sha256:<digest>

Production Kapitan inventories should always use a pinned tag or digest. Use latest only during active development.


Consuming the published artifact

Once pushed, reference the image in any target's inventory:

parameters:
  kapitan:
    dependencies:
    - type: oci
      source: ghcr.io/kapicorp/generators:1.2.0
      output_path: components/generators
      # No subpath needed when the artifact was pushed from inside the directory
    compile:
    - input_type: kadet
      input_paths:
      - components/generators
      output_path: .

If you pushed from a parent directory (e.g. oras push ... my-generator/), add subpath: my-generator to tell Kapitan which subdirectory to extract:

    - type: oci
      source: ghcr.io/kapicorp/generators:1.2.0
      output_path: components/generators
      subpath: my-generator

Then compile with --fetch:

kapitan compile --fetch

--fetch only pulls if the artifact is not already cached locally — subsequent runs are instant. Use --force-fetch to invalidate the cache and re-pull regardless.

See External dependencies OCI for the full reference of available fields.