どうも、かたいなかです。
採用広報チームでのブログ推進の一環として、はてなブログにある弊社エンジニアブログ記事をGitHubで管理するしくみを整えました。
この記事では、どのようなGitHubでの記事編集フローを構築したかをまとめます。
記事のレビューのフローがバラバラ・・・
弊社のエンジニアブログの記事の運用での大きな問題のひとつに、レビューのフローが記事によってバラバラになってしまっていることがありました。
具体的には、以下のような一長一短ある複数のフローが、記事によってバラバラに運用されている状況になっていました。
- 例
- ConfluenceやGoogle Docsに一度草案を書き上げ、レビュー後にはてなブログに移動
- ⭕ 気になったところにすぐ指摘コメントを付けられる
- ❌ レビュー時に記事のプレビューが見られない
- ❌ レビュー完了後にはてなブログに記事を移動させるのが面倒
- はてなブログで直接編集した記事のレビュー事項にSlackのコメント等で指摘する
- ⭕ 記事のプレビューを見ながらレビューできる
- ❌ 指摘事項を引用してレビューコメントをSlackに書く手順が少し煩雑
- ❌ レビューコメントに対応したかが分かりづらい
- ConfluenceやGoogle Docsに一度草案を書き上げ、レビュー後にはてなブログに移動
その他の問題として、表記揺れなどの機械的に検出したい問題で、目視でのレビューに頼ってしまっている問題もありました。
これらの問題を解決するため、エンジニア採用広報チームでGitHubではてなブログの記事のレビューが行えるようなフローを整えました。
GitHubで記事を管理できるように
実装にあたっては、はてなブログ公式によるGitHubで記事を管理するためのGitHub Actionsワークフローのボイラープレートを活用しました。
弊社のブログ編集で想定している記事の執筆フローは以下のような流れです。このうち、太字にしたところは自分たちでワークフローを構築しました。
- 記事執筆開始
- はてなブログ側で手動で下書き記事を作成
- 下書き記事をもとにGitHub ActionsでPull Requestを作成
- 記事編集
- テキストエディタ&GitHubを中心とした編集
- Pull Requestのブランチに記事を書きながらコミット&プッシュ
- GitHub Actionsでtextlintを実行
- GitHub Actionsではてなブログに反映
- はてなブログのエディタでの編集
- はてなブログのエディタで画像追加等の変更を実施
- GitHub Actionsではてなブログ側の変更を取り込む
- テキストエディタ&GitHubを中心とした編集
- GitHub上で記事をレビュー
- はてなブログ側で予約投稿
- Pull Requestをマージ
- 定期実行のワークフローで公開された記事を公開済み記事のディレクトリに移動
はてなブログ公式のワークフローから変更した点について、以下で詳しく説明します。
GitHub Actionsでtextlintを実行
記事の修正時にはtextlintを実行させるようにしました。
運用開始時点のtextlintとしては、では、すでにEngineering Handbook用に使われていた設定を流用することにしました。
実際に運用してみると、エンジニアブログの記事としては厳しすぎるルールもいくつかあったため、都度議論しながらtextlintのルールを調整しています。
ワークフローに組み込む中で、自動修正可能な指摘事項についてはGitHub ActionsでSuggestさせるようにしました。
今までの大量の記事全てにtextlintを実行すると時間がかかってしまうため、PRで変更しているファイルのみに対してtextlintをかけるよう工夫しています。
ワークフローのコード
name: textlint for handbook on: pull_request: paths: - 'entries/**/*.md' - 'draft_entries/**/*.md' jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: install xargs run: sudo apt-get install findutils - name: npm install run: npm install - name: get diff files id: get-diff-files run: | # 変更したファイル名のリストを取得 # xargsで改行区切りのファイル名をスペース区切りに変換 changed_files=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...HEAD --diff-filter=d -- entries/ draft_entries/ | xargs ) echo "changed_files=$changed_files" | tee -a $GITHUB_OUTPUT - name: run textlint uses: tsuyoshicho/action-textlint@v3 if: steps.get-diff-files.outputs.changed_files != '' with: reporter: github-pr-review level: warning filter_mode: nofilter fail_on_error: true textlint_flags: ${{ steps.get-diff-files.outputs.changed_files }}
GitHub Actionsではてなブログ側の変更を取り込む
画像の追加やはてな記法での記述が必要な箇所などでははてなブログ側で編集したほうが良い場面もあります。
そのため、はてなブログ側で行った記事の変更をGitHubに取り込む機能も用意しました。
Pull Requestに /sync
とコメントすると、GitHub上のマークダウンファイルをはてなブログの下書きに同期するワークフローが実行されます。
ワークフローのコード
name: sync draft from hatenablog on: issue_comment: types: [created] jobs: pull-draft: runs-on: ubuntu-latest env: BLOGSYNC_PASSWORD: ${{ secrets.OWNER_API_KEY }} if: github.event.issue.pull_request != null && startsWith(github.event.comment.body, '/sync') steps: # /syncがコメントされた場合にいいねのリアクションをつけることで処理が開始されたことがわかりやすくする - name: create a reaction uses: actions/github-script@v7 with: script: | await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: context.payload.comment.id, content: "+1", }) # コメントが付いたPRのブランチの情報を取得 - uses: xt0rted/pull-request-comment-branch@v2 id: pull-request-comment-branch - name: generate a token id: generate-token uses: actions/create-github-app-token@v1 with: app-id: ${{ secrets.LIVESENSE_MADE_GITHUB_APP_ID }} private-key: ${{ secrets.LIVESENSE_MADE_GITHUB_APP_PRIVATE_KEY }} - uses: actions/checkout@v4 with: ref: ${{ steps.pull-request-comment-branch.outputs.head_ref }} fetch-depth: 0 token: ${{ steps.generate-token.outputs.token }} - name: setup uses: hatena/hatenablog-workflows/.github/actions/setup@v1 # 対象の記事のパスやIDを取得 - name: set entry variables id: set-entry-variables run: | # 変更があるファイルを取得(現状は1ファイルを変更したときのみ対応) entry_path=$(git diff --name-only origin/${{ steps.pull-request-comment-branch.outputs.base_ref }}..${{ steps.pull-request-comment-branch.outputs.head_ref }} --diff-filter=d -- draft_entries/ | head -n 1 ) # outputにセット echo "ENTRY_PATH=$entry_path" >> $GITHUB_OUTPUT shell: bash # fetchする - name: blogsync fetch run: | blogsync fetch ${{ steps.set-entry-variables.outputs.ENTRY_PATH }} - name: commit run: | git config user.name "actions-user" git config user.email "action@github.com" if [[ $(git diff) -eq 0 ]]; then exit 0 fi git commit -am "Sync draft from hatenablog" git push
定期実行のワークフローで公開された記事を公開済み記事のディレクトリに移動
はてなブログ公式ボイラープレートでは、Pull Requestマージ時に記事を即公開し、下書き用のディレクトリから公開記事用のディレクトリに移動するようなフローが用意されていました。
弊社では記事公開に予約投稿機能や予約投稿時に同時にXに投稿する機能を活用しているため、そのまま使うことはできませんでした。
そこで、記事の投稿は今まで通り、予約投稿によって行うことにしました。そのうえで、記事を公開するワークフローの代わりに、定期的に記事が公開されたかをチェックし、公開されていたら記事を移動するPRを自動作成するワークフローを作成しました。
ワークフローのコード
name: move newly published entries on: schedule: - cron: '1/15 * * * *' workflow_dispatch: {} jobs: move-newly-published-entries: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: setup uses: hatena/hatenablog-workflows/.github/actions/setup@v1 - name: check if entries' date have past id: check-if-entries-date-have-past run: | if ls draft_entries/*.md > /dev/null 2>&1; then echo "There are draft entries" else echo "There are no draft entries" exit 0 fi touch /tmp/to_be_published for file in draft_entries/*.md; do echo "Checking $file" draft_published_date=$(yq --front-matter=extract '.Date' $file) # 日付形式として正しいかチェック if date --date="$draft_published_date" > /dev/null 2>&1; then echo "Valid date format: $draft_published_date" else echo "Invalid date format: $draft_published_date" continue fi draft_published_date_in_unix_time=$(date --date="$draft_published_date" '+%s') now=$(date '+%s') if [ $draft_published_date_in_unix_time -lt $now ]; then echo "Publishing $file" echo "$file">>/tmp/to_be_published fi done # xargsで改行区切りをスペース区切りに echo "to_be_published=$(cat /tmp/to_be_published | xargs)" | tee -a $GITHUB_OUTPUT # ファイルをフェッチ&移動する - name: blogsync fetch if: steps.check-if-entries-date-have-past.outputs.to_be_published != '' id: blogsync-fetch env: BLOGSYNC_PASSWORD: ${{ secrets.OWNER_API_KEY }} run: | entry_root=$(yq '.default.local_root' blogsync.yaml) for file in "${{ steps.check-if-entries-date-have-past.outputs.to_be_published }}"; do blogsync fetch $file # fetchしてまだDraftなら何もしない is_draft=$(yq --front-matter=extract 'select(.Draft)' "$file") if [[ -n "$is_draft" ]]; then git restore -- "$file" continue fi # fetchすると公開したURLが取得できるので、URLに合ったディレクトリに移動。 url=$(yq --front-matter=extract '.URL' "$file") if [[ -z "$url" ]]; then echo "URL is not found in $file" exit 1 fi entry_path="${entry_root}/$(echo $url | sed 's/https:\/\///').md" mkdir -p $(dirname $entry_path) mv $file $entry_path echo "$entry_path" >> /tmp/published done # xargsで改行区切りをスペース区切りに echo "published=$(cat /tmp/published | xargs)" | tee -a $GITHUB_OUTPUT - name: construct pull request data if: steps.check-if-entries-date-have-past.outputs.to_be_published != '' id: construct-pull-request-data run: | for file in "${{ steps.blogsync-fetch.outputs.published }}"; do title=$(yq --front-matter=extract '.Title' $file) url=$(yq --front-matter=extract '.URL' $file) echo "$title" >> /tmp/published-entries-title echo "$url" >> /tmp/published-entries-url done # xargsで改行区切りをスペース区切りに pr_title="【記事公開】$(cat /tmp/published-entries-title | xargs)" entry_root=$(yq '.default.local_root' blogsync.yaml) url_array=$(cat /tmp/published-entries-url | xargs -L 1 echo -) pr_body=$(cat <<-EOF はてなブログに公開したファイルを/${entry_root}に移動しました - 対象の記事 ${url_array} EOF ) echo "pr_title=$pr_title" | tee -a $GITHUB_OUTPUT { echo 'pr_body<<EOF' echo -e "$pr_body" echo EOF } | tee -a $GITHUB_OUTPUT - name: create pull request if: steps.check-if-entries-date-have-past.outputs.to_be_published != '' id: create-pull-request uses: peter-evans/create-pull-request@v6 with: title: ${{ steps.construct-pull-request-data.outputs.pr_title }} branch: from-draft-to-publish commit-message: | ${{ steps.construct-pull-request-data.outputs.pr_title }} labels: | skip-push body: | ${{ steps.construct-pull-request-data.outputs.pr_body }} - name: Enable Pull Request Automerge if: steps.check-if-entries-date-have-past.outputs.to_be_published != '' && steps.create-pull-request.outputs.pull-request-operation == 'created' uses: peter-evans/enable-pull-request-automerge@v3 with: pull-request-number: ${{ steps.create-pull-request.outputs.pull-request-number }} merge-method: squash
GitHubで記事を管理できるようにしてどうだったか
エンジニアが慣れ親しんだGitHubの便利なUIでレビューが行えることに加えて、textlintでの自動チェックも実行されることで、かなりレビューが効率化されたように感じます。
また、はてなブログのUIで編集するフローを作成したことで運用に柔軟さをもたらしており、エンジニア以外のGitHubに慣れ親しんでいないメンバーがこれまで通りの仕組みではてなブログを編集する際にも対応できるようになっています。
効率化された執筆&レビューのフローによって、今後ますます弊社ブログが充実したコンテンツで溢れることを願っています!