🗂

GitLab から GitHub へ移行するには? リポジトリ / Issue / CI/CD をまとめて検証

に公開

CICD ツールや開発プラットフォームはいろいろありますが、「どれがよく使われているのか?」という観点でまずはざっくり人気感を押さえておきたいと思い、直近 1 年間の Google トレンドを比較してみました。
GitHub、Jira、GitLab、Azure DevOps の 4 つで相対的な検索関心度を並べると、おおむね GitHub が頭ひとつ抜けていて、その後ろに Jira、GitLab、Azure DevOps という順に続くイメージでした。

GitHub が強いとはいえ、GitLab も普通に使われていますし、「いま GitLab を使っているけど、今後は GitHub 側に寄せたい」という相談はよくあります。もちろん、その逆も然りです。

本記事では、GitLab から GitHub へ移行する際の手順と注意点 を、実際のリポジトリを使って検証した結果としてまとめます。

「コードはどうやって持っていくの?」「Issues は?」「パイプラインは?」といった、現場で一番困りがちな 3点

  • リポジトリ(ソースコードと履歴)
  • Issue(課題管理)
  • CI/CD パイプライン

について、それぞれどこまで自動でいけるかを記録としてまとめます。

まずは、GitHub と GitLab で似ているようで違う用語をざっくり揃えておきます。

GitHubの用語 役割イメージ GitLabでいちばん近いもの 補足
Enterprise (アカウント) 複数の Organization をまとめる最上位の管理単位。請求・ポリシー・監査ログなどをまとめて管理する枠。 GitLab インスタンス全体(Self-Managedの場合)もしくはトップレベル Group(SaaS運用での実質的な最上位) GitLab には GitHub Enterprise Account に相当する「SaaS上の親テナント」という概念がないので、目的によって近いものが変わります。
Organization チーム/部門ごとにリポジトリを束ねるスペース。メンバーや権限、シークレットのスコープなどを管理する単位。 Group(グループ) GitLab の Group はサブグループを階層化できるので、より柔軟です。
Repository ソースコード、Issue、Pull Request などの単位そのもの。 Project(プロジェクト) GitLab では「Project = リポジトリ + Issue + Merge Request + CI/CD」の一式です。

つまり本記事では、「GitLab の Project」を「GitHub の Repository」に移行する、という話になります。
GitHub 側にも “Project” という別機能があるので、この記事ではあえて GitLab 側も「リポジトリ」という言葉に統一します。

前置きはこのくらいにして、以降では リポジトリ / Issue / パイプライン の順で、実際の移行手順と結果を見ていきます。

0. 前提

今回検証した環境は次のとおりです。

要素 説明 備考
GitLab https://gitlab.com/union.dml-group/event-attendees-app 移行元 (GitLab 側の Project)
GitHub https://github.com/yutaka-art/event-attendees-app 移行先 (GitHub 側の Repository)

移行元となる GitLab プロジェクトは以下のような構成です。
データベースから取得したデータを返却する簡単なWebアプリケーションを例としています。

移行先となる GitHub のリポジトリは、あらかじめ「空の状態」で作成しておきます(README などを自動生成しない状態がやりやすいです)。

1. GitLab(プロジェクト) → GitHub(リポジトリ)

まずはコードと履歴(コミットログ、ブランチ、タグなど)をすべて GitHub に持っていきます。

1.1 GitLab 側リポジトリを mirror でクローンする

git clone --mirror は “bare リポジトリ” として全ての refs(branches, tags, notes, HEAD など)を丸ごと持ってきてくれます。
「そのまま引っ越したい」という用途ではこれが一番忠実です。

git clone --mirror https://gitlab.com/union.dml-group/event-attendees-app.git

これで event-attendees-app.git というディレクトリが作成されます。
通常のワーキングツリーはなく、中身はいわゆる bare repo です。

cd event-attendees-app.git

カレントディレクトリを合わせておきます。

1.2 現在のリモートを確認する(GitLab の URL が origin になっているはず)

git remote -v

期待される出力イメージはこんな感じです。

origin  https://gitlab.com/union.dml-group/event-attendees-app.git (fetch)
origin  https://gitlab.com/union.dml-group/event-attendees-app.git (push)

いまは origin が GitLab を向いていて、push も GitLab に行く状態だと確認できればOKです。

1.3 push 先を GitHub リポジトリに差し替える

GitHub 側にはすでに空のリポジトリ
https://github.com/yutaka-art/event-attendees-app
がある前提とします。この URL を push 先に設定し直します。

git remote set-url は2通りの使い方があります。状況に合わせて選びます。

パターンA: fetch は GitLab のまま残し、push だけ GitHub に向ける
移行検証中など、GitLab 側もまだ触りたいときに便利です。

git remote set-url --push origin https://github.com/yutaka-art/event-attendees-app.git

このあと git remote -v を見ると、こうなっているはずです。

origin  https://gitlab.com/union.dml-group/event-attendees-app.git (fetch)
origin  https://github.com/yutaka-art/event-attendees-app.git (push)

パターンB: origin 自体をまるごと GitHub に置き換える
「もう GitLab は参照しない。本番で切り替える」という場合はこちらがシンプルです。

git remote set-url origin https://github.com/yutaka-art/event-attendees-app.git

この場合の git remote -v はこうなります。

origin  https://github.com/yutaka-art/event-attendees-app.git (fetch)
origin  https://github.com/yutaka-art/event-attendees-app.git (push)

1.4 GitHub 側へ全履歴を一括プッシュ(mirror)

最後に、GitHub 側へ全ての refs を push します。
mirror で clone しているので、単純に git push --mirror でOKです。

git push --mirror

1.5 まとめ(コマンドだけざっと並べると)

# 1. GitLabから全refsをmirror clone
git clone --mirror https://gitlab.com/union.dml-group/event-attendees-app.git
cd event-attendees-app.git

# 2. 今のリモート確認
git remote -v

# 3. push先をGitHubに変更(検証的にpushだけ変える場合)
git remote set-url --push origin https://github.com/yutaka-art/event-attendees-app.git
# 完全移行なら
# git remote set-url origin https://github.com/yutaka-art/event-attendees-app.git

# 4. GitHubへ全refsをpush
git push --mirror
# または
# git push --mirror origin

GitLab 側で使っていたブランチなども、GitHub 側にそのまま移っていることが確認できます。

2. GitLab(Issue) → GitHub(Issues)

次は Issue(課題管理)です。
ここは GitHub が公式で用意している「GitHub Enterprise Importer」ではカバーできない範囲がまだ多く、サードパーティ/OSS ツールに頼ることになります。

今回使ったのは node-gitlab-2-github という OSS です

https://github.com/piceaTech/node-gitlab-2-github

「GitLab の Issue や Merge Request の内容を GitHub Issues に持っていく」ことを目的にしたツールです。

2.1 ツールの取得とセットアップ

リポジトリを任意のディレクトリに clone し、npm で依存パッケージを入れます。

git clone https://github.com/piceaTech/node-gitlab-2-github.git
cd node-gitlab-2-github
npm install

2.2 Personal Access Token (PAT) の発行

GitLab 側・GitHub 側それぞれで PAT を作っておき、移行時に使います。
必要なスコープは最低限に絞って発行してください。

  • GitLab
    ユーザー設定 > プロファイルを編集 > パーソナルアクセストークン から発行
    スコープは apiread_repository を付与します。

  • GitHub
    Settings > Developer settings > Personal access tokens > Tokens (classic) から発行
    スコープは repo を付与します。

2.3 設定ファイルの作成

ツールは settings.ts を読み込みます。
サンプル設定ファイルがあるので、それをコピーして編集します。

cp sample_settings.ts settings.ts

settings.ts の例はこんな感じです(必要な値は自分の環境に合わせて置き換えてください)

settings.ts
import Settings from './src/settings';

export default {
  gitlab: {
    token: '<your-gitlab-token>',
    projectId: null, // 後で置き換える
    listArchivedProjects: true,
    sessionCookie: "",
  },
  github: {
    owner: '<your-organization-name or your-personal-account-name>',
    ownerIsOrg: false, // Organizationに移したい場合はtrue
    token: '<your-github-token>',
    token_owner: '<your-account-name>',
    repo: '<migration-repository-name>',
    recreateRepo: false,
  },
  usermap: {
    '<gitlab-user-name>': '<github-account-name>'
  },
  projectmap: {
    'event-attendees-app': 'event-attendees-app'
  },
  // ...省略...
} as Settings;

2.4 ツール実行(対象プロジェクトの確認)

一度ツールを実行して、まずは「GitLab 側でどのプロジェクトが対象になるか」を確認します。
この段階では実データの移行はまだ行いません(ドライランのイメージです)。

npm run start

問題なく動けば、対象となる GitLab プロジェクトの ID が列挙されます。
これは後の手順で指定する必要があります。

2.5 ツール実行(実際の移行)

settings.tsgitlab.projectId をいま取得したプロジェクト ID に書き換えます。

export default {
  gitlab: {
    // ...
    projectId: <2.4で出力されたプロジェクトID>,
    // ...
  },
  // ...
}

その上で、もう一度実行します。

npm run start

GitHub 側の Issues を見ると、GitLab の Issue がいい感じにインポートされています。
Milestone や Label もある程度マッピングされていました。

3. GitLab(CI/CD パイプライン) → GitHub(Actions)

最後はパイプラインです。

ここが一番「期待しすぎるとつらい」領域で、GitLab CI/CD の実行環境そのものを GitHub Actions にそのまま持っていくことはできません。

ただし、「.gitlab-ci.yml をベースに、GitHub Actions っぽいワークフロー(.github/workflows/*.yml)を自動生成する」ための支援ツールは存在します。それが gh-actions-importer です。

https://github.com/github/gh-actions-importer

このツールはいくつかのモードを持っていて、

  • 監査(audit)
  • コスト/実行量の見積もり(forecast)
  • ドライラン変換(dry-run)
  • 実際にPRを作る移行(migrate)

といったステップで、段階的に移行を手伝ってくれます。
※ Docker が動く環境が必要なので、事前に用意しておきます。

3.0 ツールの初期設定

まず CLI 拡張を GitHub CLI (gh) にインストールします。

gh extension install github/gh-actions-importer

次に資格情報を設定します。

gh actions-importer configure

設定時の入力イメージはこんな感じです。

項目 値の例 補足
Which CI providers are you configuring? GitLab CI 今回は GitLab → GitHub の移行なのでこれ
Personal access token for GitHub GitHubで発行したPAT repo, workflow など必要な権限を付与
Base url of the GitHub instance https://github.com GitHub.com以外(GHES等)の場合は適宜変更
Private token for GitLab GitLabで発行したPAT read_api が必要
Base url of the GitLab instance https://gitlab.com Self-Managedの場合はそのURLに変更

最後にツール本体をアップデートしておきます。

gh actions-importer update

3.1 audit: GitLab 全体のパイプラインを棚卸しする

audit コマンドでは、指定した GitLab グループ以下のプロジェクトを走査して、

  • どんなパイプラインがあるのか
  • それらを GitHub Actions にどの程度マッピングできそうか
    をレポートとしてまとめてくれます。
gh actions-importer audit gitlab `
  --output-dir tmp/audit `
  --namespace <your-gitlab-group>

出力されるディレクトリ構成の例はこんな感じです。

.\tmp\audit
│  audit_summary.md
│  workflow_usage.csv
│
├─log
│      valet-20251028-025836.log
│      valet-20251028-030938.log
│
└─union.dml-group
    ├─event-attendees-app
    │  │  config.json
    │  │  source.yml
    │  │
    │  └─.github
    │      └─workflows
    │              event-attendees-app.yml
    │
    └─migration-lab-to-hub-deletion_scheduled-75614825
        │  config.json
        │  source.yml
        │
        └─.github
            └─workflows
                    migration-lab-to-hub-deletion_scheduled-75614825.yml

3.2 forecast: GitHub Actions の利用量を見積もる

forecast コマンドは、GitLab 上のパイプライン実行履歴からメトリックを集計し、
「GitHub Actions に移したらどれくらい実行時間 / ランナー利用量になりそうか」を試算します。
コスト見積もりやライセンス検討の材料になるフェーズです。

gh actions-importer forecast gitlab `
  --output-dir tmp/forecast `
  --namespace <your-gitlab-group>

出力イメージ

.tmp\forecast
│  forecast_report.md
│
├─jobs
│      10-28-2025-03-14_jobs_0.json
│
└─log
        valet-20251028-031442.log

3.3 dry-run: GitLab パイプラインを Actions 形式に変換してみる

dry-run コマンドは、GitLab のパイプライン定義(.gitlab-ci.yml)を解析し、
相当する GitHub Actions ワークフロー(.github/workflows/*.yml)をローカルに生成してくれます。
この時点では、まだ GitHub 側には反映されません。

gh actions-importer dry-run gitlab `
  --output-dir tmp/dry-run `
  --namespace <your-gitlab-group> `
  --project <your-gitlab-project>

出力イメージ

.\tmp\dry-run
├─log
│      valet-20251028-030513.log
│      valet-20251028-030853.log
│
└─union.dml-group
    └─event-attendees-app
        └─.github
            └─workflows
                    event-attendees-app.yml

3.4 migrate: 実際に GitHub リポジトリへ PR を作る

最後に migrate
これは、GitLab のパイプラインを変換したうえで、GitHub 側のターゲットリポジトリに Pull Request を作ってくれます。
つまり「本番環境への持ち込み」の第一歩です。

gh actions-importer migrate gitlab `
  --target-url https://github.com/yutaka-art/event-attendees-app `
  --output-dir tmp/migrate `
  --namespace <your-gitlab-group> `
  --project <your-gitlab-project>

出力例

.\tmp\migrate
└─log
        valet-20251028-032918.log

GitHub 側を見ると、実際にワークフローを追加する Pull Request が作成されています。

3.5 どこまで「そのまま」使えるのか?

ここが重要なポイントです。
今回の GitLab パイプラインは、.NET のユニットテストとカバレッジ計測、そしてコンテナイメージのビルド・ACR (Azure Container Registry) へのプッシュ、という比較的よくある内容です。

まず元の .gitlab-ci.yml はこんな感じでした(一部抜粋)

.gitlab-ci.yml
workflow:
  rules:
    - if: '$CI_COMMIT_BRANCH == "main""
      changes:
        - src/**/*
        - database/**/*
    - when: never

variables:
  BUILD_CONFIGURATION: "Release"

stages:
  - test
  - deploy_dev

# (ユニットテスト & カバレッジ)
unit_test_job:
  stage: test
  image: mcr.microsoft.com/dotnet/sdk:8.0
  script:
    - echo "Discovering test projects..."
    - TEST_PROJECTS_LIST=$(find . -type f -name '*.Tests.csproj')
    - ...
    - dotnet restore ...
    - dotnet build ...
    - dotnet test ... --collect:"XPlat Code Coverage"
    - カバレッジXMLを coverage/ にコピー
  artifacts:
    when: always
    expire_in: 1 week
    paths:
      - coverage/
      - test-results/
    reports:
      junit: test-results/*.trx
      coverage_report:
        coverage_format: cobertura
        path: coverage/*.xml

# (Dev環境向けのコンテナビルド & プッシュ)
deploy_dev_job:
  stage: deploy_dev
  needs: ["unit_test_job"]
  image: docker:26
  services:
    - name: docker:26-dind
      command: ["--tls=false"]
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
    ACR_LOGIN_SERVER: "cravanfr.azurecr.io"
    IMAGE_NAME: "eventattendeesapp"
    IMAGE_TAG: "latest"
  when: manual
  script:
    - docker login "$ACR_LOGIN_SERVER" -u "$ACR_USERNAME" -p "$ACR_PASSWORD"
    - docker build ...
    - docker push ...
  environment:
    name: dev

これを GitHub Actions Importer で変換すると、次のようなワークフローになりました(自動生成されたものをほぼそのまま掲載します)

ツールで変換したワークフロー.yaml
name: union.dml-group/event-attendees-app
on:
  push:
  workflow_dispatch:
concurrency:
  group: "${{ github.ref }}"
  cancel-in-progress: true
env:
  ACR_USERNAME: "${{ secrets.ACR_USERNAME }}"
  ACR_PASSWORD: "${{ secrets.ACR_PASSWORD }}"
jobs:
  unit_test_job:
    runs-on: ubuntu-latest
    container:
      image: mcr.microsoft.com/dotnet/sdk:8.0
    if: # Unable to map conditional expression to GitHub Actions equivalent
#         ${{ github.ref }} == "main" || false
    timeout-minutes: 60
    env:
      BUILD_CONFIGURATION: Release
    steps:
    - uses: actions/checkout@v4.1.0
      with:
        fetch-depth: 20
        lfs: true
    - run: echo "Discovering test projects..."
    - run: TEST_PROJECTS_LIST=$(find . -type f -name '*.Tests.csproj')
    - run: echo "Found test projects:"
    - run: echo "$TEST_PROJECTS_LIST"
    - run: mkdir -p coverage
    - run: mkdir -p test-results
    - run: |
        for proj in $TEST_PROJECTS_LIST; do
          echo "=== Processing $proj ==="
          NAME=$(basename "$proj" .csproj)
          echo "[restore] $proj"
          dotnet restore "$proj"
          echo "[build] $proj"
          dotnet build "$proj" --no-restore --configuration "$BUILD_CONFIGURATION"
          echo "[test]  $proj (with coverlet.collector)"
          dotnet test "$proj" \
            --no-build \
            --configuration "$BUILD_CONFIGURATION" \
            --collect:"XPlat Code Coverage" \
            --logger "trx;LogFileName=$(pwd)/test-results/${NAME}-test-results.trx"
          echo "[gather coverage] $proj"
          RESULT_DIR=$(dirname "$proj")/TestResults
          FOUND_FILE=$(find "$RESULT_DIR" -type f -name "coverage.cobertura.xml" | head -n 1 || true)
          if [ -n "$FOUND_FILE" ]; then
            cp "$FOUND_FILE" "coverage/${NAME}-coverage.cobertura.xml"
          else
            echo "No coverage.cobertura.xml found for $proj"
          fi
        done
    - run: echo "==== coverage dir ===="
    - run: ls -R coverage || true
    - run: echo "==== test-results dir ===="
    - run: ls -R test-results || true
#     # 'artifacts.junit' は GitHub Actions に相当するネイティブ機能がないため未変換
#     # 'artifacts.coverage_report' も同様
    - uses: actions/upload-artifact@v4.1.0
      if: always()
      with:
        name: "${{ github.job }}"
        retention-days: 7
        path: |-
          coverage/
          test-results/
  deploy_dev_job:
    needs: unit_test_job
    runs-on: ubuntu-latest
    container:
      image: docker:26
    if: github.event_name == 'workflow_dispatch'
    environment:
      name: dev
    timeout-minutes: 60
    services:
      docker:26-dind:
        image: docker:26-dind
        options: docker:26-dind --tls=false
    env:
      BUILD_CONFIGURATION: Release
      DOCKER_HOST: tcp://docker:2375
      DOCKER_TLS_CERTDIR: ''
      ACR_LOGIN_SERVER: cravanfr.azurecr.io
      IMAGE_NAME: eventattendeesapp
      IMAGE_TAG: latest
    steps:
    - uses: actions/checkout@v4.1.0
      with:
        fetch-depth: 20
        lfs: true
    - uses: actions/download-artifact@v4.1.0
    - run: echo "=== Deploy to Dev (build & push container image) ==="
    - run: echo "Logging in to $ACR_LOGIN_SERVER ..."
    - run: docker login "$ACR_LOGIN_SERVER" -u "$ACR_USERNAME" -p "$ACR_PASSWORD"
    - run: echo "Building image..."
    - run: docker build -t "$ACR_LOGIN_SERVER/$IMAGE_NAME:$IMAGE_TAG" -f src/Dockerfile ./src
    - run: echo "Pushing image to $ACR_LOGIN_SERVER ..."
    - run: docker push "$ACR_LOGIN_SERVER/$IMAGE_NAME:$IMAGE_TAG"
    - run: echo "Done. Image pushed as $ACR_LOGIN_SERVER/$IMAGE_NAME:$IMAGE_TAG"

一見それっぽいのですが、実際にはいくつか課題があります。

  • lfs: true になっているが、実際には Git LFS を使っていない/Runner で LFS をセットアップしていない

    - uses: actions/checkout@v4.1.0
      with:
        lfs: true
    

    このままだと不要な処理やエラーの元になることがあります。

  • 条件付き実行 (if:) が十分に変換されていない
    GitLab の rules: のように main ブランチだけ動かす、特定パスの変更時だけ動かす、といった条件は GitHub Actions では on.push.paths など別の表現に落とし込む必要があります。Importer はここを完全には自動化しきれません。
    コメントにも # Unable to map conditional expression... と正直に書かれています。

  • 環境変数に secrets を直接マージしている

    env:
      ACR_USERNAME: "${{ secrets.ACR_USERNAME }}"
      ACR_PASSWORD: "${{ secrets.ACR_PASSWORD }}"
    

    こういう「ワークフロー全体の env で secrets を展開する」書き方は、ジョブ境界・ステップ境界で意図どおりに渡らないことがあります。GitHub Actions では、jobs.<job>.steps[*].env に都度渡すほうが確実です。

  • Docker の扱いが GitLab 前提のまま
    GitLab CI では docker:dind サービスを sidecar 的に起動するのが定番ですが、GitHub Actions の ubuntu-latest ランナーは基本的に Docker がローカルでそのまま使えるので、無理に dind 形式に合わせる必要はありません。
    つまり、Importer が吐いた YAML は「GitLab の世界観をそのまま Actions に持ち込もうとしている」ので、ここも手修正ポイントになります。

まとめると、Importer は「たたき台としての YAML」を生成してくれるツールです。
“そのまま本番運用できる定義を自動で作ってくれる魔法ツール” ではない、という理解が正しいと思います。

最後に GitHub Actions として正常に動くワークフローを記載します。

name: CI/CD (event-attendees-app)

on:
  push:
    branches:
      - main
    paths:
      - 'src/**'
      - 'database/**'
  workflow_dispatch:

concurrency:
  group: ${{ github.ref }}
  cancel-in-progress: true

jobs:
  build_and_test:
    runs-on: ubuntu-latest
    container:
      image: mcr.microsoft.com/dotnet/sdk:8.0
    env:
      BUILD_CONFIGURATION: Release

    steps:
      - name: Checkout source
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          lfs: false

      - name: Locate solution or projects
        id: locate
        shell: bash
        run: |
          set -euo pipefail
          sln=$(ls -1 *.sln 2>/dev/null || true)
          if [ -z "$sln" ]; then
            sln=$(ls -1 src/*.sln 2>/dev/null || true)
          fi
          if [ -n "$sln" ]; then
            echo "mode=sln"     >> $GITHUB_OUTPUT
            echo "target=$sln"  >> $GITHUB_OUTPUT
            echo "Using solution: $sln"
            exit 0
          fi

          mapfile -t projects < <(find . -type f -name '*.csproj' ! -name '*.Tests.csproj' | sort)
          if [ ${#projects[@]} -eq 0 ]; then
            echo "No buildable *.csproj found (excluding *Tests.csproj)." >&2
            exit 1
          fi
          echo "mode=proj" >> $GITHUB_OUTPUT
          echo "target=$(printf "%s " "${projects[@]}")" >> $GITHUB_OUTPUT
          printf "Using projects:\n%s\n" "${projects[@]}"

      - name: Discover test projects
        id: discover
        shell: bash
        run: |
          set -euo pipefail
          TEST_PROJECTS_LIST=$(find . -type f -name '*.Tests.csproj' | sort || true)
          echo "Found test projects:"
          echo "$TEST_PROJECTS_LIST"
          echo "projects=$TEST_PROJECTS_LIST" >> $GITHUB_OUTPUT

      - name: Restore and Build
        shell: bash
        run: |
          set -euo pipefail
          if [ "${{ steps.locate.outputs.mode }}" = "sln" ]; then
            dotnet restore "${{ steps.locate.outputs.target }}"
            dotnet build   "${{ steps.locate.outputs.target }}" --no-restore --configuration "$BUILD_CONFIGURATION"
          else
            for p in ${{ steps.locate.outputs.target }}; do
              dotnet restore "$p"
              dotnet build   "$p" --no-restore --configuration "$BUILD_CONFIGURATION"
            done
          fi

      - name: Test with coverage
        shell: bash
        run: |
          set -euo pipefail
          mkdir -p coverage test-results
          if [ -z "${{ steps.discover.outputs.projects }}" ]; then
            echo "No test projects found. Skipping tests."
            exit 0
          fi

          # 複数行の安全なループ
          while IFS= read -r proj; do
            [ -z "$proj" ] && continue
            echo "=== Testing $proj ==="
            NAME=$(basename "$proj" .csproj)

            dotnet test "$proj" \
              --no-build \
              --configuration "$BUILD_CONFIGURATION" \
              --collect:"XPlat Code Coverage" \
              --logger "trx;LogFileName=${NAME}-test-results.trx" || true

            RESULTS_DIR="$(dirname "$proj")/TestResults"
            COV_FILE=$(find "$RESULTS_DIR" -type f -name "coverage.cobertura.xml" | head -n 1 || true)
            [ -n "$COV_FILE" ] && cp "$COV_FILE" "coverage/${NAME}-coverage.cobertura.xml" || echo "No coverage for $proj"

            TRX_FILE=$(find "$RESULTS_DIR" -type f -name "*.trx" | head -n 1 || true)
            [ -n "$TRX_FILE" ] && cp "$TRX_FILE" "test-results/${NAME}-test-results.trx" || echo "No TRX for $proj"
          done <<< "${{ steps.discover.outputs.projects }}"

      - name: Upload test artifacts (coverage & trx)
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-artifacts
          retention-days: 7
          path: |
            coverage/
            test-results/

  build_and_push_image:
    needs: build_and_test
    runs-on: ubuntu-latest
    if: github.event_name == 'workflow_dispatch'
    env:
      BUILD_CONFIGURATION: Release
      ACR_LOGIN_SERVER: cravanfr.azurecr.io
      IMAGE_NAME: eventattendeesapp
      IMAGE_TAG: latest
      ACR_USERNAME: ${{ secrets.ACR_USERNAME }}
      ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }}

    steps:
      - name: Checkout source
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          lfs: false

      - name: Download test artifacts
        uses: actions/download-artifact@v4
        with:
          name: test-artifacts

      - name: Login to Azure Container Registry
        shell: bash
        run: |
          echo "${ACR_PASSWORD}" | docker login "${ACR_LOGIN_SERVER}" --username "${ACR_USERNAME}" --password-stdin

      - name: Build container image
        shell: bash
        run: |
          docker build \
            -t "${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}" \
            -f src/Dockerfile ./src

      - name: Push container image
        shell: bash
        run: docker push "${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}"

まとめ

  • リポジトリ(コードと履歴)
    git clone --mirrorgit push --mirror でかなり素直に移行できます。ブランチやタグも含めてほぼ忠実に持っていけます。

  • Issue(課題管理)
    OSS ツール(node-gitlab-2-github)を使えばある程度移行できます。
    ただし現時点(2025-10-27 時点)ではソースの手修正が必要でした。
    また、Merge Request は Pull Request として完全には再現されず、クローズ済みMRが Issue として取り込まれるなどの割り切りが発生します。

  • CI/CD パイプライン
    gh-actions-importer を使うと GitLab CI 設定から GitHub Actions の下書き (ワークフローYAMLや PR) を生成できます。
    ただし、環境変数や secrets の扱い、Docker のまわし方、ブランチ条件などは手で直す必要があり、そのままでは動かないケースがあります。
    結局「人間がレビューして仕上げる前提の半自動化ツール」と考えるのが現実的です。

今回の検証結果が、GitLab から GitHub へ移行を検討している方の初期調査や、見積もりの材料になればうれしいです。

参考リンク / Reference URLs

Issue移行ツール関連

CI/CD移行ツール関連

GitHub Actions Importer ドキュメント

全体リファレンス(日本語版)

ベースとなるホスト

GitHubで編集を提案

Discussion