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

Migrate to fremforge from Azure DevOps

This guide covers migration from Azure DevOps Services (dev.azure.com) to fremforge. The equivalent Azure DevOps Server (on-premises) migration is similar but authentication and API-endpoint details differ. Adapt the token and endpoint portions below for your specific deployment.

Azure DevOps is the most structurally different of the three major migration sources. ADO has separate products for Repos, Pipelines, Boards, Test Plans, and Artifacts; fremforge ships Repos + CI/CD + Artifacts + basic Issues as one integrated surface and does not ship equivalents for Test Plans or the full Boards feature set. ADO migration is therefore partly a scope decision as well as a technical migration: some ADO features you may be relying on have no fremforge destination.

Feature parity gaps

fremforge does not have equivalents for: Azure Boards work items (no issue hierarchy or sprint planning), Test Plans (no test case management), Azure Artifacts feeds migrated automatically (packages must be re-published), and Classic pipelines (YAML pipelines migrate; designer pipelines do not). If these are blockers, evaluate the migration scope before starting.

Read the GitHub migration guide and the GitLab migration guide first if you have not. Some foundational steps (SSH keys, third-party integrations) are identical and we do not repeat them in full here.

Before you start

What you need on your side:

  • Project Administrator role on the Azure DevOps project(s) you are migrating.
  • A personal access token on ADO with Code (Read), Code (Status), Work Items (Read), Build (Read), Release (Read), and Identity (Read) scopes at a minimum.
  • Admin access to the fremforge organisation you are migrating to.
  • For Pipelines migration: an inventory of YAML pipeline files, classic (non-YAML) pipelines if any, service connections (these become OIDC federation), variable groups, and agent pools you depend on.

What we recommend:

  • Convert any classic (non-YAML) pipelines to YAML pipelines in ADO before migrating. Migration from classic pipelines to Forgejo Actions is effectively a ground-up rewrite; converting to YAML first gives you a portable starting point and also improves your ADO setup.
  • Identify which Work Item types in Boards you genuinely use. fremforge (Forgejo) has Issues and Milestones but does not have Epics, Features, User Stories, or Tasks as distinct types. They all collapse to Issues with labels. If your process depends on Work Item type hierarchy, plan the collapse before migration.
  • Identify Test Plan dependencies. fremforge does not have a direct equivalent; teams that depend heavily on ADO Test Plans typically keep ADO Test Plans running as a separate tool or migrate to a dedicated test management tool.

Time estimate:

  • Single repository with simple pipeline, small Boards usage: 1-2 hours.
  • Repository with moderate pipeline complexity and multi-stage release pipelines: 3-6 hours.
  • Organisation-wide migration with many pipelines, variable groups, and service connections: 1-2 engineering weeks dominated by pipeline conversion and service-connection replacement.

Step 1: Repository and history migration

Forgejo’s native importer supports Azure DevOps as a source. It copies Git history and, depending on your ADO configuration, Work Items (imported as Issues) and Pull Requests.

Using the native importer (UI path)

From the fremforge admin UI:

  1. Click + New → Migration.
  2. Select Azure DevOps as the source.
  3. Paste the repository URL (e.g. https://dev.azure.com/acme/project/_git/backend).
  4. Paste your ADO personal access token.
  5. Tick the content types to import.
  6. Target the destination organisation and repository name.
  7. Click Migrate repository.

Using the API (scriptable path)

# Build the JSON body via jq so the PAT is JSON-string-escaped safely
# (Azure DevOps PATs can contain `/`, `+`, `=` characters which are
# fine in JSON strings but a raw shell-interpolation can still break
# if the PAT happens to contain `"` from a copy-paste error).
jq -nc \
  --arg clone_addr    "https://dev.azure.com/acme/project/_git/backend" \
  --arg auth_username "fremforge" \
  --arg auth_token    "$ADO_TOKEN" \
  --arg repo_owner    "acme" \
  --arg repo_name     "backend" \
  '{clone_addr: $clone_addr, auth_username: $auth_username, auth_token: $auth_token,
    service: "git", repo_owner: $repo_owner, repo_name: $repo_name,
    issues: false, pull_requests: false, lfs: true}' \
| curl -X POST "https://frem.sh/api/v1/repos/migrate" \
    -H "Authorization: Bearer ${FREMFORGE_TOKEN}" \
    -H "Content-Type: application/json" \
    -d @-

Note: ADO is not one of Forgejo’s recognised migration service values. Upstream Forgejo’s wire-level enum is git, github, gitea, gitlab, gogs, onedev, gitbucket, codebasegit is the plain-Git fallback (some legacy docs call it “plain” but the wire value is the literal string git). Setting "service": "git" explicitly (as in the body above) is the canonical value; the ADO PAT in auth_token carries the authentication, and the clone URL is a standard HTTPS git remote that the plain-Git importer handles correctly. "lfs": true opts the importer into fetching LFS objects through the source-side LFS endpoint — required for ADO repos with LFS content. Work Items and PR metadata are then imported separately via a scripted workflow using the ADO REST API (see below); the plain-git importer brings refs only.

auth_username is required for ADO. Azure DevOps PATs use HTTP Basic Auth where the password is the PAT and the username must be any non-empty string (Microsoft docs: “When using a PAT, the username can be any non-empty string”). Forgejo’s plain-Git importer would otherwise construct https://{auth_token}@… style URLs, which AzDO’s Git server treats as the username and rejects 401. Use "auth_username": "fremforge" (or any non-empty string — the value doesn’t matter to AzDO, only the basic-auth shape does).

Mapping ADO concepts to fremforge

Azure DevOps conceptfremforge (Forgejo) concept
Organisation(no direct equivalent, typically one fremforge org per ADO organisation)
ProjectOrganisation, or prefix pattern on repository names
RepositoryRepository
Pull RequestPull Request
Work Item: Epic, Feature, User Story, Task, BugIssue with label (epic, feature, story, task, bug)
Area PathLabel or custom milestone
Iteration PathMilestone
Build PipelineForgejo Actions workflow
Release PipelineForgejo Actions workflow with deployment-environment jobs
Service ConnectionOIDC Federation configuration
Variable GroupEnvironment secrets at org or environment level
Agent PoolRunner label / BYO runner group
Test Plan / Test SuiteNot a fremforge feature, external tool
Artifact Feedfremforge package registry
WikiWiki

Work Items → Issues

Work Items do not import via the native Forgejo importer. Migration script:

#!/bin/bash
# Requires: curl, jq, ADO_TOKEN, FREMFORGE_TOKEN
ADO_ORG="acme"
ADO_PROJECT="project"
FREMFORGE_ORG="acme"
FREMFORGE_REPO="backend"

# Query all work items in the project
curl -s -u ":${ADO_TOKEN}" \
  "https://dev.azure.com/${ADO_ORG}/${ADO_PROJECT}/_apis/wit/wiql?api-version=7.1" \
  -H "Content-Type: application/json" \
  -d '{"query":"SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject]=@project"}' \
  | jq -r '.workItems[].id' \
  | while read wi_id; do
    # Fetch full work item
    ado_wi=$(curl -s -u ":${ADO_TOKEN}" \
      "https://dev.azure.com/${ADO_ORG}/_apis/wit/workitems/${wi_id}?api-version=7.1")
    
    title=$(echo "${ado_wi}" | jq -r '.fields."System.Title"')
    body=$(echo "${ado_wi}" | jq -r '.fields."System.Description" // ""')
    wi_type=$(echo "${ado_wi}" | jq -r '.fields."System.WorkItemType"' | tr '[:upper:]' '[:lower:]')
    state=$(echo "${ado_wi}" | jq -r '.fields."System.State"')

    # Create fremforge issue
    curl -s -X POST "https://frem.sh/api/v1/repos/${FREMFORGE_ORG}/${FREMFORGE_REPO}/issues" \
      -H "Authorization: Bearer ${FREMFORGE_TOKEN}" \
      -H "Content-Type: application/json" \
      -d "{
        \"title\": \"${title}\",
        \"body\": \"**Migrated from Azure DevOps work item #${wi_id}**\n\n${body}\",
        \"labels\": [\"${wi_type}\"],
        \"closed\": $([[ "${state}" == "Closed" || "${state}" == "Done" || "${state}" == "Resolved" ]] && echo true || echo false)
      }"
  done

Adapt as needed. This is a starter, not a drop-in. Large migrations should add rate-limit backoff and comment preservation.

WIQL pagination. Azure DevOps caps a single WIQL response at 20,000 work items. The script above issues one query and iterates that result set — fine for the common case. Before kicking it off, count first:

COUNT=$(curl -s -u ":${ADO_TOKEN}" \
  "https://dev.azure.com/${ADO_ORG}/${ADO_PROJECT}/_apis/wit/wiql?api-version=7.1" \
  -H "Content-Type: application/json" \
  -d '{"query":"SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject]=@project"}' \
  | jq '.workItems | length')
echo "Work items in scope: ${COUNT}"

If COUNT is above ~19,000, partition the query (for example by [System.CreatedDate] quarter or by [System.WorkItemType]) and run the script once per partition, or escalate to the WIQL-paging runbook (operator-driven, ticket support@frem.sh). The single-query path silently truncates at the cap — there is no error response, just a short result set, which is the failure mode you do not want during migration.

Step 2: Secrets and variables

Azure DevOps secret stores:

  • Pipeline variables (inline in YAML), rewrite at migration time.
  • Variable Groups (at project level, optionally linked to Azure Key Vault), re-create as fremforge org-level or environment-level secrets.
  • Secret variables (marked as secret, not returned via ADO API), must be retrieved from your secrets-of-record and re-entered.
  • Key Vault references, if the variable group is linked to Azure Key Vault, migration strategy is either (a) keep Key Vault, have fremforge runners authenticate to Azure via OIDC federation and fetch at job time, or (b) copy the values into fremforge secrets directly.

Strong recommendation: replace Azure service principal secrets with OIDC federation to Azure (see Step 4).

Step 3: Pipelines → Forgejo Actions conversion

Azure Pipelines YAML and Forgejo Actions YAML have similar shape but substantially different vocabulary. Conversion is manual. Automated conversion is deferred to Phase 2 pending customer demand.

Conversion patterns

Stages and jobs

Azure Pipelines YAML:

trigger: [main]

stages:
  - stage: Build
    jobs:
      - job: BuildApp
        pool:
          vmImage: ubuntu-latest
        steps:
          - script: make build
            displayName: 'Build'

  - stage: Test
    dependsOn: Build
    jobs:
      - job: TestApp
        pool:
          vmImage: ubuntu-latest
        steps:
          - script: make test
            displayName: 'Test'

Forgejo Actions equivalent:

name: CI

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: fremforge
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: make build

  test:
    needs: build
    runs-on: fremforge
    steps:
      - uses: actions/checkout@v4
      - name: Test
        run: make test

Key conversions:

  • trigger:on: push: with branch filters.
  • pr:on: pull_request:.
  • schedules:on: schedule: with cron expressions.
  • stages / jobs / stepsjobs / steps (one level flatter; stages has no direct equivalent, its serialisation is achieved by needs: between jobs).
  • pool: vmImage: ubuntu-latestruns-on: fremforge (fremforge runners use an Alpine 3.22-based image, not Ubuntu, see Runner image for the pre-installed tooling list and how to install language toolchains via setup-* actions).
  • script:run: with the same shell command.
  • task: <name>@<version>uses: <action-path>@<version> where the action is an equivalent marketplace action. Most ADO tasks do not have a direct Forgejo Actions equivalent and are rewritten as run: steps or small composite actions.

ADO Tasks that commonly need conversion

ADO TaskForgejo Actions replacement
UseNode@1actions/setup-node@v4
UsePythonVersion@0actions/setup-python@v5
UseDotNet@2actions/setup-dotnet@v4
UseJavaVersion@1actions/setup-java@v4
PublishBuildArtifacts@1actions/upload-artifact@v4
DownloadBuildArtifacts@0actions/download-artifact@v4
Cache@2actions/cache@v4
Docker@2 / DockerCompose@0docker/build-push-action@v6 or direct docker CLI in run:
AzureCLI@2azure/cli@v2 with OIDC-federation credentials (see Step 4)
AzureKeyVault@2Native fremforge secrets (preferred) or azure/cli@v2 + az keyvault secret show
SonarCloudPrepare@1 / SonarCloudAnalyze@1sonarsource/sonarcloud-github-action@master
PublishTestResults@2dorny/test-reporter@v1 or job-level artifact upload + manual report rendering
HelmDeploy@0helm/helm-action@v1 or direct helm CLI
KubernetesManifest@0azure/k8s-deploy@v5 (OIDC-authed)

Conditions

ADO condition: → Forgejo Actions if:.

ADO:

- task: Deploy
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))

Forgejo Actions:

- name: Deploy
  if: success() && github.ref == 'refs/heads/main'
  run: make deploy

Templates and Library

ADO templates (extends:, template:) map to Forgejo Actions reusable workflows (uses: <repo>/.forgejo/workflows/template.yaml@ref) or composite actions (uses: ./.forgejo/actions/<name>). Conversion is manual but mechanical.

Multi-stage release pipelines → Environment-gated deploy jobs

ADO release pipelines with approval gates map to Forgejo Actions workflows that use environment: <name> to trigger the required-reviewer / approval flow at environment level. Configure environment protection rules in the fremforge admin UI at Repository → Settings → Environments → <name>.

Agent pools → Runner labels

ADO pool: vmImage: windows-latest → Forgejo Actions runs-on: windows-latest. fremforge-provided hosted runners are Linux amd64 only. Windows and macOS are not available on the standard plan.

If your pipelines require Windows or macOS runners, two paths:

  1. BYO runners: register a Windows or macOS runner on your own infrastructure with the label windows-latest or macos-latest. Your pipelines target them via runs-on:.
  2. Keep those pipelines on ADO: for teams migrating the bulk of their CI but keeping Windows/Mac builds on existing infrastructure. Not ideal but supported as a transitional state.

Step 4: Service Connections → OIDC Federation

Azure DevOps uses Service Connections to store cloud credentials. fremforge replaces these with OIDC federation, which is the preferred approach for every cloud target, not just Azure.

For Azure

  1. In Azure AD, create an App Registration and configure a federated credential with:
    • Issuer: https://frem.sh/_app/runner/oidc (single global issuer for all tenants — see build/runners/oidc/ for the JWKS endpoint and the full GitHub-Actions-compatible claim set)
    • Subject: repo:<fremforge-org>/<repo>:ref:refs/heads/main (or your preferred scoping pattern; the subject claim carries the org so per-tenant isolation is enforced here)
    • Audience: api://AzureADTokenExchange
  2. Grant the App Registration the required role assignments on the target Azure subscription(s).
  3. In your Forgejo Actions workflow:
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: fremforge
    steps:
      - uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
      - run: az account show

No client secret. No long-lived credential. Token is issued at job time, scoped to this workflow, expires within the hour.

For T Cloud

As with GitLab migrations (GitLab Step 4), fremforge supports native OIDC federation to T Cloud agencies. Configure in the fremforge admin UI at Settings → OIDC Federation.

For AWS / GCP

Standard OIDC-to-AWS / OIDC-to-GCP patterns work the same as from GitHub Actions. Configure the trust relationship with https://frem.sh/_app/runner/oidc as the issuer (single global issuer for all tenants — pin the trust policy condition on the repository_owner claim for per-tenant isolation, same pattern as GitHub Actions’ repository_owner filter on token.actions.githubusercontent.com).

Step 5: SSH key re-registration

See GitHub migration Step 4.

Step 6: Boards migration (partial)

As noted, fremforge does not ship Work Item type hierarchy. Migration options:

  1. Flatten: all Work Items → Issues with type labels. Queries and reports rebuild around fremforge’s search.
  2. Keep Boards separate: export ADO Work Items to a flat data file, archive, and continue using ADO Boards for backlog management while migrating code and CI only. This is a common interim state for teams whose Boards usage is sophisticated.
  3. Replace with a dedicated PM tool: Linear, Jira, Shortcut, GitHub Projects (if staying in GitHub ecosystem but leaving ADO). The fremforge webhooks + REST API integrate with all of the above.

The import script in Step 1 handles option 1 with label-based flattening.

Step 7: Artifact feeds → fremforge package registry

ADO Artifact feeds map to the fremforge package registry. Supported formats on fremforge:

  • npm
  • Maven
  • Docker/OCI containers
  • NuGet
  • PyPI
  • RubyGems
  • Composer
  • Go modules
  • Generic

Migration is push-oriented: each publishing pipeline republishes artifacts to the fremforge registry after migration. Historical artifact versions on ADO remain available there. Decommission at your own cadence.

Step 8: Third-party integration reconnection

See GitHub migration Step 5. ADO-specific integrations (Teams Service Hooks, ServiceNow, ADO-specific webhooks) reconnect as fremforge webhooks.

Step 9: Verify and decommission

Same as the GitHub guide.

Common pitfalls specific to ADO migrations

Classic pipelines cannot be auto-migrated.

If you have classic (non-YAML) pipelines, convert to YAML in ADO first. Then apply the conversion patterns in Step 3. Skipping the YAML intermediate step means re-creating pipelines from the ADO UI’s logical shape, which is error-prone.

Service Connections with Azure Key Vault references.

A Service Connection that reads from Azure Key Vault at pipeline execution time needs two-stage migration: (1) set up OIDC federation so the fremforge runner can az login; (2) the workflow then pulls the secret with az keyvault secret show at job time, rather than embedding it in fremforge secrets. This mirrors the ADO pattern exactly.

Release pipelines with approval gates.

ADO release pipelines with manual approval gates before production deploy map to Forgejo Actions with environment: protection rules. The fremforge admin UI lets you configure required reviewers per environment. The feature set is equivalent; only the UX differs.

Test Plan dependency.

Manual test execution tracked in ADO Test Plans has no fremforge destination. Teams that depend on this need to pick a replacement tool (Xray on Jira, Zephyr, qTest) or accept the scope reduction.

Get help

support@frem.sh with subject line MIGRATION, AZURE DEVOPS, <your-specific-issue>.


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