Skip to main content
Private preview. fremforge is in private preview — invited customers only. Content is still subject to change. Request access →
From GitHub

Migrate to fremforge from GitHub

This guide walks a typical GitHub team through a migration to fremforge. It covers GitHub.com, GitHub Enterprise Cloud (GHEC), and GitHub Enterprise Server (GHES). Read it end-to-end before starting. Mid-migration surprises are expensive.

If you have not run the pre-migration checklist against your current GitHub tenant yet, do that first. It surfaces large repositories, LFS usage, Marketplace action dependencies, and third-party integrations that need specific attention.

Before you start

What you need on your side:

  • An owner-role account on the GitHub organisation you are migrating from.
  • A personal access token on GitHub with repo, admin:org, workflow, and admin:public_key scopes (classic PAT) or the fine-grained equivalent.
  • Admin access to the fremforge organisation you are migrating to (the org is created at signup).
  • For Actions migration: an inventory of the GitHub Marketplace actions you currently use and their versions.

What we recommend:

  • Freeze new PRs and merges to the main branches you are migrating for the migration window. Concurrent writes during migration cause reconciliation pain you do not need.
  • Run the migration against a non-production test organisation on fremforge first if you have any doubt about how your workflows adapt. The 30-day free trial is designed for exactly this.
  • Migrate one repository end-to-end and verify it builds cleanly before bulk-migrating the rest.

Time estimate (planning ranges, not commitments):

  • A single small repository (≤100 MB, no LFS, simple Actions): 15-30 minutes of hands-on time plus CI verification.
  • A medium repository (≤1 GB, some LFS, moderate Actions complexity): 1-2 hours including workflow adaptation.
  • A bulk migration of 20-50 repositories: 1-2 engineering days spread across a week, dominated by Actions adaptation rather than the repo migration itself.

These ranges come from documentation review against Forgejo upstream performance characteristics and 2 fremverk-internal pre-launch migrations; actual times will be informed by our first 5 customer migrations. They are planning aids, not contractual SLAs. Real-world migration time scales with: (a) repo count and total LFS volume; (b) Actions complexity and the number of uses: references that need rewriting against fremforge’s runner mirror; (c) team size that can absorb the verification load. If you’re migrating > 50 repos or have non-trivial Actions usage, plan for at least 2× the upper bound as a buffer and run a single-repo pilot first.

Feature parity matrix

What fremforge ships today vs GitHub Enterprise Cloud, focused on the security/auth surface customers ask about most:

fremforge defaults new orgs to HTTPS-only Git access (SSH disabled by default since 2026-05-22) with the pre-registered Git Credential Manager OAuth app — developers get MFA via your org’s IdP on every push refresh. The table below shows where each capability lands.

CapabilityGHECfremforgeNotes
HTTPS + GCM (OAuth interactive, MFA via IdP)Recommended defaultPre-registered OAuth app — developers run two git config lines from the Secure sign-in guide. MFA via your org’s IdP fires on every token refresh.
HTTPS + PAT push (scoped + expiry)fremforge admins can cap PAT lifetime org-wide. Use for CI/scripts, not human push.
SSH key push (any algorithm)⚠️ Disabled by defaultPer-org toggle on <org>/_admin/auth-policy/ — re-enable if your workflow needs SSH. Reason: source-IP enforcement isn’t structurally possible on SSH (see below).
SSH certificate authorityNative admin UI at <org>/_admin/auth-policy (sub-section under “Allow SSH protocol”) for orgs that have re-enabled SSH — paste CA pub key, principal policy, optional “require cert auth”.
OIDC tokens for CI workload identityForgejo Actions ships /api/actions/.well-known/openid-configuration + JWKS. AWS/Azure/GCP/T Cloud federation works today.
Hardware-backed SSH key requirement⚠️ Policy only✅ Policy + watchdogToggle on <org>/_admin/auth-policy/; hourly watchdog emits tenant.auth_policy.hw_key_violation audit event per non-hw-backed key.
Signed commit enforcementOrg-wide toggle + active propagator that creates/PATCHes per-repo branch-protection rules.
Pre-receive hook secret scanning✅ Push protection✅ Gitleaks pre-receiveDifferent vendor, same security gate.
Per-org IP allowlist — web UI + REST APIConfigure CIDRs at <org>/_admin/security.
Per-org IP allowlist — HTTPS Git pushapi streaming proxy at git smart-HTTP paths gates on customer IP via Bunny XFF.
Per-org IP allowlist — HTTPS Git pull/cloneSame api streaming proxy path.
Per-org IP allowlist — SSH Git push/pull❌ N/A by architectureForgejo’s built-in SSH server doesn’t propagate source IPs to enforcement hooks + the OTC load balancer SNATs SSH connections. The kill-switch approach (ssh_disabled=true, default) replaces IP-based SSH enforcement; orgs that need real-IP gating on Git operations use HTTPS-only + IP allowlist scope=web+git.
SSH audit-trail (timing + fingerprint resolution)⚠️ Vendor-specificLTS scraper resolves fingerprint → user via Forgejo’s public_key table + emits git.ssh_pull.ip_allowlist_observed_violation + git.ssh.tenant_disabled_observed audit events. Useful for “post-offboarding key usage” alerts.
IP-allowlist audit-only preview mode“Audit only” toggle that logs would-be denials without blocking — useful for safely rolling out an allowlist before enforcing.
Per-tenant BYOIDC❌ One SAML/Entra org per GHEC tenant✅ Native — every customer org binds its own IdPMatters for multi-tenant SaaS hosting on fremforge.
Multi-source OIDC SSO⚠️ Single SAML/OIDC✅ Multi-sourceMultiple IdP buttons on the tenant login page.
EU-only data plane❌ US✅ Native (T Cloud EU-DE)Hard requirement for many EU regulated customers.
Per-user pricing$21/user/mo (GHEC)$0 (no per-user license)fremforge bills on org capacity + storage.
Apply review suggestions in PRClick-through commit in a PR review’s ```suggestion code-fence — head-SHA + branch verified server-side. Customer docs at Develop → Issues and PRs → Apply review suggestions. Shipped 2026-05-24.
Public docs wiki on private repos❌ GitHub Pages requires public repo or GHEC Pages add-on✅ Native opt-in per repoAnonymous public-read of the repository wiki at frem.sh/<org>/<repo>/wiki[/...] independent of whether the underlying repo is public or private. Customer docs at Develop → Wiki and public docs. Shipped 2026-05-24.
Customer-tunable audit retention❌ fixed 7y (no customer control)✅ Per-tenant {90 / 180 / 365 / 730}d hot-tier + 3y WORMTenant admin sets the queryable hot-tier window at Authentication policy → Audit log retention; 3-year WORM hash-chain archive unchanged. Defaults: 90d standard plan, 365d enterprise. Shipped 2026-05-24.

The strategic difference vs GHEC: fremforge treats HTTPS+GCM as the primary path because MFA propagates naturally through the IdP at every token refresh; SSH is opt-in per org because its IP enforcement is structurally weaker on this stack. For most enterprise security posture asks (“MFA on every push”, “IP-restrict where developers can push from”), the HTTPS+GCM default is strictly stronger than the SSH-key default GHEC inherits from GitHub’s history.

Step 1: Repository and history migration

fremforge runs upstream Forgejo, which ships a native importer for GitHub repositories. The importer copies the full Git history, issues, pull requests, comments, labels, milestones, wiki content, and releases, not just the default branch.

Using the native importer (UI path)

From the fremforge admin UI for your organisation:

  1. Click + New → Migration.
  2. Select GitHub as the source.
  3. Paste the GitHub repository URL (e.g. https://github.com/acme/backend).
  4. Paste your GitHub personal access token. The token is used for this import only and is not stored after the import completes.
  5. Tick the content types to import: Wiki, Issues, Pull requests, Releases, Labels, Milestones.
  6. Choose the target organisation and repository name on fremforge. Defaults mirror the GitHub source.
  7. Click Migrate repository.

The importer runs in the background. For a typical 100-500 MB repository with moderate issue/PR history, expect 2-10 minutes. A progress indicator in the admin UI shows the current stage (cloning Git history, importing issues, importing PRs, importing releases).

Using the API (scriptable path)

For bulk migrations, script the import against the fremforge API. The migration, repo, issue, and PR endpoints are the Forgejo-native API surface that fremforge exposes unchanged at frem.sh/api/v1/...; they are not separately documented in the fremforge OpenAPI spec (which covers only the fremforge control-plane additions, seats, policy, findings, exports, mandates). For the live Forgejo-native endpoint reference, see docs.frem.sh/api/openapi.yaml or the upstream Forgejo OpenAPI docs at docs.codeberg.org/api/ — fremforge does not host the Forgejo Swagger UI in production (intentionally disabled as a known recon target). All endpoints below are standard Forgejo/Gitea API.

# Build the JSON body via jq so the token is JSON-string-escaped safely
# (a token containing `"` or `\` would otherwise break the body or
# corrupt your migration auth).
jq -nc \
  --arg clone_addr "https://github.com/acme/backend.git" \
  --arg auth_token "$GITHUB_TOKEN" \
  --arg repo_owner "acme" \
  --arg repo_name  "backend" \
  '{clone_addr: $clone_addr, auth_token: $auth_token,
    service: "github", repo_owner: $repo_owner, repo_name: $repo_name,
    wiki: true, issues: true, pull_requests: true,
    releases: true, labels: true, milestones: true,
    lfs: true}' \
| curl -X POST "https://frem.sh/api/v1/repos/migrate" \
    -H "Authorization: Bearer ${FREMFORGE_TOKEN}" \
    -H "Content-Type: application/json" \
    -d @-

Parallelise across repositories with GNU parallel or a small driver script. Rate-limit to 3-5 concurrent imports to avoid saturating either GitHub’s PAT rate limit or your local bandwidth.

What the importer imports vs does not

Imports correctly:

  • All branches and tags, with full commit history.
  • Issues with titles, bodies, comments, labels, milestones, assignees (mapped by email where the user exists on fremforge).
  • Pull requests including review comments, merge status, close reason. Open PRs become open PRs; closed PRs are closed.
  • Releases including binary attachments (moved to fremforge’s OBS-backed LFS pool).
  • Wiki pages.

Does not import:

  • GitHub Actions workflow run history. Historical runs stay on GitHub; new runs start fresh on fremforge.
  • Secret values and environment variables, you re-enter these (see Step 2).
  • Third-party integrations configured on GitHub. These need reconnecting (see Step 5).
  • GitHub Projects (classic or Projects v2). Forgejo does not have a 1:1 equivalent; basic project-board content can be re-created manually or via the fremforge Projects feature (Phase 1.5).
  • Dependabot alerts and security advisories tied to GitHub’s Advisory Database. fremforge’s dependency scanning runs independently on Trivy/osv-scanner and produces its own findings.
  • GitHub Discussions. No equivalent in Forgejo today.
  • Per-user notification preferences. Members re-subscribe on fremforge.

Large repositories

If your repository is over 2 GB or your LFS pool exceeds 5 GB, the UI-driven importer may time out. The Forgejo /repos/migrate endpoint does not expose a per-call timeout knob, so the recommended fallback is to run a direct Git-mirror clone locally and git push --mirror into a pre-created empty fremforge repository:

# Clone the GitHub repo bare with full history
git clone --mirror https://github.com/acme/backend.git
cd backend.git

# If the repo uses LFS, fetch all LFS content
git lfs fetch --all

# Create an empty repo on fremforge (via admin UI or API), then:
git remote set-url origin https://frem.sh/acme/backend.git
git push --mirror
git lfs push --all origin

For this path, issues and PR history are not included. Import them separately via the Forgejo migrate API (POST /api/v1/repos/migrate, see the canonical curl example at the top of this page) with "wiki": false, "issues": true, "pull_requests": true, point the request at the already-pushed fremforge repo URL so only the metadata is fetched from GitHub.

Pre-receive secret-scanning will gate this push. fremforge runs gitleaks as a Forgejo pre-receive hook on every push, including the --mirror shape above. If your historical commits contain real secrets — leaked AWS keys, expired Stripe tokens, embedded private keys — the push is rejected with the matching commit SHAs printed and the entire mirror operation fails atomically. This is intentional: importing a public-history secret into fremforge does NOT silently rotate it on the upstream provider, so the safe path is to scrub-then-import. Two recovery options:

  1. Scrub history first with git-filter-repo (recommended) or BFG Repo-Cleaner, then re-push. Notify the secret-owner that the upstream provider’s history is also leaky regardless of what you do on the destination — rotate the leaked credentials at the source.
  2. Request a one-shot bypass from support@frem.sh for known-revoked secrets in archived history. Bypass is logged, audited, and tied to a written confirmation that the secrets are already rotated.

Land main first to keep the LFS push useful. Because git push --mirror is atomic against gitleaks (all refs in one transaction; one reject = empty mirror = LFS push has nothing to push to), it’s worth staging a single-branch push first to confirm the rules-feed before mass-pushing every ref. Between git remote set-url and git push --mirror:

git push origin main --force-with-lease

If main passes, the rest of the refs almost always pass (gitleaks rules are content-not-branch-based). If main is rejected, scrub or request bypass before re-attempting the mirror — there’s no value in pushing other refs while the secret-bearing commit on main is still live.

The hook is the same gitleaks rules-feed that protects every fremforge repo, not a migration-specific check. Run pre-migration-check.sh before starting; it surfaces the same findings the hook would catch, so you can scrub before the migration window opens.

LFS storage quota and SFS Turbo capacity

git lfs push --all origin writes objects into your fremforge tenant’s LFS quota. Plan quotas:

Seat planBundled LFS storageSoft cap before throttling
Standard (€30/seat/mo)10 GiB per seat, pooled at the org level90% of pool, uploads start returning 429 quota_warning
Enterprise-on-demandNegotiatedPer contract

Two practical limits sit underneath the plan quota:

  • SFS Turbo capacity for bare git repos is sized for the working tree, not for LFS, LFS objects land in OBS. If the repo’s bare-.git (post-git gc --aggressive) exceeds ~5 GiB, contact support@frem.sh before the migration so we can confirm SFS Turbo headroom on the workload share.
  • OBS LFS pool scales linearly with payment but is rate-limited at the OBS request layer. git lfs push of more than ~100 GiB in a single session may need to be split into batches of ≤50 GiB to avoid OBS request-rate throttling. The error shape is HTTP 503 with X-Reserved: ratelimit on the response, back off 60 s and resume; LFS is resumable.

If you’re not sure how big the LFS pool will be, run git lfs ls-files --size --all | awk '{s+=$2} END {print s/1024/1024/1024 " GiB"}' against the source repo before kicking off the mirror push.

Step 2: Secrets and environment variables

GitHub stores secrets at organisation, environment, and repository levels. fremforge uses the same three scopes.

Critical: secret values cannot be exported from GitHub. GitHub’s API returns secret names but never values. You either know the value (rotate on migration, recommended) or retrieve it from your secrets-management source of truth (password manager, vault, CI orchestration tooling).

Re-entering secrets

For each secret, decide:

  • Is this a long-lived credential that should be replaced by OIDC federation anyway? If yes, configure OIDC federation to T Cloud (see Step 3) and delete the secret rather than migrating it.
  • Is this a third-party API key or service credential with no OIDC alternative? Rotate the credential at the source, then set the new value in fremforge.
  • Is this a cryptographic key used inside builds? Rotate it, then set in fremforge.

Set secrets at the appropriate scope in the fremforge admin UI:

  • Organisation secrets, available to every repository in the org. Set at Settings → Secrets → Organisation.
  • Environment secrets, available only in the named environment (e.g. production, staging). Set at Settings → Environments → <env> → Secrets.
  • Repository secrets, scoped to one repository. Set at Repository → Settings → Secrets.

Environment variables (non-secret config)

GitHub Actions supports env: and vars: at workflow, job, and step levels. fremforge Actions supports the same syntax; no migration needed for inline variables.

For organisation-level and repository-level variables (vars: context in GitHub), fremforge has the same scoping. Set at Settings → Variables.

Step 3: Actions workflow adaptation

Forgejo Actions is broadly compatible with GitHub Actions but not bit-for-bit identical. A workflow that runs on GitHub will usually run on fremforge with two or three specific adjustments. The top-20 Marketplace actions compatibility matrix at docs.frem.sh/marketplace-compat documents which actions work as-is, which need a wrapper, and which have no fremforge equivalent.

Changes that apply to almost every workflow

1. Uses references can stay as actions/*@version for the major GitHub actions.

fremforge’s runner resolves uses: directives through fremverk’s EU-only mirror at https://frem.sh/mirrors/<action-name>, runner pods do not perform direct github.com egress at job startup (DPA Annex A.6). This works for actions/checkout, actions/setup-node, actions/setup-go, actions/setup-python, actions/setup-java, actions/cache, actions/upload-artifact, actions/download-artifact, and the rest of the official actions/* set already mirrored. No change to your uses: lines is required for mirrored actions; if a reference is missing, see the runtime resolution note below.

2. ${{ secrets.GITHUB_TOKEN }} semantics differ slightly.

GitHub ships an automatic token with every workflow that has scoped permissions to the calling repository. Forgejo ships an equivalent as ${{ secrets.FORGEJO_TOKEN }} or ${{ secrets.GITEA_TOKEN }}. The fremforge runner also exposes ${{ secrets.GITHUB_TOKEN }} as an alias for the Forgejo token so most workflows Just Work, but if a workflow expects specific GitHub API behaviours behind that token (e.g. triggering GitHub Checks API), it needs adaptation. For Forgejo-native check runs the equivalent API lives at frem.sh/api/v1/repos/{owner}/{repo}/check-runs.

3. Actions that call the GitHub REST API directly need adaptation.

If a workflow uses curl https://api.github.com/... or octokit to call GitHub’s REST API directly, those calls need to target https://frem.sh/api/v1/... using the fremforge API (largely GitHub-compatible at the REST surface). See the OpenAPI spec.

4. Default step shell differs.

Forgejo runner default shell is bash with POSIX flags. GitHub’s runner defaults differ subtly between Linux and Windows/macOS. If your workflow depends on specific shell quoting behaviour, explicitly set shell: bash (Linux), shell: pwsh (Windows), shell: sh (POSIX-only).

Changes that apply to some workflows

if: github.* context.

Forgejo Actions provides github.* context variables with the same names as GitHub Actions. Almost all if expressions port unchanged. Edge cases involve GitHub-specific metadata (e.g. github.event.pull_request.auto_merge) where Forgejo may not expose the exact same property.

Marketplace actions that are GitHub-API-specific.

Examples: peter-evans/create-pull-request, marocchino/sticky-pull-request-comment, nwtgck/actions-netlify. Most of these work out of the box because fremforge’s REST surface accepts their API calls. A handful need minor patches. The top-20 matrix documents specific workarounds.

Workflows that require rewriting

If your workflow uses GitHub Enterprise-specific features (organisation rulesets API, GHES-specific metrics endpoints, Copilot extensions API, GitHub Packages beyond npm/Docker/Maven), those features need replacement. Usually the replacement is fremforge-native:

  • Organisation rulesets → fremforge org-level policy (admin UI → Settings → Policy).
  • GitHub Packages → fremforge package registry (supports npm, Maven, Docker, NuGet, PyPI, RubyGems, Composer, Go).
  • Copilot Extensions → out of scope for fremforge by design (see the AI posture post at www.frem.sh).

Step 4: SSH key re-registration

SSH public keys are per-user and do not migrate. Each developer adds their SSH public key to their fremforge account:

  1. Sign in to fremforge.
  2. Settings → SSH Keys → Add Key.
  3. Paste the public key (~/.ssh/id_ed25519.pub or equivalent).

The host key fingerprints for frem.sh are published at www.frem.sh/ssh-fingerprints. Developers should verify on first SSH connection to prevent silent MITM. The fingerprint shows at first connection; developers compare against the trust page, confirm, and the key pins for subsequent connections.

For teams using SSH certificate authorities, fremforge supports trusted CA configuration directly from the org admin UI at <your-org>/_admin/auth-policy — the SSH CA section sits under the “Allow SSH protocol” toggle (folded into Authentication policy on 2026-05-26; standalone /admin/ssh-ca/ redirects here). Paste your CA’s OpenSSH public key, pick a principal policy (username / email / opaque), and optionally enforce “require cert auth” so plain public-key sign-in is refused. See the SSH certificate authority section of the Authentication policy guide for the end-to-end setup including Smallstep / Vault integration examples. Every user authenticating with a certificate the CA issues is admitted with the permissions of the matching fremforge account.

For teams where SSO is enforced (sso_required = true at org level), SSH authentication additionally validates an OIDC refresh token against the tenant’s IdP on every connection (cached 15 minutes). This means deprovisioning a user on the IdP side blocks SSH access within 15 minutes even if their SSH key is still registered on fremforge. No separate key cleanup needed.

Step 5: Third-party integration reconnection

GitHub-configured integrations (webhooks, CI notifications to Slack/Teams, deploy hooks to Vercel/Netlify, issue-tracker sync to Jira/Linear, code-review tools like Sourcegraph or Graphite) do not migrate automatically. Each needs reconnection.

Webhooks

GitHub webhooks are at Repository → Settings → Webhooks. fremforge webhooks are at the same location in the admin UI. Webhook payload shape is largely compatible; the main differences:

  • The X-GitHub-Event header on GitHub is X-Forgejo-Event (or X-Gitea-Event for Gitea-compatibility consumers) on fremforge. Most generic webhook receivers accept both.
  • The delivery identifier is X-Gitea-Delivery + X-Hook-UUID (analogue of GitHub’s X-GitHub-Delivery). Both are present on every fremforge webhook fire, most receivers key on either.
  • Event types are a close superset of GitHub’s core events. push, pull_request, pull_request_review, issues, issue_comment, release, create, delete, fork, star are all present.
  • Signature header is X-Forgejo-Signature (HMAC-SHA256 with your webhook secret).

For receivers written against GitHub’s API (Slack, Discord, PagerDuty integrations), there are two options:

  1. Reconfigure the receiver to use its Forgejo-native integration. Slack’s Forgejo integration, Discord’s Forgejo webhook target, etc. usually exist and are the cleaner path.
  2. Adapt the webhook at the receiver side. If the receiver is a custom internal service, add Forgejo-event handling alongside its GitHub-event handling. Usually a small change.

CI integrations (CircleCI, TravisCI, Jenkins)

If you run CI outside the forge (CircleCI, external Jenkins, Buildkite), the migration is straightforward: point those CI systems at fremforge repositories instead of GitHub repositories. Most external CI systems support Forgejo/Gitea as a source; the configuration change is in the CI system, not in fremforge.

Most teams migrating to fremforge use this opportunity to consolidate on fremforge’s hosted runners and retire the external CI. Runner minutes are included in the flat seat price. See the TCO post on the fremverk blog for the maths.

Deploy targets (Vercel, Netlify, Cloudflare Pages)

Each service reconnects to fremforge either via native support (Vercel and Netlify ship Forgejo/Gitea git-source integrations) or via a generic Git-push deploy hook. Document your new webhook URLs and update in each service’s dashboard.

Issue-tracker sync (Jira, Linear, Zendesk)

If your issue-tracker sync is Git-commit-based (linking commits to tickets by ID), it keeps working. The commit SHAs are preserved through migration. If the sync is GitHub-API-based (listening for PR events, querying GitHub issues), reconfigure it to target fremforge webhooks or disable it.

Step 6: DNS and custom domains (if applicable)

fremforge serves repository content at frem.sh/<org>/<repo> only. Customer-owned custom Git domains are not supported, there is no equivalent of GitHub Enterprise Server’s “custom hostname” feature where TLS terminates with your cert on git.company.com. Two consequences worth being explicit about:

Vanity CNAME without TLS

A vanity-CNAME at the customer’s DNS (e.g. internal git.company.com CNAME → frem.sh) is technically possible but the TLS certificate served is always fremforge’s *.frem.sh cert, NOT a *.company.com cert. A browser hitting https://git.company.com/... will show a name-mismatch warning unless the customer also provisions and serves their own cert via their own reverse proxy in front. This is a customer-side undertaking, fremforge does not terminate TLS for vanity hostnames, does not accept customer-supplied certs, and does not run an SNI-multi-cert layer.

Recommended path for customers migrating off GitHub Enterprise Server: adopt the frem.sh/<org>/* URL pattern directly. Update internal links + CI configs + documentation to the new URLs. The vanity-CNAME-with-customer-cert path adds operational debt (cert renewal, proxy ops) and isn’t a fremforge-supported configuration.

Bring an Enterprise-on-Demand contract if your procurement requires a vanity-domain SLA, that opens a discussion about a dedicated edge tier with customer cert termination, but it’s out of scope for the self-service tier.

What does and does not migrate

This section enumerates the GitHub artefacts that pass through migration with which fidelity. Use it as the pre-migration alignment checklist.

ArtefactMigrates?Fidelity notes
Repository content (commits, branches, tags)SHA-stable; full history preserved
LFS objects✓ (≤ 5 GB pool, UI importer; larger via mirror-clone)Same OIDs; size limit is operational not protocol
Issues✓ best-effortForgejo’s importer assigns sequential Issue.Index values to imported issues; gaps in the source numbering will not be preserved (e.g. GitHub #1, #2, #42 ends up #1, #2, #3 on Forgejo). Cross-issue references in comment bodies are rewritten to match the assigned indices when the importer can resolve them; un-resolvable references stay literal text. If exact-number fidelity matters (long-running issue trackers with public bookmarks), the renumber-via-API path is to pre-create placeholder issues for each gap on the destination before importing, then delete them post-import.
Pull requests✓ best-effortPRs land on the same shared number-space as Issues (Forgejo’s combined counter, like Gitea), so the relative ordering is preserved but the absolute numbers track the issue-row’s caveat above: gaps in the source numbering collapse on import. If your source has Issue #1, PR #2, Issue #5, PR #7, the destination ends up Issue #1, PR #2, Issue #3, PR #4. Use the same pre-create-placeholder workaround documented for issues if exact-PR-number fidelity matters (public bookmarks, deep links). Outstanding PRs migrate as draft state if open at migration time.
PR review comments (inline, line-anchored)⚠ partialAnchored line-comments map to plain PR comments (cross-forge data-model limitation). See “PR review comments import as issues” below.
Branch protection rules✗, re-createGitHub’s branch_protections rule shape doesn’t map to Forgejo’s BranchProtection shape. Pre-migration: capture each rule via gh api /repos/<owner>/<repo>/branches/<name>/protection. Post-migration: re-create via the fremforge admin UI Repo defaults tab + per-repo override. A .gitea/branch-protection.yaml template checked into the org can declare desired state; today applies are operator-side via the admin UI (branch-protection-as-code via API is a Phase-2 surface).
Webhooks✗, not preservedForgejo’s migrate endpoint does not enumerate webhooks from the source forge (GitHub/GitLab downloaders don’t iterate hooks; there is no migrate webhooks flag in MigrateRepoOptions). Customers land with zero webhooks on the destination side. Re-create webhooks on fremforge after migration from the inventory pre-migration-check.sh produces — it lists each source webhook’s URL + events. HMAC secrets are not retrievable from GitHub, so each receiving integration also needs a new secret rotated in on the source-service dashboard once the fremforge URL is re-registered. fremforge also does not re-deliver historical events.
Submodules⚠ URL-rewrite required.gitmodules URLs that point at github.com/<org>/<repo> remain literal github.com URLs after migration. Either (a) update each .gitmodules entry to the new fremforge URL post-migration and commit, OR (b) use git config submodule.<name>.url <fremforge-url> per-clone if the parent repo is not under your control. The fremforge importer does not rewrite .gitmodules automatically because submodule URLs are content the project author owns.
Org-level Actions secrets✗, re-enterSecrets are unrecoverable from GitHub via API by design. Re-enter at the fremforge admin UI Settings → Actions secrets tab. Pre-migration: list with gh secret list --org <org> so you have the names to re-enter.
Org-level Actions variables (non-secret)✓ via exportPlain variables can be exported via gh variable list --org <org> --json name,value > gh-vars.json and re-imported through the fremforge admin UI. For bulk import via API, pipe the export through the variables endpoint: jq -c '.[]' < gh-vars.json | while read row; do n=$(jq -r .name <<<"$row"); v=$(jq -r .value <<<"$row"); curl -fsS -X POST "https://frem.sh/api/v1/orgs/<org>/actions/variables" -H "Authorization: Bearer $T" -H "Content-Type: application/json" -d "{\"name\":\"$n\",\"value\":\"$v\"}"; done. Same shape works for repo-level variables (swap /orgs/<org>/ for /repos/<owner>/<repo>/).
GitHub Discussions✗, no equivalentForgejo does not ship a Discussions equivalent today. Export to a separate archive if discussion threads are load-bearing for your org.
GitHub Projects (boards)✗, no equivalentProject tracking re-builds in fremforge issue + label + milestone.

Resumability

The Forgejo /repos/migrate endpoint is not resumable. If the call fails partway (network drop, source-API rate-limit, etc.), the partially-imported repository is left in an inconsistent state. Recovery procedure:

  1. Delete the partial repo via the fremforge admin UI Settings → Delete.
  2. Re-issue the migrate call immediately. If it returns 422 with repository already exists, wait 5 seconds and retry — this is RDS replication lag on the read-replica path, not a Forgejo tombstone cycle.
  3. If the second attempt fails the same way, use a fresh repo name.

For repos where re-running the import is expensive (large LFS, many issues), use the local-mirror fallback documented under “Large repositories”, git push --mirror is naturally resumable because git itself handles network drops gracefully.

Post-migration verification

Beyond Step 7’s “verify-and-decommission” list below, the post-migration verification checklist that customers most often forget:

  • Diff commit count. git rev-list --count HEAD against the GitHub clone vs. the fremforge clone. The numbers must match exactly.
  • Diff branch list. git branch -r | sort on each side and diff the output. Stale refs/pull/* refs from GitHub do not migrate (Forgejo numbers PRs in its own ref space); everything else should match.
  • Diff tag list. git tag --sort=v:refname on each side; expect identical lists.
  • Diff LFS pointer count. git lfs ls-files | wc -l; numbers should match exactly. If they don’t, run git lfs fetch --all && git lfs push --all origin against the fremforge remote.
  • Issue / PR count + numbering. gh issue list --limit 5000 --state all --json number | jq length on GitHub vs. fremforge /api/v1/repos/<org>/<repo>/issues?state=all (with pagination). Pagination loop for fremforge: n=0; for p in $(seq 1 200); do c=$(curl -fsS "https://frem.sh/api/v1/repos/<org>/<repo>/issues?state=all&page=$p&limit=50" -H "Authorization: Bearer $T" | jq length); [ "$c" = 0 ] && break; n=$((n + c)); done; echo $n. Numbering preserved.
  • CI green. Trigger a no-op build on main and verify the Forgejo Actions run reaches success. This catches uses: mirror gaps (the most common post-migration CI break).

If any of these surface drift, do not decommission the GitHub source until the drift is resolved.

Step 7: Verify and decommission

After migration:

  1. Clone the migrated repository from fremforge, run the build, verify tests pass.
  2. Open a test PR to verify the pre-receive secret scanner, dependency scanner, and signed-commit verification are active as expected.
  3. Check the admin UI Findings view to confirm no surprising scan results on the migrated history.
  4. Verify webhooks deliver correctly via Settings → Webhooks → Recent Deliveries.
  5. Verify SSO sign-in works for every user who will be accessing the repository.
  6. Update documentation and internal wikis to point at the new repository URLs.
  7. After 30 days of successful operation on fremforge, set the GitHub repositories to archived state. Do not delete them; keep them for audit history.

Rollback (partial, plan accordingly)

Migration is partially reversible, not fully reversible. Be honest about what does and doesn’t round-trip before you commit production traffic.

What rollback CAN recover:

  1. Git history, git push --mirror from a local fremforge clone back to the GitHub repository restores branches, tags, commits.
  2. Webhooks + integrations, re-enable on the GitHub side; you’ll need to manually update the webhook target URLs back to GitHub-hosted endpoints.

What rollback can NOT recover with full fidelity:

  1. Issues, PRs, comments created after migration, fremforge’s Settings → Export produces a JSON dump but GitHub’s import API uses a different schema, so re-import is a manual mapping exercise. Issue numbers will not match; cross-references in commit messages will dangle. Author attribution requires every contributor to have a GitHub account again. Reactions, review threads, and inline-comment line-pinning typically don’t survive the round-trip.
  2. CI history + Actions logs created on fremforge, not exportable in any equivalent format.
  3. fremforge-specific findings, secret-scan rejections, dependency-scan results, signed-commit attestations created on fremforge don’t have a GitHub equivalent to import into.

Treat migration as a one-way door for production use. Run a single-repo pilot on a non-critical repository first; verify CI + webhooks + SSO work; then migrate the rest in batches. The “reversible within 30 days” framing applies only to git-history-only rollback, don’t rely on it for full collaboration-state recovery.

Common pitfalls

LFS objects not visible after migration.

The UI importer handles LFS for small pools (<5 GB) automatically. For larger pools, run git lfs fetch --all on a local mirror clone and git lfs push --all origin to fremforge manually.

Actions workflows fail on first run with “action not found”.

The mirror at https://frem.sh/mirrors/ (see Actions resolution above) covers the standard actions/* set. A failed uses: reference means the action hasn’t been mirrored yet, open a ticket at support@frem.sh with the missing reference and the mirror sweep will pick it up on the next cycle; or pin to a previously-resolved version that’s already cached.

PR review comments import as issues.

The Forgejo importer maps PR review comments to PR comments, not to line-level review comments. This is a limitation of the cross-forge data model; inline review comments lose their anchor-line context. Workaround: for repositories where inline comments are load-bearing, export them to a separate data file via GitHub’s API pre-migration and preserve them as an archival attachment on the migrated PR.

Users not auto-mapped.

The importer maps GitHub users by email address. Users whose GitHub email does not match their fremforge email (e.g. personal email on GitHub, company email on fremforge) show as “Unknown user” on imported issues. Either pre-map by adding the GitHub-side email as a secondary email on the fremforge account before import, or accept the “Unknown user” display and rely on the original attribution visible in commit history.

Get help

If something in this guide does not work for your setup:

  • Email support@frem.sh with the subject line starting MIGRATION, GITHUB - and a description of where you got stuck.
  • If you are on the early-adopter plan, you have a direct line to the fremforge build team for migration assistance at no additional charge.

See also: GitLab migration guide · Azure DevOps migration guide · Top-20 Marketplace actions compatibility matrix · Pre-migration checklist.