Migrating from GitHub
Start with a neosource account. Imports read from GitHub without changing the
source repository. The CLI also updates your local remotes, preserving the old
one as github.
For what does and does not survive the move in general, see Migrating to neosource.
Move the code
Section titled “Move the code”Import through the web or bring an existing local checkout.
From the web, with your GitHub account linked
Section titled “From the web, with your GitHub account linked”Use a linked GitHub account to import public or private repositories.
- Go to neosource.dev/new and choose Import from GitHub.
- Connect GitHub. This is a one-time authorization, and it starts narrow: read access to public data only.
- Add private repositories if you need them — one more click, one more consent screen. GitHub words that consent as read and write access because that is the narrowest scope it offers for private repositories; importing only ever reads.
- Pick repositories. They are grouped by owner — your account first, then each organization — and a group’s checkbox takes the whole organization at once. Every row links out to its source on GitHub if you need to check which one you are looking at. A repository whose name already exists under the target owner is marked “A repo with this name already exists” and cannot be selected.
- Optionally tick Also sync issues and pull requests before importing. This is the one decision that is hard to change later — see Keep issues in sync.
- Import selected. Every chosen repository appears as a row at once and
moves through Waiting, Queued, Importing and Done (or Failed). The page sends
four requests at a time, polls progress every few seconds, and retries a busy
response after the server’s
Retry-After. Leaving while rows are still unsent asks for confirmation.
Imported repositories keep their GitHub visibility: a public repository is public on neosource, and a private (or internal) one is private. If neosource cannot read a repository’s visibility from GitHub, it imports it as private. Change it per repository in its settings at any time.
From a public Git URL
Section titled “From a public Git URL”The simplest path, and not GitHub-specific: paste any public HTTPS Git URL at neosource.dev/new → Import from URL.
No OAuth, no token, no account linkage. The URL is validated when the import is queued — it must be HTTPS, must not carry embedded credentials, and must resolve to a publicly routable host.
Use this for public repositories, or for a source that isn’t GitHub at all.
Bring a local checkout, with neo adopt
Section titled “Bring a local checkout, with neo adopt”If the project is already cloned on your machine, the CLI moves it in one
command — and, unlike the import paths, it also repoints your working copy so
git push goes to neosource from then on.
cd ~/code/my-projectneo adopt --dry-run # print the plan, change nothingneo adoptWhat it does, in this order:
- Creates the repository on neosource if it does not exist (private by default — adopt takes over an existing checkout, so the safe default is not to publish it).
- Pushes
refs/heads/*andrefs/tags/*by URL — never forced, never--mirror. - Only then rewrites your remotes: neosource becomes
origin, and your previousoriginis renamed togithub. Branches that tracked the oldoriginare re-pointed at the new one.
Because the remote surgery happens after the pushes succeed, a failed adopt
leaves your checkout exactly as it was. The old URL is never deleted — it is
still there under git remote -v as github.
neo adopt --to myorg/my-project # different owner or nameneo adopt --visibility public # only applies if it has to be createdneo adopt --old-remote upstream # call the demoted remote something elseMove everything at once
Section titled “Move everything at once”neo migrate is neo adopt for a whole projects directory:
neo migrate ~/code --dry-runneo migrate ~/codeIt scans immediate children by default (--depth 1, the ~/projects/<repo>
layout — raise it for ~/projects/<group>/<repo>), skips dotfile directories, never
follows symlinks, and stops at the first checkout on a path — a clone vendored
inside another repository is not adopted separately.
Failure is per repository, not per run. A repository that fails is reported and the scan continues, because aborting the batch would leave you with a half-migrated directory and no idea which half. Re-running is the retry: an already-adopted checkout is recognised and does no remote surgery a second time. The exit status is non-zero if anything failed, so a script does not have to parse the summary.
Start with --dry-run on a directory you have not migrated before.
Keep GitHub as a mirror
Section titled “Keep GitHub as a mirror”You do not have to choose. Pass --mirror-back and neosource registers a push
mirror to the GitHub repository your origin pointed at, so the old home keeps
receiving your commits:
neo adopt --mirror-backneo migrate ~/code --mirror-backThis needs repo admin on the neosource side and a linked GitHub account
carrying the admin:public_key scope, because the mirror authenticates with a
deploy key registered on the GitHub repository. If either is missing the command
says which one and carries on — a refused mirror does not fail the adopt, and
re-running with --mirror-back once you have granted the scope registers it.
Registering the same mirror twice is a no-op.
This is the recommended shape for a project that has users: develop here, keep the old location mirrored, update the GitHub README to point at the new home, and let people move over at their own pace.
Attach or detach GitHub ref sync
Section titled “Attach or detach GitHub ref sync”Beyond a one-time push mirror, a repository can be attached to a GitHub
upstream in GitHub-managed mode (/<owner>/<repo>/settings/github, or
neo repo github attach|confirm|cancel|detach). While attached, GitHub remains
the single ref authority: pushes reconcile against upstream, and if attaching
to an existing GitHub repository displaces any local branch tips, neosource
preserves those commits under refs/heads/neosource-preserved/… rather than
discarding them. Detaching (neo repo github detach) makes neosource the sole
authority again without deleting preserved refs.
A Read-only mirror can also detach: Move to neosource (Settings →
GitHub, or neo repo github detach) stops the pull from GitHub, makes the
repository neosource-managed, and keeps every ref and synced issue. A metadata
crawl already in flight may finish once. Afterwards you can choose Keep
GitHub updated, which registers the exact-replica GitHub App push mirror
after an explicit consent in the same panel. Both consent dialogs list what
the push would delete or overwrite on GitHub (GET /api/repos/{owner}/{repo}/mirrors/github/preview) and, when something would be
lost, require typing the repository name. If the GitHub App is installed but not
yet linked, Connect GitHub App (or neo repo github connect) finds the
installation for you; a suspended or uninstalled installation answers 403
github_app_install_required. While a repository still pulls
from GitHub, a replica push mirror aimed at that upstream is refused with a
relationship_conflict (409), since it would overwrite the source it mirrors.
Keep issues in sync
Section titled “Keep issues in sync”Importing brings the code. How the repository keeps following GitHub
afterwards is its relationship, chosen per repository in the import picker
— Plain import, Sync with GitHub or Read-only mirror — or changed
later in the repository’s Settings → GitHub panel
(/<owner>/<repo>/settings/github; the old settings/github-sync address
redirects there). GitHub push mirrors are managed in the same panel;
Settings → Mirrors holds only non-GitHub mirrors.
- Plain import: a one-time copy. Nothing follows GitHub afterwards.
- Sync with GitHub: git-only. GitHub owns the branches and tags; pushes
here go to GitHub first, and pushes on GitHub show up here. Native issues and
pull requests stay native, but a native pull request cannot be merged here —
its page says why. Turning sync on shows a preview of what happens to every
branch and tag first, and any local tip it would displace is preserved under
neosource-preserved/rather than discarded. Turning it off keeps every ref. - Read-only mirror: the repository stays GitHub’s, and neosource keeps a synced copy of its code, issues, comments and labels for reading.
On a read-only mirror, synced issues stay attached to their repository. GitHub controls their title, body, open/closed state, comments, and labels; file new issues on GitHub. neosource manages lane, priority, cycle, and assignees.
For work managed entirely in neosource, create a team ticket.
Requirements and limits
Section titled “Requirements and limits”Sync with GitHub works on any repository, imported or not, including one that already has native issues and pull requests. It requires:
- repo admin on the neosource repository, and
- the neosource GitHub App installed on the GitHub repository, with the repository linked to it — the panel shows each missing step and how to fix it.
A Read-only mirror requires:
- repo admin on the neosource repository,
- a linked GitHub identity with a stored access token — that account becomes the importer whose token the backfill and subsequent polls run on,
- a repository that was imported through the web GitHub import — one imported from a plain URL, or adopted from a local checkout, has no GitHub identity to mirror, and
- a repository with no natively-created issues or pull requests yet.
If a mirror’s metadata sync exhausts its failure budget it enters an error
state. Enabling it again re-arms it: the state returns to backfilling and
resumes from where its cursors left off rather than starting over.
Next steps
Section titled “Next steps”Set up CI
Section titled “Set up CI”Files in .github/workflows/ are checked for compatibility but do not run until
you enable them. Review that report on the Actions tab, then either run them
in place with neo repo set --ci-workflow-source neosource-and-github, or move
them to .neosource/workflows/ on the default branch.
Replace job container images with packages: and check the other differences
in Coming from GitHub Actions.
Point your clone at the new home
Section titled “Point your clone at the new home”If you imported from the web rather than using neo adopt, your local checkout
still pushes to GitHub. Repoint it:
git remote rename origin githubgit remote add origin ssh://git@neosource.dev/<owner>/<repo>.gitgit push -u origin mainAnyone else with a clone does the same, or re-clones.
Tell people
Section titled “Tell people”Update the GitHub repository’s README and description to say where development happens now. If you set up mirroring back, the GitHub copy keeps working for anyone who has not moved yet, which makes that notice the only thing they have to act on.
Watch an import
Section titled “Watch an import”Imports run in the background and the repository page shows progress. If you want it programmatically:
GET /api/repos/{owner}/{repo}/import-statusFrequently asked questions
Section titled “Frequently asked questions”Does importing change anything on GitHub?
No. Imports read. neo adopt writes only to your local remotes, and only after
its pushes to neosource have succeeded. The one exception is --mirror-back,
which registers a deploy key on the GitHub repository — because you asked it to.
Do I lose my commit history? No. Every branch and every tag comes across as real objects, not a squashed snapshot or a shallow clone, and your commit IDs are unchanged.
Can I keep pushing to GitHub?
Yes — either keep the github remote and push to both by hand, or register a
push mirror with --mirror-back and let neosource do it.
My repository is private. Is it private here? Yes. An import from GitHub keeps the repository’s GitHub visibility, so a private repository stays private (and a public one public). Change it per repository afterwards in its settings.
Do my open pull requests come across? No. The branches do, so no work is lost, but the review threads stay on GitHub. Re-open the PRs here against the branches that arrived.
Can I import issues from Linear or Jira? Not yet. GitHub is the only issue importer today — see what is not built yet.
What if the import fails halfway?
Re-run it. Imports are addressed by owner/name and are safe to retry, and
neo migrate in particular is designed around re-running as the retry path.