🪐

Homebrewを参考にしたHabanero Beeの簡易的なアップグレード手法

2021/04/15に公開

ここ最近は自身で開発している Habanero Bee のことばかり書かせていただいていますが、今回もHabanero Beeを開発していく上で学んだことについて書いていこうと思います。

Habanero Beeについては以前zennに書いた、こちらの記事を参照いただけたらと思います。

https://zenn.dev/shinshin86/articles/e199ce2a9baba6

既にデプロイ済みのHabanero Beeのアップグレードについて

ここではHabanero Beeを用いてサイトをデプロイした場合の、Habanero Beeの最新versionへのアップグレードについて書きます。
一体何を書いているのかと思われる方もいると思うので、まずはHabanero BeeのデプロイとDeploy to Netlifyボタンの挙動について書きます。

Habanero Beeのデプロイ

Habanero Beeのデプロイについては こちら で書いているので詳細は省きますが、Deploy to Netlifyボタンを用いてデプロイを行います。
Deploy to Netlifyボタンを用いることで、コードに触れることなく、サイト作成に必要なスプレッドシートのデータがあればサイトの公開が可能です。
(またデプロイの際にはGitHubとNetlifyのアカウントも必要になります)

デプロイがいかに簡単に行えるを示すYouTube動画も上のリンク先に用意しているので、よろしければ御覧ください。

Deploy to Netlifyの挙動について

上にも書いたように、Habanero BeeではDeploy to Netlifyボタンを用いてサイトのデプロイを行います。
このDeploy to Netlifyボタンの挙動ですが、指定されたGitHubリポジトリと同じものが自分のリポジトリとして作成されて、そのリポジトリがNetlify連携された状態になります。
そしてここは重要なポイントですが、自分のリポジトリとして作成される際はForkではなくCopyされます

Deploy to Netlifyボタンを押した場合、対象のリポジトリはコピーされます

このCopyされるという挙動は、一度作成されたリポジトリのupstreamへの追従を難しくします。
forkであれば、upstreamの修正を取り込むだけで事足りるのですが、copyの場合はcopy元への参照関係が途切れてしまうので、アップグレードを行う際に少し手間を掛けてしまうことになります。
(厳密に言えば、新たにremoteリポジトリを追加することで、upstream側の修正を取り込んでいくこともできそうな気がします(未検証)。ただ、このやり方で行けたとしても、普段の運用でそのような反映方法を採用するのは、いささか複雑すぎます)

まだHabanero Beeはガンガン開発している段階で、簡単にアップグレードするための仕組みを作成するだけのリソースはありません。
ですが、既に私はドッグフーディングも兼ねて、Habanero Beeで複数のサイトを運用しています。
Habanero Beeのアップグレードを行った場合、私はそれらの適用を本番環境での検証にもすぐに回していきたいため、簡単にアップグレードを行うための仕組みが必要でした。

というわけで、まずはDeveloper向けで良いので、簡易的なアップグレードの仕組みを作ることにしました。

Homebrewから学ぶ、シェルスクリプトの実行手法

Homebrewというmacユーザには馴染み深いパッケージマネージャーがあります。

https://brew.sh/index_ja

こちらのインストール方法はとても簡単で、下記のコマンドをターミナルに貼り付けて実行するだけです。

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

やっていることは凄くシンプルで、コマンド内に書かれている下記のURL先にある.shファイルをターミナルで実行しているに過ぎません
(後述しますが、このやり方は便利であると同時に、実行対象のソースが信頼できない場合はリスクを伴う場合もあります)

https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh

Homebrewのインストールコマンドは何をやっているのか?

行っている実行の詳細を見ていくために、このコマンドの中身を紐解いていこうと思います。

bash -cオプションについて

man bashで表示された -c のオプション内容を意訳させていただくと、下記のような内容になります。

-cオプションが指定されている場合、コマンドは文字列から読み込まれます。 文字列の後に引数がある場合は、$0から始まるパラメーターに割り当てられます。

例えば下記のようなコマンドを実行すると、bash 上で help を実行した場合と同様の結果が得られます。

/bin/bash -c "help"

また、 $() はコマンドの実行結果を格納できます。
つまり、下記のコードを実行してもhelpが表示されます。

/bin/bash -c "$(echo help)"

curl -fsSLオプションについて

次はcurlです。
-fsSLといった形で、複数のオプションが付いています。
それぞれのオプションの内容を man curl を参照しながら見ていこうと思います。
なお、以下に続く文章はmanの内容を私の方で意訳したものとなります。

-fオプションについて

HTTPサーバーエラーが起きた際に何も出力せずに失敗する。
これは主に、スクリプトなどが失敗したときにうまく対処できるようにするためです。
通常のケースではHTTP サーバーがドキュメントの配信に失敗した場合、その旨を示す HTML ドキュメントが返されます(多くの場合、その理由などもここに記述されます)。
このフラグを使用すると、curlはそのような出力をせずにエラー22を返します。
この方法はフェイルセーフではなく、成功しなかった応答コードがすり抜けてしまう場合があります。特に認証が関係する場合(応答コードは401 および 407 です)。

-sオプションについて

これはサイレント、または静音モードです。プログレスメーターやエラーメッセージを表示しません。 Curl を無言にします。リダイレクトしない限り、要求されたデータはターミナル /stdout にも出力されます。
プログレスメーターを無効にしてエラーメッセージを表示するには、このオプションに加えて -S , または --show-error を使用します。

-Sオプションについて

sオプション ( -silent ) と一緒に使用すると、curl が失敗したときにエラーメッセージを表示します。

-Sについては、-sと一緒に使うことが前提となったオプションであることがわかります。

-Lオプションについて

HTTPリクエストされたページが別の場所に移動したことがサーバーから報告された場合 (Location: ヘッダーと 3XX 応答コードで示される)、このオプションを使用すると curl はリクエストを新しい場所でやり直します。
-i、または --include または -I, --head と一緒に使用すると、リクエストされたすべてのページのヘッダが表示されます。
認証が使用されている場合、 curl は最初のホストにのみ認証情報を送信します。リダイレクトで curl が別のホストに移動した場合、ユーザーとパスワードを傍受することはできません。これを変更する方法については、--location- trusted を参照してください。--max-redirs オプションを使用すると、追跡するリダイレクトの量を制限することができます。
curl がリダイレクトをたどり、そのリクエストがプレーンな GET ではない場合 (POST や PUT など)、HTTP レスポンスが 301、302, または303 であれば、GET で次のリクエストを行います。レスポンスコードがその他の 3xx コードだった場合、curl は変更されていない同じメソッドを使用して次のリクエストを再送信します。
30xレスポンスの後に GET 以外のリクエスト メソッドを GET に変更しないように curl に指示するには、そのための専用オプションを使用します。専用のオプション --post301--post302--post303を使用することで、30xレスポンスの後に GET 以外のリクエスト メソッドを GET に変更しないように curl に指示できます。

curl -fsSLオプションはなにをしているのか?

manページを一つ一つ見てきましたが、要は何をしているのかというと、 HTTPリクエストに関するメッセージやcurl自体のメッセージは適宜抑えつつも、curl自体に関するエラーメッセージは出力するよ。もしリダイレクトする必要がある場合は対応もするよという感じになるかと思います。
(ちょっとざっくり説明し過ぎなので、この説明は正確ではないかもしれません。)

というわけで、下記のコマンドを手元のmacで実際に実行すると、

curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh

https://raw.githubusercontent.com/Homebrew/install/HEAD/install.shの内容がそのままターミナルに出力されます。この出力内容をbashで実行している、というのが Homebrew のインストールコマンドが行っていることとなります。

リモートに存在するシェルスクリプトを手元で実行することの怖さ

既にここまでの説明で感じてる方も多いと思いますが、このインストールコマンドの仕組みはあまり気持ち良いものではありません。
なぜなら、リモートにあるシェルスクリプトを手元に持ってきていきなり実行しているからです。

もしそのシェルスクリプト内に、実行者のPCに不利益を働くような処理が書かれていたらどうなるでしょうか?

そのシェルスクリプトが本当に信頼できるコードなのか?当然ながら実行前に見極める必要があります。

そのため、こういった形で手元で何らかの処理を行うようなコマンドは、実行前に取得先のコードを一応ざっくりと読むようにしています。とてもアナログな手法ですが、仕方ありません。
ただ、Homebrewのインストールスクリプトは結構量もあるので、読み解くのも大変です。正直、私は中身を見てぼんやりと処理の流れを読んだぐらいで終わっています(意味ない。。)。
(もしかしたらこういうのをチェックするようなツールとかがあるのかしらん?調べていませんが)

Homebrewのような有名なソフトウェアならある程度安全かもしれませんが、そこまで有名でないプロジェクトで利用されているような場合、必ずチェックしてみたほうが良いでしょう。
(といっても、昨今は有名なソフトウェアでも、コードが意図しない形で改竄されていたようなケースもあるので過信厳禁ですが。。例を下記に貼ります)

https://www.itmedia.co.jp/enterprise/articles/1811/28/news082.html

Habanero Beeのアップグレードスクリプトの内容

さてさて、話は変わり、
まだ全然有名でないプロジェクトの一つであるHabanero Beeのアップグレードスクリプトとしてもこの手法を用いています。

ご安心ください。Habanero Beeで用いるシェルスクリプトの量は大して多くありませんし、難しいことを行っていないので、何をしているかを読み解くのにそこまで時間はかかりません。
2021年4月15日時点のソースコードを貼ります。
(なお、明日以降、修正を組み込んでいくこともありますので、一応実行前は下記に記載したリポジトリからコードの内容はご確認ください)

#!/bin/bash
# Run this script at the root of the Habanero Bee project you want to update
PROJECT_ROOT=`pwd -P`
PROJECT_NAME="$(basename -- "$(pwd)")"
UPSTREAM_DIR="habnaero-bee-upstream"
cd ../
mkdir $UPSTREAM_DIR
cd $UPSTREAM_DIR
git clone https://github.com/shinshin86/habanero-bee.git ${PROJECT_NAME}
cd ${PROJECT_NAME}
rm -rf .git/
cp -r "${PROJECT_ROOT}/.git" .
git status # Check the changes

echo "SUCESS: update to the latest"
echo "============================"
echo "INFO: Use this command to navigate to the directory where the update was completed."
echo "cd ../${UPSTREAM_DIR}/${PROJECT_NAME}"
echo "============================"
echo "INFO: If you do not need it, delete this (${PROJECT_ROOT}) directory."

なお、このコードはこちらのリポジトリで管理しています。
https://github.com/shinshin86/habanero-bee-shell-script-of-update-to-latest

アップグレードスクリプトの内容

上のスクリプトが何をしているかというと

  1. カレントディレクトリのひとつ上に habnaero-bee-upstream というディレクトリを作成する
  2. habnaero-bee-upstreamのディレクトリ内に最新のhabanero-beeリポジトリをcloneする
  3. rm -rf .git/を用いて、git管理を削除
  4. 既存のHabanero Beeプロジェクト内の.git/をコピーして持ってくる

以上です。

だいぶ大雑把なスクリプトですね。。今解説を書きながらもっとスマートにやれるのではないだろうか?と感じました。。
(同じように感じた方がいましたらPRお待ちしていますm(_ _)m)

既存の.gitディレクトリを最新のhabanero-beeプロジェクトに持ってくることで、最新のコードが適用された状態となります。
あとは、下記のようにコマンドを打って、remoteにpushすることで完了です。

git add .
git commit -m 'Apply version of {version number}`
git push origin main

リモートに存在するシェルスクリプトを実行するだけという気軽さ

リモートに存在するシェルスクリプトを実行するというリスクを上に書きましたが、ではなぜこのような手法をとったのか?というと、それは手軽に実行できることに尽きます。

ここからはHabanero Bee自体のプロジェクトの話になりますが、まだHabanero Bee自体は開発途上であり、いわゆるβ版的な立ち位置となります。
本来であれば、スマートなアップグレードの仕組みを提供すべきですが、その仕組みを作るためのリソースはありません。

そこでコマンド一つで簡単に最新版にアップグレードできる仕組みをまずは作ろうと思い、Homebrewのインストール手法を採用した流れとなります。

実際、このスクリプトは非常に役立っており、複数のHabanero Beeによるサイトを運用していても、アップグレードはすぐに終わります。

Habanero Beeのアップグレード方法

ここまで長々とHomebrewからヒントを得た、Habanero Beeのアップグレードについて書かせていただきましたが、実際にHabanero Beeのアップグレードを行う際は、作成したHabanero Beeのルートにて、下記のコマンドを叩けば完了します。

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/shinshin86/habanero-bee-shell-script-of-update-to-latest/main/update.sh)"

ちなみにHabanero Beeでは最新の機能を利用したいユーザに向けてcanary版も提供しています。
もしCanary版にアップグレードしたい場合は下記のコマンドを叩くことで完了します。
(一度Canaryに変えた場合でも、Stableに戻すことは可能です。その場合、再度上のコマンドを叩くことでStable版のコードに差し替わります)

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/shinshin86/habanero-bee-shell-script-of-update-to-latest/main/update-canary.sh)"

というわけで、長々と書かせていただきましたが、これで以上となります。
最後まで読んでいただき、ありがとうございます。

Discussion