Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Publishing a Harness

A harness is one file, so it travels as one OCI layer with no tarball around it.

berm push ghcr.io/org/example:v1 ./target/riscv64imac-unknown-none-elf/release/example
berm deploy example ghcr.io/org/example:v1

deploy takes a file or a reference, not two commands: a path that exists is read, and anything else is resolved as a reference and pulled. The bytes reach the service the same way either way, so bermd never talks to a registry, holds no credentials, and still restores from --root on start without a network.

The artifact

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "artifactType": "application/vnd.berm.harness.v1",
  "config": { "mediaType": "application/vnd.berm.manifest.v1+json", "digest": "sha256:…" },
  "layers": [ { "mediaType": "application/vnd.berm.harness.v1", "digest": "sha256:…" } ]
}

The config blob is the .berm.abi section carried out of the ELF byte for byte, so what a registry serves cannot disagree with what the image holds. It is also what makes a listing cheap: the tools, their schemas and the usage are one small blob, and reading them never means pulling the harness.

Nothing about the harness is repeated in annotations. Only org.opencontainers.image.source and .revision are set, from GITHUB_REPOSITORY and GITHUB_SHA when a build has them, which is what makes GHCR show a package against its repository.

One digest

Because the layer is the ELF and nothing else, the layer’s digest is sha256 of the ELF — the same hash berm ls reports, carrying the registry’s sha256: prefix:

$ berm push 127.0.0.1:5000/berm/fixture:v1 ./fixture
127.0.0.1:5000/berm/fixture:v1
  digest  sha256:222890c498ed28f4bf60670a223141489d9879020bd1890111b8c11ac79fa31d

$ shasum -a256 ./fixture
222890c498ed28f4bf60670a223141489d9879020bd1890111b8c11ac79fa31d

A pulled image is checked against the digest the registry advertised before it goes anywhere, so a registry that serves the wrong bytes is caught by the deploy rather than by the model.

What push refuses

berm push reads the manifest out of the ELF before it uploads anything, which means a broken image is refused by whoever built it rather than at deploy on someone else’s machine. Reading it never runs the guest.

From a workflow

GITHUB_TOKEN is the credential, and with berm push the step is one line — there is no action to install.

permissions:
  packages: write

steps:
  - run: cargo build --release --target riscv64imac-unknown-none-elf
  - run: berm push ghcr.io/${{ github.repository }}:${{ github.ref_name }} \
           target/riscv64imac-unknown-none-elf/release/example
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Locally, GITHUB_TOKEN=$(gh auth token) does the same. ~/.docker/config.json is not read.

Being found

Pushing makes a harness fetchable, not findable. Nothing can enumerate the harnesses on a registry — GitHub’s Packages API refuses to list even public packages without a token — so an index has to be told a harness exists.

berm publish ghcr.io/org/example:v1
berm search "read a file"

--index <url>, or BERM_INDEX, or the default list below.

publish records a reference and nothing else. The index pulls the artifact itself, anonymously, and fills in the digest, tools and usage from the config blob — so a listing describes what a registry will actually serve rather than what a publisher claimed, and a harness nobody can pull cannot be listed.

Entries are keyed by digest, which is what makes them safe to keep: the bytes at a digest never change, so a recorded description of them cannot go stale. Re-pushing a tag adds an entry rather than rewriting one.

The list is a git repository

One JSON Lines file per harness, one line per version — so a copy of an index is a clone, and searching your copy needs no service, no credential and no network. berm search keeps that copy for you under ~/.berm/index, cloning it the first time and not touching the network again:

berm search "read a file"                     # the default list
git -C ~/.berm/index/github.com/crabtalk/berm-index pull    # refresh it

The default is https://github.com/crabtalk/berm-index.git. --index or BERM_INDEX names another, and takes three shapes: a directory to read as-is, a .git URL to keep a copy of, or the URL of a service. Publishing needs the service, because appending to the list means holding a credential for the repository; reading never does.

BERM_TOKEN carries a credential when the service in front of you wants one. Never a GitHub token: an index has no business holding something that could act as you elsewhere.

berm-indexd serves those two routes over a directory, so the loop can be run without deploying anything. What it writes is what a clone holds, so the same directory reads back with no service at all.

berm push talks to no index at all — publishing is a separate act, so an upload that succeeded is never undone by a listing that failed.