Using jujutsu (jj) with Soft Serve + custard
jujutsu (jj) is a Git-compatible VCS that uses git as its
storage backend. This means it works naturally with Soft Serve and custard — no special server
support required. Replace your-host with your forge domain throughout.
What works unchanged
jj stores its history in a standard git repo. jj git clone and jj git push speak plain git to
Soft Serve over SSH (port 23231). Once pushed, every object — commits, trees, blobs, branches —
is ordinary git data. custard reads that data the same way it reads any git repo: commits show in
the log, file trees browse normally, blobs highlight with syntax color, refs list cleanly.
custard never knows whether a repo was authored in jj, stock git, or any other tool. There is nothing to configure on the forge side.
# clone via jj
jj git clone soft:<repo>
# push changes
jj git push --bookmark main # or --all
jj git clone names the remote origin (pointing at soft:<repo>), the same as stock git.
Keep it named origin: custard's release verb pushes with git push origin, so the remote
must be origin for releases to work. (soft: here is just the SSH host alias for the clone
URL — not the remote's name.)
Concept mapping
| jj concept | What lands in git / custard |
|---|---|
| commit | git commit — appears in log, tree, blob views |
| bookmark | git branch — appears on the refs page under Branches |
jj describe |
edits the commit message of the working commit |
jj git push --bookmark |
pushes a branch to Soft Serve |
change ID (abc123...) |
local-only; never pushed, never visible in custard |
| op log | local-only; the forge has no concept of it |
| annotated tag | not supported (see below) |
jj change IDs and the operation log are entirely local. They exist inside your .jj/ directory and
are never transmitted over git push — so custard never sees them and has no way to display them.
The tag gap
jj's native commands cannot create or move annotated git tags. This matters because custard's Homebrew release pipeline is triggered when a tag is pushed (via a Soft Serve webhook), and the version pill on a repo's page normally reads from the highest semver tag.
Most jj repos here don't hit this gap. If your repo is colocated (a .git alongside .jj —
the default jj git init --colocate layout), plain git still works in it, so the
custard release vX.Y.Z CLI — which runs git tag + git push — releases exactly like it does in a
git repo. Stay in the default source: tag. This is the recommended path; reach for bookmark
mode only if you have a reason not to run git tag at all.
Bookmark mode is the fix for the case where you won't touch git tags — a non-colocated jj repo, or
a workflow that releases purely through jj. Instead of tagging, you create or move a release/vX.Y.Z
bookmark (a plain git branch) to the commit you want to ship, then push it; custard detects the
bookmark push and treats it as a release.
Don't cross the streams.
custard releasealways tags, regardless ofsource. In asource: bookmarkrepo it will tag and push successfully and print✓ released— but the server ignores tag pushes in bookmark mode, so nothing publishes. Pick one: tag mode +custard release, or bookmark mode +jj bookmark set release/… && jj git push. Never bookmark mode + the CLI.
Tag-based repos are unaffected — bookmark mode is opt-in.
Cutting a release in bookmark mode
One-time setup
Add brew.source: bookmark to .custard.yaml in the repo:
brew:
enabled: true
package: "." # or ./cmd/<name>
source: bookmark # opt in to bookmark-mode releases
# bookmark_prefix: release/ # default; change only if you need a different prefix
# must end in "/" so the version is its own path segment
Register the release webhook (same command as tag mode; see MANUAL.md for webhook secret generation and registration details):
ssh soft repo webhook create <name> https://your-host/hooks/release \
-e branch_tag_create -c json -s "$WEBHOOK_SECRET"
Per-release workflow
# 1. Finish the commit that will ship
jj describe -m "Add the new feature"
# 2. Create or move the release bookmark to the current working commit
jj bookmark set release/v1.2.0
# 3. Push the bookmark
jj git push --bookmark release/v1.2.0
The webhook fires on the branch push. custard strips the prefix (release/) to extract the
version (v1.2.0), builds the tarball, writes the formula to the homebrew-tap repo, and publishes.
The version pill on the repo's page shows v1.2.0, labelled with the tip commit's subject line (jj
bookmarks carry no annotated message — the commit subject stands in).
Versions must be vMAJOR.MINOR.PATCH (no pre-release suffixes).
Installing
brew tap you/tap https://your-host/git/homebrew-tap.git
brew trust you/tap
brew install <name>
brew upgrade <name>
Deploys don't use tags or bookmarks
The tag/bookmark question above is a release (Homebrew) concern only. custard preview and
custard promote don't care about tags or bookmarks — they deploy the newest non-empty, pushed
commit (a clean git archive of it). So brew.source mode is irrelevant to deploying, and there's
nothing extra to configure for jj.
jj works transparently here: the empty working-copy commit sitting on top after a push is skipped
automatically, so custard deploys the real commit under it (@-). The only requirement is the usual
one — push first, because custard archives the pushed commit, not your working copy:
jj git push --bookmark main # or whatever bookmark tracks your trunk
custard preview # deploys the newest non-empty pushed commit → URL
custard promote # promotes that preview → production
How each jj operation shows up in custard
| jj operation | Effect in custard |
|---|---|
jj commit / jj describe + push |
new commit(s) appear in log; tree/blob views update |
jj bookmark set <name> + push |
branch appears (or updates) on the refs page |
jj bookmark set release/vX.Y.Z + push |
release publishes; version pill and tap formula update |
history rewrite + jj git push --force |
custard reads the new state (read-only, safe) |
jj git push --all |
all bookmarks land on Soft; all show on the refs page |
| change ID / op log | local-only; never visible in custard |
What's intentionally not supported
No auto-detection. custard cannot infer from the objects in a repo that it was authored with
jj. Bookmark mode must be enabled explicitly with brew.source: bookmark in .custard.yaml.
One mode at a time. A repo is in either tag mode (default) or bookmark mode. Switching is a one-way config flip — see MIGRATION.md for the procedure. After the flip, the tag path stops triggering releases and the bookmark path takes over. The two modes never mix, and there is no cross-mode version comparison.
Scope limit — the index list. The version shown for each repo on the forge's main index (repo
list) is always derived from the highest semver git tag. That code path does not read .custard.yaml
and is not mode-aware. For a pure-jj repo this means the index list shows no version; the
mode-aware version pill lives on each repo's own page and refs page, which is the primary surface.
This is a deliberate tradeoff and is not expected to change unless the index read is refactored.