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.
Move the code
Section titled “Move the code”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.
- 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.
- 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.
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.
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.
Keep issues in sync
Section titled “Keep issues in sync”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.
Requirements and limits
Section titled “Requirements and limits”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.
Next steps
Section titled “Next steps”Your CI will not run yet
Section titled “Your CI will not run yet”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.
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. 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.