relaxed package delivery
One tool to create, host, sign and operate package repositories from a
git-backed workspace. Builds are deterministic, plans are reviewable, artifacts
are immutable, and before anything is published a real client — pip, apt,
dnf, apk, helm — installs from the built tree in a container.
MIT licensed. See LICENSE and NOTICE.md.
Six formats today. Debian (apt), RPM (dnf/yum), Alpine (apk), PyPI
(pip), Helm, and raw for artifacts that carry no ecosystem metadata. Debian,
RPM, Alpine and Helm are signed with the scheme their own clients verify.
Four hosts today. A local directory, a directory reached over ssh, GitHub Pages, and S3-compatible object storage — though not every format on every host; see the table below.
Planned, not built: npm, OCI, Cargo, Go, Maven and Nix repositories, and publishing into registries you do not own (AUR, Homebrew, nixpkgs, npmjs, PyPI, ghcr). PLAN.md has the design; none of it works yet.
From an empty directory to a published PyPI repository:
go run ./cmd/snailmail init --name example # creates the Git repository if there is none
go run ./cmd/snailmail setup pypi --name python --output public/python
go run ./cmd/snailmail add python ./dist/*.whl
git add .gitignore snailmail.toml repos/python.lock.toml
git commit -m "configure Python repository"
go run ./cmd/snailmail plan
go run ./cmd/snailmail applyplan writes what it intends to do; apply does it and records a publication
ledger entry. Nothing reaches a host until a plan has been applied.
Each command prints the one that usually comes next, so the sequence can be
followed without this page. The git commit in the middle is not incidental:
desired state is reviewed as a diff, and plan reads committed state only.
apply reports each phase as it runs — a publication takes minutes, most of it
building and running a real client per format — and writes that narration to
stderr, so --json still emits exactly one document on stdout.
apply --dry-run builds, verifies and checks every gate, then stops before
writing anything to a host. It is not the same as --structural-only, which only
skips client verification and still publishes.
Every command that takes --workspace also accepts --root.
| Host | pypi | deb | rpm | apk | helm | raw |
|---|---|---|---|---|---|---|
| local directory | yes | yes | yes | yes | yes | yes |
ssh directory (rsync) |
yes | yes | yes | yes | yes | yes |
| GitHub Pages | yes | yes | yes | yes | yes | yes |
| S3 / R2 / GCS | yes | — | unsigned only | — | yes | yes |
An object store makes a revision live by writing one object, so it can serve a
format only where a single path switches. Debian needs a Release and its detached
signature to become live together; Alpine has one index per architecture; and a
signed yum repository has to switch repomd.xml with the repomd.xml.asc that
signs it. Those are structural limits, not missing work — a local directory, an
ssh directory or GitHub Pages commits a whole tree at once and serves all six.
The rsync host serves every format but publishes no preview site, so it works
under the auto gate and is refused under pr and approval, which exist to
review one. Publishing over ssh has the detail.
One caveat for object storage: the browsable index.html is generated fresh for
every revision, so it is kept with the release rather than at the repository root.
Clients are unaffected — they fetch simple/, index.yaml, repodata/ or
SHA256SUMS — but there is no human-browsable page at the root of a bucket-hosted
repository yet.
Phase 2 is complete and Phase 3 signing is implemented. Concretely: git-backed
workspaces with init, setup, add, plan and apply; immutable blobs;
reviewable plans; publication ledgers; per-repository managed release switching;
auto, pr and signed-approval gates; encrypted local RSA4096 OpenPGP keys
with keys new|publish|audit|rotate; receipt-backed Debian key rotation; and
placement-level promote and yank.
Review gates read GitHub, GitLab, Forgejo and Gitea, and snailmail ci emits a
GitHub Actions or GitLab CI pipeline derived from the workspace.
Replacing an existing managed release needs an atomic directory-entry exchange, so that path is implemented on Linux and macOS; creating a first release is portable. There is no Windows build yet.
Additional key backends, more formats, and an interactive setup remain Phase 3
work; import is done and has its own page.
The front page is the shape of the thing. Everything else is one page per question, because a reference manual and an introduction want different orders and this file was trying to be both.
| Where repositories are published | ssh, object storage, GitHub Pages, and what each can serve |
| Deciding what a repository publishes | promote, yank, prune, collect, rollback, and raw artifacts |
| Signing and review | keys, rotation, and the gates that hold a publication for a person |
| Taking on a repository that already exists | import, adopt, and inspecting somebody else's |
| Running it | status, check, exit codes, concurrency, and how large a workspace can get |
| Shipping snailmail itself | the container image, generated CI, and binary size |
ARCHITECTURE.md is the implementation contract and PLAN.md the broader design.