GitHub ActionsでGitHubの日報をXに自動投稿する方法

# はじめに
皆さま初めまして。  
HapinessChainというオンラインスクールで奮闘中のKeiと申します。  

https://happiness-chain.com/

ここでは学習した内容をメンターに日報として報告していますが、  
成果をXにも投稿したく今まではGithub上にある日報を手動でコピーしてポストしていました。

大した労力ではないですが、毎日となると案外手間なので、  
GitHubプッシュ時にコミットした内容を自動的にポストするようにしました。  

実際にやってみるとそこまで難しくなく実装できたので、
今回はGitHub Actionsを利用してポストを自動投稿するやり方を紹介します。

環境構築と実装については以下の記事を参考にさせて頂いております。  

https://qiita.com/hcrane/items/e048dae58d79d703959b

# X Developer Portalの登録
`X Developer Portal`は、開発者がXのAPIを利用してアプリケーションを構築するためのプラットフォームになります。  

まずはX Developer Portalにプロジェクトとアプリケーションを登録します。

1. 右上の`開発者ポータル`を押下します
2. Xにログインしていない場合はログインしてください
3. 画面下の`Sign up for Free Account`を選びます
4. `Describe all of your use cases of Twitter’s data and API`欄に使用用途を英語で記載し、`Submit`を押下してください。内容で精査されることは基本的にありません

画面左側の`Project & Apps`にプロジェクトとAPIが作成されていると思います。

![iScreen Shoter - Google Chrome - 241008111514.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3857595/298e2acd-8af0-fa06-866b-2b9bed3fb2af.jpeg)

# 認証設定
デフォルトではXへの書き込み権限がないためまずは認証設定を行います。  

1. 遷移していない場合は`開発者ポータル`画面に遷移します
2. 左側のメニューより`[Project & Apps] > [xxxxxxAccountName]`を選択し、APIの管理画面に遷移します
3. 画面中央の`User authentication settingsの[Set up]`を押下します
4. `App permissions`は`Read and write`をまたは`Read and write and Direct Message`を選択します。`Read`だとXにポストできません
5. `Type of App`の欄ですが今回はWebアプリなので`Web App Automated App or Bot`を選択します
6. `App info`の`Callback URI`と`Website URL`欄はOAuth認証が必要な場合において設定します。必須項目ではありますが、今回は特に使用しないので自分のXアカウントのURLを指定してください
7. 内容に問題なければ`Save`を押下してください

![iScreen Shoter - Google Chrome - 241008105351.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3857595/4c5a6520-8178-dac2-b819-20598d39f558.jpeg)


 これでXへの書き込み権限が付与されます。

 # APIキーの取得
 Xへの連携用のキーを取得します。  
 今回は以下の5つのAPIキーを使用します。  

+ Consumer API Key
+ Consumer API Secret
+ Bearer Token
+ Access Token
+ Access Token Secret

1. 左側のメニューより`[Project & Apps] > [xxxxxxAccountName]`を選択し、APIの管理画面に遷移します
2. 画面上部のタブより`Keys and tokens`を選択します
3. 以下の項目において`generate`を押下し、APIキーを取得してください。紛失時は再度生成する必要があるため、しっかりとメモしておいてください
    + API Key and Secret
    + Bearer Token
    + Access Token and Secret

![iScreen Shoter - Google Chrome - 241008110744.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3857595/3f1af8c4-f2a7-6843-2571-26a0005558df.jpeg)

キー取得後の画面になります。初回実行時は`generate`となっているので、押下して取得します。  
注意点は`Access Token and Secret`の赤枠欄が`Read Write`または`Read Write and Direct Message`となっている事を確認してください。  
認証設定の節でも説明しましたが、`Read`となっているとXにポストできません。
なっていない場合はもう一度認証設定の節を確認して設定してください。
なお、設定後はキーの再生成が必要になります。

# Githubリポジトリの作成
作成していない場合はGitHubリポジトリを新規作成してローカル環境にクローンします。  

なおリポジトリ作成後に`Personal Access Tokens`を設定してコミットできるようにしておきます。

権限内容についてですが、
`Actions`、`Workflows`はワークフローを作成するので`Read and write`としてください。
またSecretsから環境変数を読み込むので`Secrets`は`Read`としてください。

# GitHubに環境変数を設定
後述するGitHubActionsのスクリプトからAPIキーを読みませるために環境変数を設定します。  
GitHubリポジトリの管理画面を表示し、以下の画像を参考に設定してください。

+ TWITTER_ACCESS_TOKEN
+ TWITTER_ACCESS_TOKEN_SECRET
+ TWITTER_BEARER_TOKEN
+ TWITTER_CONSUMER_KEY
+ TWITTER_CONSUMER_SECRET

![iScreen Shoter - Google Chrome - 241008023625.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3857595/9f0f9fd1-4c3c-039b-a698-9041cbb449fd.jpeg)

# スクリプトファイルの作成
Github Acitonsで実行するスクリプトファイルを作成します。  
今回はpythonで作成し`tweetpy`というX APIを使用して連携します。
作成したスクリプトをローカルリポジトリの直下に配置してください。  

今回の仕様として、リポジトリ内に`YYYY/MM/DD.md`という日報が存在し、  
Githubプッシュ時にファイル名の日付が最新の日報をXにポストします。

```python:tweet.py
import tweepy
import os
import glob
from datetime import datetime

# Twitter APIのキーとトークンを環境変数から取得
ck = os.environ['TWITTER_CONSUMER_KEY']
cs = os.environ['TWITTER_CONSUMER_SECRET']
bt = os.environ['TWITTER_BEARER_TOKEN']
at = os.environ['TWITTER_ACCESS_TOKEN']
ats = os.environ['TWITTER_ACCESS_TOKEN_SECRET']

# Twitter APIのクライアントを初期化
client = tweepy.Client(
    bearer_token=bt,
    consumer_key=ck,
    consumer_secret=cs,
    access_token=at,
    access_token_secret=ats
)

# 最新の.mdファイルを取得
def get_latest_markdown_file():
    # サブフォルダ内のすべての.mdファイルを再帰的に取得
    markdown_files = glob.glob('**/*.md', recursive=True)
    
    if not markdown_files:
        return None

    latest_file = None
    latest_date = None

    for file in markdown_files:
        # ディレクトリ名とファイル名から日付を取得
        # 例: 2024/09/20.md から "2024-09-20" を抽出
        path_parts = file.split('/')
        if len(path_parts) >= 3:
            date_str = f"{path_parts[-3]}-{path_parts[-2]}-{path_parts[-1][:-3]}"  # YYYY-MM-DD
            try:
                date = datetime.strptime(date_str, "%Y-%m-%d")
            except ValueError:
                continue  # 日付が無効な場合はスキップ

            # 最新の日付を更新
            if latest_date is None or date > latest_date:
                latest_date = date
                latest_file = file

    return latest_file

# ツイート内容を取得
latest_md_file = get_latest_markdown_file()

if latest_md_file:
    with open(latest_md_file, 'r', encoding='utf-8') as file:
        content = file.read()

    # ツイート内容をトリミング
    if len(content) > 264:
        content = content[:261] + '...'  # トリミングして「...」を追加

    # ハッシュタグを追加
    hashtags = "#今日の積み上げ"
    tweet_content = f"{content}\n\n{hashtags}"

    # ツイートを投稿
    try:
        client.create_tweet(text=tweet_content)
        print('Tweet sent successfully!')
    except tweepy.errors.Forbidden as e:
        print('Error posting tweet: Forbidden', e)
    except Exception as e:
        print('Error posting tweet:', e)
else:
    print('No markdown files found.')
```

# ワークフロー定義の作成
Github Actionsで実行するワークフローファイルを作成します。  
クローンしたローカルリポジトリ内に`.github/workflows`ディレクトリを作成して  
`.yml`ファイルを作成してください。  
今回は便宜上`tweet.yml`としておきます。  

GitHubプッシュ時にGitHub Actionsのトリガーが起動するように設定しているので、  
ワークフローファイルとスクリプトを配置するだけで実行されるようになります。

```yml:tweet.yml
# ワークフローの名称。
name: Create X Post

# トリガーの指定。GitHubにプッシュされた時に実行する
on: 
  push:
    branches:
      - main

# ワークフローの作成。pythonのバージョンは実行環境に応じて変更してください。
jobs:
  tweet:
    runs-on: ubuntu-latest
    environment: X_Automation
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.9'

      # X API(tweepy)を使用して連携を実施します
      - name: Install dependencies
        run: |
          pip install tweepy

      # 実行ファイルと環境変数の読み込み
      - name: Post article
        env:
          TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
          TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
          TWITTER_BEARER_TOKEN: ${{ secrets.TWITTER_BEARER_TOKEN }}
          TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
          TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
        run: python tweet.py
```

最終的な構成は以下になります。
![iScreen Shoter - Cursor - 241008105804.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3857595/e2b6d96b-be57-7e09-4471-f92c891c00e4.jpeg)


# Github Actionsのログを確認
問題無ければプッシュ時にポストされていると思います。 
Xにポストされているか確認しましょう。

もしされていない場合はGitHubアクション内でエラーが発生していると思うので、  
GitHubのリポジトリ管理画面より`Actions`タブを押下してエラー内容を確認してください。  

![iScreen Shoter - Google Chrome - 241008112207.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3857595/3936a32e-3c13-d90b-0d7a-12f48b85c0bf.jpeg)

参考程度に筆者が沼ったのは以下でした。  

+ 環境変数のキーと値に誤りがある
+ 認証設定で`read and write`になっていない
+ 認証設定変更語にトークンの再取得をしていない

上記が正しく設定できていないと、  
X ポスト時に権限エラーで`403 Forbidden`が表示されていると思います。  

![iScreen Shoter - Google Chrome - 241008110229.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3857595/0ec2d562-8c7c-8da3-af2d-7a458e7ddf85.jpeg)

# おわりに
GitHub Actionsを利用してコミット内容をXにポストする方法を紹介させて頂きました。  
非常に簡単で色々応用ができそうですね。  
定期的に学習して更に便利な使い方を模索していきたいと思います。