GitLab CI から GitHub Actions への移行でハマった N 個のこと


この記事はコンシューマーチームブログリレー5日目の記事です。

こんにちは。エムスリーのコンシューマーチームエンジニアの園田です。

以前のポストにもあるように、エムスリーでは GitLab から GitHub EE へ移行しています。

www.m3tech.blog

コンシューマーチームで管理している GitLab リポジトリも、いくつか GitHub に移行しました。当然ですが、GitLab CI の CI/CD パイプラインは GitHub Actions に置き換える必要があります。移行前は「GitLab でできることは GitHub でもできるんでしょ?」と軽く考えていたのですが、実際に移行を進めてみると、想定外の制限や落とし穴に数多く遭遇しました。

本稿では、GitLab CI から GitHub Actions へ移行する際に直面したいくつかの課題と、それぞれの解決策を共有します。これから GitHub への移行を検討されている方の参考になれば幸いです。

お断り: この記事は社内LTで発表した内容を生成AIでリライトしたものです。ほぼ手直ししていますが、絵文字など生成 AI っぽい箇所も残っています。

課題1: 開発ブランチでの手動実行ができない

最初に遭遇したのは、開発中のブランチでワークフローを手動実行できないという問題でした。

GitLab CIでは、when: manualを使うことで、任意のブランチで任意のジョブを手動実行できます。これは開発中のデバッグやテストに非常に便利な機能でした。

しかしGitHub Actionsのworkflow_dispatchトリガーは、デフォルトブランチ(通常はmain)にマージされたワークフローファイルでのみ手動実行が可能という制限があります。

つまり、次のような状況では手動実行ができません:

  • 新しいワークフローを開発中のブランチ
  • デバッグ用の一時的なジョブを追加したブランチ
  • 既存ワークフローを修正中のブランチ

GitLabのwhen: manualとの比較

GitLab CIでは、次のように簡単に手動実行ジョブを定義できました:

debug-job:
  stage: test
  when: manual
  script:
    - echo "Debug information..."
    - ./debug-script.sh

このジョブは、どのブランチからでもGitLabのUIから手動で実行できます。開発中の機能をテストしたり、本番環境へのデプロイ前に最終確認したりする際に重宝していました。

ワークアラウンド

こちら、ざっと調べただけでも多くの方が同じ問題に直面しているようで、ワークアラウンドも探せばすぐに見つかりました:

qiita.com

こちらの記事を参考に次のような「プレースホルダー」となるワークフローを定義しました:

name: Debug Workflow

on:
  workflow_dispatch:
    inputs:
      debug_mode:
        description: 'Debug mode'
        required: false
        type: boolean

jobs:
 
  nothing:
    runs-on: ubuntu-latest
    steps:
      - name: Placeholder
        run: echo "This is a placeholder job for manual dispatch"

開発時の手順:

  1. debug.ymlに実際のデバッグロジックを実装
  2. 開発ブランチにコミット
  3. Actions のdebug.yml を実行し、開発ブランチを選択して手動実行(開発ブランチのコードが使われる)
  4. デバッグ完了後、debug.yml の内容を本来あるべきファイルに転記する
  5. debug.ymlを元のプレースホルダーに戻す

運用での課題

この方法でも次のような不便さが残ります:

  • デバッグ後に本来のファイルに転記する作業と、debug.yml を元に戻す作業を忘れるリスクがある
  • このファイルが存在すること自体がなんか気持ち悪い
  • ワークフローのリストに表示される名称も変更されるため紛らわしい

こういった課題を解決・軽減可能なナレッジがあれば知りたいです。

課題2: CI成功時のみPRマージ可能とする設定の罠

次に見つかった大きな問題のひとつとして、「CI成功時のみPRマージを許可する」という基本的な設定に関するものでした。

GitLabでは、Merge Requestに対して「パイプラインが成功したらマージ可能」という設定が簡単に行えます。

これと似たような機能で GitHub では「ジョブ名を指定して、それらのジョブがすべて成功していたらマージ可能にする」という設定が可能です。
しかし、この機能を利用して制限をかけると、条件分岐(if)でスキップされたジョブがステータスチェックの対象になってしまい、マージがブロックされるという問題が発生しました。

具体的には、次のようなワークフローを実装していました:

changes:
  runs-on: ubuntu-latest
  outputs:
    changed: ${{ steps.changes.outputs.changed }}
  steps:
    - uses: actions/checkout@v5
    - id: changes
      run: |
       
        echo "changed=true" >> $GITHUB_OUTPUT


lint:
  needs: changes
  if: ${{ needs.changes.outputs.changed == 'true' }}
  runs-on: ubuntu-latest
  steps:
    - name: Run linter
      run: echo "Linting code..."

このワークフローで、changed == 'false'の場合、lintジョブはスキップされます。しかし、ブランチ保護ルールでlintをステータスチェック対象に設定していると、スキップ = 未実行 = マージ不可となってしまうのです。

解決策

この問題を解決するため、すべてのジョブの結果を集約する「チェックポイントジョブ」を実装しました:

check-all-jobs:
  runs-on: ubuntu-latest
 
  needs: [changes, lint, test, build]
 
  if: always()

  steps:
    - name: Check merge conditions
     
      if: |
        contains(needs.*.result, 'failure')
        || contains(needs.*.result, 'canceled')
      run: exit 1

    - name: All checks passed
      run: echo "All required checks passed successfully"

ifalways() を指定しているので、needs で指定したジョブが失敗していようがスキップされていようが必ず実行されます。
そのうえで、step の中で if により成否を判定しています。

このチェックポイントジョブのみをステータスチェック対象とすることにより、スキップされたジョブは成功したジョブとして扱えるようになります。

課題3: Slack通知がノイジー

こちらは GitHub の問題ではなく Slack が提供している公式の GitHub Slack App の問題なのですが、通知設定が複雑でノイジーにならないように設定するのが非常に困難です。

GitLabでの設定方法

GitLabでは、Slackへの通知設定が非常にシンプルでした。プロジェクトの設定画面から、GUIで次のような操作が可能です:

  1. Slack Integrationを有効化
  2. Webhook URLを入力
  3. 通知したいイベントをチェックボックスで選択
    • パイプライン成功/失敗
    • デプロイメント
    • マージリクエスト
    • など

すべての設定がGUI上で完結し、直感的に管理できました。

GitHubでの現実

一方、GitHub Slack App での通知設定は、コマンドベースで行う必要があります。

Slackへの通知を設定するには、各リポジトリで次のようなコマンドを実行します:

# リポジトリをSlackチャンネルに接続
/github subscribe owner/repo

# 通知種別を個別に設定
/github subscribe owner/repo issues
/github subscribe owner/repo pulls
/github subscribe owner/repo deployments
/github subscribe owner/repo commits:main

# 不要な通知を解除
/github unsubscribe owner/repo deployments

この方法には次の課題があります:

  1. 設定が煩雑: リポジトリごと、通知種別ごとにコマンドを実行する必要がある
  2. フィルタリングの制限: 通知内容の細かいフィルタリングができない、または限定的
  3. 全通知か選択通知か: すべて受け取るとノイジーだが、絞りすぎると重要な通知を見逃す
  4. 組み合わせが必要: 例えば、プルリクエストとそのコメントは別の通知種別なので、両方購読する必要がある

特に大きな課題

デプロイの承認依頼も Slack 通知で受け取りたいのですが、こちらは deployments ではなく workflows の通知となります。
ところが、この workflows を購読すると、すべてのジョブの通知が飛んでくるのでとんでもなくノイジーです。

本番環境へのリリース承認依頼通知が埋もれてしまってリリースが滞る、といったことが実際にありました。

実際の運用での対処法

workflows をすべて購読するとノイジーなのですが、workflows の購読はフィルタリング構文があるので、それを利用してデプロイに関連するワークフローのみを購読する、デプロイ専用のチャンネルを作成しました。

  • リリースチャンネル: releases を購読
  • デプロイ関連チャンネル: deployments および デプロイ関連ジョブのみに限定した workflows を購読
  • PR/レビュー関連チャンネル: pullsreviews, comments などを購読

とはいえ、これをやってもまだまだ全然ノイジーです。
より運用性を向上させるのであれば、Slack SDK などを使って通知の仕組みを整えてあげる必要がありそうです。

まとめ

  • GitHubの通知設定はコマンドベースで煩雑
  • フィルタリングができない/限定的なため、ノイジーになりがち
  • デプロイ承認やレビュー依頼などは複数の通知種別を購読する必要がある
  • 通知種別ごとにSlackチャンネルを分けて対処している

課題4: ジョブ共通化の制限とその影響

GitLabの柔軟性

GitLab CIでは、ジョブの共通化に関して YAML 構文を利用した柔軟な仕組みが用意されています。

.template: &template
  before_script:
    - echo "Common setup"
  script:
    - echo "Common processing"


job1:
  extends: .template
   *template
  script:
   
    - !reference [.template, script]
    - echo "Additional processing for job1"


include:
  - local: '.gitlab/ci/templates.yml'
  - project: 'group/common-templates'
    file: '/templates/deployment.yml'

このように、GitLabでは以下が可能です:

  1. extendsによる継承
  2. !referenceによる部分的な参照と拡張
  3. includeによる外部テンプレートの取り込み
  4. 柔軟な組み合わせと上書き

GitHubの2つの選択肢

GitHub Actionsには、ジョブの共通化に2つの方法があります:

1. Composite Action(ステップのまとまり)

name: 'Setup Environment'
description: 'Common setup steps'
runs:
  using: "composite"
  steps:
    - name: Checkout
      uses: actions/checkout@v3
    - name: Setup Node
      uses: actions/setup-node@v3
      with:
        node-version: '18'
    - name: Cache dependencies
      uses: actions/cache@v3
      with:
        path: node_modules
        key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}


jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: ./.github/actions/setup
      - name: Build
        run: npm run build

Composite Actionの特徴:

  • ✅ ステップの再利用が可能
  • ✅ ローカルまたは公開アクションとして利用可能
  • uses: actions/checkout@v3などの特定のアクションと組み合わせにくい
  • ❌ UI上でログがまとめられ、デバッグ時の視認性が悪い
  • ❌ ジョブレベルの設定(runs-onなど)は共通化できない

2. Reusable Workflow(ワークフロー全体)

name: Reusable Deploy Workflow

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    secrets:
      DEPLOY_TOKEN:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Deploy
        run: ./deploy.sh
        env:
          TOKEN: ${{ secrets.DEPLOY_TOKEN }}


jobs:
  production-deploy:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
    secrets:
      DEPLOY_TOKEN: ${{ secrets.PRODUCTION_TOKEN }}

Reusable Workflowの特徴:

  • ✅ ワークフロー全体の再利用が可能
  • secretsの引き継ぎが可能(強力な機能)
  • ❌ 部分的な取り込みや上書きができない

それぞれの制限事項と使い分け

GitLab CIの柔軟性に比べると、GitHub Actionsの共通化には明確な制約があります:

要件 GitLab CI Composite Action Reusable Workflow
ステップの再利用
部分的な参照・拡張
ジョブレベル設定の共通化
外部ファイルインクルード

GitHub でもYAML Anchor を使って部分的な参照は可能になりましたが、GitLab と違いマージ()が使えないため、実際に利用可能なユースケースはほとんどありません。

まとめ

  • 共通化は「Composite Action」と「Reusable Workflow」を適材適所で使い分ける
  • Composite Action はステップをまとめられるが UI 出力が見づらい
  • Reusable Workflow はワークフロー全体を共通化できるが部分的な参照・拡張ができない
  • GitLab からの移行時は、構成の分割と再利用単位の再設計が必要

課題5: Reusable Workflow と Environment の連携方法

これは、移行後に数ヶ月間も勘違いしていた大きな落とし穴でした。

GitHubの公式ドキュメントに書かれている一文を読んで、「Reusable WorkflowではEnvironmentが使えない」と完全に誤解していました。そのため、環境別にワークフローファイルを複製したり、Composite Actionで無理やり回避したりと、不必要に複雑な実装をしていました。

Environmentとは

GitHubの「Environment」は、デプロイ先の環境を定義する機能です。主に次の用途で使用します:

  1. 環境ごとのシークレット管理: 本番環境とステージング環境で異なるAPIキーなどを管理
  2. デプロイ承認フロー: 本番デプロイ前に承認を必須にする
  3. デプロイ履歴の管理: 環境ごとのデプロイ履歴を可視化

何を勘違いしていたのか

公式ドキュメントには以下のように記載されています:

Jobs that call reusable workflows cannot use the environment keyword.

この一文を読んで、「Reusable Workflowを使うとEnvironmentの機能がまったく使えない」と思い込んでしまいました。

実際、呼び出し側のジョブレベルでenvironmentを指定することはできません:

jobs:
  deploy:
    uses: ./.github/workflows/reusable-deploy.yml
    environment: production 
    with:
      env_name: production

実際の解決方法

しかし、Reusable Workflow内のジョブでenvironmentを指定することは可能です。

環境名をinputsとして受け取り、それをReusable Workflow内でenvironmentに指定することで、Environmentの機能を活用できます:

name: Reusable Deploy Workflow

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    secrets:
      DEPLOY_TOKEN:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
   
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v3
      - name: Deploy
        run: ./deploy.sh
        env:
         
          TOKEN: ${{ secrets.DEPLOY_TOKEN }}
name: Deploy

on:
  push:
    tags:
      - 'prod_v*'
      - 'dev_v*'

jobs:
  deploy-to-prod:
    if: startsWith(github.ref, 'refs/tags/prod_v')
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production 
    secrets: inherit

  deploy-to-dev:
    if: startsWith(github.ref, 'refs/tags/dev_v')
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: dev
    secrets: inherit

この方法の利点

  1. 承認フローが機能する: Environment設定で承認を有効にすれば、デプロイ前の承認が必須になる
  2. 環境ごとのシークレット管理が簡単: Environment設定でシークレットを管理でき、自動で注入される
  3. デプロイ履歴が可視化される: GitHub UI上で環境ごとのデプロイ履歴が確認できる
  4. ワークフローの共通化: 環境別に別々のワークフローを作る必要がなくなる

この解決方法に気づいたのは、以下のZenn記事のおかげです。同じ誤解をしている方は多いと思うので、ぜひ参考にしてください。

zenn.dev

重要な注意点: vars の暗黙的な依存関係

上記の記事にはないのですが、実際に運用してみてわかった重要な注意点があります。

Environment固有のvariablesを使いたい場合、Reusable Workflow側で直接参照することになります:

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
   

jobs:
  deploy:
    environment: ${{ inputs.environment }} 
    steps:
      - name: Deploy
        env:
         
          DEPLOY_URL: ${{ vars.DEPLOY_URL }}

問題は、inputssecretson.workflow_callで明示的にインターフェースを定義できますが、varsは宣言する方法がありません

そのため、Reusable Workflowでvarsを使う場合は、暗黙的な依存関係となってしまいます。これを防ぐため、必ず以下のような対応が必要です:

1. コメントで明記する



on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string

2. READMEやドキュメントに記載する

  • 各Environmentに設定すべきvariablesをドキュメント化
  • 必須のvariablesと任意のvariablesを明確に区別

3. Terraform で IaC する

執筆中に気づいたのですけど、GitHub 公式の Terraform Provider があることを知りました。
Environments や Variables の管理がソースコードでできるので、もっと早く知りたかった・・・!

補足: secrets の評価タイミング

ちなみに、secretsは遅延評価されるという仕様があります:

jobs:
  deploy-to-prod:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
    secrets:
      API_KEY: ${{ secrets.API_KEY }} 
on:
  workflow_call:
    secrets:
      API_KEY: { required: true }
jobs:
  deploy:
    environment: ${{ inputs.environment }} 
    steps:
      - name: Deploy
        env:
          API_KEY: ${{ secrets.API_KEY }} 

この仕様を見ると、varsも同じように遅延評価されるのでは?と考えたくなりますが、実際にはvarsは遅延評価されませんvarsは呼び出し側のコンテキストで評価されます。
何の役にも立たない仕様ですが「varsも同じように遅延評価されるのでは?」と試行錯誤するのを防ぐための参考として記載しておきます。

まとめ

  • Reusable Workflowの呼び出し側ではenvironmentを指定できない(これは事実)
  • しかし、Reusable Workflow内部のジョブでenvironment: ${{ inputs.environment }}と指定することは可能(これに気づかなかった!)
  • この方法により、Environmentのすべての機能(承認フロー、環境別シークレット、デプロイ履歴)を活用しながらワークフローを共通化できる
  • 重要: varsはインターフェースとして宣言できないため、必要なvariablesはコメントやドキュメントに明記すること
  • 公式ドキュメントの記述は正確だが、誤解を招きやすい。実装前にコミュニティ記事も確認することをおすすめします

その他の課題

移行中に遭遇したその他の課題も簡単に紹介します。

クロスリポジトリのジョブ実行

GitLabのtrigger機能のような標準機能がなく、GitHub Appの作成とトークン管理が必要です。

steps:
  - name: Generate token
    id: generate_token
    uses: tibdex/github-app-token@v1
    with:
      app_id: ${{ secrets.APP_ID }}
      private_key: ${{ secrets.APP_PRIVATE_KEY }}

  - name: Trigger workflow in other repo
    uses: actions/github-script@v6
    with:
      github-token: ${{ steps.generate_token.outputs.token }}
      script: |
        await github.rest.actions.createWorkflowDispatch({
          owner: 'your-org',
          repo: 'other-repo',
          workflow_id: 'deploy.yml',
          ref: 'main'
        });

キャッシュの実行環境依存

ホストランナーとコンテナランナーではキャッシュが共有されません。

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/cache@v3
        with:
          path: node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
      - run: npm ci

  test:
    runs-on: ubuntu-latest
    container: node:18 
    needs: build
    steps:
      - uses: actions/cache@v3
        with:
          path: node_modules
         
          key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
      - run: npm test 

対処法は実行環境を統一するか、Artifactで受け渡す方法があります。

JIRA Server連携

公式統合はJIRA Cloudのみ対応。JIRA Serverとの連携にはGitHub Actionsで自動リンク作成ワークフローを独自実装しました。

on:
  pull_request:
    types: [opened, edited]

jobs:
  jira-link:
    if: contains(github.event.pull_request.title, 'PROJ-')
    steps:
      - uses: ./.github/actions/jira-link-issue

まとめ

GitLab CIからGitHub Actionsへの移行は、想像以上に大変な作業でした。表面的には似ている両者ですが、細部の仕様や設計思想の違いにより、多くの課題に直面しました。

主な学び:

  1. ステータスチェックの考え方が異なる: 個別ジョブの状態管理が重要
  2. 柔軟性に制限がある: 手動実行、ジョブ共通化など、GitLabほど柔軟ではない
  3. 通知設定が煩雑: コマンドベースの設定が必要
  4. Reusable Workflowの制約: Environment との共存に一手間が必要

しかし、これらの課題を乗り越えた今、GitHub Actionsの良い点も見えてきました:

  • 豊富なMarketplace: 公開アクションの質と量が素晴らしい
  • 活発なコミュニティ: 問題解決のための情報が豊富
  • エコシステムの広さ: GitHub全体との統合が強力
  • 継続的な改善: 頻繁に新機能が追加される

最後まで読んでいただき、ありがとうございました。本稿がGitHubへの移行を検討されている方にとって、少しでも参考になれば幸いです。


関連リンク


We are hiring

エムスリーでは GitLab や GitHub を活用した CI/CD パイプラインの構築・運用しています。DevOps やインフラ自動化、ワークフロー改善に興味がある方、ぜひ一緒に働きませんか。軽く話を聞いてみるだけでも OK ですので、ぜひともカジュアル面談をお申し込みください!

エンジニア採用情報はこちら!

jobs.m3.com

インターン応募はこちら!

open.talentio.com




元の記事を確認する

関連記事