Oci
The OCI rules model image movement and deterministic image assembly without a
Docker daemon. ociPull() fetches an immutable layout, ociBuild() composes
file layers and metadata, ociPush() publishes a local image, and
ociMirror() copies directly between registries. A managed Crane toolchain
handles registry operations.
Available goals
| Goal | Tool | Product module |
|---|---|---|
imp gen-lockfiles | Crane | //rules/oci/toolchain |
Set up Crane
Registry operations use the pinned Crane release provided by the OCI rule:
import "//rules/oci";
The release is downloaded into a shared named cache on first use. Pure local
composition of a "scratch" image does not invoke Crane, but pulls, pushes,
mirrors, and builds based on a pulled image do.
Pull a base and build an image
import { ociPull, ociBuild } from "//rules/oci";
export const alpine = ociPull({
repo: "docker.io/library/alpine",
digest: "sha256:...",
});
export const app = ociBuild({
base: alpine,
path: ".",
layers: [{
srcs: ["bin/server"],
path: "/usr/local/bin",
mode: "0755",
}],
entrypoint: ["/usr/local/bin/server"],
env: { LOG_LEVEL: "info" },
workdir: "/srv",
});
A pull requires exactly one of tag or digest. Tags are resolved on every
invocation because they can move; the resolved digest and image contents are
then stored in the shared OCI cache. Prefer a digest when reproducibility
matters.
ociBuild accepts an ociPull/ociBuild target as its base or the literal
"scratch". It does not interpret a Dockerfile and cannot run RUN commands.
Each layer stages workspace files selected by srcs at the requested image
path, with optional exclusions, ownership, and mode. Layer tarballs, config,
manifest, and OCI index are assembled deterministically, so identical inputs
produce identical image content.
Build and package locally
imp build //images:app
imp package //images:app
build leaves the OCI layout in the build graph/CAS. package writes
dist/images/app/image.tar, an OCI archive suitable for tools such as
podman load or docker load. Pulled images can be packaged in the same way.
Push or mirror
import { ociPush, ociMirror } from "//rules/oci";
export const publish = ociPush({
image: app,
repo: "registry.example.com/acme/server",
tag: "latest",
});
export const mirror = ociMirror({
from: { repo: "docker.io/library/alpine", tag: "3.23" },
to: { repo: "registry.example.com/mirror/alpine", tag: "3.23" },
});
Select either target with imp build. Push and mirror are intentionally
impure: they perform registry side effects on every invocation and are never
replayed from the task cache. Mirror uses a registry-to-registry Crane copy and
does not materialize the image locally.
Credential sourcing is not implemented yet. The authentication seam is wired through every registry operation, but currently supplies no tools or environment, so these rules are presently suitable for public/anonymous registries only. Do not assume an ambient Docker login will be visible inside the hermetic execution environment.
The current image builder emits one image manifest. Multi-platform indexes and Dockerfile-compatible command execution are outside this rule's present surface.
Targets
ociPull
const ociPull
Declare an OCI image pull label: fetches repo:tag (or repo@digest) into the shared oci-storage named cache as an OCI-layout directory, for use as an ociBuild() base or an ociPush() source.
| Parameter | Type | Description |
|---|---|---|
| opts | object | |
| opts.repo | string | Image repository, e.g. "docker.io/library/alpine" or "ghcr.io/org/image". |
| [opts.tag] | string | Mutable tag — resolved to a digest at build time via crane digest. |
| [opts.digest] | string | Immutable sha256:... digest — skips the resolve step entirely. |
Returns: object Exported label handle.
ociBuild
const ociBuild
Declare an OCI image build label: composes base plus layers into a new image by hand-assembling its OCI layout (layer blobs, config blob, manifest blob, index.json) — no Dockerfile, no commands executed inside a container. One or more file-staging specs; each becomes one deterministic layer tarball. Deliberately no platforms/multi-arch surface yet — single-manifest only for this pass. When multi-arch support is added, it attaches here (an OCI index assembled via crane index append over one build per platform) without needing to change layers'/base's shape.
| Parameter | Type | Description |
|---|---|---|
| opts | object | |
| [opts.path="."] | string | Directory (relative to the declaring BUILD.js) layers[].srcs glob against. |
| opts.base | object | "scratch" |
| opts.layers | Array<{srcs: string[], path: string, exclude?: string[], uid?: number, gid?: number, mode?: string}> | |
| [opts.entrypoint] | string[] | Image ENTRYPOINT. |
| [opts.cmd] | string[] | Image CMD. |
| [opts.env] | Record<string,string> | Image environment variables. |
| [opts.labels] | Record<string,string> | Image labels. |
| [opts.user] | string | Image USER. |
| [opts.workdir] | string | Image WORKDIR. |
Returns: object Exported label handle.
ociPush
const ociPush
Declare an OCI image push label: publishes an ociPull()/ociBuild() image to repo:tag.
| Parameter | Type | Description |
|---|---|---|
| opts | object | |
| opts.image | object | An ociPull()/ociBuild() label handle. |
| opts.repo | string | Destination repository. |
| opts.tag | string | Destination tag. |
Returns: object Exported label handle.
ociMirror
const ociMirror
Declare an OCI mirror label: re-hosts an image from one registry ref to another via crane copy (registry-to-registry, no local disk materialization) — e.g. mirroring a public upstream image into an internal registry without a Dockerfile.
| Parameter | Type | Description |
|---|---|---|
| opts | object | |
| opts.from | object | Source ref: { repo, tag } or { repo, digest }. |
| opts.to | object | Destination ref: { repo, tag }. |
Returns: object Exported label handle.