custard — operating manual
Day-to-day use of a custard forge over Soft Serve.
Replace your-host with your forge domain and you/tap with your tap namespace throughout.
The pieces
- Web forge:
https://your-host— custard, behind Caddy (auto-TLS) on your server. - Git server: Soft Serve on the server. An ssh
softalias →your-host:23231; remotes aresoft:<repo>. - Homebrew tap: a
homebrew-taprepo →brew installstraight from your host.
A handy ~/.ssh/config alias:
Host soft
HostName your-host
Port 23231
IdentityFile ~/.ssh/id_ed25519
Everyday git
# new repo
ssh soft repo create thing -d "what it is"
cd thing && git init && git add -A && git commit -m "init" && git branch -M main
git remote add origin soft:thing && git push -u origin main
# clone
git clone soft:thing # ssh
git clone https://your-host/git/thing.git # public, read-only
# admin (run `ssh soft help` for everything)
ssh soft repo list | info <n> | description <n> "…" | private <n> true|false | webhook …
Privacy
ssh soft repo private thing true # hide from the public forge
ssh soft repo private thing false # publish
With --soft-serve-db set, only public repos appear on the forge; private/hidden → 404.
Versioning (git tags)
git tag -a v0.2.0 -m "release name"
git push origin v0.2.0
- Shows as a version pill in the header + index; the annotated message is labelled on the refs page.
- Highest semver wins. Bump anytime — tags are independent of commits, no code change needed.
Issues
backlog/tasks/*.md (Backlog.md) render at /r/<repo>/issues. Groups order
in progress → paused → backlog → done. Manage with the backlog CLI.
Deploy a web app — the custard CLI
.custard.yaml:
ci:
- npm test
deploy:
# any command that prints a deployment URL (netlify/fly/rsync work too).
# Vercel needs its project pinned — see "What's in the build" below.
preview: VERCEL_ORG_ID=… VERCEL_PROJECT_ID=… vercel deploy --yes
promote: VERCEL_ORG_ID=… VERCEL_PROJECT_ID=… vercel promote {{url}} --yes
custard check # run ci on the working tree (fast loop)
custard preview # build pushed commit → run ci → deploy preview → URL
custard promote # promote the last preview → production
What's in the build
custard deploys the newest non-empty, pushed commit — a clean git archive of it, not your
working tree. Two consequences:
- Pushed only. custard never runs
git push; it deploys what's on the remote, so deployed == what's in the repo. Push first, thenpreview/promote. (Under jj, the working copy is an empty commit that normally sits on top after a push — custard skips it and deploys the described commit beneath, so you never ship an empty tree.) - Gitignored + uncommitted files are ABSENT. The archive contains only committed, tracked files.
So:
- If
ciusesnpm ci, thepackage-lock.jsonmust be committed (a gitignored lockfile →npm cifails in the export).node_modules,.env, and build artifacts are likewise gone — yourcimust regenerate whatever it needs. - Vercel:
.vercel/project.json(gitignored) isn't present, so a barevercel deploycreates a stray new project. Pin the target withVERCEL_ORG_ID=… VERCEL_PROJECT_ID=… vercel deploy --yes(IDs aren't secrets; auth still comes from your localvercel login). custard warns on an unpinned Vercel deploy, and surfaces the real*.vercel.appURL even though Vercel also prints API/inspect links.
- If
Preview privacy: Vercel previews are often behind Deployment Protection — the preview URL redirects to an SSO login and is viewable only by your team. Production (your custom domain) is public. Toggle it in Project → Settings → Deployment Protection.
Release a CLI to Homebrew
.custard.yaml:
brew: { enabled: true, package: "." } # or ./cmd/<name>
One-time per repo, register the webhook (secret = your WEBHOOK_SECRET):
ssh soft repo webhook create <name> https://your-host/hooks/release \
-e branch_tag_create -c json -s "$WEBHOOK_SECRET"
Release + install:
custard release v0.2.0 # checks → tag → push → tap publishes
brew tap you/tap https://your-host/git/homebrew-tap.git
brew trust you/tap # newer Homebrew gates third-party taps
brew install <name> ; brew upgrade <name>
Updating the forge
cp deploy/deploy.env.example deploy/deploy.env # first time: fill REMOTE, DOMAIN, secrets…
deploy/deploy.sh # build static binary, ship, restart
Automatic niceties
- README/markdown images with relative paths render; gifs sit embossed; click any image for a lightbox.
- Deploy badge (✓ in production / 👁 preview / ⚠ unverified) shows only for repos with a
deploy:in.custard.yaml. - README is its own tab;
codeis the file tree.
See MIGRATION.md to move a repo off GitHub.