How Fork Routing Works for GitHub Pull Requests in no-mistakes

no-mistakes routes GitHub pull requests through a contributor's fork by storing an optional fork_url in the local database and passing it to the gh CLI via the --head flag during PR creation.

The no-mistakes toolchain automates pull request workflows against upstream repositories while preserving the standard GitHub fork-and-contribute model. When a repository is initialized with a fork URL, the pipeline automatically constructs SCM commands that target the upstream repository while using the fork's branch as the source.

Where the Fork URL Is Stored

Fork routing begins with persistence in the local SQLite database. When you run no-mistakes init --fork-url <url>, the value is written to the fork_url column of the repos table defined in internal/db/repo.go.

INSERT INTO repos (id, working_path, upstream_url, fork_url, default_branch, created_at) …

Throughout the pipeline execution, this value is accessible via sctx.Repo.ForkURL from the configuration model in internal/config/config.go. If this string is empty, the tool assumes you are working directly against the upstream repository; if populated, it triggers fork-aware host construction.

Building the SCM Host with Fork Support

The routing decision occurs in internal/pipeline/steps/host.go within the buildHost function. For GitHub repositories, the logic resolves both the upstream slug and an optional fork slug before instantiating the host.

case scm.ProviderGitHub:
    // Resolve the owner/name slug …
    repo := github.RepoSlug(sctx.Repo.UpstreamURL)
    if repo == "" && sctx.Run.PRURL != nil {
        repo = github.RepoSlug(*sctx.Run.PRURL)
    }
    forkRepo := ""
    if sctx.Repo.ForkURL != "" {
        forkRepo = github.RepoSlug(sctx.Repo.ForkURL)   // ← fork routing
    }
    return github.NewWithFork(cmdFactory,
        func() bool { return stepCLIAvailable(sctx, provider) },
        repo, forkRepo), ""
  • repo captures the upstream repository slug (e.g., owner/main-repo).
  • forkRepo captures the fork slug only when sctx.Repo.ForkURL is non-empty.

The constructor github.NewWithFork in internal/scm/github/github.go wires the gh CLI to include both --repo <upstream> and --head <fork> arguments. This instructs GitHub to open a PR from the fork into the upstream repository. If fork_url is empty, the code falls back to github.New, which omits the --head flag and creates the PR from the current checkout.

PR Creation Without Fork Logic

The actual PR step in internal/pipeline/steps/pr.go contains no conditional fork logic. The Execute method simply delegates to the host interface.

created, err := host.CreatePR(ctx, branch,
    sctx.Repo.DefaultBranch, scm.PRContent(content))

Because the host was already configured with fork awareness during the buildHost phase, host.CreatePR automatically expands to a gh pr create --repo <upstream> --head <fork> command when appropriate. This separation of concerns keeps the PR step provider-agnostic while the host encapsulates the GitHub-specific fork routing.

Provider Limitations and Guardrails

Fork routing is explicitly limited to GitHub. The buildHost function contains guardrails for other providers in internal/pipeline/steps/host.go:

case scm.ProviderGitLab:
    if sctx.Repo.ForkURL != "" {
        // Fork PR routing for GitLab is intentionally not half‑wired.
        return nil, "fork PR routing for GitLab is not implemented"
    }
...
case scm.ProviderBitbucket:
    if sctx.Repo.ForkURL != "" {
        // Fork PR routing for Bitbucket is intentionally not half‑wired.
        return nil, "fork PR routing for Bitbucket is not implemented"
    }

Attempts to use a fork_url with GitLab or Bitbucket result in a clear error message and pipeline skip, preventing half-implemented behavior.

End-to-End Workflow Example

  1. Initialize the repository with the contributor's fork URL.
  2. Query the database to verify persistence.
  3. Execute the pipeline, which constructs the appropriate gh command.

When sctx.Repo.ForkURL is present, the effective CLI command generated by the host resembles:

gh pr create --repo owner/no-mistakes \
             --head jdoe/no-mistakes-fork \
             --base main \
             --title "feat: add X" \
             --body "## What Changed …"

Practical Code Examples

Initializing a Repository with a Fork URL

no-mistakes init --fork-url https://github.com/jdoe/no-mistakes-fork.git

Verify the configuration via the database helper:

repo, _ := db.GetRepoByPath("./")
fmt.Println(repo.ForkURL) // → https://github.com/jdoe/no-mistakes-fork.git

Running the Pipeline

no-mistakes run

The PR step automatically invokes the fork-aware host logic from internal/pipeline/steps/host.go (lines 24–37).

Disabling Fork Routing

To create a PR directly from the current checkout without using the fork:

// Clear the fork URL before a run
repo.ForkURL = ""
db.UpdateRepoForkURL(repo.ID, "")

This forces buildHost to use github.New instead of github.NewWithFork, omitting the --head flag from the resulting gh command.

Summary

  • Storage: The fork_url column in internal/db/repo.go persists the contributor's fork location.
  • Host Construction: internal/pipeline/steps/host.go detects the fork URL and instantiates github.NewWithFork to enable routing.
  • Command Generation: The GitHub host in internal/scm/github/github.go translates the fork slug into a --head flag for the gh CLI.
  • Provider Scope: Fork routing is implemented only for GitHub; GitLab and Bitbucket explicitly reject fork URLs with clear error messages.
  • Step Delegation: internal/pipeline/steps/pr.go remains agnostic of fork logic, relying on the pre-configured host to handle upstream vs. fork targeting.

Frequently Asked Questions

Can I use fork routing with GitLab or Bitbucket repositories?

No. As implemented in internal/pipeline/steps/host.go, the buildHost function returns an explicit error if sctx.Repo.ForkURL is non-empty for GitLab or Bitbucket providers. The error messages state that "fork PR routing for [Provider] is not implemented," preventing ambiguous behavior.

What happens if I don't specify a fork URL during initialization?

If the fork_url column is empty (or cleared via the database), buildHost invokes github.New instead of github.NewWithFork. The resulting PR creation command omits the --head flag, causing gh pr create to open a pull request from the current local branch directly against the upstream repository.

How does no-mistakes resolve repository slugs when both upstream and fork URLs are present?

The buildHost function in internal/pipeline/steps/host.go uses github.RepoSlug() to parse the owner/name pair from both sctx.Repo.UpstreamURL (stored as repo) and sctx.Repo.ForkURL (stored as forkRepo). These slugs are passed separately to github.NewWithFork, ensuring the --repo flag targets the upstream while the --head flag references the fork.

Is it possible to switch between fork and non-fork PR creation for the same repository?

Yes. You can toggle fork routing by updating the fork_url value in the database using db.UpdateRepoForkURL() or by re-running no-mistakes init with or without the --fork-url flag. The pipeline reads this value fresh during each buildHost invocation, allowing you to alternate between contributing from a fork and pushing directly to the upstream repository.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →