🍔

シェルスクリプトでZennの投稿をGitHubプロフィールに自動反映してみる

2021/07/06に公開1

やりたいこと

  • GitHubプロフィールにZennの投稿最新5件を表示したい
  • それをよしなに自動で反映できるようにしたい

モチベ

mikkameさんのこれと
https://twitter.com/mikkameee/status/1360887240587571201?s=20

ikawahaさんのこれ
https://zenn.dev/ikawaha/articles/20210221-c8f2d9ac028ae49d551a

をみて、シェルスクリプトでできるんでは!?と思いついたからです。
かっこいいですよね、こういうのさっとできると・・・

成果物

よかったらスターください!!!

Image from Gyazo

GitHubはこちら

手順

  1. GitHub Actionsでcron動かし、シェルスクリプトで定期的にZennのRSSフィードを取得
  2. README.mdに上書きして、前回と差分があればコミットしてmainブランチにプッシュする

これだけです。

GitHub Actionsでシェルスクリプトを定期実行する

まず、GitHub Actionsで定期実行をする部分です。

ポイントは、以下。

  1. トリガーとしてcronを使う
  2. テストやデバッグしやすいように手動で実行できるようworkflow_dispatchもつけておく
  3. シェルスクリプトをactions内で実行できるよう、chmodで実行権限を付与する
  4. シェルスクリプトへのパスを${GITHUB_WORKSPACE}を使って表現する(使わなくてもOKだと思う)
  5. スクリプト実行後にREADME.mdを直前のコミットと比較して差分があればコミットしてプッシュ
name: Update README
# 1. トリガーとして`cron`を使う
on:
  schedule:
    - cron: '0 * * * *' # 1時間ごとに実行
  # 2. テストやデバッグしやすいように手動で実行できるよう`workflow_dispatch`もつけておく
  workflow_dispatch:

jobs:
  execute:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: update zenn rss feed
        # 3. シェルスクリプトをactions内で実行できるよう、`chmod`で実行権限を付与する
        # 4. シェルスクリプトへのパスを`${GITHUB_WORKSPACE}`を使って表現する(使わなくてもOKだと思う)
        run: |
          chmod +x "${GITHUB_WORKSPACE}/rss-feed.sh"
          "${GITHUB_WORKSPACE}/rss-feed.sh"
      - name: git commit
        run: |
          git config --local user.email "${GITHUB_ACTOR}@users.noreply.github.com"
          git config --local user.name "captain-blue210"
          git add README.md
         # 5. スクリプト実行後に`README.md`を直前のコミットと比較して差分があればコミットしてプッシュ
          git diff --cached --quiet || git commit -m "Update Zenn RSS feed"; git push origin main

いくつか初めて知ったこともあるのでかいつまんで書いておきます。

- name: update zenn rss feed
   run: |
      chmod +x "${GITHUB_WORKSPACE}/rss-feed.sh"
      "${GITHUB_WORKSPACE}/rss-feed.sh"

${GITHUB_WORKSPACE}は、ワークフローでcheckoutアクションを使用している場合は、リポジトリのコピーと同じになります。

- name: git commit
   run: |
      git config --local user.email "${GITHUB_ACTOR}@users.noreply.github.com"
      git config --local user.name "captain-blue210"
      git add README.md
      git diff --cached --quiet || git commit -m "Update Zenn RSS feed"; git push origin main

git diff --cachedで直前のコミットとのdiffを取ることができ、さらに--quietをつけると、

  • 差分あり -> exit codeとして1を出力
  • 差分なし -> exit codeとして0を出力
    することができるので、||を使ってexit code1だった場合(差分があった場合)にgit commitgit pushを実行させます。

||は終了コードが0以外の場合に、右側のコマンドを実施してくれます。

シェルスクリプト本体について

最後に処理本体であるスクリプトについて解説してみます。ただ、私のシェル芸力が足らずあんまりスマートなスクリプトではありません・・・
つよつよエンジニアの方、こう書くといいよ!というのがあればコメントいただけると泣いて喜びます😂

#!/bin/bash

set -o errexit
set -o nounset
set -o pipefail
set -o xtrace

RESULT=$(
  curl https://zenn.dev/captain_blue/feed 2>/dev/null |
    grep -oE '(<title>.*?</title>|<link>.*?</link>)' |
    sed 1,4d |
    awk '{
      if(NR%2){
        line=$0
      }else{
        print line $0;
        line=""
      }
    }END{
      if(line)
        print line
      }' |
    sed -e "s/<title><\!\[CDATA\[\(.*\)\]\]><\/title><link>\(.*\)<\/link>/- [\1](\2)/" |
    sed -n 1,5p
)

sed -i -z 's|<\!-- LATEST_ARTICLES_START -->.*<!-- LATEST_ARTICLES_END -->|<!-- LATEST_ARTICLES_START -->|g' ./README.md

echo "$RESULT" | while read -r line; do
  echo "$line" >>./README.md
done

echo '<!-- LATEST_ARTICLES_END -->' >>./README.md
curl https://zenn.dev/captain_blue/feed 2>/dev/null |
   grep -oE '(<title>.*?</title>|<link>.*?</link>)' |
   sed 1,4d |

まず、curlコマンドを使ってZennのRSSを叩きます。(エラーは/dev/nullにポイしてます)。
次にgreptitlelinkタグに一致した部分だけ抽出して、1~4行目は不要なので削除しています。

現時点だと以下のようにtitlelinkが改行されてしまっています。

~省略~
<title><![CDATA[FlutterのSafeAreaを使って上下のあいつらを気にしない方法]]></title>
<link>https://zenn.dev/captain_blue/articles/introduce-sarearea-in-flutter</link>
~省略~

これを次の処理でマークダウンの形に整形できるように、titlelinkを一行にしていきます。

<title><![CDATA[FlutterのSafeAreaを使って上下のあいつらを気にしない方法]]></title><link>https://zenn.dev/captain_blue/articles/introduce-sarearea-in-flutter</link>

そこでawkコマンドを使って一行にしています。

awk '{
  if(NR%2){
     line=$0
  }else{
     print line $0;
     line=""
  }
}END{
  if(line)
     print line
}' |

NR%2は、現在の行番号が奇数かどうかをチェックしています。省略しないて書くとNR%2==1ですが、awkでは1は真となるみたいで省略しても大丈夫なようです。

奇数だった場合は悪書としてpという変数に$0、現在処理している行全体を代入します。
※奇数行はすべてtitleなので、title行全体ということになりますね。

偶数だった場合は上記で代入しておいたtitleの行とlinkの行をprintを使って連結して出力し、pを空文字にします。
printでは改行は出力されないため、これでtitlelinkを1行にすることができました。

ENDでは、最終行を読み込んだあとに実行されpに値が入っていればそれを出力します。

次に、sedコマンドと正規表現を組み合わせて、awkで一行にしたtitlelinkから値を抜き出し、マークダウンの形に整形します。

sed -e "s/<title><\!\[CDATA\[\(.*\)\]\]><\/title><link>\(.*\)<\/link>/- [\1](\2)/" |

ex.

- [FlutterのSafeAreaを使って上下のあいつらを気にしない方法](https://zenn.dev/captain_blue/articles/introduce-sarearea-in-flutter)

取得して整形した記事データの1~5行目を取得しています。

sed -n 1,5p

そして、ここまでで取得->整形したデータをREADME.mdに書き込む処理が以下になります。

sed -i -z 's|<\!-- LATEST_ARTICLES_START -->.*<!-- LATEST_ARTICLES_END -->|<!-- LATEST_ARTICLES_START -->|g' ./README.md

echo "$RESULT" | while read -r line; do
  echo "$line" >>./README.md
done

echo '<!-- LATEST_ARTICLES_END -->' >>./README.md

まず、下準備としてREADME.mdに書き込む目印となるコメントをつけておきます。
<!-- LATEST_ARTICLES_START --><!-- LATEST_ARTICLES_END -->の間に記事一覧を追記します。

<!-- 省略 -->
## Latest 5 articles
<!-- LATEST_ARTICLES_START -->
<!-- LATEST_ARTICLES_END -->

最初にsedコマンドでコメントの間と<!-- LATEST_ARTICLES_END -->を消してしまいます。

sed -i -z 's|<\!-- LATEST_ARTICLES_START -->.*<!-- LATEST_ARTICLES_END -->|<!-- LATEST_ARTICLES_START -->|g' ./README.md

そのあと、whileで取得したデータを一行ずつ追記していきます。

echo "$RESULT" | while read -r line; do
  echo "$line" >>./README.md
done

最後に<!-- LATEST_ARTICLES_END -->を再度ファイルに追記して完了です。
※ 次回以降に記事一覧を全消しするときに必要。

echo '<!-- LATEST_ARTICLES_END -->' >>./README.md

ハマった点と対応策

ちょっとしたファイル編集に便利なsedコマンドですが、sed -e "s/置換前/置換後/で置換するとき今回のように置換後の値に改行が含まれているとうまく認識されず、sedのシンタックスエラーとなってしまい一括で置換ができず苦労しました。

置換前として複数行に含まれる値を対象にする、であれば-zオプションで可能なのですが、置換後の文字列が複数行に渡る場合はうまくいきません。

最終的に、一括で置換するのを諦めて一行ずつファイルに追記する形に方向転換することでやりたいことを達成できました💪

echo "$RESULT" | while read -r line; do
  echo "$line" >>./README.md
done

あと、地味にtitlelinkを連結して一行にする部分もハマりました。最初sedでやろうとしてうまくいかずawkに変更してif文を使うことで実現しました。

課題・・・

1点だけまだ対応できてない部分があって、GitHub Actions上からZennのRSSフィードを取得すると最新順になっていないようで、ちょっと古い5件が表示されてしまっています・・・
スクリプトでデータを取得したあとに、公開日でソートした上で出力すればよさげなんですがまだ未対応です🙄
気が向いたら対応してこの記事も更新しようと思います!

参考

Discussion

amano yuyaamano yuya

読ませていただきました!
本記事を参考にさせていただき、自分も再現できました!