🙌

はてなブログ作成から投稿までを自動化したGitHub Actionsのワークフロー

2022/12/25に公開約8,400字

はじめに

こんにちは、M-Yamashitaです。この記事は、GitHub Actions Advent Calendar 2022 の23日目の記事です。投稿が遅れてしまいすみません。

今回の記事は、はてなブログの下書き作成、更新、公開までをGitHub Actionsで自動化した話です。
以前「自動化大好きエンジニアLT会 - vol.9」でこの自動化について登壇しました。この話では、スライドで紹介できなかった実際のワークフローとその説明について伝えます。

https://rakus.connpass.com/event/259685/

https://speakerdeck.com/myamashii/hatenaburoguzuo-cheng-karatou-gao-madewogithub-actionsdezi-dong-hua-suru

この記事で伝えたいこと

  • 各ワークフローの紹介

前提

はてなブログでは、はてなブログAtomPubが公開されています。ここでは、はてなブログの記事の参照、作成、公開、削除できるREST APIが公開されています。
https://developer.hatena.ne.jp/ja/documents/blog/apis/atom/`

このREST APIのCLIクライアントとなるものがblogsyncとなります。今回の自動投稿では、GitHub Actionsのワークフローに加えて、OSSのblogsyncを使っています。
https://github.com/x-motemen/blogsync

ワークフローの紹介

はてなブログの下書き作成、更新、公開を行なうワークフローやconfigファイルを以下リポジトリにて公開しています。
以降の説明では、このリポジトリを使用して詳細を説明します。
https://github.com/M-Yamashita01/hatena-blog-workflows

ディレクトリ構成

はてなブログに自動投稿するためのリポジトリ構成は以下のとおりです。

.
├── .github
│   └── workflows
├── blogsync.yaml
└── draft.md

blogsync.yamldraft.mdについての説明は後述します。

blogsync用の設定ファイル

blogsync.yamlはblogsyncを使用する際に必要な構成ファイルです。この中身は以下のとおりです。

blogsync.yaml
m-yamashii.hatenablog.com:
  username: xxxxx
  password: $HATENA_API_KEY
default:
  local_root: entries

それぞれの項目について、blogsyncのREADMEから引用します。

キー(例: motemen.hatenablog.com ): ブログのドメイン。はてなブログのダッシュボードからブログの設定画面などを開いたとき、URL に含まれる文字列です。技術的には AtomPub API における「ブログID」になります。
"default" という名前のキーは特別で、すべてのブログの項目のデフォルト値として扱われます。
<blog>.username: そのブログに投稿するはてなユーザの ID。
<blog>.password: そのブログに投稿するための API キー。はてなユーNザのパスワードではありません。ブログの詳細設定画面 の「APIキー」で確認できます。
<blog>.local_root: ブログのエントリを格納するパスのルート。

passwordにはAPIキーをそのまま載せるわけにはいかなかったので、$HATENA_API_KEYと置いています。なお、APIキー自体はGitHub secretに保存しています。この$HATENA_API_KEYはGitHub Actions実行時に、secretの値に変えるようにしました。詳しくは後述します。

下書き記事について

はてなブログの下書きをGitHub Actionsで自動作成するにあたり、下書きのテンプレートをdraft.mdとして用意しました。
中身はタイトルとサンプル文章を載せるシンプルな下書きテンプレートとなっています。

---
Title: xxxxx
---

テスト文章です。

各ワークフロー説明

各ワークフローでは、yamlをそれぞれ載せており、説明が必要と思われる部分のみ記載しています。
そのため、チェックアウトやインストールなどのステップについての説明は省いています。

はてなブログの下書き作成

https://github.com/M-Yamashita01/hatena-blog-workflows/blob/main/.github/workflows/create-draft-blog.yml

ワークフロー開始トリガー

on: create

jobs:
  create-blog:
    name: Create draft blog
    if: ${{ contains(github.ref, 'create-blog') }}
    runs-on: ubuntu-latest

下書き作成では、ブランチ作成、かつブランチ名に指定名を持つ場合をトリガーにこのワークフローを実行させます。
もし指定名を持つという条件を持たせないと、記事作成以外の目的でブランチを作成した場合でもワークフローが実行されてしまいます。その実行は意図に反した動きなので、ブランチ名が指定名の場合という制限を持たせています。

configファイルのAPIキー置換

    - name: Replace secrets key to secrets value
      uses: jacobtomlinson/gha-find-replace@v2
      with:
        find: "$HATENA_API_KEY"
        replace: "${{ secrets.HATENA_API_KEY }}"
        include: blogsync.yaml
        regex: false

このステップでは、blogsync用の設定ファイルであるconfig.yamlのAPIキー($HATENA_API_KEY)を、GitHubのsecretに保存しているAPIキーに置換しています。
プライベートリポジトリにしておけば、configファイルにAPIキーをハードコードし、このステップをなくしても良いのかもしれませんが、コードの漏洩リスクがあるため、secretに保存したAPIキーを参照するにようにしています。

はてなブログの下書き作成

    - name: Post hatena draft blog to hatena blog
      run: |
        ~/go/bin/blogsync post --draft m-yamashii.hatenablog.com < draft.md

このステップでは、はてなブログの下書きをはてなブログ上に投稿します。
blogsync postコマンドを使うことで、テンプレート記事(draft.md)を基に、下書きをはてなブログ上に作成できます。
コマンドを実行すると、はてなブログに投稿した下書きブログが、entries/m-yamashii.hatenablog.com/entry/2022/10/10/073657.mdのようなファイルパスで、GitHub Actionsのランナー上に作成されます。

Run ~/go/bin/blogsync post --draft m-yamashii.hatenablog.com < draft.md
      POST ---> https://blog.hatena.ne.jp/m_yamashii/m-yamashii.hatenablog.com/atom/entry
       201 <--- https://blog.hatena.ne.jp/m_yamashii/m-yamashii.hatenablog.com/atom/entry
     store entries/m-yamashii.hatenablog.com/entry/2022/10/10/073657.md

作成されたmdファイルの上部には、YAML Frontmatter形式のメタデータが記載されます。

---
Title: xxx
Date: 2022-10-10T07:36:57+09:00
URL: https://m-yamashii.hatenablog.com/entry/2022/10/10/073657
EditURL: https://blog.hatena.ne.jp/m_yamashii/m-yamashii.hatenablog.com/atom/entry/xxxxx
Draft: true
---

この各項目の説明については、blogsyncのREADMEから引用します。

Title: エントリのタイトル。
Date: ブログに表示されるエントリの投稿日時。2006-01-02T15:04:05-07:00 といったフォーマットを期待しています。
URL: エントリの URL。これは自動的に与えられ、書き換えても効果はありません。
EditURL: エントリを一意に区別する URL。名前のとおり、AtomPub の編集用の URL です。
Category: エントリーのカテゴリの配列
Draft: この値が "yes" のとき、下書きとして扱われます。

この下書きブログは、ユーザーでブログを更新するために必要になります。そのため、次ステップでコミット、プッシュをします。

作成された下書きをコミット

    - name: Commit draft blog
      uses: stefanzweifel/git-auto-commit-action@v4
      with:
        commit_message: Create draft blog.
        file_pattern: 'entries/**/*.md'
        commit_author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

前述のステップで作成された下書きブログを、stefanzweifel/git-auto-commit-actionアクションを使用してコミットします。
コミット対象のファイルは、file_patternに正規表現を使用することで指定できます。

ワークフロー作成し始め当初では、作成された下書きブログのみを指定する方法を考えていました。ですが、以下2点の理由によりentries/**/*.mdのような正規表現の指定にしました。

  • blogsync postコマンドを行なうステップにおいて、下書きブログのファイルパス取得が難しそうであること
  • このジョブで新たに作成されるファイルは下書きブログ1つのみであるため、正規表現での指定にしたとしても取得されるファイルは1つのみとなる

下書き更新

https://github.com/M-Yamashita01/hatena-blog-workflows/blob/main/.github/workflows/update-blog.yml

ブランチで変更された記事のファイルパス取得

    - name: Get changed files in the entries folder
      id: changed-files
      uses: tj-actions/changed-files@v34
      with:
        files: |
          entries/**

tj-actions/changed-filesは、変更されたファイルパス一覧を取得してくれるアクションです。
https://github.com/tj-actions/changed-files

ここではfilesにentries/**を指定しているので、このアクションはentriesディレクトリ以下の変更ファイルをすべて取得します。
このワークフローを実行するブログ作成ブランチでは、変更されるファイルは基本的にentries配下のmdファイル1つのみなので、この方法を採用しています。
このアクションで取得した変更ファイルは、後述の「はてなブログの下書き更新」でsteps.changed-files.outputs.all_changed_files の形式で使用します。

はてなブログの下書き更新

    - name: Update blog in hatena
      run: |
        ~/go/bin/blogsync push ${{ steps.changed-files.outputs.all_changed_files }}

blogsync postコマンドを使用して、はてなブログの下書きを更新します。ここで使用する更新対象の記事は、前述の取得した変更ファイルとなります。

全体公開

https://github.com/M-Yamashita01/hatena-blog-workflows/blob/main/.github/workflows/publish-blog.yaml

投稿日時の修正

    - name: Remove Date value from draft blog
      uses: jacobtomlinson/gha-find-replace@v2
      with:
        find: 'Date: \d{4}-\d\d-\d\dT\d\d:\d\d:\d\d[+-]\d\d:\d\d'
        replace: "Date:"
        include: ${{ steps.changed-files.outputs.all_changed_files }}
        regex: true

このステップでは、下書きブログに記載されているDate項目の日時を、ワークフロー実施時の日時に修正します。
Dateはブログに表示されるエントリの投稿日時です。はてなブログの画面上では以下画像の部分になります。

このDateは下書きブログ作成のワークフローで作られているので、このステップで修正しないと、下書きブログ作成のワークフロー実行日時が投稿日時となってしまいます。
修正に使用するファイルは、tj-actions/changed-filesアクションで取得したmdファイルを使用します。

下書きフラグを公開フラグに変更

    - name: Replace draft flag to false
      uses: jacobtomlinson/gha-find-replace@v2
      with:
        find: "Draft: true"
        replace: "Draft: false"
        include: ${{ steps.changed-files.outputs.all_changed_files }}
        regex: false

このステップでは、下書きブログに記載されているDraft項目のフラグをtrueに変更します。
この変更をしてblogsyncを行なうことで、下書きブログを全体公開にできます。

公開したブログをgit管理下にあるオリジナルのブログに上書き

      run: |
        latestBlogFilePath=`git ls-files --others --exclude-standard`
        cat $latestBlogFilePath > ${{ steps.changed-files.outputs.all_changed_files }}
        git mv -f ${{ steps.changed-files.outputs.all_changed_files }} $latestBlogFilePath

前ステップのblogsync pushコマンド実行した場合、最新ブログのファイルが新しく作成されます。
このステップ実行直前では、以下のような2つのファイルがあります。

  • (1)GitHub Actionsのワークフローで作成され記事を更新してきたもともとのブログ
    • ブログのファイルパスには、下書き作成ワークフローで作成された日時がある(例:entries/entries/m-yamashii.hatenablog.com/entry/2022/10/10/073657.md)
    • git管理化
  • (2)blogsync pushコマンドにより新しく作成されたブログ
    • ブログのファイルパスには、この全体公開ワークフローで作成された日時がある(例:entries/entries/m-yamashii.hatenablog.com/entry/2022/10/11/123456.md)
    • git管理外

(2)のブログをコミットし、(1)のブログを削除する方法もありますが、git履歴がなくなる問題があります。
そのため、(2)のブログ内容を(1)のブログにcatで移し替え、git mvで(1)を(2)にリネームすることで、今までの履歴を持たせられます。またgit mvすることで投稿日時とファイルパスの日時を一致させられるメリットもあります。
この一連のコマンドを実行した後、次ステップで(2)をコミットすることで、ワークフローは完了となります。

制限・仕様事項

スライドでも説明したとおり、はてなブログ上で直接編集した場合、GitHub上のブログとのマッピングができない問題があります。
また、GitHub上の下書きブログに画像をアップロードして公開した場合、公開したブログで画像が見えてしまうようです。
※登壇資料では、画像が見えない制限ありと記載していました。

これはGitHub側にて、アップロードしたURLがプライベートリポジトリであっても全体公開となってしまう仕様のためのようです。
参考資料:
https://tech.coincheck.blog/entry/2022/10/21/113906

おわりに

この記事では、GitHub ActionsとOSSのblogsyncを使って、はてなブログの下書き作成、更新、公開を自動化したワークフローの話をしました。
ワークフローのコードだけでは把握しづらい意図や背景を紹介できたかなと思います。
この記事が誰かのお役に立てれば幸いです。

Discussion

ログインするとコメントできます