1. はじめに
前回の記事では、GitHubにpushすると、GitHub ActionsがCloudflare Workersへ自動で公開するしくみを作りました。
ただ、このしくみには弱点があります。mainにpushすると、中身に間違いがあっても、そのまま公開されてしまいます。
今回の検証は小さな静的ページなので、それでも大きな問題にはなりません。でも、仕事で扱う大きな動的ページでは、そうはいきません。公開する前に、自動で確かめるしくみが必要だと思いました。
そこで今回は、公開の前にHTMLをチェックして、間違いがあれば公開しないようにしました。さらに、わざとHTMLを壊して、本当に公開が止まるのかを確かめています。
イベント・on:・ランナー・既定値の説明は、前回の記事に書いたので省略します。この記事では、前回と違うところだけを説明します。
なお、記事の内容は、断りがない限り自分の環境で動かして確かめていますが、CI/CDは初心者です。間違いなどがあれば、コメントで教えていただけるとうれしいです。
2. 作ったもの
プルリクエストを作るとチェックだけが動き、mainにマージするとチェックのあとに公開されるしくみです。
check.ymlには入口が2つあります。プルリクエストの道は、マージする前に結果を知るためのものです。mainの道は、公開の前の関所です。
3. 手順
環境は前回と同じです。無料プランのPrivateリポジトリを使っています。チェックはGitHub Actionsの中で動くので、自分のPCにNode.jsは入れていません。
ファイルは2つ足し、1つ変えました。
tobaru-net-test/
├─ .htmlvalidate.json … チェックのルール(新規)
└─ .github/
└─ workflows/
├─ check.yml … チェックの指示書(新規)
└─ deploy.yml … チェックを呼ぶように変更
(1) チェックのルールを決める
HTMLのチェックにはhtml-validateを使いました。ルールを自分で作らなくても、できあいのrecommendedがあります。
.htmlvalidate.jsonです。
{
"extends": ["html-validate:recommended"]
}
(2) check.ymlを書く
name: Check
on:
# mainに向けたプルリクエスト
pull_request:
branches: [main]
# ほかのワークフローから uses: で呼ばれたとき
workflow_call:
jobs:
html:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
# ランナーにファイルを取り出す
- uses: actions/checkout@v5
# public/ の中の .html を調べる
- name: Validate HTML
run: npx --yes html-validate@11.18.0 public/
(3) deploy.ymlを変える
変えたのは、checkジョブの追加とneeds: checkだけです。
jobs:
# 公開の前の関所。中身は check.yml
check:
uses: ./.github/workflows/check.yml
deploy:
# check が成功したときだけ動く
needs: check
runs-on: ubuntu-latest
# これ以降は前回と同じ
4. 試してみた
結果を先に書くと、チェックが赤のとき、マージは止まりませんでしたが、公開は止まりました。
| やったこと | 結果 |
|---|---|
| そのままのページでプルリクエストを作る | 赤 |
| 指摘されたところを直して、同じブランチにpush | 緑 |
| 緑のままマージ |
check→deployと動き、公開された |
</main>を消して、赤のままマージ |
マージはできたが、deployは実行されず、公開中のページは変わらなかった |
(1) 壊す前に、もう赤になった
ブランチをpushしただけでは、何も動きません。check.ymlの条件はプルリクエストで、deploy.ymlの条件はmainへのpushだからです。プルリクエストを作ると、Check / htmlが動き始めました。
わざと壊すのは、この次の予定でした。ところが、何も壊していないのに赤になりました。
ログを開くと、原因が書いてあります。
index.htmlの1行目の<!doctype html>が、小文字だと指摘されています。ブラウザでは正しく表示されていますが、recommendedのdoctype-styleというルールは大文字を求めます。
(2) 直してpushすると、緑になった
<!DOCTYPE html>に直して、同じブランチにpushしました。プルリクエストを作り直さなくても、チェックは自動で動き直します。
(3) 緑のままマージすると、公開された
マージするとmainへのpushが起きて、deploy.ymlが動きます。checkのあとにdeployが動き、公開中のページも<!DOCTYPE html>に変わりました。
(4) 赤のままマージしても、公開は止まった
いよいよ本番です。別のブランチで</main>を消して、プルリクエストを作りました。チェックは赤になりましたが、Merge pull requestのボタンは押せます。
あえてマージすると、checkが赤になり、deployはスキップされました。0秒なのは、一度も始まっていないからです。
このときmainのコードからは</main>が消えていましたが、https://test.tobaru.netには</main>が残っていました。コードは壊れたが、公開中のページは守られたということです。
検証のあとは、壊した変更をgit revertで取り消して、もう一度プルリクエストからマージしました。チェックは緑で、公開も通っています。
5. なぜ公開が止まるのか
公開を止めているのは、deploy.ymlのneeds: checkです。画面の設定は何も変えていません。
(1) 赤と緑は、何で決まるの?
コマンドの終了コードで決まります。0なら成功、0以外なら失敗です。
4章のログの最後にも、Process completed with exit code 1.とあります。Actionsは「DOCTYPE should be uppercase」という文章を読んでいるのではありません。公式ドキュメントにも、ステップの成功と失敗は終了コードに基づいて報告されると書かれています(Workflow syntax)。
つまり、間違いがあるときに0以外で終わるコマンドなら、何でもチェックとして使えます。
(2) needs:がないと、どうなるの?
checkとdeployが同時に動くので、チェックが赤でも公開されるはずです(ここは試していません)。
ジョブは既定で並列に動きます。上から順に書いてあっても、順番はつきません。needs:を書くと、次の決まりが効きます。
If a job fails or is skipped, all jobs that need it are skipped
必要としているジョブが失敗すると、そのジョブはスキップされます。この決まりのおかげで、deployが赤を判断する処理は一切書いていません。
(3) なぜcheck.ymlを別のファイルにしたの?
プルリクエストとmainの両方で、同じチェックを使いたかったからです。
ただ、needs:は同じファイルの中のジョブしか指定できません。そこでcheck.ymlのon:にworkflow_callを書いて、ほかのワークフローから呼べるようにしました(公式ドキュメント)。deploy.ymlのcheckジョブは、それをuses:で呼んでいるだけです。
deploy.ymlの中にジョブとしてcheckがあるので、needs: checkと書けます。
プルリクエストのときのチェックと、マージのときのチェックは別の実行です。公開を止めているのは、マージのときの結果です。
(4) なぜマージは止まらなかったの?
無料プランのPrivateリポジトリでは、マージを止めるルールが効かないからです。
GitHubには、チェックが緑にならないとマージできなくする設定があります。ただ、SettingsのRulesetsを開くと、次の警告が出ていました。
古い形式のBranch protection ruleも同じです。ルールは作れますが、リポジトリをTeamなどの組織アカウントに移すまで適用されません。
だから、止めているものを分けて考える必要があります。
| しくみ | 止めるもの | 今回の環境 |
|---|---|---|
| Rulesets・Branch protection rule | マージ | 効かない |
needs: check |
公開 | 効く |
needs:では、mainのコードが壊れることは防げません。防げるのは公開だけです。それでも、ワークフローのファイルに書くだけなので、無料プランでも使えます。
6. 前回と違うところ
しくみ自体は前回と同じで、イベントが起きたらon:に合うワークフローが動きます。初めて出てきたものだけを説明します。
(1) pull_requestは、いつ動くの?
types:を書かなければ、opened・synchronize・reopenedの3つで動きます(公式ドキュメント)。
| 名前 | いつ |
|---|---|
opened |
プルリクエストを作った |
synchronize |
ブランチに新しいcommitをpushした |
reopened |
閉じたプルリクエストを開き直した |
4章の(2)で、pushしただけでチェックが動き直したのはsynchronizeのおかげです。これも前回と同じ、書かなかったときの既定値です。
(2) uses:のactions/checkout@v5は何?
持ち主/リポジトリ@バージョンの形で、GitHub上で公開されている部品を借りる書き方です。actions/checkoutは、GitHubが公開しているgithub.com/actions/checkoutのことです。
前回のcloudflare/wrangler-action@v4も、同じ形です。
(3) なぜcheckoutが必要なの?
ランナーは毎回新しく用意され、リポジトリのファイルは入っていないからです。この行がないと、調べるpublic/がありません。
プルリクエストのときに取り出されるのは、ブランチの先頭ではありません。actions/checkoutは既定で、イベントの対象のcommitを取り出します(action.yml)。pull_requestの場合、それはrefs/pull/番号/mergeです。
つまり、「mainにマージしたらこうなる」という仮のcommitを調べています。ブランチ単体では問題がなくても、マージ後に壊れる場合に気づけます。
(4) run:は何?
ランナーのシェルでコマンドを実行する書き方です。今回のコマンドはこれだけです。
npx --yes html-validate@11.18.0 public/
| 部分 | 意味 |
|---|---|
npx |
npmのパッケージを取ってきて実行する。package.jsonがなくても動く |
--yes |
インストールの確認に答える人がいないので、先に「はい」と渡す |
@11.18.0 |
バージョンを固定する。新版のルール追加で、ある日突然赤にならないように |
public/ |
調べるフォルダ。中の.htmlをすべて調べる |
プルリクエストで変更したファイルだけを調べるのではありません。実際、4章の(1)の赤は、プルリクエストで変更していないindex.htmlで出たものです。
今回は小さなページ1つなので、全部調べても問題ありません。規模が大きくなって時間がかかるようになったら、変更したファイルだけに絞ることも検討が必要かもしれません。
Node.jsを入れる手順はありません。GitHubのランナーには最初からNode.jsが入っています。
7. ほかの言語のチェックツール
今回はHTMLだけですが、言語が変わってもチェックの入れ方は同じです。run:に書くコマンドが変わるだけです。
| 対象 | ツール | チェックするもの |
|---|---|---|
| HTML | html-validate(今回使った) | 文法・書き方 |
| HTML | HTMLHint | 書き方 |
| HTML | lychee | リンク切れ |
| JavaScript / TypeScript | ESLint | 書き方 |
| TypeScript | tsc | 型 |
| Python | Ruff | 書き方 |
| Python | mypy | 型 |
| 共通 | Prettier | 整形 |
| 共通 | gitleaks | コードに混ざったパスワードなど |
こうしたツールには、今回のrecommendedのようにできあいのルールが用意されているものが多くあります。自分で作り込む必要があるのは、アプリの動きを確かめるテストの中身くらいです。
8. まとめ
プルリクエストの時点で間違いに気づけて、うっかりマージしてしまっても、公開中のページは守られます。この2つを実際に確かめられたので、今回の検証は成功だと思います。
今回分かったことを並べると、次の3つです。
- 赤と緑を決めるのは、コマンドの終了コード。Actionsは出力の文章を読んでいない
- ジョブは既定で並列に動く。
needs:を書くと、相手が失敗したときにスキップされる - 無料プランのPrivateリポジトリではマージは止められない。それでも、公開は
needs:で止められる
次は、プルリクエストを作るとAIがレビューしてくれるしくみを試す予定です。








