Skip to main content

Check out Port for yourself ➜ 

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

  1. Go to the data model page of your portal.

  2. Click on + Blueprint.

  3. Click on the {...} Edit JSON button in the top right corner.

  4. 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": {}
    }
  5. Click on Save to create the blueprint.

Set up GitHub integration

  1. Set up Port's GitHub integration by following Port's setup guide for GitHub.

  2. Configure the mapping:

    1. From the data sources page, locate the GitHub integration you installed and click on it.

    2. Under the Mapping field, paste the following mapping configuration:

      GitHub mapping configuration (click to expand)
      createMissingRelatedEntities: true
      resources:
      - kind: repository
      selector:
      query: 'true'
      port:
      entity:
      mappings:
      identifier: .name
      title: .name
      blueprint: '"githubRepository"'
      properties:
      readme: file://README.md
      url: .html_url
      defaultBranch: .default_branch
      language: .language
  3. 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

  1. Go to the Self-service page.

  2. Click on + Action.

  3. Click on {...} Edit JSON to enter JSON mode.

  4. 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
    }
  5. 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.
  6. Click Save to create the action.

Workflow name

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."
Required secrets

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 with repo scope for cloning the source repositories. Create one at: GitHub → Settings → Developer settings → Personal access tokens
  • GITLAB_TOKEN: Your GitLab Personal Access Token with api scope. Create one at: GitLab → Settings → Access Tokens
  • GITLAB_HOST: Your GitLab instance hostname (e.g. gitlab.com or gitlab.mycompany.com)
  • GITLAB_NAMESPACE: Your GitLab username or group (e.g. myusername or mycompany/engineering)
  • PORT_CLIENT_ID and PORT_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

  1. Go to the Self-service page.

  2. Find the "Migrate Repository to GitLab" action.

  3. Click Execute.

  4. Select a GitHub repository from the dropdown.

  5. Optionally override the GitLab project name.

  6. Choose the visibility level (private, internal, or public).

  7. Click Execute to start the migration.

  8. Monitor the action execution in Port's logs.

  9. Verify that the repository is successfully created in your GitLab namespace with full commit history.

What happens during migration

  1. Repository selection - you select a GitHub repository from your Port catalog.
  2. Project creation - the workflow creates a new GitLab project via the GitLab API.
  3. History preservation - full Git history is fetched, including all branches and tags.
  4. Push to GitLab - all branches and tags are pushed to the new GitLab project.
  5. Verification - the workflow confirms successful migration and displays the new GitLab URL.

Troubleshooting

"Failed to create project" error

  • Verify your GITLAB_TOKEN has api scope permissions.
  • Check that GITLAB_NAMESPACE exists 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.