DenoとGitHub ActionsでZennの最新記事をGitHub Profileに掲載して自動でCommitまでやる
最近、Zennの記事をけっこう書いています。
そこで、Zennでの活動状況を他のプラットフォームでもアピールしたいと思いました。
今回はGitHubのプロフィールページ(つまり、自分のユーザー名と同名のリポジトリのREADME.md
)にZennの最新記事を載せたいと思います。
タイトル通り、Deno 🦕 を使っていきます。
RSSでZennの最新記事を取得する
まず、Zennの最新記事を取得します。
以前、DenoでZennの記事を取得するDenoモジュールを作成しました。
これを使ってもよいのですが、勉強を兼ねて別の手段を使います。
ZennではRSSフィードが提供されているので、ここから記事を取得してみます。
DenoのRSSモジュールを使います。
とりあえずシンプルな例をやってみましょう。
rssフィードを取得し、deserializeFeed
で整形します。
取得に使っているKyはこちらの記事で紹介しています。
import { deserializeFeed, ky } from "./deps.ts";
const { feed, feedType } = await deserializeFeed(
await ky("https://zenn.dev/kawarimidoll/feed").text(),
);
console.log(feedType);
console.log(feed);
実行します。
実行に使っているVelociraptorはこちらの記事で紹介しています。
velociraptor.ymlの設定
最初のrssの取得以外で必要な権限も追加してあります
allow:
- write=./assets/zenn.png,README.md
- read=README.md
- net=zenn.dev,res.cloudinary.com
scripts:
update_profile:
cmd: update_profile.ts
lint: deno lint
fmt: deno fmt
pre-commit:
cmd:
- vr lint
- vr fmt
gitHook: pre-commit
❯ vr update_profile
RSS 2.0
{
"xmlns:dc": "http://purl.org/dc/elements/1.1/",
"xmlns:content": "http://purl.org/rss/1.0/modules/content/",
"xmlns:atom": "http://www.w3.org/2005/Atom",
version: "2.0",
channel: {
title: "kawarimidollさんのフィード",
description: "Zennのkawarimidollさん(@kawarimidoll)のフィード",
link: "https://zenn.dev/kawarimidoll",
image: {
url: "https://zenn.dev/images/logo-only-dark.png",
title: "kawarimidollさんのフィード",
link: "https://zenn.dev/kawarimidoll"
},
generator: "zenn.dev",
lastBuildDate: 2021-06-29T05:58:52.000Z,
href: "https://zenn.dev/kawarimidoll/feed",
rel: "self",
type: "application/rss+xml",
"atom:link": {
href: "https://zenn.dev/kawarimidoll/feed",
rel: "self",
type: "application/rss+xml"
},
language: "ja",
items: [
{
title: "DenoとGraphQLでGitHubの草をターミナルに表示する",
description: "GitHubの草が何となく好きです。どれくらい活動しているかが可視化され、自信やモチベーションになる気がします。\nしかし普段ターミナルに生息しているので、わざわざWebページを開くのは不便です。\nせっ...",
link: "https://zenn.dev/kawarimidoll/articles/1679844a116395",
ispermalink: "true",
guid: "https://zenn.dev/kawarimidoll/articles/1679844a116395",
pubDate: 2021-06-29T02:39:47.000Z,
url: "https://res.cloudinary.com/dlhzyuewr/image/upload/s--Nl_Q3RZo--/co_rgb:222%2Cg_south_west%2Cl_text:n...",
length: "0",
type: "image/png",
enclosure: [Object],
"dc:creator": "kawarimidoll"
},
{
title: "Denoと...
(略)
ふむ。 https://zenn.dev/kawarimidoll/feed の内容をシリアライズしてくれていますね。
メインの記事の内容はfeed.channel.items
に入っています。こちらを取り出してみましょう。
普通に取り出そうとすると型チェックで止められてしまうため、いろいろimport
して型ガード処理も追加します。
- import { deserializeFeed, ky } from "./deps.ts";
+ import { deserializeFeed, ky, Feed, FeedType, RSS1, RSS2 } from "./deps.ts";
const { feed, feedType } = await deserializeFeed(
await ky("https://zenn.dev/kawarimidoll/feed").text(),
);
- console.log(feedType);
- console.log(feed);
+ const isRss2 = (
+ feed: Feed | RSS1 | RSS2,
+ feedType: FeedType.Atom | FeedType.Rss1 | FeedType.Rss2,
+ ): feed is RSS2 => feed && feedType === FeedType.Rss2;
+ if (!isRss2(feed, feedType)) {
+ throw new Error("Invalid feed type");
+ }
+ const item = feed.channel?.items[0];
+ if (!item) {
+ throw new Error("Item not found");
+ }
+ console.log(item);
これで実行します。
❯ vr update_profile
{
title: "DenoとGraphQLでGitHubの草をターミナルに表示する",
description: "GitHubの草が何となく好きです。どれくらい活動しているかが可視化され、自信やモチベーションになる気がします。\nしかし普段ターミナルに生息しているので、わざわざWebページを開くのは不便です。\nせっ...",
link: "https://zenn.dev/kawarimidoll/articles/1679844a116395",
ispermalink: "true",
guid: "https://zenn.dev/kawarimidoll/articles/1679844a116395",
pubDate: 2021-06-29T02:39:47.000Z,
url: "https://res.cloudinary.com/dlhzyuewr/image/upload/s--Nl_Q3RZo--/co_rgb:222%2Cg_south_west%2Cl_text:n...",
length: "0",
type: "image/png",
enclosure: {
url: "https://res.cloudinary.com/dlhzyuewr/image/upload/s--Nl_Q3RZo--/co_rgb:222%2Cg_south_west%2Cl_text:n...",
length: "0",
type: "image/png"
},
"dc:creator": "kawarimidoll"
}
最新記事の情報を取得できました。
記事のOGP画像をダウンロードする
取得できた記事のタイトルとリンクがあれば最新記事の表示はできるのですが、Zennはcloudinaryで動的に生成されるOGP画像が提供されているので、これを使って画像リンクとして掲載したいと思います。
このOGP画像のURLをREADMEに直接書く方法でも実現できますが、今回はいろいろ勉強したいので、downloadモジュールを使ってリポジトリ内に画像をダウンロードする手段を採ります。
先程の取得結果では、画像URLはitem.url
で取得できそうなのですが、Item
の型定義にurl
がないらしく、アクセスしようとするとlspに怒られてしまいました。
item.enclosure.url
にも同様の内容が入っているので、これを使用します。
// importにdownloadを追加する必要あり
// itemの定義まで同じ、その後に追記
const { link, enclosure } = item;
if (!link) {
throw new Error("Link not found");
}
const url = enclosure?.url;
if (!url) {
throw new Error("URL not found");
}
console.log({ link, url });
const file = "zenn.png";
const dir = "./assets";
await download(url, { file, dir });
実行します。保存先ディレクトリは事前に作っておきましょう。
❯ mkdir assets
mkdir: created directory 'assets'
❯ vr update_profile
{
link: "https://zenn.dev/kawarimidoll/articles/1679844a116395",
url: "https://res.cloudinary.com/dlhzyuewr/image/upload/s--Nl_Q3RZo--/co_rgb:222%2Cg_south_west%2Cl_text:n..."
}
❯ ls assets
zenn.png
rssで取得した画像をダウンロードすることができました。
READMEを更新する
画像とリンクが手に入ったので、これをREADMEに反映させます。
READMEの書き換え位置の特定として、<!-- zenn-article-link-next-line -->
というコメントの次の行にZenn記事へのリンクがあるとして、これを最新記事のものに置換します。
URLの正規表現は以下のものを使います。
"https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#]+"
rssから取得できたlink
を使ってreplace
を行います。
// await download(url, { file, dir }); まで同じ
const readme = "./README.md";
const text = await Deno.readTextFile(readme);
const urlStr = "https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#]+";
const replaceFlg = "<!-- zenn-article-link-next-line -->";
const regex = new RegExp(`(${replaceFlg}\n.*)${urlStr}`);
await Deno.writeTextFile(readme, text.replace(regex, `$1${link}`));
これを実行すると、URLが更新されます。
## Latest Zenn article
<p align="center">
<!-- zenn-article-link-next-line -->
- <a href="https://zenn.dev/kawarimidoll/articles/old_article"><img alt="Zenn" src="assets/zenn.png"></a>
+ <a href="https://zenn.dev/kawarimidoll/articles/1679844a116395"><img alt="Zenn" src="assets/zenn.png"></a>
</p>
なお、レイアウト調整のため、markdown内にhtmlをそのまま書いていますが、markdown形式[![Zenn](assets/zenn.png)](https://zenn.dev/kawarimidoll/articles/xxxxx)
でも同じ結果になります。
自動で更新してCommitする
以上の作業をGitHub Actionsを使用して自動で行い、Commitまでやります。
Profileのリポジトリに、これまで作ったファイルを配置します。
ではworkflowの定義を追加しましょう。
定期実行やVelociraptorのアクションを使える点に関しては、以前宣伝ツイート用のアクションを作った記事で紹介していますのでご覧ください。
name: Update profile
on:
# 03:20UTC -> 12:20JST
schedule:
- cron: "20 3 * * *"
jobs:
update:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v2
- uses: denoland/setup-deno@v1
- uses: jurassiscripts/setup-velociraptor@v1
- run: VR_HOOKS=false vr update_profile
- name: Git commit
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
if [ -n "$(git status --porcelain)" ]
then git commit -aqm 'update profile' && git push origin
else echo 'nothing to commit, working tree clean'
fi
最後のところでコミットユーザーをgithub-actions[bot]
に設定しています。
これでGitHubのコミットグラフ( https://github.com/kawarimidoll/kawarimidoll/commits/master )にbotが表示されます。
コミット権限のあるトークンを使って自分でコミットしたように見せかけても良いですが、github-actions
を明示しておいたほうが自動化されていることがわかりやすくて良い気がします。
また、変更されたファイルがない場合にgit commit
しようとするとエラーコードが返され、ワークフローが失敗したとみなされてアラートが飛んでくるため、git status --porcelain
を使って更新有無を判定しています。
なお、ワークフロー実行中にvr
コマンドが実行されることにより、(定義されている場合は)Velociraptorのgit-hookが設定されます。
これが後のCommitを止める可能性があるため、実行の際にVR_HOOKS=false
環境変数を設定しておくと安全です。
ということで、これでZennの最新記事をGitHubに載せることができました。
現在どんな感じになっているかは実際のプロフィールページをご覧ください。
おわりに
Denoを使ってGitHubのページを更新してみました。
まあ 手動だったらこんな宣伝しない のですが、自動化するのは楽しいものです。
今後もいろんなことをDenoで自動実行したいと思います。
今回のコードはプロフィールリポジトリで動いています。
参考
Discussion