# 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` / `` with your values. See also [MANUAL.md](MANUAL.md). ## Step 1 — move the git repo (every migration) ```sh # 1. create it on Soft (or let it autocreate on first push) ssh soft repo create -d "what it is" # 2. point the local clone at Soft git remote set-url origin soft: # full move, or: git remote add soft soft: # 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 true|false ``` > **Remote naming — `origin` is always the primary.** Decide which forge is primary, > then name accordingly (see DevFlow `decisions/0003`; custard's `release` verb pushes > with `git push origin`, so the release target must be `origin`): > - **Soft Serve primary** (full move) → Soft is `origin`, GitHub (if kept) becomes `github`: > `git remote set-url origin soft:` then `git remote rename origin github` first > if you're demoting an existing GitHub `origin`. The clone URL is `soft:` — the > `soft:` is the SSH host alias, not the remote name. > - **GitHub stays primary** (Soft as a backup mirror) → leave GitHub as `origin`, add > Soft as `soft`: `git remote add soft soft:`. Push to the mirror explicitly > (`git push soft …`); custard release stays pointed at the GitHub `origin`. ## Step 2 — drop GitHub-only cruft (full self-host) - `.github/workflows/*` — GitHub Actions → replaced by `.custard.yaml` `ci` + `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. ```sh 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](https://backlog.md): tasks live in `backlog/tasks/` and render on `/r//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: 1. Build locally (Hugo/Jekyll/Astro/HTML) → an output dir (`public/`, `dist/`, …). 2. Add a Caddy site block for the domain → `file_server` over the output dir. 3. Point the domain's DNS at the server; Caddy issues TLS. 4. **Optional — gate through custard** with rsync in `.custard.yaml`: ```yaml ci: ["npm run build"] deploy: preview: "rsync -a ./public/ :/var/www/-preview/" promote: "rsync -a ./public/ :/var/www//" ``` Then `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**: 1. `vercel link` the repo to the existing Vercel project (keeps the prod URL), then note the `orgId` + `projectId` it writes to `.vercel/project.json`. 2. In Vercel's dashboard, **disconnect the GitHub integration** (stop it deploying the old repo). 3. `.custard.yaml` — **pin the project with env IDs.** custard deploys a clean `git archive HEAD` export, so `.vercel/project.json` (gitignored) is not present; without the IDs the CLI creates a stray new project. The IDs are identifiers, not secrets — safe to commit. Auth still comes from your local `vercel login`. ```yaml ci: ["pnpm test", "pnpm build"] deploy: preview: VERCEL_ORG_ID=team_xxx VERCEL_PROJECT_ID=prj_xxx vercel deploy --yes promote: VERCEL_ORG_ID=team_xxx VERCEL_PROJECT_ID=prj_xxx vercel promote {{url}} --yes ``` 4. 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 ```yaml # .custard.yaml brew: { enabled: true, package: "." } # or ./cmd/ ``` ```sh ssh soft repo webhook create 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 ``` 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) ```sh git remote add github git@github.com:you/.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](https://github.com/martinvonz/jj), 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`: ```yaml 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](JUJUTSU.md) for the full jj usage guide, including how each operation appears in custard and the per-release push workflow.