Package anatomy

A hull package is a directory containing a package manifest (hull.yaml), a defaults file (values.yaml), and a tree of templates. This guide walks through every file and directory hull recognises, what each is for, and the conventions around it.

The minimum

my-pkg/
├── hull.yaml
├── values.yaml
└── templates/
    └── deployment.yaml

Three things: a package manifest, a values file (may be empty), and at least one template. Everything else is optional.

The full layout

my-pkg/
├── hull.yaml                       # required — package identity, version, layers, environments
├── values.yaml                     # required — default values (may be empty: {})
├── values.schema.json              # optional — JSON Schema validation for merged values
├── .hullignore                     # optional — patterns excluded when packaging
├── templates/                      # required — manifests (with ${...} expressions)
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── notes.yaml                  # any {message: ...} document → release notes
│   └── _helpers.yaml               # underscore prefix → partial, not emitted
├── crds/                           # optional — CRDs applied first, waited for Established
│   └── widget-crd.yaml
├── hooks/                          # optional — lifecycle Jobs / Pods
│   ├── pre-install.yaml
│   └── post-upgrade.yaml
├── tests/                          # optional — Pods run by `hull test`
│   └── connection.yaml
├── files/                          # optional — embedded files readable via the Files API
│   └── default.conf
├── profiles/                       # optional — named value overlays (--profile prod)
│   └── prod.yaml
├── policies/                       # optional — package-defined policy rules
│   └── require-resources.yaml
├── README.md                       # optional — surfaced by `hull show readme`
├── LICENSE                         # optional
└── hull.lock                       # generated by `hull dependency update`; commit it

hull create <name> scaffolds a working subset of this: hull.yaml, values.yaml, .hullignore, and templates/ with a Deployment, a Service, a _helpers.yaml partial, and a notes.yaml.

File-by-file

hull.yaml

The package manifest. Required. Declares name, version, apiVersion, and optionally layers, requires, environments, and metadata. Full reference: hull.yaml.

values.yaml

Default configuration. Required (may be {}). Hull deep-merges layer values, environment values, the selected profile, -f files, and --set flags on top of this. Reference: values.yaml. Authoring guide: Values.

values.schema.json

Optional JSON Schema (draft 2020-12) describing the expected shape of the merged values. When present, hull template, hull install, and hull upgrade validate before render and abort on any violation with a precise path and reason. Reference: values.schema.json. Guide: Schema validation.

templates/

Required. Every *.yaml / *.yml file is rendered through the template engine and the result is treated as Kubernetes manifests. Files starting with an underscore (_*.yaml) are partials: their entries are loaded into the engine so other templates can splice them, but they are never emitted as standalone manifests.

Inside a template, expressions use four lowercase namespaces (no leading dot):

  • values — the merged values map.
  • release{name, namespace, revision, ...}.
  • package{name, version, ...} mirroring hull.yaml.
  • capabilities — cluster info; kubeVersion, apiVersions.has(...).

The Files.* accessors (below) are bound by name, not under a namespace.

Partials and includes

A partial is a named entry in a _*.yaml file:

# templates/_helpers.yaml
common-labels:
  app: "${values.name}"
  version: "${package.version}"

Splice it into a manifest structurally with the $include directive — the partial’s map merges into the parent key:

# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: "${values.name}"
  labels:
    $include: common-labels

renders:

metadata:
  labels:
    app: hello
    version: 0.1.0
  name: hello

The include function (${include "name"}) is the string-valued equivalent. For the full directive set — $if, $each, $switch, $include — see Control flow.

notes.yaml and release notes

Hull recognises release notes by shape, not filename: any rendered document whose only key is message: (a string) becomes the release notes rather than a manifest. The scaffold ships one at templates/notes.yaml:

# templates/notes.yaml
message: |
  ${package.name} has been installed successfully.
  Namespace: ${release.namespace}
  Run "kubectl get deployments" to verify.

The rendered message prints after install/upgrade and is stored in the release record (hull get notes <release>).

crds/

Optional. YAML in crds/ is applied before templates/, and hull waits for each CRD to reach Established=true before continuing, so later templates can reference the custom resources without a race. CRDs are applied as-is — they are not rendered through the template engine. Include them in local renders with hull template --include-crds.

hooks/

Optional. Jobs and Pods that run at lifecycle points. The hook’s event comes from its filename (pre-install.yaml, post-upgrade-migrate.yaml, …), refined by $hook* directives at the top of the document. Full guide: Hooks.

tests/

Optional. Pods that hull test <release> runs on demand (never during install). Stored in the release record at install/upgrade time. See Hooks → Tests.

files/

Optional. Non-template files exposed to templates via the Files.* functions. Keys are paths relative to the package root, so a file at files/default.conf is read as files/default.conf:

  • ${Files.Get "files/default.conf"} — contents as a string.
  • ${Files.Lines "files/default.conf"} — contents as a list of lines.
  • ${Files.Glob "files/certs/*.pem"} — matching files as a path: contents map.
  • ${Files.AsConfig "files/configs"} — every file under the directory as a basename: contents map.
  • ${Files.AsSecrets "files/secrets"} — same, base64-encoded.
data:
  conf: ${"files/default.conf" | Files.Get | quote}

profiles/

Optional. A profile is a named values overlay. Activate one with `–profile

`: ```yaml # profiles/prod.yaml replicas: 5 resources: requests: { cpu: 500m, memory: 512Mi } ``` ```sh hull install my-app . --profile prod ``` The profile merges on top of `values.yaml` and below environment and CLI overrides. See [Values → Profiles](/hull/guides/values.html#profiles). ### `policies/` Optional. Declarative match-and-require rules that [`hull policy check`](/hull/cli/policy-check.html) evaluates against the rendered manifest (e.g. "every Pod must set `runAsNonRoot`"). Policies ship with the package. ### `README.md` and `LICENSE` Optional. `hull show readme .` prints the README; `hull show all .` includes both README and metadata. See [`hull show`](/hull/cli/show.html). ### `hull.lock` Generated by `hull dependency update`. Pins the resolved version, ref, and digest of every layer and required package. **Commit it** — without it, two builds can resolve different versions of the same layer when the constraint allows it. See [Layers → The lockfile](/hull/guides/layers.html#the-lockfile). ## How rendering works Hull renders in this order: 1. Load `hull.yaml` and resolve layers (recursively). 2. Merge values — layers (in declared order), the package's `values.yaml`, the selected environment, the selected profile, `-f` files, then `--set` / `--set-file` / `--set-string` / `--set-json`. 3. Validate against `values.schema.json` if present. 4. Apply `crds/` (unchanged) and wait for them. 5. Render `templates/` — non-underscore files become manifests; partials load but are not emitted; `message:` documents become notes. 6. Render `hooks/` and `tests/` with the same engine. ## Naming conventions - DNS-1123 names: `^[a-z]([-a-z0-9]*[a-z0-9])?$`, max 63 characters. - Derive resource names from `${release.name}` (or `${values.name}`) so two releases of the same package don't collide. - Set `metadata.namespace: ${release.namespace}` on namespaced resources so [`hull drift`](/hull/cli/drift.html) can match them against the live cluster. - Prefer `app.kubernetes.io/name` and `app.kubernetes.io/instance` labels for compatibility with k8s tooling. Hull adds `managedBy=hull` automatically. ## Lifecycle from author to operator ```sh # author hull create my-pkg # edit values.yaml, templates/, ... hull lint . hull template . hull package . # → my-pkg-1.0.0.hull.tgz hull publish my-pkg-1.0.0.hull.tgz --oci oci://reg.example.com/charts/my-pkg # operator hull pull oci://reg.example.com/charts/my-pkg --version 1.0.0 --destination ./pulled hull install my-app ./pulled/my-pkg -n prod --create-namespace ``` See [`hull package`](/hull/cli/package.html), [`hull publish`](/hull/cli/publish.html), and [`hull pull`](/hull/cli/pull.html). The per-command reference lives under [`docs/cli/`](/hull/cli/README.html).