Migrate repositories from GitHub to GitLab using Port workflows
Send this guide to your coding agent.
Prerequisite: Install Port MCP
Open plan mode if your tool supports it; otherwise present the plan below filled in and wait for my approval. Implement this Port guide in my org via MCP: https://docs.port.io/guides/all/migrate-project-from-github-to-gitlab Read the raw markdown version at https://docs.port.io/guides/all/migrate-project-from-github-to-gitlab.md - it contains every tab and code block without page markup. Goal: get the guide's core flow working end-to-end in my org; adapting it to fit my existing setup takes priority over matching the guide 1:1. Plan: 1. Confirm MCP is connected, in the right org, with sufficient permissions. 2. If the guide offers alternative implementation paths (tabs), pick the one matching my installed integrations and tools, confirm it with me, and implement only that path. 3. Diff the guide's data model (blueprints, properties, relations, workflows, actions, agents, automations, integrations, webhook data sources, secrets) against mine. 4. Propose adaptations for gaps, reusing existing blueprints/relations over guide-named duplicates. 5. Flag what needs a UI click, credential, or secret from me, testing MCP capability empirically before ruling anything out. If the guide has a "Set up via API" section, use it for anything MCP can't do before treating a step as UI-only. 6. Stop on any blocker and give me options. Approving this plan authorizes the writes it lists; pause only for writes beyond what's listed. Build: - Extend blueprint schema additively when upserting; don't remove or overwrite existing properties, and treat type conflicts as a blocker, not an auto-fix. - Never print secret values into the chat or logs; ask me to set them in Port, or write them via the secrets API without echoing them back. - List any mock data in the plan, minimal and labeled mock; once approved, seed it without re-asking, and tell me what you seeded. - For anything the guide writes downstream (e.g. a webhook target), use a real entity, not a mock. - For pages/widgets, use the real page identifier from the app URL, not a guessed slug. - When you hit a UI step confirmed (not assumed) unsupported via MCP and not covered by the guide's API sections, pause, give exact clicks, then resume via MCP. - Validate and give links after each meaningful step (only a tool-returned URL, no guessed paths); don't proceed if the last run wasn't a success. Done: - Run the guide's "Let's test it" steps where possible (e.g. execute a workflow test run) and confirm the expected output exists in Port. - Summarize adaptations, seeded data, what was mocked or skipped, remaining UI steps, and how to verify.
This guide demonstrates how to migrate repositories from GitHub to GitLab using a Port workflow together with a GitHub Actions pipeline.
We will model your repositories, expose a repeatable migration workflow that both developers and AI agents can trigger, and keep full visibility of every migration through Port's workflow runs.
Once implemented you will be able to:
- Set up GitHub integration so Port discovers repositories and metadata.
- Model repositories with a blueprint to track your GitHub repositories.
- Trigger a repeatable migration workflow from Port's UI, a single API call, or an AI agent connected through Port's MCP server.
- Automatically create GitLab projects and push complete repository history.
Port workflows are currently in open beta and available to all users. Workflows may undergo changes without prior notice.
Prerequisites
You should have the following in place for this migration:
- A Port account with the onboarding process completed.
- A GitHub organization containing the repositories you want to migrate.
- A GitLab account (self-hosted or cloud) with permissions to create projects.
- A GitLab namespace or group to serve as the migration destination.
Set up data model
To represent your GitHub repositories in Port, we need to create a blueprint, set up the GitHub integration, and configure the data source. Skip to the section below if you already have an existing GitHub integration.
Create the GitHub Repository blueprint
-
Go to the data model page in Port.
-
Click on
+ Blueprint. -
Click on the
{...} Edit JSONbutton in the top right corner. -
Copy and paste the following JSON schema:
GitHub Repository blueprint (click to expand)
{"identifier": "githubRepository","title": "GitHub Repository","icon": "Github","schema": {"properties": {"url": {"title": "URL","format": "url","type": "string","icon": "Link"},"readme": {"title": "README","type": "string","format": "markdown","icon": "Book"},"defaultBranch": {"title": "Default Branch","type": "string","icon": "GitVersion"},"language": {"title": "Language","type": "string","icon": "Code"}},"required": []},"mirrorProperties": {},"calculationProperties": {},"aggregationProperties": {},"relations": {}} -
Click on
Saveto create the blueprint.
Set up GitHub integration
-
Set up GitHub in Port by following the GitHub Ocean integration guide.
-
Configure the mapping:
-
From the data sources page, locate the GitHub integration you installed and click on it.
-
Under the Mapping field, paste the following mapping configuration:
GitHub mapping configuration (click to expand)
createMissingRelatedEntities: trueresources:- kind: repositoryselector:query: 'true'port:entity:mappings:identifier: .nametitle: .nameblueprint: '"githubRepository"'properties:readme: file://README.mdurl: .html_urldefaultBranch: .default_branchlanguage: .language
-
-
Click on the Save & Resync button at the bottom right corner.
Build the workflow
The migration workflow has two nodes: a self-service trigger that collects the migration details, and an integration action that dispatches the GitHub Actions pipeline that performs the migration.
The trigger's title, description, and inputs are written for both audiences, so the same definition works whether a developer submits the form in Port's UI or an AI agent invokes it through Port's MCP server - no separate integration is needed for agent access.
Migration workflow (click to expand)
{
"identifier": "migrate_to_gitlab",
"title": "Migrate Repository to GitLab",
"icon": "GitLab",
"description": "Migrate a GitHub repository to GitLab with full commit history. Creates the destination GitLab project automatically.",
"allowAnyoneToViewRuns": true,
"nodes": [
{
"identifier": "trigger",
"title": "Migrate Repository to GitLab",
"config": {
"type": "SELF_SERVE_TRIGGER",
"contexts": [
{
"on": "ENTITY",
"userInput": "repository"
}
],
"userInputs": {
"properties": {
"repository": {
"title": "Repository",
"type": "string",
"format": "entity",
"blueprint": "githubRepository",
"description": "The GitHub repository to migrate. Use the repository's catalog identifier, which is the repository name without the organization prefix (for example, 'payment-service')."
},
"org": {
"title": "GitHub organization",
"type": "string",
"description": "The GitHub organization or user that owns the repository (for example, 'erioluwa-port')."
},
"project_name": {
"title": "GitLab Project Name",
"type": "string",
"description": "The name to give the new GitLab project. Leave empty to reuse the repository's name."
},
"visibility": {
"title": "Visibility",
"type": "string",
"enum": ["private", "internal", "public"],
"default": "private",
"description": "The visibility level for the new GitLab project. Use 'private' unless the user explicitly asks for internal or public visibility."
}
},
"required": ["repository", "org", "visibility"]
},
"permissions": {
"roles": ["Member"]
}
}
},
{
"identifier": "migrate",
"title": "Migrate to GitLab",
"description": "Dispatches the GitHub Actions pipeline that clones the repository and pushes it to GitLab.",
"config": {
"type": "INTEGRATION_ACTION",
"installationId": "YOUR_GITHUB_INTEGRATION_ID",
"integrationProvider": "github-ocean",
"integrationInvocationType": "dispatch_workflow",
"integrationActionExecutionProperties": {
"org": "YOUR_GITHUB_ORG",
"repo": "YOUR_GITHUB_REPO",
"workflow": "migrate-to-gitlab.yml",
"workflowInputs": {
"org": "{{ .outputs.trigger.org }}",
"repository": "{{ .outputs.trigger.repository }}",
"project_name": "{{ .outputs.trigger.project_name }}",
"visibility": "{{ .outputs.trigger.visibility }}"
},
"reportWorkflowStatus": true
}
}
}
],
"connections": [
{
"sourceIdentifier": "trigger",
"targetIdentifier": "migrate"
}
]
}
Before creating the workflow, replace these placeholders:
installationId: your GitHub integration ID from the data sources page.organdrepoinintegrationActionExecutionProperties: the GitHub org and repo that will hostmigrate-to-gitlab.yml(the automation repo, not the repository being migrated).
Create the workflow with a single API call:
curl --location --request POST 'https://api.port.io/v1/workflows' \
--header 'Authorization: Bearer <YOUR_API_TOKEN>' \
--header 'Content-Type: application/json' \
--data '<paste the workflow JSON from above, with your placeholders replaced>'
See API credentials for how to generate <YOUR_API_TOKEN>.
You can also create the workflow visually: go to the Workflows page, click + Workflow, click Skip to editor, paste the JSON above, then click Save.
Create the GitHub Actions workflow
Create a file in your automation repository at .github/workflows/migrate-to-gitlab.yml. This pipeline handles the actual migration steps - creating the GitLab project and pushing the complete repository history.
GitHub Actions workflow (click to expand)
You'll need to set up the following secrets in your automation repository (under Settings → Secrets and variables → Actions):
GH_TOKEN: A GitHub Personal Access Token withreposcope for cloning the source repositories. Create one at: GitHub → Settings → Developer settings → Personal access tokensGITLAB_TOKEN: Your GitLab Personal Access Token withapiscope. Create one at: GitLab → Settings → Access TokensGITLAB_HOST: Your GitLab instance hostname (e.g.gitlab.comorgitlab.mycompany.com)GITLAB_NAMESPACE: Your GitLab username or group (e.g.myusernameormycompany/engineering)
name: Migrate to GitLab
on:
workflow_dispatch:
inputs:
org:
description: 'GitHub organization or user that owns the repository (for example, erioluwa-port)'
required: true
type: string
repository:
description: 'Identifier of the repository being migrated (its name in the Port catalog)'
required: true
type: string
project_name:
description: 'Project name (leave empty to use repository name)'
required: false
type: string
visibility:
description: 'Project visibility'
required: true
type: choice
options:
- private
- internal
- public
default: private
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- name: Clone GitHub repository (full history)
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_ORG: ${{ inputs.org }}
GITHUB_REPO: ${{ inputs.repository }}
run: |
git clone --mirror \
"https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_ORG}/${GITHUB_REPO}.git" \
repo.git
echo "Cloned ${GITHUB_ORG}/${GITHUB_REPO} with full history"
- name: Set project name
id: project
env:
INPUT_PROJECT_NAME: ${{ inputs.project_name }}
GITHUB_REPO: ${{ inputs.repository }}
run: |
PROJECT_NAME=""
if [ -n "$INPUT_PROJECT_NAME" ] && [ "$INPUT_PROJECT_NAME" != "null" ]; then
PROJECT_NAME="$INPUT_PROJECT_NAME"
fi
if [ -z "$PROJECT_NAME" ] || [ "$PROJECT_NAME" = "null" ]; then
# Strip org prefix (e.g. "org/repo" → "repo")
PROJECT_NAME="${GITHUB_REPO##*/}"
echo "Using repository name as fallback: $PROJECT_NAME"
fi
echo "name=$PROJECT_NAME" >> $GITHUB_OUTPUT
echo "Using project name: $PROJECT_NAME"
- name: Create GitLab project
id: create_project
env:
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
GITLAB_HOST: ${{ secrets.GITLAB_HOST }}
GITLAB_NAMESPACE: ${{ secrets.GITLAB_NAMESPACE }}
PROJECT_NAME: ${{ steps.project.outputs.name }}
VISIBILITY: ${{ inputs.visibility }}
run: |
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "https://${GITLAB_HOST}/api/v4/projects" \
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"${PROJECT_NAME}\",
\"path\": \"${PROJECT_NAME}\",
\"namespace_id\": \"$(curl -s "https://${GITLAB_HOST}/api/v4/namespaces?search=${GITLAB_NAMESPACE}" -H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" | jq -r '.[0].id')\",
\"visibility\": \"${VISIBILITY}\",
\"initialize_with_readme\": false
}")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | head -n-1)
if [ "$HTTP_CODE" -eq 201 ]; then
echo "✅ GitLab project created successfully"
PROJECT_URL=$(echo "$BODY" | jq -r '.http_url_to_repo')
echo "url=$PROJECT_URL" >> $GITHUB_OUTPUT
echo "Project URL: $PROJECT_URL"
elif [ "$HTTP_CODE" -eq 400 ] && echo "$BODY" | jq -e '.message.path[]' | grep -q "has already been taken"; then
echo "⚠️ Project already exists, will use existing project"
PROJECT_URL="https://${GITLAB_HOST}/${GITLAB_NAMESPACE}/${PROJECT_NAME}.git"
echo "url=$PROJECT_URL" >> $GITHUB_OUTPUT
else
echo "❌ Failed to create project. HTTP code: $HTTP_CODE"
echo "Response: $BODY"
exit 1
fi
- name: Configure Git
run: |
git config --global user.name "GitHub Actions Bot"
git config --global user.email "actions@github.com"
- name: Push to GitLab
env:
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
GITLAB_URL: ${{ steps.create_project.outputs.url }}
run: |
cd repo.git
GITLAB_HOST=$(echo $GITLAB_URL | sed -E 's|https?://([^/]+)/.*|\1|')
GITLAB_PATH=$(echo $GITLAB_URL | sed -E 's|https?://[^/]+/(.*)|/\1|')
git remote add gitlab https://oauth2:${GITLAB_TOKEN}@${GITLAB_HOST}${GITLAB_PATH}
# Push all branches and tags
git push gitlab --mirror --force
- name: Verify migration
run: |
echo "✅ Migration completed successfully!"
echo "Repository has been pushed to: ${{ steps.create_project.outputs.url }}"
echo ""
echo "Refs pushed:"
cd repo.git && git show-ref
INTEGRATION_ACTION nodes do not support secrets. GitHub dispatch uses your installed GitHub integration for authentication. The secrets above are for the GitHub Actions job, not the Port workflow.
Let's test it
From Port's UI
- Go to the Self-service hub page, or open a
githubRepositoryentity and use its bolt (⚡) menu. - Find "Migrate Repository to GitLab" and click
Execute. - Select the repository (pre-filled if launched from an entity's bolt menu), enter the GitHub organization, optionally override the GitLab project name, and choose a visibility level.
- Click
Executeto start the migration. - Monitor progress on the workflow run page.
- Verify that the repository is successfully created in your GitLab namespace with full commit history.
From an AI agent
Any agent connected to Port's MCP server can run this migration directly from a natural-language request, with no form to fill in:
"Migrate the payment-service repository from GitHub to GitLab, keep it private."
The agent calls list_self_service_triggers, matches the request against this workflow's description, resolves payment-service against the githubRepository blueprint, and calls trigger_run:
{
"type": "WORKFLOW",
"identifier": "migrate_to_gitlab",
"nodeIdentifier": "trigger",
"inputs": {
"repository": "payment-service",
"org": "my-github-org",
"visibility": "private"
}
}
The workflow runs exactly as it would from the UI, and the agent reports back the resulting GitLab project URL once the run completes. See expose workflows as tools for the full mechanics.
What happens during migration
- Repository selection - a repository is selected from the Port catalog, either through the form or resolved by an agent.
- Project creation - the pipeline creates a new GitLab project via the GitLab API.
- History preservation - full Git history is fetched, including all branches and tags.
- Push to GitLab - all branches and tags are pushed to the new GitLab project.
- Verification - the pipeline confirms successful migration and reports its status back to the workflow run.
Extend the workflow
- Restrict who can trigger migrations: add a permissions policy to the trigger node, for example limiting execution to the team that owns the repository, or requiring a named agent identity for unattended runs.
- Notify on completion: add a
WEBHOOKnode aftermigrateto post a Slack or Teams message once the GitHub Actions run reports its status back to Port. - Explore more workflow examples for inspiration.
Troubleshooting
"Failed to create project" error
- Verify your
GITLAB_TOKENhasapiscope permissions. - Check that
GITLAB_NAMESPACEexists and is accessible. - Ensure the project name doesn't already exist in the namespace.
"Authentication failed" error
- Verify your GitHub and GitLab secrets are correctly set on the automation repository.
- Check that your GitLab token hasn't expired.
- Confirm the token has access to the specified namespace.
"Project already exists" warning
- The pipeline will attempt to use the existing project.
- If you want a fresh migration, delete the existing GitLab project first.
- Or provide a different project name to avoid conflicts.