Migrate repositories from GitHub to GitLab using Port self-service actions
This guide demonstrates how to migrate repositories from GitHub to GitLab using Port's self-service actions together with a GitHub Actions workflow.
We will use Port to model your repositories, trigger a standardised migration flow, and keep visibility of progress using logs and entity tracking.
Once implemented you will be able to:
- Set up GitHub integration so Port discovers repositories and metadata.
- Model repositories with blueprints to track your GitHub repositories.
- Trigger a repeatable migration flow from Port that runs a GitHub Actions workflow.
- Automatically create GitLab projects and push complete repository history.
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 your portal, 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 of your portal.
-
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 Port's GitHub integration by following Port's setup guide for GitHub.
-
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.
Set up self-service actions
We'll create a self-service action that allows users to trigger the migration flow from Port's UI.
Create the migration action
-
Go to the Self-service page.
-
Click on
+ Action. -
Click on
{...} Edit JSONto enter JSON mode. -
Copy and paste the following action configuration:
Self-service action configuration (click to expand)
{"identifier": "migrate_to_gitlab","title": "Migrate Repository to GitLab","icon": "GitLab","description": "Migrate a GitHub repository to GitLab with full history and create the project automatically","trigger": {"type": "self-service","operation": "DAY-2","userInputs": {"properties": {"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": "Override GitLab project name (leave empty to use repository name)"},"visibility": {"title": "Visibility","type": "string","enum": ["private", "internal", "public"],"default": "private","description": "Project visibility level"}},"required": ["org", "visibility"]},"blueprintIdentifier": "githubRepository"},"invocationMethod": {"type": "INTEGRATION_ACTION","installationId": "YOUR_GITHUB_INTEGRATION_ID","integrationActionType": "dispatch_workflow","integrationActionExecutionProperties": {"org": "YOUR_GITHUB_ORG","repo": "YOUR_GITHUB_REPO","workflow": "migrate-to-gitlab.yml","workflowInputs": {"{{ spreadValue() }}": "{{ .inputs }}","port_context": {"runId": "{{ .run.id }}","blueprint": "{{ .action.blueprint }}","entity": "{{ .entity }}"}},"reportWorkflowStatus": true}},"requiredApproval": false,"allowAnyoneToViewRuns": true} -
Replace the following fields:
installationId: The ID of your GitHub integration in Port (find it on the data sources page).org: Your GitHub organization where the workflow resides.repo: The repository where the GitHub Actions workflow is stored.
-
Click
Saveto create the action.
Make sure the workflow name matches the filename of your GitHub Actions workflow (in this guide, migrate-to-gitlab.yml).
Create the GitHub Actions workflow
Create a file in your repository at .github/workflows/migrate-to-gitlab.yml. This workflow handles the actual migration steps - creating the GitLab project and pushing the complete repository history.
GitHub Actions workflow (Click to expand)
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
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
port_context:
description: 'JSON string with runId, blueprint, and entity from Port'
required: true
type: string
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- name: Inform Port - migration started
uses: port-labs/port-github-action@v1
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
operation: PATCH_RUN
runId: ${{ fromJson(inputs.port_context).runId }}
logMessage: "Starting GitHub → GitLab migration... 🚀"
- name: Clone GitHub repository (full history)
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_ORG: ${{ inputs.org }}
GITHUB_REPO: ${{ fromJson(inputs.port_context).entity.identifier }}
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: ${{ fromJson(inputs.port_context).entity.identifier }}
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: Inform Port - GitLab project ready
uses: port-labs/port-github-action@v1
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
operation: PATCH_RUN
runId: ${{ fromJson(inputs.port_context).runId }}
logMessage: "GitLab project '${{ steps.project.outputs.name }}' is ready. Pushing repository history... 📦"
- 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
- name: Inform Port - migration complete
if: success()
uses: port-labs/port-github-action@v1
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
operation: PATCH_RUN
runId: ${{ fromJson(inputs.port_context).runId }}
logMessage: |
Migration completed successfully! ✅
Repository is now available at: ${{ steps.create_project.outputs.url }}
- name: Inform Port - migration failed
if: failure()
uses: port-labs/port-github-action@v1
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
operation: PATCH_RUN
runId: ${{ fromJson(inputs.port_context).runId }}
status: FAILURE
logMessage: "Migration failed ❌. Check the workflow logs for details."
You'll need to set up the following secrets in your GitHub 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)PORT_CLIENT_IDandPORT_CLIENT_SECRET: Used by the Port GitHub Action to send log updates back to Port. Find these in Port → Settings → Credentials
Let's test it
-
Go to the Self-service page.
-
Find the "Migrate Repository to GitLab" action.
-
Click
Execute. -
Select a GitHub repository from the dropdown.
-
Optionally override the GitLab project name.
-
Choose the visibility level (private, internal, or public).
-
Click
Executeto start the migration. -
Monitor the action execution in Port's logs.
-
Verify that the repository is successfully created in your GitLab namespace with full commit history.
What happens during migration
- Repository selection - you select a GitHub repository from your Port catalog.
- Project creation - the workflow 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 workflow confirms successful migration and displays the new GitLab URL.
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 all three secrets are correctly set in GitHub.
- Check that your GitLab token hasn't expired.
- Confirm the token has access to the specified namespace.
"Project already exists" warning
- The workflow 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.