はてなブログから OSS blogsync への寄付のご報告と「HatenaBlog Workflows Boilerplate」でのGitHub Workflow の仕組みと工夫

この記事は HatenaBlog Workflows Boilerplate を利用してレビュー・公開をしております。

はてなエンジニアアドベントカレンダー 2023 の 25 日目の記事です。

こんにちは。はてなブログのサブディレクターの id:AirReader です。

先日、組織でのはてなブログ運営を GitHub 上で行うためのテンプレートリポジトリ「HatenaBlog Workflows Boilerplate」を公開しました。このテンプレートをご利用いただくと、組織でのブログ運営に求められるワークフロー(下書きの作成、更新、内容のレビューや公開など)を GitHub 上で行うことができます。

さて、本記事の前半は私より同機能の裏側で利用している OSS blogsync への寄付のご報告を、後半は id:d-haru から同機能の仕組みや工夫について共有いたします。

blogsync への寄付について

はてなブログより、OSS の blogsync (のメンテナの id:Songmu さん)へ寄付を行いました。 blogsync は、先日公開したはてなブログ運営を可能にするテンプレートリポジトリ「HatenaBlog Workflows Boilerplate」の機能を提供する上で中心となる重要なツールとして利用したため寄付を行うことにしました。

はてなでは、core-js、ecspresso、Let's Encrypt への寄付を行っています。その告知では、寄付の意図について以下のように記載しています。これははてなブログにおいても同様です。

ほかの多くの企業と同様、はてなのウェブサービスの大部分は、創業以来、多くの OSS によって支えられています。世のソフトウェア開発者がオープンにしてきた処理系やライブラリ、コミュニティのおかげで、初期のはてなから今にいたるまで、さまざまなプロダクトを開発し、事業として世の中に価値をもたらすことができています。 (中略) 私たちのプロダクトが依存する重要な OSS たちの多くは、個人のモチベーションと時間の捻出によって支えられています。その継続性を別の方向からサポートするためにも、直接的な手段としての金銭的な寄付を行うことにしました。

また、HatenaBlog Workflows Boilerplate の作成に当たっては、複数の企業が公開している技術ブログの運用エントリを参考にさせていただきました。技術情報だけではなく、組織の知見や悩みなどをオープンにしていただけたことで、はてなブログとして改善のお手伝いができたなと感じます。改めてご利用の皆様にお礼を申し上げます。 引き続き、はてなブログは利用者の皆様の情報発信を促進しつつ、共に発展していきたいと思います。

元々、blogsync は弊社 CTO の id:motemen が開発し公開したものですが、メンテナを移譲、リポジトリの変更などを行い、現在は id:Songmu さんが管理しています

Workflow の仕組みと工夫

はてなのブログチームで Web アプリケーションエンジニア 及び スクラムマスターをしている id:d-haru です。ここからはその仕組みなどについて説明させていただきます。

元々はてな内でも様々なブログの記事コンテンツを blogsync と ワークフローを利用して GitHub リポジトリで管理していました。

しかし、チームごとにバラバラだったりと整理しきれていませんでした。また、インターネット上でもはてなブログの記事をリポジトリで管理する方法を公開されている方々がチラホラといらっしゃる状態でした。

これらの知見を集約しつつ、改めて整理したものが今回の Boilerplate になります。

reusable workflow の活用

Boilerplate を公開するに当たって、根幹となる workflow の処理を reusable workflow として分離しました。

実際のコードは下記になります。

素朴にテンプレートリポジトリを公開する形も考えましたが、これだと機能追加や軽微な修正をした結果を反映させることが困難になってしまい、変更に弱くなってしまいます。

この課題を解決するために、workflow の処理は別のリポジトリに分離して Boilerplate はあくまで workflow の呼び出しにとどめることにしました。

このおかげで Boilerplate 自体はいじらずにコアのロジックを変更を即座に反映させることができるようにしています。

例として、Boilerplate にある workflow (呼び出し元)の 1 つである下書き記事作成のcreate draftアクションの設定は下記になります。

name: create draft

on:
  workflow_dispatch:
    inputs:
      title:
        description: "Title"
        required: true

jobs:
  create-draft:
    uses: hatena/hatenablog-workflows/.github/workflows/create-draft.yaml@v1
    with:
      title: ${{ github.event.inputs.title }}
      BLOG_DOMAIN: ${{ vars.BLOG_DOMAIN }}
    secrets:
      OWNER_API_KEY: ${{ secrets.OWNER_API_KEY }}

重要なのはuses: hatena/hatenablog-workflows/.github/workflows/create-draft.yaml@v1の部分です。

呼び出し元では reusable workflow を呼び出しているだけで複雑なことは可能な限りしないようにしています。

実際に上記で呼び出している reusable workflow が下記になります。

name: "[Reusable workflows] create draft and pull from hatenablog"

on:
  workflow_call:
    inputs:
      title:
        required: true
        type: string
      BLOG_DOMAIN:
        required: true
        type: string
    secrets:
      OWNER_API_KEY:
        required: true

jobs:
  post_draft_and_pull_from_hatenablog:
    name: create draft and pull from hatenablog
    runs-on: ubuntu-latest
    env:
      BLOGSYNC_PASSWORD: ${{ secrets.OWNER_API_KEY }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: setup
        uses: hatena/hatenablog-workflows/.github/actions/setup@v1
      - name: setup draft
        run: |
          envsubst < draft.template > draft
        env:
          TITLE: ${{ inputs.title }}
      - name: set blog domain
        id: set-domain
        run: |
          domain=${{ inputs.BLOG_DOMAIN }}
          echo "BLOG_DOMAIN=$(echo $domain | tr -d '\n\r ')" >> "$GITHUB_OUTPUT"
      - name: post draft to hatenablog
        id: post-draft
        run: |
          entry_path=$(blogsync post --draft ${{ steps.set-domain.outputs.BLOG_DOMAIN }} < 'draft')
          echo "ENTRY_PATH=$entry_path" >> "$GITHUB_OUTPUT"
      - name: pull
        run: |
          blogsync pull ${{ steps.set-domain.outputs.BLOG_DOMAIN }}
      - name: pull draft by title
        uses: hatena/hatenablog-workflows/.github/actions/create-draft-pull-request@v1
        with:
          title: ${{ inputs.title }}
          BLOG_DOMAIN: ${{ steps.set-domain.outputs.BLOG_DOMAIN }}
          ENTRY_PATH: ${{ steps.post-draft.outputs.ENTRY_PATH }}

reusable workflow としての定義は下記のように記述するだけです。inputs や secrets などを設定しておくことで呼び出し元から値を渡すことも可能です。

on:
  workflow_call:
    inputs:
      title:
        required: true
        type: string
      BLOG_DOMAIN:
        required: true
        type: string
    secrets:
      OWNER_API_KEY:
        required: true

blogsync の活用

冒頭でも書きましたが blogsync が記事の取得、登録、更新といったコアの部分を担っています。

blogsync では AtomPub APIの CLI クライアントで、記事の取得、投稿、更新などができます。

下書きの投稿では下記のように指定することで投稿が可能です。

blogsync post --draft <blog> < <path/to/file>

これがあるおかげで Boilerplate では workflow そのものの作成に集中することができました。

Boilerplate のリリース当初は blogsync や AtomPub API には特に変更せずに当時できることを前提に作成しましたが、最近では Maintener である id:Songmu さんともコミュニケーションをとっており API 側の機能追加を含めて協力的に*1*2改善を図っています。

公開してみてどうだったか?

特に Tech ブログを運営している企業の方から直接はてなの社員に「導入しましたよ〜」という声を頂いたりしており、大変嬉しく思っています。

今年のアドベントカレンダーでもたくさんの利用者の声を見かけて、開発者冥利に尽きます。

また、自分たちも課題に感じているもあれば、他にもハッとするような言及もありました。こういった意見を参考に、引き続きより使いやすくしていけるようにしたいと思います。

まとめ

ブログを GitHub で管理するためのツールなどはプラットフォームごとに様々な形がありますが、OSS を組み合わせてこれらを形にして提供する取り組みはあまり経験のないことで、とても楽しく取り組むことができました。

とはいえ、まだまだ自分たちが使っていても痒いところに手が届いていない部分や、こういう機能あったら便利だろうな〜というアイデアもいくつかある状態です。また、利用時の記事を読んでいると新たな発見や、執筆スタイルの多様性にも気付かされました。

より多くの人が便利に利用できるように、引き続き良くしていきたいと思います。