Skip to content

Migrating from GitHub

This guide moves a project from GitHub to neosource. It assumes you have a neosource account; everything else is covered here.

Nothing in it deletes anything on GitHub. An import reads from GitHub and never writes to it, and the CLI path demotes your old remote rather than dropping it. If you decide halfway through that you would rather not, stop — you have lost nothing.

For what does and does not survive the move in general, see Migrating to neosource.

There are three ways in. Pick by what you have in front of you.

From the web, with your GitHub account linked

Section titled “From the web, with your GitHub account linked”

The path most people want, and the only one that reaches 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.
  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.

Imported repositories are private on neosource regardless of how they were set on GitHub. Change that per repository in its settings once you are happy with what landed.

The simplest path, and not GitHub-specific: paste any public HTTPS Git URL at neosource.dev/newImport 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.

Importing brings the code. Issues, comments and labels are a separate opt-in, available only for repositories that came in through the web GitHub import — a repository imported from a plain URL, or adopted from a local checkout, has no GitHub identity to sync against.

Enable it at import time with the checkbox, or afterwards at /<owner>/<repo>/settings/github-sync. There is one posture, a read-only mirror: the repository stays GitHub’s, and neosource keeps a synced copy of its issues, comments and labels for reading.

What arrives are issues, on the repository they came from — GitHub issues are repo-scoped intake, which is exactly what an issue is here, so nothing is reshaped on the way in. They do not become tickets: a ticket is a team’s own planning work, and rewriting someone’s bug report as one would be a claim about who owns it.

The split is the point. GitHub owns issue title, body, open/closed state, comments and labels, and remains where new repository issues are filed — creating one here answers 409 and steers you to a ticket instead. neosource owns the planning overlay on top — lane, priority, cycle, assignees — which is the layer GitHub does not have, so the two do not fight.

Enabling sync 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 from GitHub, and
  • a repository with no natively-created issues or pull requests yet.

If a sync exhausts its failure budget it enters an error state. Calling enable again re-arms it: the state returns to backfilling and resumes from where its cursors left off rather than starting over.

neosource reads .neosource/workflows/, and nothing in .github/workflows/ is ever registered or dispatched. Those files are parsed, and the repository’s Actions tab lists each one with whether it would run as-is and any warnings against it — so you get a report rather than silence. But an imported repository has no CI until you move and adapt the files.

Start with Coming from GitHub Actions, which covers what carries over unchanged and what has to be rewritten. The main rewrite is that jobs declare tools with packages: rather than choosing a container image — see the job environment.

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. Imported repositories are private on neosource regardless of their GitHub visibility, and you change that per repository afterwards.

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.