GitHub Actionsを使用してワークフローを自動化する実践（テンプレートを含む）

### Github Actionsの簡単な紹介

継続的インテグレーションとデプロイメントに関して話すとき、GitHub Actionsは非常に優れた選択肢です。GitHub Actionsは、開発者がコードを簡単に設定、テスト、デプロイできる、GitHubが提供する組み込みの自動化ワークフローサービスです。

GitHub Actionsの独特の点は、そのシンプルで使いやすい設計思想にあり、初心者から経験豊富な開発者まで、誰でもすぐに使い始めることができます。

GitHub Actionsを使用すると、構築、テスト、デプロイ、通知など、さまざまなタスクを実行するために、事前に定義されたワークフローまたはカスタムワークフローを利用できます。プロジェクトの要件に応じて、YAMLファイルを使用してワークフローを簡単に構成できます。YAMLファイルを定義するだけで、GitHub Actionsは定義されたタスクを自動的に実行します。

自動化されたビルドやテスト、継続的デプロイメントの実現に使われるGitHub Actionsは、強力で柔軟なツールです。Docker、npm、Mavenなどの豊富なエコシステムやサードパーティーサービスが統合されており、開発者はインターネット上からリソースを簡単に見つけて、簡単な修正と設定を行うことで安全に使用できます。

### 実例紹介

この記事では、次の内容とそれに対応するテンプレートをまとめます：

- PythonコードのテストにPytestを使用する簡単な方法
- PRが通過したコードをGCS（Google Cloud Storage）に自動デプロイする方法
- 実行結果に応じたメッセージ通知（Slack）
- 実行結果に応じたメッセージ通知（Teams）（2つの方法）

### PythonコードのテストにPytestを使用する簡単な方法

これは超初心者向けのYAMLファイルで、この例を使うとすぐにこの機能を学ぶことができます。

前提条件：

- コードがGitHubリポジトリにホストされていること。
- プロジェクトがPythonで書かれていること。
- テストフレームワークとしてPytestを使用していること。
- プロジェクトの依存関係は、rootレベルの `requirements.txt` ファイルで定義されていること。主に `pytest` のみを記述したらOK。

```yaml
name: Python Test

on:
  push:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest  

    steps:
    - name: Checkout code
      uses: actions/checkout@v2  

    - name: Set up Python
      uses: actions/setup-python@v2 
      with:
        python-version: 3.8 

    - name: Install dependencies
      run: pip install -r requirements.txt 

    - name: Run tests
      run: pytest 

    # this will just run the command but can not prevent the deploy, add other logic please
```

注意事項：

- このワークフローは、コードが`main`ブランチにプッシュされた場合にのみトリガーされます。
- GitHub Actionsの事前定義されたアクションを使用して、コードのチェックアウト、Python環境のセットアップ、および依存関係のインストールなどの一般的なタスクを実行しています。
- ここではテストステップのみが実行され、デプロイやその他のロジックは含まれていません。
- pytestに関する詳細は別の場所で学習してください。

まとめ：この例では、GitHub Actionsを使用して自動化されたテストプロセスを簡単に設定し、コードがコミットされるたびに事前に定義されたテストを通過することを確認できます。自動化されたテストは、テストプロセスの一貫性と再現性を確保し、手動でテストを実行することによる差異を排除します。その後、マージの拒否や通知などの設定を行うことができます（以下のステップで説明します）。

### PRを通過したコードのGoogle Cloud Storage（GCS）への自動デプロイ

Google Cloud Storage（GCS）へのコードの自動デプロイには多くの利点があります。デプロイプロセスを大幅に高速化することができます。自動化されたワークフローを設定した後は、デプロイは数分で完了します。手動でのデプロイに必要な時間を待つ必要はありません。

同時に、人為的なエラーはデプロイプロセスでよくある問題です。構成エラーやファイルの欠落などが発生する可能性があります。自動化されたデプロイでは、人間の介入の機会が減り、エラーの可能性が低くなります。

自動化されたデプロイは、すべてのデプロイが一貫していることを保証し、手動デプロイでの差異を排除します。これにより、エラーが発生した場合でも、問題を再現しやすくなり、以前のバージョンに戻すことが容易になります。

GCSへのデプロイタスクには、以下のような前提タスクが含まれます：

- セキュリティ上の理由から、Workload Identity（これに関する内容はGCPの設定を含みますので、詳細は別の記事や公式ドキュメントをご覧ください。これは安全で制御可能な認証管理方法です）の設定、および使用するサービスアカウントに対するGCSバケットのアクセス権限の付与が必要です。ObjectUser権限のみ必要ですが、Actionsがコンテンツの作成を行えるようにします。
- 上記の設定で得られた `workload_identity_provider` と `service_account` を使用して、以下のファイルを作成し、それをリポジトリにデプロイするだけで完了します。

```yaml
name: deploy_to_gcs
on:
  workflow_dispatch:
  push:
    branches:
      - main
    paths:
      - the_trigger_path_in_your_repo/**  # src/foldername/**


jobs:
  deploy:
    runs-on: ubuntu-latest

    permissions:
      contents: 'read'
      id-token: 'write'  # will not delete file in gcs

    steps:
      - id: 'checkout'
        uses: 'actions/checkout@v3'

      - id: 'auth'
        uses: 'google-github-actions/auth@v1'
        with:
          # set up by workload identity in gcp
          workload_identity_provider: 'projects/${{ vars.PROJECT_ID }}/locations/global/workloadIdentityPools/${{ vars.WORKLOAD_IDENTITY_POOL }}/providers/github'
          # grant policy to SA to access gcs
          service_account: ${{ vars.SERVICE_ACCOUNT }}

      - id: 'upload-folder'
        uses: 'google-github-actions/upload-cloud-storage@v2'
        with:
          path: 'path_in_your_repo/the_same_folder_name_in_bucket'
          destination: 'bucket_name'
```

注意事項：

- ここでのデプロイには削除操作は含まれておらず、削除操作を追加することもできます。
- 上記の`uses`は、GitHubからテンプレートを取得していますが、最善の実践は常に公式サイトから最新のライブラリを使用することです。
- `workload_identity_provider`と`service_account`の設定は、変数をリポジトリの変数として保存することで、比較的安全な方法です。もちろん、ハードコーディングしても問題ありませんが、重要なのはリポジトリが公開されていないことです。

### 実行結果に基づいてメッセージ通知（Slack）

ActionsとSlackの通知は、以下の例を使って簡単に実装できます。

前提条件：

- ワークフローのトリガー条件は、コードが`main`ブランチにプッシュされた場合です。
- GitHubリポジトリのSecretsに、Slack通知を送信するための`SLACK_WEBHOOK_URL`という名前のSecretを設定する必要があります。

```yaml
name: slack_notification
on:
  push:
    branches:
      - main

env:
  SLACK_USERNAME: DeployBot  # anything you like
  SLACK_ICON: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png  # anything you like
  SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}


jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - id: 'checkout'
        uses: 'actions/checkout@v3'

      # 成功
      - name: 'Slack Notification on Success'
        uses: 'rtCamp/action-slack-notify@v2'
        if: ${{ success() }}
        env:
          SLACK_TITLE: 'Deploy / Success'
          SLACK_COLOR: 'good'
          SLACK_MESSAGE: 'your_message:rocket:'

      # 失敗
      - name: 'Slack Notification on Failure'
        uses: 'rtCamp/action-slack-notify@v2'
        if: ${{ failure() }}
        env:
          SLACK_TITLE: 'Deploy / Failure'
          SLACK_COLOR: 'danger'
          SLACK_MESSAGE: 'your_message:cry:'
```

注意事項：

- このワークフローでは、デプロイの成功と失敗の両方でSlack通知を送信するための2つのステップが設定されています。
- デプロイが失敗した場合、Slack通知は赤色または緑色で表示され、タイトルと内容はそれぞれ設定できます。さらに詳細なフォーマットに関する情報は、私のタスクの関係上、まだ完全に整理されていませんが、あなたのプロジェクトで必要な場合は、自己探求を行ってください。面白いことが待っています。

### 実行結果に基づいてメッセージ通知（Teams）（2つの方法）

Teams通知とSlack通知は同じ機能ですが、対象が異なります。ここでは2つの方法を挙げています。これは、私のタスクでは、リリース内容をより細かく制御する必要があるためです。たとえば、Teamsのメンション機能を追加したり、より多くの情報を含めたりする必要があります。また、メンション機能はこの2年間でよりよく統合されるようになってきました。そのため、ここでは2つの方法を挙げています。

シンプルな通知を送信したい場合は、`crul`コマンドを使用するだけで十分です。以下にコードを示します：

```yaml
name: Notification

on:
  push:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest  

    steps:
    - name: Checkout code
      uses: actions/checkout@v2  

    #-- Teams通知 --#
    # way 1
    - name: 'Teams Notification on Success'
      if: ${{ success() }}  # ${{ failure() }} change the message in MESSAGE too
      run: |
        MESSAGE="ジョブ詳細:  ${{ github.repository }} ComposerのGCSへのデプロイは成功しました。(コミットメッセージ: ${{ github.event.head_commit.message }} | 作成者: ${{ github.event.head_commit.author.name }} | タイムスタンプ: ${{ github.event.head_commit.timestamp }})"
        curl -X POST -H "Content-Type: application/json" -d "{\"@type\": \"MessageCard\",\"title\": \"デプロイ成功\",\"text\": \"$MESSAGE\"}" ${{ secrets.TEAMS_WEBHOOK_URL }}
      env:
        WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }}
```

この方法を使用すると、多くの情報をTeamsに送信できます。ネット上でメンション付きのコマンドラインを見つけるかもしれませんが、私のテストでは成功しませんでした。何度かのテストと書式の変更でも、メンバーをメンションすることがうまくいきませんでした。そのため、Pythonスクリプトを実行する方法に切り替えました。以下にコードを示します（最後の部分のみ含まれています）：

```yaml
#### other steps
    # way 2
    - name: 'Teams Notification on Success'
      if: ${{ success() }}  # ${{ failure() }} change the parameter in run code too
      run: |
        pip install -r requirements.txt
        python notify_teams.py ${{ secrets.TEAMS_WEBHOOK_URL }} 'success' ${{ github.event.repository.url }} ${{ github.event.head_commit.message }} ${{ github.event.head_commit.author.name }} ${{ github.event.head_commit.timestamp }}
      env:
        WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }}
```

プロセスで使用されるrequirementsファイルにはrequestsパッケージのみが含まれています。そして、Pythonコードはスクリプト内に入れてます。スクリプトの主な内容は次のとおりです。コメントや理解に必要ない部分は省略していますので、本番環境では適宜追加してください。

このスクリプトで使用される外部ファイルは、メンバーをメンションするためのリストだけです。これにより、分離管理が容易になります。

（コードの説明は割愛します。）

```python
import requests
import sys
from mention_members import MENTION_MEMBERS

def _prep_job_message(info, repository, commit_message, author, timestamp):
    """
    Returns:
        dict: 通知メッセージ
    """
    entries = []
    mentions_text = f'デプロイ成功しました！\n\n'
    if info == 'failure':

        for m in MENTION_MEMBERS:
            entries.append(
                {
                    'type': 'mention',
                    'text': f'<at>{m}</at>',
                    'mentioned': {
                        'id': m,
                        'name': m.split('@')[0]
                    },
                }
            )
        mentions = ''.join([f'<at>{m}</at> ' for m in MENTION_MEMBERS])
        mentions_text = f'{mentions}\n\nデプロイ失敗しました！\n\n'

    text = f'{mentions_text}リポジトリ：{repository}\n\nコミットメッセージ：{commit_message}\n\n作成者：{author}\n\nタイムスタンプ：{timestamp}'

    message = {
        'type': 'message',
        'attachments': [{
            'contentType': 'application/vnd.microsoft.card.adaptive',
            'content': {
                'type': 'AdaptiveCard',
                'body': [{
                    'type': 'TextBlock',
                    'text': text,
                    'wrap': True
                }],
                '$schema': 'http://adaptivecards.io/schemas/adaptive-card.json',
                'version': '1.0',
                'msteams': {
                    'entities': entries,
                    'width': 'Full',
                },
            },
        }],
    }

    return message


def notify_message(webhook_url, info, repository, commit_message, author, timestamp):
    response = requests.post(
        webhook_url,
        json=_prep_job_message(info, repository, commit_message, author, timestamp),
    )
    print(f'response: {response}')


if __name__ == "__main__":
    webhook_url = sys.argv[1]
    info = sys.argv[2]
    repository = sys.argv[3]
    commit_message = sys.argv[4]
    author = sys.argv[5]
    timestamp = sys.argv[6]

    notify_message(webhook_url, info, repository, commit_message, author, timestamp)
```

注意事項：

ここでは、メッセージを送信するための`AdaptiveCard`タイプのフォーマットが使用されています。さらにカスタマイズした方法については、以下のリンクから詳細を見つけることができますので、ぜひ探索してください。

https://learn.microsoft.com/ja-jp/microsoftteams/platform/task-modules-and-cards/cards/cards-format?tabs=adaptive-md%2Cdesktop%2Cconnector-html

要約：以上がすべての内容です。GitHub Actionsの機能に関するさらなる情報は、Googleが最良の先生です。