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), andIdentity (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:
- Click + New → Migration.
- Select Azure DevOps as the source.
- Paste the repository URL (e.g.
https://dev.azure.com/acme/project/_git/backend). - Paste your ADO personal access token.
- Tick the content types to import.
- Target the destination organisation and repository name.
- 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, codebase — git 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 concept | fremforge (Forgejo) concept |
|---|---|
| Organisation | (no direct equivalent, typically one fremforge org per ADO organisation) |
| Project | Organisation, or prefix pattern on repository names |
| Repository | Repository |
| Pull Request | Pull Request |
| Work Item: Epic, Feature, User Story, Task, Bug | Issue with label (epic, feature, story, task, bug) |
| Area Path | Label or custom milestone |
| Iteration Path | Milestone |
| Build Pipeline | Forgejo Actions workflow |
| Release Pipeline | Forgejo Actions workflow with deployment-environment jobs |
| Service Connection | OIDC Federation configuration |
| Variable Group | Environment secrets at org or environment level |
| Agent Pool | Runner label / BYO runner group |
| Test Plan / Test Suite | Not a fremforge feature, external tool |
| Artifact Feed | fremforge package registry |
| Wiki | Wiki |
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)
}"
doneAdapt 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 testKey conversions:
trigger:→on: push:with branch filters.pr:→on: pull_request:.schedules:→on: schedule:with cron expressions.stages/jobs/steps→jobs/steps(one level flatter;stageshas no direct equivalent, its serialisation is achieved byneeds:between jobs).pool: vmImage: ubuntu-latest→runs-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 viasetup-*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 asrun:steps or small composite actions.
ADO Tasks that commonly need conversion
| ADO Task | Forgejo Actions replacement |
|---|---|
UseNode@1 | actions/setup-node@v4 |
UsePythonVersion@0 | actions/setup-python@v5 |
UseDotNet@2 | actions/setup-dotnet@v4 |
UseJavaVersion@1 | actions/setup-java@v4 |
PublishBuildArtifacts@1 | actions/upload-artifact@v4 |
DownloadBuildArtifacts@0 | actions/download-artifact@v4 |
Cache@2 | actions/cache@v4 |
Docker@2 / DockerCompose@0 | docker/build-push-action@v6 or direct docker CLI in run: |
AzureCLI@2 | azure/cli@v2 with OIDC-federation credentials (see Step 4) |
AzureKeyVault@2 | Native fremforge secrets (preferred) or azure/cli@v2 + az keyvault secret show |
SonarCloudPrepare@1 / SonarCloudAnalyze@1 | sonarsource/sonarcloud-github-action@master |
PublishTestResults@2 | dorny/test-reporter@v1 or job-level artifact upload + manual report rendering |
HelmDeploy@0 | helm/helm-action@v1 or direct helm CLI |
KubernetesManifest@0 | azure/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 deployTemplates 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:
- BYO runners: register a Windows or macOS runner on your own infrastructure with the label
windows-latestormacos-latest. Your pipelines target them viaruns-on:. - 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
- 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
- Issuer:
- Grant the App Registration the required role assignments on the target Azure subscription(s).
- 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 showNo 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
Step 6: Boards migration (partial)
As noted, fremforge does not ship Work Item type hierarchy. Migration options:
- Flatten: all Work Items → Issues with type labels. Queries and reports rebuild around fremforge’s search.
- 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.
- 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.