ZOZO TECH BLOGを支える技術 #2 執筆をサポートするCI/CD

ogp

はじめに

こんにちは、CTO/DevRelブロックの堀江(@Horie1024)です。本記事はZOZO DevRelチームによる連載「ZOZO TECH BLOGを支える技術」の2本目の記事です。

前回の記事ではZOZO TECH BLOGの概要とその運用について紹介しました。今回の記事ではTECH BLOGの運用プロセスのうち記事の執筆に焦点を当て、執筆とそのレビュー体制を支えるCI/CDフローの整備について紹介します。

目次

ZOZO TECH BLOGでのCI/CDの活用

CI/CDは、Continuous Integration(継続的インテグレーション)およびContinuous Delivery(継続的デリバリー)の略です。ソフトウェア開発プロセスの自動化による品質向上を目的とした手法です。CIでは、開発者がソースコードをリポジトリへプッシュするたびにビルドプロセスを実行しコードを統合します。そしてCDでは、CIの成果物を任意の環境へ自動的にデプロイします。これにより、開発者はより迅速かつ信頼性の高いソフトウェアを提供できるようになります。

ZOZO TECH BLOGを支える技術 #1 これまでとこれからで記事公開までのおおまかなプロセスを紹介しています。このうち、記事の執筆に関わるプロセスは次のとおりです。

  • 記事の執筆とテストページへのデプロイ
  • 記事のレビュー
  • 指摘箇所の修正と再レビュー

この3つのプロセスを記事が完成するまで繰り返します。これらのプロセスはソフトウェア開発と変わらないと言っても違和感はないでしょう。ZOZO TECH BLOGの記事は全てソースコードと同様にGitHubリポジトリで管理しており、記事のレビューもPull Requestを介して行います。執筆のプロセスにもCI/CDの手法を適用することで執筆者の執筆をサポートし、ZOZO TECH BLOGとしてより良い記事を公開することに繋がります。

記事の静的解析と文章校正

投稿する記事には正確で分かりやすい文章であることが求められます。文章の質の担保はレビューによって行いますが、人によるレビューの実施前に自動化された文章校正を実施する事でレビューの効率化を図れます。

CIによって反復的に実行されるプロセスには一般的に次のようなものがあります1

  • ソースコードのコンパイル
  • 分析(静的解析、動的解析等)
  • テスト

ZOZO TECH BLOGでは、これらのプロセスのうち分析にあたる静的解析を記事に対して行い文章校正をします。

具体的には、文章がリポジトリにプッシュされることをトリガーにtextlintを実行し、その結果をPull Requestにコメントとして書き込みます。これにより、記事の執筆者は文章校正の結果を確認できます。

記事のプレビュー環境へのデプロイ

記事が読者にどう表示されるかを確認することは読みやすい記事を執筆する上で重要です。意図しない改行や文字化けが発生していないか、記事中のコードブロックが正しく表示されているかや画像のサイズが適切かなど実際に記事を表示して確認することが望ましいです。

ZOZO TECH BLOGでは、記事の執筆はMarkdownで行っており、それをはてなブログに投稿することで公開しています。このため、実際に記事がどのように表示されるかを確認できるプレビュー環境を非公開のはてなブログとして整備しています。

文章がリポジトリにプッシュされることをトリガーに、はてなブログのAPIを利用して執筆中の記事がプレビュー環境にデプロイされます。このプレビュー環境は記事の執筆者が記事のプレビューを確認するためだけに利用します。また、2023年8月時点では本番環境への公開はDevRelチームが手動で行っています。

CI/CDフローの構築

CI/CDフローを整備することで実現したことは次の2点です。

  • 執筆中の記事の文章校正
  • 執筆中の記事のプレビュー環境へのデプロイ

これらを実現するCI/CDフローがどのように構成されているかを紹介します。

CI/CDフローの概要

文章校正とプレビュー環境へのデプロイを行うCI/CDフローの概要を図に示します2。CIサービスとしてはGitHub Actionsを用いています3。文章校正とプレビュー環境へのデプロイ共にPull Requestの作成またはコミットのプッシュをトリガーにワークフローを実行します。

CI/CDフローの概要

文章校正

文章校正は次のようなプロセスで実現します。

  • 差分の検出
  • textlintの実行
  • 実行結果のフィードバック

これらのプロセスをPull Requestの作成またはコミットのプッシュをトリガーに実行します。textlintによる文章校正の結果はPull Requestにコメントとして書き込まれ執筆者にフィードバックされます。この一連の流れを実行するワークフロー定義は次のとおりです。

name: "textlint & reviewdog"

on:
  pull_request:
    paths-ignore:
      - '**/README.md'

env:
  REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
  linter:
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout"
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: "Setup nodejs"
        uses: actions/setup-node@v3
        with:
          node-version: 16
          cache: 'yarn'

      - name: "Setup reviewdog"
        uses: reviewdog/action-setup@v1
        with:
          reviewdog_version: latest

      - name: "Install node dependencies"
        run: yarn install

      - name: textlint and reviewdog
        if: ${{ (github.event_name == 'pull_request') }}
        run: |
          DIFF_FILES=`git diff --name-status origin/master --diff-filter=MA | grep -E ".*.md" | cut -f2`
          if [ -z "${DIFF_FILES}" ]; then exit 0; fi
          $(yarn bin)/textlint --ignore-path config/.textlintignore -c config/.textlintrc -f checkstyle $(echo ${DIFF_FILES}) | reviewdog -f=checkstyle -name="textlint" -reporter=github-pr-review --fail-on-error=true -filter-mode=added

textlint and reviewdog stepで文章校正とPull Requestへのフィードバックをします。具体的な処理内容は次のとおりです。

  • 差分の検出
  • textlintの実行
  • reviewdogでのPull Requestへのコメント追加

textlintの実行時には次のオプションを指定しています4

オプション名 指定内容 オプションの概要
--ignore-path config/.textlintignore textlintの対象から除外するファイルを.textlintignoreに記載し指定
-c config/.textlintrc textlintの設定を.textlintrcに記載し指定
-f checkstyle 出力形式にcheckstyleを指定

また、textlintの実行結果をPull Requestにコメントとしてフィードバックするためにreviewdogを使用しています。reviewdogはtextlintの実行結果を受け取り、その結果をPull Requestにコメントとして書き込みます。reviewdogの実行時には次のオプションを指定しています5

オプション名 指定内容 オプションの概要
-f checkstyle 入力形式にcheckstyleを指定
-reporter github-pr-review 結果の出力にPull Requestへのコメントを指定
-fail-on-error true 1つでもエラーが発生した場合にジョブを失敗させるよう指定
-filter-mode file 追加されたファイル単位で結果をフィルタリングするよう指定

プレビュー環境へのデプロイ

記事のプレビュー環境へのデプロイは次のようなプロセスで実現します。

  • フォーマット・画像のアップロード
  • プレビューへの記事の反映

これらのプロセスを文章校正と同様にPull Requestの作成またはコミットのプッシュをトリガーに実行します。

フォーマット・画像のアップロード

プレビュー環境へのデプロイにおいて画像の扱いを考慮する必要があります。ZOZO TECH BLOGの標準的な記事のディレクトリ構成は次のとおりです。

articles
└── sample_article
    ├── entry.md
    └── images
        └── sample.png

entry.mdから画像を参照する場合は次のように記述します。

![sample](./images/sample.png)

ここで、entry.mdをプレビュー環境へデプロイする際、./images/sample.pngをはてなフォトライフへアップロードします。そして、./images/のパスをhttps://cdn-ak.f.st-hatena.com/images/fotolife/に変換しentry.mdの該当箇所を書き換えます。この一連の処理をフォーマットと呼んでいます。フォーマットが完了するとentry.mdは次のようになります。

![sample](https://cdn-ak.f.st-hatena.com/images/fotolife/<USERNAME>/<UPLOADED_IMAGE_PATH>)

GitHub Actionsでこのフォーマット処理を行うワークフロー定義は次のとおりです。entry.mdの差分を検出し、差分のあるファイルに対してフォーマットを行います。

name: "format & post to hatenablog"

on:
  pull_request:
    paths: 
      - '**/entry.md'

env:
  HATENA_ACCESS_TOKEN: ${{ secrets.HATENA_ACCESS_TOKEN }}
  HATENA_ACCESS_TOKEN_SECRET: ${{ secrets.HATENA_ACCESS_TOKEN_SECRET }}
  HATENA_CONSUMER_KEY: ${{ secrets.HATENA_CONSUMER_KEY }}
  HATENA_CONSUMER_SECRET: ${{ secrets.HATENA_CONSUMER_SECRET }}  

jobs:
  format:
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout"
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: "Upload Image"
        run: |
          ARTICLE_PATH=`pwd`/`git diff --name-status origin/master --diff-filter=MA | grep -E "articles/.*.md" | cut -f2`
          cd scripts
          bundle
          bundle exec ruby format_article.rb $ARTICLE_PATH ../entry.md

      - name: "mv formatted article"
        run: mkdir /tmp/workspace && cp entry.md /tmp/workspace/formatted_entry.md

      - name: "Upload formatted_entry.md for job: post_to_hatenablog"
        uses: actions/upload-artifact@v3
        with:
          name: formatted_entry
          path: /tmp/workspace/formatted_entry.md

このワークフロー定義では、Upload Image stepでformat_article.rbを実行することで画像のアップロードと記事のフォーマットを行います。ここではてなフォトライフへのアップロードにはrlho/hatena_fotolifeを使用しています。

次にmv formatted article stepでフォーマット済みの記事を/tmp/workspace/formatted_entry.mdに移動します。最後にUpload formatted_entry.md for job: post_to_hatenablog stepでフォーマット済みの記事を成果物としてアップロードします。フォーマット済みの記事は後続のpost_to_hatenablog jobでプレビュー環境へデプロイします。

プレビューへの記事の反映

プレビュー環境は非公開のはてなブログです。成果物としてアップロードされたフォーマット済みの記事をプレビュー環境へ反映するには、はてなブログへの投稿が必要になります。

フォーマット済みの記事を受け取りはてなブログへの投稿するワークフロー定義は次のとおりです。

env:
  BLOG_USERNAME: ${{ secrets.BLOG_USERNAME }}
  BLOG_DOMAIN: ${{ secrets.BLOG_DOMAIN }}
  BLOG_API_KEY: ${{ secrets.BLOG_API_KEY }}

jobs:
  format: ...

  post_to_hatenablog:
    needs: format
    runs-on: ubuntu-latest

    steps:
      - name: "Checkout"
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: "Setup hatenablog CLI"
        uses: x-motemen/blogsync@v0

      - name: "Download formatted_entry.md from job: format"
        uses: actions/download-artifact@v3
        with:
          name: formatted_entry
          path: /tmp/workspace

      - name: "cp formatted_entry.md"
        run: cp /tmp/workspace/formatted_entry.md .

      - name: "Add blogsync config"
        run: |
          echo -e "${BLOG_DOMAIN}\":\"\n  username\":\" ${BLOG_USERNAME}\n  password\":\" ${BLOG_API_KEY}\ndefault\":\"\n  local_root\":\" ./\n  omit_domain: true" | tr -d \" >> blogsync.yaml

      - name: "Create/Update article"
        run: |
          blogsync pull $BLOG_DOMAIN
          BRANCH=${{ github.head_ref }}
          ARTICLE_PATH=`git diff --name-status origin/master --diff-filter=MA | grep -E "articles/.*.md" | cut -f2`
          # 整形したものに置き換え
          mv -f formatted_entry.md $ARTICLE_PATH
          if grep '^URL:' $ARTICLE_PATH; then
            # ブログが投稿されている場合は投稿されている記事を更新
            blogsync push $ARTICLE_PATH
          else
            # ブログが一度も投稿されてない場合は投稿
            blogsync post --title=$BRANCH --custom-path=$BRANCH $BLOG_DOMAIN < $ARTICLE_PATH

            # 記事メタデータの付与
            BLOGSYNC_PATH="entry/${BRANCH}.md"
            cp -r $ARTICLE_PATH $ARTICLE_PATH.old
            head -8 $BLOGSYNC_PATH | cat - $ARTICLE_PATH.old > $ARTICLE_PATH
          fi
          # 画像のアップロード・メタデータが付与されていればコミットしてpush
          if git status -s | grep articles; then
            git config --global user.email ${BOT_EMAIL}
            git config --global user.name 'TechBlog Bot'
            git checkout $BRANCH
            git add $ARTICLE_PATH
            git commit -m "Add hatena blog meta data"
            git push ${TECH_BLOG_REPO_URL} $BRANCH
          fi

      - name: "Print blog URL"
        run: |
          ARTICLE_PATH=`git diff --name-status origin/master --diff-filter=MA | grep -E ".*.md" | cut -f2`
          echo `grep -e "^URL:" $ARTICLE_PATH | awk '{print $2}'`

Download formatted_entry.md from job: format stepでフォーマット済みの記事を成果物としてダウンロードします。はてなブログへの投稿にはx-motemen/blogsyncを使用し、Add blogsync config stepでblogsyncの設定ファイルを作成します。そして、Create/Update article stepでは次のことを行います。

  • 公開済みの記事一覧を取得
  • 記事の新規投稿または更新

公開済みの記事一覧を取得

blogsync pullコマンドで公開済みの記事一覧を取得します6

blogsync pull $BLOG_DOMAIN

記事の新規投稿または更新

記事がプレビュー環境に投稿されている場合は更新し、投稿されていない場合は新規投稿します。

blogsync pushコマンドで記事を更新します7。titleとcustom-pathはブランチ名、投稿内容は標準入力で指定します。

blogsync post --title=$BRANCH --custom-path=$BRANCH $BLOG_DOMAIN < $ARTICLE_PATH

投稿後、記事は自動的にダウンロードされentry/${BRANCH}.mdに保存されます。この時に、記事の先頭には記事のURL等のメタデータが付与されています8。例えば、本記事の場合次のようになります。

Title: ZOZO TECH BLOGを支える技術 #2 執筆をサポートするCI/CD
Date: 2023-08-08T09:55:22+09:00
URL: https://<BLOG_DOMAIN>/entry/techblog-writing-support-by-ci-cd
EditURL: https://blog.hatena.ne.jp/<BLOG_USERNAME>/<BLOG_DOMAIN>/atom/entry/...
CustomPath: techblog-writing-support-by-ci-cd

このメタデータのURLが記事のプレビューURLとなりますが、メタデータはZOZO TECH BLOGのリポジトリでは管理されていません。リポジトリ管理下にあるのは記事の本文のみです。そのため、記事の本文にメタデータを付与しZOZO TECH BLOGのリポジトリにコミットします。

記事の本文へのメタデータの付与は次のように行います。

BLOGSYNC_PATH="entry/${BRANCH}.md"
cp -r $ARTICLE_PATH $ARTICLE_PATH.old
head -8 $BLOGSYNC_PATH | cat - $ARTICLE_PATH.old > $ARTICLE_PATH

そして、ZOZO TECH BLOGのリポジトリにプッシュします。

git config --global user.email ${BOT_EMAIL}
git config --global user.name 'TechBlog Bot'
git checkout $BRANCH
git add $ARTICLE_PATH
git commit -m "Add hatena blog meta data"
git push ${TECH_BLOG_REPO_URL} $BRANCH

事例紹介

今回紹介したCI/CDフローが実際にどう活用されているか、ZOZO TECH BLOGでの事例を紹介します。

文章校正

「GitHub Copilotの全社導入とその効果」の執筆中に指摘があった例を紹介します。

techblog.zozo.com

丸かっこの指摘

冗長な表現の指摘

文字数の指摘

textlint-disableの利用

文章校正の結果を無視したい場合もあります。例えば、次の例ではアンケートの選択肢の部分でtextlintの指摘が出てしまいます。

アンケートの選択肢についてのtetlintの指摘

この場合、次のようにtextlint-disableを利用することで、指摘を無視できます9

<!-- textlint-disable -->
文章校正を無効にしたい文章
<!-- textlint-enable -->

校正ルールの運用

技術文書向けのtextlintルールプリセットであるtextlint-rule-preset-ja-technical-writingをはじめとする複数のルールを導入しています。このプリセットには、文章中の漢字の連続文字数を制限するtextlint-jatextlint-rule-max-kanji-continuous-lenが含まれています。このルールは、次のように漢字が連続していることを指摘します。

漢字の連続文字数の指摘

しかし、平均削減金額のように指摘を無視したい場合があります。textlint-disableを利用しても良いですが、恒久的な対応として例外に登録可能です。

漢字の連続文字数の例外登録

記事のプレビュー

プレビュー環境は記事に付与されたメタデータからアクセスできます。例えば、本記事のプレビューは次のようになります。

プレビュー表示

導入成果

CI/CDフローの導入によって、執筆者とレビュアの双方で記事の体裁の指摘が減少しています。また、自動的にプレビュー環境が生成されるため表示確認も容易になっています。DevRelチームとしても記事の体裁の指摘が減少しているため、記事の内容にフォーカスしてレビューできています。

レビューパフォーマンス

レビューのパフォーマンスを定量的に評価することは難しいですが、レビュー開始からアプルーブまでの時間を指標として見てみます。

Findy Team+の「レビュー分析」でZOZO TECH BLOGリポジトリのレビュー開始からアプルーブまでの時間を表示しました。集計期間は過去1年間です。ZOZO TECH BLOGの場合、レビュー開始からアプルーブまで平均で225.5時間かかっています。

過去1年間のレビューからアプルーブまでの平均時間

2023年2月からリードタイムが大きく改善しています。DevRelチームの発足によるレビュー体制強化が大きな要因として考えられます。

次は、DevRelチームが発足した2023年2月1日から2023年8月14日までで集計した結果です。この期間の平均リードタイムは、レビュー開始からアプルーブまで平均で145時間です。

2023年2月以降のレビューからアプルーブまでの平均時間

ここで、ZOZO TECH BLOGの月別の記事公開数は次のようになります。

公開月 公開本数
2023年2月 7
2023年3月 16
2023年4月 4
2023年5月 12
2023年6月 15
2023年7月 8
2023年8月 2

3月、5月、6月は公開記事数の増加によりレビューが重なりました。しかし、DeRelチームですべて捌ききることができ、レビュー開始からアプルーブまでの時間の増加もチームとしての許容範囲内に収められています。レビューの負荷が高い状況でも大きくパフォーマンスは悪化していません。

CI/CDフロー整備の影響

CI/CDフローの整備は1年以上前に行われていますので、レビューパフォーマンス改善の主要因ではありません。しかし、CI/CDフローの整備によってレビュアがレビューする際の効率化が図られているため、レビュー体制を強化した効果がより発揮されていると考えることはできそうです。

CI/CDフローの整備を進めていく上で「レビュー開始からアプルーブまでの時間」のような具体的な指標を追うことは、整備の方向性を模索する上で重要な検討材料となります。また、記事のアイデアを出す段階から公開されるまでの流れを見直すことで、記事の公開までの時間をより短縮できるかもしれません。

今後の展望

整備したCI/CDフローのサポートによって記事の執筆およびレビューの効率化が実現されています。しかし、まだまだ改善の余地があります。例えば、アップロードする画像サイズの最適化は著者にその実施を委ねていますが、CI/CDフローの中で実施できた方が執筆者の負担は減るでしょう。また、AIの活用についても検討を重ねていきたいです。文章校正やレビューについてAIを活用したり、記事のテーマや構成案の検討にも活用できそうです。

まとめ

本記事では、ZOZO TECH BLOGの執筆をサポートするCI/CDフローについて紹介しました。ZOZOではDevRelチームを中心に執筆をサポートする体制を整えています。今後も引き続き執筆環境を改善することで執筆者が少しでも書きやすい環境になり、それにより記事の執筆者が増え、社内の取り組みをより多く社外に発信できればと思います。

さいごに

ZOZOでは一緒に楽しく働くエンジニアを絶賛募集中です。ご興味のある方は下記リンクからぜひご応募ください。

corp.zozo.com


  1. ビルドという言葉はコンパイルと同じ意味で使われる場合もありますが、CIの文脈ではコンパイルだけでなく、テストや分析まで含まれることがあります。
  2. GitHub Actionsのアイコンは公式サイトから引用しています。
  3. 当初はCIサービスとしてCircleCIを利用していましたがGitHub Actionsに移行しました。
  4. textlintのオプションはtextlintリポジトリのREADME.mdから確認できます。
  5. reviewdogのオプションはreviewdogリポジトリのREADME.mdから確認できます。
  6. https://github.com/x-motemen/blogsync#%E3%82%A8%E3%83%B3%E3%83%88%E3%83%AA%E3%82%92%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8Bblogsync-pull
  7. https://github.com/x-motemen/blogsync#%E3%82%A8%E3%83%B3%E3%83%88%E3%83%AA%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8Bblogsync-push
  8. https://github.com/x-motemen/blogsync#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88
  9. textlint-enableで再度有効にする必要があります。
カテゴリー