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, ...}mirroringhull.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 apath: contentsmap.${Files.AsConfig "files/configs"}— every file under the directory as abasename: contentsmap.${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