This is stevedore, an Elixir project.
About Stevedore
Stevedore is a library-first, daemonless OCI toolkit for Elixir — everything you can do to a container image except run it. It operates on OCI artifacts at rest (as bytes): fetch, inspect, copy, mirror, build, modify, analyze, sign, verify, and serve images. Running them (namespaces, mounts, cgroups, isolation) is in motion and explicitly out of scope — that belongs to the consumer (e.g. Tank, a separate orchestrator). It takes its feature inspiration from Skopeo, and extends it with the crane/oras create-modify-analyze surface and a registry server.
Invariants that shape the code
- Library-first, weightless. Depending on Stevedore starts no processes and pulls no heavy
deps. The HTTP client (
req), the server (plug/bandit), and compression NIFs (zstd) are optional deps, opted into per mode. There is nomod:entry; the only thing that boots a process tree is an explicitStevedore.start_link/1(the registry server). - Daemonless & no database. Talk to registries and on-disk layouts directly. For on-disk
transports the filesystem (a
Store) is the source of truth. - Native crypto/archives. Use
:crypto,:public_key,:zlib. A NIF only where the BEAM genuinely can't reach a primitive (zstd). Never shell out toskopeo,cosign,openssl,gpg, ortarat runtime. - Digest-preserving. Manifests and blobs move as raw bytes, never re-serialized — keep
rawalongside decodedjson; digests are computed overraw. - Pure core, pluggable shells. Format logic is pure functions over structs. Storage,
transports, serving, and CLI sit behind behaviours — the two seams are
Stevedore.Store(blob/file I/O) andStevedore.Transport(where images live).copyis the primitive everything composes from: transport → transport. - Elixir
~> 1.18. Use the built-inJSONmodule (added in 1.18) — nojason.
Spec fidelity
When implementing a wire format, cite the authoritative section in a comment (name the section, link a stable URL). Primary references:
- OCI Image Spec — https://github.com/opencontainers/image-spec (manifest, image-index, config, descriptor, layer media types, whiteouts).
- OCI Distribution Spec — https://github.com/opencontainers/distribution-spec (pull/push, blob uploads, tags list, referrers API).
- Docker Registry HTTP API v2 — https://distribution.github.io/distribution/spec/api/.
- Sigstore signature spec — https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md.
Code style
Keep it simple. Prefer the most obvious solution that works. Don't add abstraction, configurability, or generality until a second caller needs it.
Comment intent, not mechanics. A comment explains why, or names a non-obvious constraint — never restate what the code plainly says.
# BAD: restates the code # increment the counter count = count + 1 # GOOD: explains why # Retry once before giving up: the upstream API returns a transient # 503 on cold start, but is reliably up by the second request. retry(request, max: 1)Keep comments concise — a sentence or two.
When implementing an existing spec or wire format, cite the authoritative source in a comment — name the specific section, and link it where a stable URL exists.
Match the style, naming, and comment density of the file you are editing.
Documentation
- Every module has a clear
@moduledoc(@moduledoc falseonly for genuinely internal modules) — what it is and the one or two things a reader needs to use it correctly. - Every public function has a clear
@docpreceding it — concise, stating what it does, with a## Examplesiex>doctest wherever one is useful and applicable (deterministic, no network). Don't restate the spec or pad with the obvious. - Keep the two committed guides current as the code grows:
docs/EXAMPLES.md— end-to-end, lifecycle-organized usage of the whole verb surface. Examples are phase-tagged; keep them truthful as phases land.docs/REFERENCES.md— the specs implemented, with sections mapped to modules.docs/TESTING.md— the test strategy: tag taxonomy, asymmetric interop, oracles, how to run each slice. Read it before adding integration/interop tests. All three are surfaced in the ExDoc build.tmp/(the plan + step docs) is git-ignored scaffolding.
Elixir guidelines
@moduledoc/@doc/@specare mandatory as above. Document private functions only when intent isn't obvious.Every public function has a
@spec— no exceptions. Add@type/@typepfor non-trivial shapes.Model domain data as structs, not bare maps or loose tuples. Use
@enforce_keysfor required fields, declare a@type t, and tag function heads with%Mod{}.defmodule Stevedore.Thing do @enforce_keys [:name, :items] defstruct [:name, :items, status: :pending] @type t :: %__MODULE__{ name: String.t(), items: [Stevedore.Item.t(), ...], status: :pending | :active | :done } endError handling — shape follows how much context the failure carries:
- Context-rich failure →
%Stevedore.X.Error{}(one struct per subsystem;defexception+message/1; uniform rendering, plus honest extras only where non-nil). Errors from dependencies that bubble up are passed through as-is — don't re-wrap without adding context. - Context-free condition → a bare atom (
{:error, :not_found}), like stdlibFile/:gen_tcp. Caller input mistake → a tagged tuple
{:error, {:bad_input, reason}}. Do notraisefor these — keep them in the{:ok, _} | {:error, _}world forwithpipelines.
- Context-rich failure →
Never nest multiple modules in one file — risks cyclic dependencies and compilation errors.
Don't use
String.to_atom/1on external input — memory-leak risk.
Mix guidelines
- Always run
mix formatbefore a git commit.
Test guidelines
- Use
start_supervised!/1to start processes — it guarantees cleanup between tests. - Avoid
Process.sleep/1andProcess.alive?/1:To wait for a process to finish, use
Process.monitor/1and assert the DOWN message:ref = Process.monitor(pid) assert_receive {:DOWN, ^ref, :process, ^pid, :normal}To synchronize before the next call, use
_ = :sys.get_state(pid).