Migrate repositories from Bitbucket to GitHub using Port self-service actions
This guide demonstrates how to migrate repositories from Bitbucket to GitHub using Port's self-service actions together with a GitHub Actions workflow.
We will use Port to model your repositories, trigger a standardized migration flow, and keep visibility of progress using scorecards and logs.
Once implemented you will be able to:
- Set up Bitbucket and GitHub integrations so Port discovers repositories and metadata.
 - Model repositories with blueprints and scorecards to track migration readiness and status.
 - Trigger a repeatable migration flow from Port that runs a GitHub Actions workflow.
 
Prerequisitesโ
You should have the following in place for this migration:
- A Port account (sign up at https://port.io to create one)
 - A Bitbucket account with the specified project and set of repositories you want to migrate to GitHub
 - A GitHub organization where you have owner permissions or permissions to create a repository
 
Set up data modelโ
To represent your Bitbucket and GitHub repositories in your portal, we need to create blueprints for each service type, set up data source integrations, and configure scorecards to track migration status.
Create the GitHub Service 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 Service blueprint (Click to expand)
{
"identifier": "githubService",
"title": "GitHub Service",
"icon": "Github",
"schema": {
"properties": {
"url": {
"title": "URL",
"format": "url",
"type": "string",
"icon": "Link"
},
"readme": {
"title": "README",
"type": "string",
"format": "markdown",
"icon": "Book"
},
"language": {
"title": "Language",
"type": "string",
"icon": "DefaultProperty"
},
"codeowners": {
"type": "string",
"title": "Codeowners",
"description": "Codeowners file",
"icon": "Team",
"format": "markdown"
},
"jenkinsfile": {
"type": "string",
"title": "Jenkins Configuration",
"description": "Jenkins",
"icon": "CICD",
"format": "markdown"
}
},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"aggregationProperties": {},
"relations": {}
} - 
Click on
Saveto create the blueprint. 
Create the Bitbucket Service 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:
Bitbucket Service blueprint (Click to expand)
{
"identifier": "bitbucketService",
"title": "Bitbucket Service",
"icon": "Service",
"schema": {
"properties": {
"url": {
"title": "URL",
"format": "url",
"type": "string",
"icon": "Link"
},
"readme": {
"title": "README",
"type": "string",
"format": "markdown",
"icon": "Book"
},
"language": {
"title": "Language",
"type": "string",
"icon": "DefaultProperty"
},
"codeowners": {
"type": "string",
"title": "Codeowners",
"description": "Codeowners file",
"icon": "Team",
"format": "markdown"
},
"jenkinsfile": {
"type": "string",
"title": "Jenkins Configuration",
"description": "Jenkins",
"icon": "CICD",
"format": "markdown"
}
},
"required": []
},
"mirrorProperties": {
"github_service_name": {
"title": "githubServiceName",
"path": "githubService.$title"
}
},
"calculationProperties": {},
"aggregationProperties": {},
"relations": {
"githubService": {
"title": "Github Repo URL",
"target": "githubService",
"required": false,
"many": false
},
"project": {
"title": "Project",
"target": "bitbucketProject",
"required": false,
"many": false
}
}
} - 
Click on
Saveto create the blueprint. 
Set up GitHub integrationโ
- 
Set up the 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: true
resources:
- kind: repository
selector:
query: 'true'
port:
entity:
mappings:
identifier: .name
title: .name
blueprint: '"githubService"'
properties:
readme: file://README.md
url: .html_url
language: .language
codeowners: file://.github/CODEOWNERS
jenkinsfile: file://Jenkinsfile - 
Click on the Save & Resync button at the bottom right corner.
 
Set up Bitbucket integrationโ
- 
Set up the Port's Bitbucket integration by following Port's setup guide for Bitbucket.
 - 
Configure the mapping:
- From the data sources page, locate the Bitbucket integration you installed and click on it.
 - Under the Mapping field, paste the following mapping configuration:
 
Bitbucket mapping configuration (Click to expand)
branch: main
resources:
- kind: repository
selector:
query: 'true'
port:
entity:
mappings:
identifier: .name
title: .name
blueprint: '"bitbucketService"'
properties:
readme: file://README.md
url: ".links.html.href"
language: .language
jenkinsfile: file://Jenkinsfile
codeowners: file://.bitbucket/CODEOWNERS
relations:
githubService:
combinator: '"and"'
rules:
- property: '"$title"'
operator: '"="'
value: .name - 
Click on the Save & Resync button at the bottom right corner.
 
Set up scorecardsโ
The scorecards for tracking migration status will be included in the two blueprints (githubService and bitbucketService). They're descriptive onlyโthere's no automated step that triggers or blocks action based on the scorecards. However, they help teams quickly see which repositories:
- Are still in Bitbucket vs. which have been migrated to GitHub,
 - Have certain configuration files (e.g. 
README.md,Jenkinsfile), - Or meet other custom criteria you want to display in the Port dashboard.
 
The migration process will automatically update relevant fields in Port, keeping these scorecards in sync with the actual state of your repositories.
Create GitHub service scorecardโ
- 
From the data model page, select the Data model page on the left sidebar.
 - 
Locate your previously-created GitHub blueprint.
 - 
Click on the Scorecards tab.
 - 
Click on New scorecard.
 - 
Paste the scorecard JSON definition below:
GitHub service scorecard (Click to expand)
{
"identifier": "bbToGithubMigration",
"title": "Migration",
"levels": [
{
"color": "paleBlue",
"title": "Basic"
},
{
"color": "bronze",
"title": "Bronze"
},
{
"color": "silver",
"title": "Silver"
},
{
"color": "gold",
"title": "Gold"
}
],
"rules": [
{
"identifier": "hasReadme",
"title": "Has Readme",
"description": "Checks if the service has a README file",
"level": "Bronze",
"query": {
"combinator": "and",
"conditions": [
{
"operator": "isNotEmpty",
"property": "readme"
}
]
}
},
{
"identifier": "hasCodeowner",
"title": "Has Codeowners",
"description": "Checks if the service has a CODEOWNERS file",
"level": "Silver",
"query": {
"combinator": "and",
"conditions": [
{
"property": "codeowners",
"operator": "isNotEmpty"
}
]
}
},
{
"identifier": "hasCICD",
"title": "Has CI/CD",
"description": "Checks if the service has a CI/CD configured",
"level": "Gold",
"query": {
"combinator": "and",
"conditions": [
{
"property": "jenkinsfile",
"operator": "isNotEmpty"
}
]
}
}
]
} 
Create Bitbucket service scorecardโ
- 
From the data model page, select the Data model page on the left sidebar.
 - 
Locate your previously-created Bitbucket blueprint.
 - 
Click on the Scorecards tab.
 - 
Click on New scorecard.
 - 
Paste the scorecard JSON definition below:
Bitbucket service scorecard (Click to expand)
{
"identifier": "bbToGithubMigration",
"title": "Migration",
"levels": [
{
"color": "paleBlue",
"title": "Basic"
},
{
"color": "bronze",
"title": "Bronze"
},
{
"color": "silver",
"title": "Silver"
},
{
"color": "gold",
"title": "Gold"
}
],
"rules": [
{
"identifier": "hasReadme",
"title": "Has Readme",
"description": "Checks if the service has a README file",
"level": "Bronze",
"query": {
"combinator": "and",
"conditions": [
{
"operator": "isNotEmpty",
"property": "readme"
}
]
}
},
{
"identifier": "hasCodeowner",
"title": "Has Codeowners",
"description": "Checks if the service has a CODEOWNERS file",
"level": "Silver",
"query": {
"combinator": "and",
"conditions": [
{
"property": "codeowners",
"operator": "isNotEmpty"
}
]
}
},
{
"identifier": "hasCICD",
"title": "Has CI/CD",
"description": "Checks if the service has a CI/CD configured",
"level": "Silver",
"query": {
"combinator": "and",
"conditions": [
{
"property": "jenkinsfile",
"operator": "isNotEmpty"
}
]
}
},
{
"identifier": "hasGitHubService",
"title": "Has GitHub equivalent",
"description": "Checks if the service has been migrated to GitHub",
"level": "Gold",
"query": {
"combinator": "and",
"conditions": [
{
"property": "github_service_name",
"operator": "isNotEmpty"
}
]
}
}
]
} - 
Click on Save to create the scorecard.
 
The Bitbucket scorecard additionally tracks the existence of a GitHub repository counterpart for each Bitbucket repository, helping you monitor migration status.
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_service_from_bitbucket_to_git_hub",
"title": "Migrate Service From Bitbucket To GitHub",
"trigger": {
"type": "self-service",
"operation": "CREATE",
"userInputs": {
"properties": {
"service": {
"type": "array",
"title": "Service",
"items": {
"type": "string",
"format": "entity",
"blueprint": "bitbucketRepository"
}
},
"visibility": {
"type": "string",
"title": "Visibility",
"default": "Internal",
"icon": "Permissions",
"description": "Visibility of the repositories selected",
"enum": [
"Private",
"Public",
"Internal"
],
"enumColors": {
"Private": "lightGray",
"Public": "lightGray",
"Internal": "lightGray"
}
}
},
"required": [
"service",
"visibility"
],
"order": [
"service"
]
},
"blueprintIdentifier": "githubService"
},
"invocationMethod": {
"type": "GITHUB",
"org": "YOUR-ORG",
"repo": "YOUR-REPOSITORY",
"workflow": "migrate-to-github.yml",
"workflowInputs": {
"repositories": "{{ [.inputs.service[] | {name: .identifier, project: .relations.project, workspace: .properties.workspace }] }}",
"visibility": "{{ .inputs.visibility | ascii_downcase }}",
"port_context": {
"run_id": "{{ .run.id }}",
"blueprint": "{{ .action.blueprint }}"
}
},
"reportWorkflowStatus": true
},
"requiredApproval": false,
"icon": "GitSubIcon"
} - 
Replace the
organdrepofields with:org: Your GitHub organization or user where the GitHub workflow residesrepo: The repository in theorgwhere the GitHub workflow resides
 - 
Click
Saveto create the action. 
Make sure the workflow name matches the filename of your GitHub Actions workflow (in this guide, "migrate-to-github.yml").
Create the GitHub Actions workflowโ
Create a file in your repository at .github/workflows/migrate-to-github.yml. This workflow handles the actual migration stepsโcloning from Bitbucket, creating the repo in GitHub, and pushing the code.
GitHub Actions workflow (Click to expand)
# This workflow is used to migrate a Bitbucket repository to GitHub repository
# The repository is cloned from Bitbucket and pushed to GitHub even when the repository does not exist on GitHub
# The workflow is triggered when a new repository is created in Bitbucket
name: Migrate Bitbucket Repositories to GitHub
on:
  workflow_dispatch:
    inputs:
      repositories:
        description: "JSON array of objects, each with 'name', 'workspace', and 'project'"
        required: true
        type: string
      visibility:
        description: "Repository visibility (public, private, internal)"
        required: true
        type: string
        default: private
      port_context:
        description: "JSON string with blueprint, run_id, etc. from Port."
        required: true
        type: string
jobs:
  migrate-bitbucket-repos:
    runs-on: ubuntu-latest
    steps:
      # 1) Inform Port that migration is starting
      - name: Inform Port about migration start
        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).run_id }}
          logMessage: "Starting Bitbucket โ GitHub repository migration... ๐"
      # 3) Loop over each repository, create on GitHub, mirror from Bitbucket
      - name: Migrate repositories
        id: migrate_repos
        run: |
          echo "Authenticating with GitHub..."
          gh auth status
          # Track successful migrations
          successful_repos=()
          REPOS_JSON='${{ inputs.repositories }}'
          echo "Raw input: $REPOS_JSON"
          for row in $(echo "$REPOS_JSON" | jq -rc '.[]'); do
            echo "$row"
            REPO_NAME=$(echo "${row}" | jq -r '.name')
            PROJECT=$(echo "${row}" | jq -r '.workspace')
            echo $REPO_NAME
            echo $PROJECT
            echo "::group::Migrating ${REPO_NAME}"
            # 3b) Mirror from Bitbucket to GitHub
            echo "Cloning ${REPO_NAME} from Bitbucket project '${PROJECT}'..."
            git clone --mirror "https://${BB_USERNAME}:${BB_PASSWORD}@bitbucket.org/${PROJECT}/${REPO_NAME}.git"
            if [ $? -ne 0 ]; then
              echo "Failed to clone Bitbucket repository: ${REPO_NAME}"
              echo "::endgroup::"
              continue
            fi
              
            cd "${REPO_NAME}.git"
            # 3a) Create GitHub repository
            echo "Creating GitHub repo ${REPO_NAME}..."
            gh repo create "${GITHUB_ORG}/${REPO_NAME}" --${{ inputs.visibility }} --push --source .
            if [ $? -ne 0 ]; then
              echo "Failed to create GitHub repository: ${REPO_NAME}"
              echo "::endgroup::"
              continue
            fi
            # # 3b) Mirror from Bitbucket to GitHub
            # echo "Cloning ${REPO_NAME} from Bitbucket project '${PROJECT}'..."
            # git clone --mirror "https://${BB_USERNAME}:${BB_PASSWORD}@bitbucket.org/${PROJECT}/${REPO_NAME}.git"
            # if [ $? -ne 0 ]; then
            #   echo "Failed to clone Bitbucket repository: ${REPO_NAME}"
            #   echo "::endgroup::"
            #   continue
            # fi
            # cd "${REPO_NAME}.git"
            # echo "Pushing mirror to GitHub..."
            # git remote set-url --push origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_ORG}/${REPO_NAME}.git"
            # git repo create ${REPO_NAME} --${{ inputs.visibility }} --source .
            # if [ $? -ne 0 ]; then
            #   echo "Failed to push to GitHub repository: ${REPO_NAME}"
            #   cd ..
            #   rm -rf "${REPO_NAME}.git"
            #   echo "::endgroup::"
            #   continue
            # fi
            cd ..
            rm -rf "${REPO_NAME}.git"
            # Add to successful migrations list
            successful_repos+=("${REPO_NAME}")
            echo "Successfully migrated ${REPO_NAME}"
            echo "::endgroup::"
          done
          # Expose the list of successfully migrated repos to future steps
          echo "successful_repos=${successful_repos[*]}" >> "$GITHUB_OUTPUT"
          echo "Successfully migrated repositories: ${successful_repos[*]}"
        env:
          GH_TOKEN: ${{ secrets.GH_TOKEN }}
          BB_USERNAME: ${{ secrets.BB_USERNAME }}
          BB_PASSWORD: ${{ secrets.BB_PASSWORD }}
          GITHUB_ORG: ${{ secrets.GH_ORG }}
        shell: bash
      # 4) Use a separate step to collect repository data for Port
      - name: Collect repository data for Port
        if: env.successful_repos != ''
        id: collect_repo_data
        run: |
          # Create a JSON array to store all repository data
          echo "repo_data=[]" >> $GITHUB_OUTPUT
          for REPO in $successful_repos; do
            echo "Collecting data for ${REPO}..."
            # Get repository details from GitHub
            REPO_INFO=$(gh api "repos/${GH_ORG}/${REPO}")
            VISIBILITY=$(echo "$REPO_INFO" | jq -r '.visibility')
            URL=$(echo "$REPO_INFO" | jq -r '.html_url')
            DESCRIPTION=$(echo "$REPO_INFO" | jq -r '.description // ""')
            # Create the payload for Port API and add to array
            REPO_PAYLOAD=$(cat << EOF
            {
                "identifier": "${REPO}",
                "title": "${REPO}",
                "blueprint": "${{ fromJson(inputs.port_context).blueprint }}",
                "properties": {
                    "visibility": "${VISIBILITY}",
                    "url": "${URL}",
                    "description": "${DESCRIPTION}",
                    "source": "bitbucket",
                    "migrationDate": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
                },
                "relations": {}
            }
            EOF
            )
            
            # Append to the output in a format that can be parsed as JSON
            echo "repo_data=$(echo $repo_data | jq --argjson new "$REPO_PAYLOAD" '. + [$new]')" >> $GITHUB_OUTPUT
            echo "Collected data for ${REPO}"
          done
        env:
          GH_TOKEN: ${{ secrets.GH_TOKEN }}
          GH_ORG: ${{ secrets.GH_ORG }}
      # 5) Final log message in Port for the migration job
      - name: Inform Port of migration completion
        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).run_id }}
          logMessage: |
            Repository migration completed! โ
            Successfully migrated: ${{ env.successful_repos }}
            Proceeding to register repositories in Port...
  # New job to upsert repositories to Port using matrix
  upsert-to-port:
    needs: migrate-bitbucket-repos
    if: needs.migrate-bitbucket-repos.outputs.repo_data != '[]'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        repo: ${{ fromJson(needs.migrate-bitbucket-repos.outputs.repo_data) }}
    steps:
      - name: Upsert repository to Port
        uses: port-labs/port-github-action@v1
        with:
          clientId: ${{ secrets.PORT_CLIENT_ID }}
          clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
          operation: UPSERT_ENTITY
          blueprint: ${{ matrix.repo.blueprint }}
          identifier: ${{ matrix.repo.identifier }}
          title: ${{ matrix.repo.title }}
          properties: ${{ toJSON(matrix.repo.properties) }}
          relations: ${{ toJSON(matrix.repo.relations) }}
      - name: Log repository registration
        run: echo "Successfully registered ${{ matrix.repo.title }} in Port"
You'll need to set up the following secrets in your GitHub repository (under Settings โ Secrets and variables โ Actions):
GH_TOKEN: Your GitHub Personal Access Token (PAT) with permissions to create repositoriesBB_USERNAME: Your Bitbucket usernameBB_PASSWORD: A Bitbucket API token. Create one following Atlassian's guide on API TokensGH_ORG: The GitHub organization where you're creating migrated reposPORT_CLIENT_IDandPORT_CLIENT_SECRET: For Port's GitHub Action
Let's test itโ
- 
Go to the Self-service page.
 - 
Find the "Migrate Service From Bitbucket To GitHub" action.
 - 
Click
Execute. - 
Choose one or more repositories from the dropdown and set the visibility.
 - 
Monitor the action execution in Port's logs.
 - 
Verify that repositories are successfully migrated to your GitHub organization.