Skip to content

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.

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.

  1. Go to neosource.dev/new and choose Import from GitHub.
  2. Connect GitHub. This is a one-time authorization, and it starts narrow: read access to public data only.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

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.

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.

Terminal window
cd ~/code/my-project
neo adopt --dry-run # print the plan, change nothing
neo adopt

What it does, in this order:

  1. 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).
  2. Pushes refs/heads/* and refs/tags/* by URL — never forced, never --mirror.
  3. Only then rewrites your remotes: neosource becomes origin, and your previous origin is renamed to github. Branches that tracked the old origin are 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.

Terminal window
neo adopt --to myorg/my-project # different owner or name
neo adopt --visibility public # only applies if it has to be created
neo adopt --old-remote upstream # call the demoted remote something else

neo migrate is neo adopt for a whole projects directory:

Terminal window
neo migrate ~/code --dry-run
neo migrate ~/code

It 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.

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:

Terminal window
neo adopt --mirror-back
neo migrate ~/code --mirror-back

This 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.

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.

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.

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.

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.

If you imported from the web rather than using neo adopt, your local checkout still pushes to GitHub. Repoint it:

Terminal window
git remote rename origin github
git remote add origin ssh://git@neosource.dev/<owner>/<repo>.git
git push -u origin main

Anyone else with a clone does the same, or re-clones.

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.

Imports run in the background and the repository page shows progress. If you want it programmatically:

GET /api/repos/{owner}/{repo}/import-status

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.