0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub README のヒーロー画像を毎日勝手に着せ替える GitHub Actions レシピ

0
Posted at

image.png

みなさんは自分が管理している GitHub リポジトリ直下の README.md には何を書かれていますか?
プログラムの概要や、使い方、ディレクトリ構成などを記載する人がほとんどだと思います。

一方で、私はリポジトリの一番上にヒーロー画像を置くことが多いです。このヒーロー画像はリポジトリ名のロゴだったり、ロゴに何かしらの意匠を加えたものを置くことが多いです。

作業をする場合に、リポジトリは 1 日に何度も見る画面です。そのため自分にとってヒーロー画像は自分の PC の壁紙にも似た感覚です。

しかしこのヒーロー画像がいつも同じだとなんか飽きるなと思いませんか。そこで自分はこの記事で GitHub Actions を使って、このヒーロー画像を自動で切り替える方法を記述します。

サンプル github リポジトリ

なぜ GitHub Actions なのか?

GitHub リポジトリのページは JS が動かないため、動的に画像を取り替えることが不可能です。他の方法として README.md から画像を取り出す API を外に設けてヒーロー画像のローテーションを実装することも可能ですが、あまりにもオーバースペックです。

そこで GitHub Actions を利用することで、比較的簡単に、ヒーロー画像を用意する以外は他に何も用意せず実装できてしまいます。

完成イメージ

サンプルリポジトリ hisashi-ito/readme-hero-rota を fork して assets/heroes/ に好きな画像を投入すれば、毎朝 README のトップが切り替わることを実感できます。

すぐ動かしたい

説明より先に動かしたい方向け、5 ステップ。基本はブラウザの GitHub Web UI だけで完結します(画像投入だけローカル CLI と選択可)。

1. fork する [Web UI]

サンプルリポジトリ を開き、ページ右上の Fork ボタン → 自分のアカウントにコピー。

2. ヒーロー画像を投入する [Web UI または ローカル]

fork した自分のリポジトリで assets/heroes/ ディレクトリを開いて、サンプル画像(hero_1.png 等)を削除し、自分の画像をアップロード。

  • Web UI でやる場合: assets/heroes/ 内で Add file → Upload files にドラッグ&ドロップ → Commit changes
  • ローカルでやる場合: git clone https://github.com/<自分>/readme-hero-rota.git → ファイル差し替え → git push

対応拡張子: .png / .jpg / .jpeg / .webp / .gif。ファイル名は自由。

3. workflow に書き込み権限を与える [Web UI]

自分の fork で Settings タブ → 左サイドバー Actions → General → ページ下部の Workflow permissions まで降りる → Read and write permissions を選択 → Save。

これを忘れると workflow は動くが最後の git push で 403 になります。

4. 手動で 1 回キックして動作確認 [Web UI]

Actions タブ → 左サイドバー Rotate README hero → 画面右の Run workflow → Branch: main のまま緑の Run workflow ボタンで実行。

数秒〜十数秒で runs リストに新ジョブが追加され、緑のチェックが付けば成功。README を再読込するとヒーローが切り替わっているはずです。

5. 翌朝、自動切替を確認 [自動 / 操作なし]

JST 07:00 (UTC 22:00) ごろに cron が発火し、自動で新ヒーローに切り替わります。GHA cron の遅延で実際の切替は JST 07:00〜08:00 程度の幅で起きます。

仕組み・設計・cron スロット選定・注意点は以降のセクションで解説します。
さらに踏み込んだ手順は docs/setup.mddocs/troubleshooting.md を参照。

副作用

GitHub Actions が特定の時間に commit / push するため、commit log の中に本処理のログが入り、履歴が見にくくなるという欠点はありますので、ご留意の上ご利用ください。

アーキテクチャ

全体像(ディレクトリ構成と動作フロー)

your-repo/
├── README.md                              ← <!-- HERO_START --> マーカーを置く
├── .github/workflows/
│   └── rotate-hero.yml                    ← cron で動く workflow(下に全文)
└── assets/heroes/                         ← ヒーロー画像プール
    ├── hero_1.png
    ├── hero_2.png
    └── hero_3.png ...

cron 発火時に workflow がやること:

  1. リポジトリを checkout
  2. assets/heroes/ から「現在の画像以外」をランダム選択
  3. README.md の HERO ブロック内の <img src> だけを差し替え
  4. github-actions[bot] で commit + push

重要な設計上のポイント

観点 採用 理由
マーカー方式 <!-- HERO_START --> ... <!-- HERO_END --> HTML/Markdown 両対応、<img> 以外の要素(<p> 等)も挟める
src だけ swap widthalt 等の他属性は保持 デザインを workflow に任せず手動で制御できる
現在の画像を除外して random 同じ画像が連日来ない UX 改善、地味に効く
stdlib only 追加 install なし 起動が速い、依存リスクなし

README 側の HERO ブロック

<!-- HERO_START -->
<p align="center">
  <img src="./assets/heroes/hero_1.png" width="80%">
</p>
<!-- HERO_END -->

<p align="center"> ラッパや width="80%" は workflow が触らないので、デザインは思いのまま。


workflow 全文(コピペ可)

.github/workflows/rotate-hero.yml:

name: Rotate README hero

on:
  schedule:
    # GitHub Actions cron は UTC。UTC 22:00 = JST 07:00。
    - cron: "0 22 * * *"
  workflow_dispatch:

permissions:
  contents: write

env:
  # 2026-06-16 以降 Node.js 24 がデフォルトになるので明示 opt-in。
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

jobs:
  rotate:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v5

      - name: Rotate hero image
        run: |
          python3 <<'PY'
          from pathlib import Path
          import random
          import re

          readme = Path("README.md")
          hero_dir = Path("assets/heroes")

          if not readme.exists():
              raise SystemExit("README.md not found")
          if not hero_dir.exists():
              raise SystemExit(f"Hero directory not found: {hero_dir}")

          images = sorted([
              p for p in hero_dir.iterdir()
              if p.is_file() and p.suffix.lower() in [".png", ".jpg", ".jpeg", ".webp", ".gif"]
          ])
          if not images:
              raise SystemExit("No hero images found in assets/heroes")

          text = readme.read_text(encoding="utf-8")
          block_re = re.compile(r"<!-- HERO_START -->.*?<!-- HERO_END -->", re.S)
          m = block_re.search(text)
          if not m:
              raise SystemExit("HERO_START / HERO_END block not found")

          sm = re.search(r'<img\s+[^>]*src="([^"]+)"', m.group(0))
          current_src = sm.group(1) if sm else None

          candidates = images
          if current_src:
              without_current = [
                  p for p in images
                  if f"./{p.as_posix()}" != current_src and p.as_posix() != current_src
              ]
              if without_current:
                  candidates = without_current

          selected = random.choice(candidates)
          image_path = f"./{selected.as_posix()}"

          new_block = re.sub(
              r'(<img\s+[^>]*src=")([^"]+)(")',
              lambda mm: mm.group(1) + image_path + mm.group(3),
              m.group(0), count=1,
          )
          text = block_re.sub(lambda _: new_block, text, count=1)
          readme.write_text(text, encoding="utf-8")
          print(f"Selected: {image_path}")
          PY

      - name: Commit and push changes
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          if git diff --quiet; then
            echo "No changes"
            exit 0
          fi
          git add README.md
          git commit -m "Rotate README hero image [skip ci]"
          git push

ロジックは Python で 40 行、workflow yaml と合わせても 90 行未満。


cron 発火時刻の選び方

GitHub Actions の cron は公式に "best-effort" とされています。docs にも「混雑時間帯で遅延しうる」と書かれていますが、具体的にどれくらい遅延するかは書かれていません。

コミュニティで広く言われている目安は、

  • 通常(off-peak): 数分〜十数分
  • 混雑時間帯(米国・欧州が両方アクティブな UTC 14:00–19:00 帯): 30 分〜1 時間超
  • 最悪ケース: 数時間 / その回はスキップ

くらい。UTC slot 選定で体感が大きく変わります。混雑しにくいスロットを選ぶための考え方を整理します。

スロット選定の考え方

UTC slot 現地時間(対応) グローバル状況
14:00–19:00 EDT 10:00–15:00 / PDT 07:00–12:00 / CET 16:00–21:00 米国 + 欧州が両方アクティブ(混雑ゾーン)
22:00–08:00 EDT 18:00–04:00 / PDT 15:00–01:00 / CET 00:00–10:00 米国 EOD → 欧州 起床までの隙間(穏やか)

なぜ peak time の UTC が悪いのか

GHA の scheduled workflow 実行キューは全 GitHub で共有されているので、米国・欧州の業務時間中はキューが詰まります。

UTC 時刻に対する米国(PDT/EDT)と欧州(CET)の業務時間オーバーラップ

  • 下軸 UTC: cron に書く値そのもの(cron: "0 22 * * *"22 がここ)
  • 上軸 JST: 日本時間で読み解く用(深夜帯に走る cron を一目で判断できる)
  • 青バー: 各地域の業務時間(09:00–17:00 ローカル)を UTC に展開
  • 赤帯: UTC 14:00–19:00(JST 23:00–04:00)— 米国朝〜昼 + 欧州夕方が重なる混雑ゾーン
  • 緑帯: UTC 22:00–08:00(JST 07:00–17:00)— 米国 EOD 後 + 欧州 就寝の穏やかなウィンドウ

:00 より :30 を選ぶ

「毎時0分」に設定されている cron が世界中で大量にあるため、:00 マークはさらに混雑します。:30 の方が空いている傾向。

スケジュール変更直後の罠

workflow を変更した直後の最初の scheduled fire はスキップされる確率が高い

GitHub のスケジューラが新しい cron 設定を認識するのに 10〜15 分のラグがある模様。push 後すぐの slot は信用せず、手動 dispatch で先に動作確認を。

推奨スロット

あなたの目的 推奨 UTC slot 日本時間
「朝起きたら新ヒーローに変わってる」体験 0 22 * * * JST 07:00
「とにかく遅延少なく」 30 6 * * * JST 15:30(欧米共に静か)
深夜帯ロマン重視 30 17 * * * JST 02:30(混雑時間帯なので遅延しがち)

本サンプルリポジトリはデフォルト UTC 22:00 を採用しています。


Node.js 24 移行への対応(2026-06-16)

GitHub の runner は 2026-06-16 から Node.js 24 がデフォルトになります。actions/checkout@v4 等の Node.js 20 ベースの action を使い続けると将来壊れます。

二段の対策:

  1. actions/checkout@v5 を使う(Node.js 24 native)
  2. FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" を workflow env に追加(他の Node.js 20 action にも opt-in 効果)
env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

これでデフォルト切替の前に "もう Node.js 24 で動いてる" 状態にできます。


必須セットアップ: workflow permissions を切り替える

GitHub Actions のデフォルト権限は read-only です。本テンプレのように workflow が git push する場合、リポジトリの Actions 設定で書き込み権限を一度有効化する必要があります(これを忘れると workflow は走るが最後の push で 403 になります)。

手順

  1. リポジトリの Settings タブを開く
  2. 左サイドバーで Actions → General をクリック
  3. ページ下部 Workflow permissions セクションまでスクロール
  4. Read and write permissions のラジオボタンに切替
  5. Save ボタンで保存

これで permissions: contents: write を要求する workflow が git push できるようになります。

補足

  • 「Allow GitHub Actions to create and approve pull requests」は OFF のままで問題ありません(本テンプレは PR を作らず直接 commit します)
  • yaml の permissions: contents: write 指定は「リポジトリ側の上限の範囲内で要求できる」ものです。リポジトリ設定が read-only のままだと、yaml に何を書いても push は通りません(yaml は下限のみ調整可)
  • この設定は fork 元 / fork 先で独立。fork した側でも同じ操作が必要です

設定し忘れた時の症状

workflow log の最後にこれが出ます:

remote: Permission to <owner>/<repo>.git denied to github-actions[bot]
fatal: unable to access 'https://github.com/<owner>/<repo>.git/': The requested URL returned error: 403

→ 上の手順で write 権限を ON にして、Actions タブから手動 re-run。


画面から手動でトリガーする

workflow に workflow_dispatch: を入れているおかげで、cron 発火を待たず GitHub のブラウザ画面から即時実行できます。動作確認、cron がスキップされた朝のリカバリ、「今すぐ違うヒーローを見たい」気分の時、いずれも 1 クリックで OK。

手順

  1. リポジトリの上部ナビから Actions タブを開く
  2. 左サイドバーのワークフロー一覧から Rotate README hero を選択
  3. ワークフロー画面右側に "This workflow has a workflow_dispatch event trigger" のバナーが出ているはずなので、その横の Run workflow ボタンを押す
  4. ドロップダウンで Branch: main のまま、緑色の Run workflow ボタンで実行
  5. 数秒〜十数秒で runs リストに新しいジョブが追加され、緑のチェックが付けば完了
  6. README を再読込すると新しいヒーローに切り替わっている

こんな時に使う

シーン 使い方
fork 直後の動作確認 cron 待ちせず即実行して、commit & push が通るか確認
画像プール入れ替え後 プールを新調したら手動で 1 回回して切替の見え方を確認
cron がスキップされた朝 UTC 22:00 が混んでスキップされたら、起きてから手動で回す
別ブランチでテスト Run workflow ダイアログで Branch を選べる(workflow_dispatch の標準機能)

Run workflow ボタンが出ない場合

workflow_dispatch: が yaml に書かれていない、あるいは workflow ファイルがまだ default branch に push されていないと、ボタンは表示されません。

  • workflow yaml に workflow_dispatch: が入っているか確認
  • そのファイルが main(default branch)に push されているか確認(他ブランチに居るだけだと UI に出ない)

anti-test-loop ガード — 同じリポジトリの test job をムダに回さない

bot commit が走るたびにテストが回ると CI 時間がもったいない。二段防御で:

1. commit message に [skip ci]

ほとんどの CI サービス(GHA 自身も)は [skip ci] を含む commit を見ると自動的に build をスキップします。本 workflow の commit message は:

Rotate README hero image [skip ci]

2. 他 workflow に paths-ignore

[skip ci] を尊重しないテストランナーや、念のための保険として、test workflow 側で paths-ignore:

on:
  push:
    paths-ignore:
      - "README.md"
      - "assets/heroes/**"
      - ".github/workflows/rotate-hero.yml"

これで [skip ci] を忘れても、ヒーロー rotation の commit では test が走らなくなります。


設定上の注意点

権限まわり(→「必須セットアップ」)と schedule 変更直後の罠(→「cron 発火時刻の選び方」)は本文で扱ったので、ここでは残りの 3 つだけ。

1. 60 日 inactive でスケジュール停止

GHA は 60 日以上 commit がないリポジトリの scheduled workflow を勝手に止めます。動かなくなったと思ったら、リポジトリに commit を 1 個入れて再起動が必要。

2. プールが 1 枚だと回転しない

「現在の画像を除外して random pick」設計なので、画像が 1 枚しかないと候補ゼロでフォールバック → 結局同じ画像 → git diff --quiet で no-op、ということになります。最低でも 5〜10 枚を推奨。

3. HERO ブロックは 1 個だけ

複数 region で別々に rotate したい場合は、workflow の Python ロジックを分岐させてください。本テンプレは最初の HERO ブロックだけ swap します。


参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?