Migrating GitHub → Soft Serve + custard
Move a repo off GitHub onto a self-hosted Soft Serve + custard forge, including deployment.
Replace your-host / you/tap / <server> with your values. See also MANUAL.md.
Step 1 — move the git repo (every migration)
# 1. create it on Soft (or let it autocreate on first push)
ssh soft repo create <name> -d "what it is"
# 2. point the local clone at Soft
git remote set-url origin soft:<name> # full move, or:
git remote add soft soft:<name> # keep GitHub as a backup mirror
# 3. push everything (history, branches, tags)
git push origin --all && git push origin --tags
# 4. set visibility (public repos show on the forge)
ssh soft repo private <name> true|false
Step 2 — drop GitHub-only cruft (full self-host)
.github/workflows/*— GitHub Actions → replaced by.custard.yamlci+custard check/preview..goreleaser.yaml— if releasing to a GitHub-hosted brew tap → replaced by custard's tap.- README badges/links pointing at github.com.
git rm -r .github .goreleaser.yaml && git commit -m "drop GitHub CI/release" && git push
Step 3 — issues → Backlog.md
GitHub Issues don't transfer. Use Backlog.md: tasks live in backlog/tasks/
and render on /r/<name>/issues. Recreate open issues by hand.
Deployment, by type
A. Static site / GitHub Pages → your server
Pages goes away with GitHub. Serve the built site from your server with Caddy:
- Build locally (Hugo/Jekyll/Astro/HTML) → an output dir (
public/,dist/, …). - Add a Caddy site block for the domain →
file_serverover the output dir. - Point the domain's DNS at the server; Caddy issues TLS.
- Optional — gate through custard with rsync in
.custard.yaml:
Thenci: ["npm run build"] deploy: preview: "rsync -a ./public/ <server>:/var/www/<site>-preview/" promote: "rsync -a ./public/ <server>:/var/www/<site>/"custard preview(build + stage) →custard promote(go live).
custard is a git forge, not a static host — Caddy serves the files; custard just runs the build/deploy if you want the check+gate flow.
B. Vercel app — stay on Vercel, point at the Soft repo
Vercel's git integration supports only GitHub/GitLab/Bitbucket, so a Soft-hosted repo can't auto-deploy via Vercel's hook. Keep Vercel as the host, deploy from the CLI:
vercel linkthe repo to the existing Vercel project (keeps the prod URL).- In Vercel's dashboard, disconnect the GitHub integration (stop it deploying the old repo).
.custard.yaml:ci: ["pnpm test", "pnpm build"] deploy: preview: vercel deploy promote: vercel promote {{url}}- Push to Soft →
custard preview(build HEAD, run ci, preview URL) →custard promote(same build → prod).
You keep Vercel hosting + URLs; you trade Vercel's auto-PR previews + GitHub checks for on-demand
custard preview + .custard.yaml ci.
C. CLI / TUI → Homebrew
# .custard.yaml
brew: { enabled: true, package: "." } # or ./cmd/<name>
ssh soft repo webhook create <name> https://your-host/hooks/release \
-e branch_tag_create -c json -s "$WEBHOOK_SECRET"
custard release v0.1.0
# install: brew tap you/tap https://your-host/git/homebrew-tap.git && brew install <name>
Replaces GitHub Releases + a GitHub-hosted tap.
D. Library / plain code
Step 1 is the whole migration — browsable on the forge, tag versions as usual.
What you give up off GitHub → the replacement
| GitHub feature | On Soft + custard |
|---|---|
| Pull requests / review | Push-based + custard check/preview gate; branches if needed |
| GitHub Actions CI | .custard.yaml ci via custard check/preview |
| Issues | Backlog.md (backlog/tasks/, shown on the forge) |
| Releases + assets | custard /dl tarballs + the tap |
| Pages | Caddy static hosting on your server |
| Vercel/Netlify git auto-deploy | custard preview/promote (CLI) |
| Dependabot / scanning | none (manual) |
Keep GitHub as a mirror (optional)
git remote add github git@github.com:you/<name>.git
git push github --mirror # occasionally
(Then you're primary-on-Soft, not fully off GitHub.)
Switching a repo from git tags to jj bookmarks
If you move a repo's workflow to jujutsu, you lose the ability
to create annotated git tags — which is how custard's Homebrew release pipeline normally triggers.
Bookmark mode lets jj repos publish releases from release/vX.Y.Z bookmarks (git branches) instead.
This is an intentional, one-way flip. Do not switch back and forth; the two modes never mix.
The config change
In .custard.yaml, set brew.source: bookmark:
brew:
enabled: true
package: "."
source: bookmark
# bookmark_prefix: release/ # default; omit unless you need a different prefix
Commit and push. After the flip, the tag-based trigger stops firing — future tag pushes are ignored
for release purposes. Releases come only from release/* bookmark pushes going forward.
Version continuity and the downgrade guard
Past tag-based releases already published to the tap are unaffected; users with those versions
installed can still brew upgrade once the first bookmark release lands.
The downgrade guard is bookmark-scoped: it compares the incoming bookmark version only against
the highest already-published release bookmark (i.e. other release/vX.Y.Z branches). It does not
consult the old git tags at all. At the moment you flip, there are zero release bookmarks, so the
guard imposes no floor — it would accept any semver version as the first bookmark release.
The real reason your first bookmark release must be >= your last published tag is Homebrew
upgrade continuity: existing tap users have that tag's version installed, and brew upgrade only
moves them forward if the new formula version is strictly higher. That is a convention you must
honour manually, not something the guard enforces. If your last tag was v1.3.0, the first
release/vX.Y.Z bookmark must be v1.3.0 or higher — otherwise existing users would never
pick up the new release via brew upgrade.
Plan the cutover version accordingly — bumping the patch is fine: v1.3.1 works.
Day-to-day workflow after the flip
See JUJUTSU.md for the full jj usage guide, including how each operation appears in custard and the per-release push workflow.