GitHub Actions の活用

仕組み解説

PC を起動しなくても、体組成データが別サービスへ自動同期される仕組み

「GitHub Actions の cron」「実際に動くプログラム」「data ブランチでのデータ保存」を、図解つきで順に説明します。

0まず全体像:何が「PC の代わり」になったのか

やりたいことはシンプルで、体組成計側のクラウドにたまった計測値(体重・体脂肪など)を取得し、別のヘルスサービスへ日次で登録するという同期処理です。 これまでは 手元の PC が「タイマー・実行するプログラム・データ置き場」の3役をすべて担っていました。だから PC を消すと動きません。今回はこの3役を、24時間動いている GitHub のサーバー に肩代わりさせただけです。 やっていること(Python コード)は同じで、動く場所と、動かすタイマーが変わったのがポイントです。

以前(ローカル) 手元の PC ① タイマー(タスク  スケジューラ) ② Python プログラム ③ data.db 各クラウド サービス PC が起動している時だけ動く ✗ 消すと同期が止まる 今回(クラウド) GitHub(24時間稼働) ① cron タイマー 9:00 / 21:30 JST ② Actions ランナー Python を実行(使い捨て) ③ data ブランチ data.db を保存 各クラウド サービス ✓ PC の電源に関係なく動く ③ の役割が今回の肝(後述)
PC が担っていた「タイマー・実行・保存」の3役を、GitHub 側の cron・ランナー・data ブランチが引き継いだ。

1GitHub Actions の cron の仕組み

GitHub Actions とは、GitHub が用意している「リポジトリ上で自動的にコマンドを実行してくれるサービス」です。 「いつ・何をするか」を書いたレシピ(ワークフローファイル)をリポジトリに置いておくと、GitHub がそのとおりに実行してくれます。 今回のレシピは .github/workflows/daily-sync.yml の1枚です。

「いつ動くか」= cron(スケジュール)

ワークフローの先頭に、こう書いてあります。

on:
  schedule:
    - cron: "0 0 * * *"     # 毎日 09:00 JST
    - cron: "30 12 * * *"   # 毎日 21:30 JST
  workflow_dispatch: {}      # 手動でも実行できるようにする

cron(クーロン)とは「決まった時刻に処理を起動する」ための、昔から使われている時刻指定の書式です。 5つの数字で「分・時・日・月・曜日」を表します。

"30 12 * * *" = 「毎日・毎月・どの曜日でも、12 時 30 分に実行」。* は「すべて(毎回)」の意味です。

⏰ 時刻は UTC で書く点に注意。 GitHub のサーバーは世界標準時(UTC)で動くので、cron も UTC で指定します。 日本(JST)は UTC より 9時間進んでいます。だから 0 0(UTC) = 9:00 JST30 12(UTC) = 21:30 JST になります。 さらにプログラム内部の時刻計算がズレないよう、ジョブに TZ: Asia/Tokyo を設定して実行環境自体も日本時間に合わせています。

「動くたびに新品のPCが立ち上がる」=ランナー

cron の時刻になると、GitHub は ランナー(runner)と呼ばれるまっさらな仮想マシン(使い捨ての Linux PC)を1台起動します。 そこでレシピの手順を上から順に実行し、終わったらマシンごと破棄します。毎回まっさらなので速くて安全ですが、 「実行中に作ったファイルは、終了とともに消える」という重要な性質があります(これが④の data ブランチが必要な理由です)。

cron 時計 9:00 / 21:30 になったら… 使い捨てランナー(毎回新品の Linux) 起動 コード取得 Python 準備 同期処理を実行 データ取得→連携先へ送信 (あなたの Python) 結果を保存 data.db を data ブランチへ 破棄 マシン 消滅 ※「破棄」で消えるのは実行中のファイル。保存した data ブランチは GitHub 上に残る(④参照)。 ※ 混雑時は数分遅れることがあるが、取りこぼしは次回実行で吸収される設計。
cron が時刻を検知 → 新品マシンを起動 → 手順を実行 → 結果を保存 → マシンを破棄。これを1日2回くり返す。

2どのプログラムが動く? リポジトリのコードとの関係

ここが一番の勘どころです。ワークフローファイル自体には、同期のロジックは1行も入っていません。 ワークフローは「手順書(レシピ)」にすぎず、実際の処理は リポジトリに置いてある Python コードsrc/synctool/…)をそのまま呼び出して実行しています。

つまりランナーは、あなたが PC のコマンドプロンプトで打っていたのとまったく同じコマンドを、GitHub のサーバー上で打っているだけです。

# ワークフローがランナー上で実行している中身(抜粋)
pip install -e .              # リポジトリの Python パッケージを入れる
python -m synctool --fetch    # 元サービスから体組成を取得して DB へ
python -m synctool --push     # 未送信ぶんを連携先サービスへ登録
レシピ(手順書) .github/workflows/ daily-sync.yml 「いつ・何のコマンドを打つか」だけ python -m synctool … を呼ぶ リポジトリの Python コード(src/synctool/) __main__.py CLI 入口・引数解釈 sync.py source_client.py → 元サービス取得・DB保存 uploader.py target_client.py → 連携先へ登録 storage.py SQLite 読み書き (data.db) クラウドでは呼ばれない(リポジトリには在るが、この用途では使わない): dashboard.py (手元で見る画面) exporter.py (別ジョブ用の機能) …同じリポジトリに同居しているが、今回のコマンドが呼ぶものだけが動く。
ワークフローは入口 __main__.py を叩くだけ。あとはリポジトリ内の各モジュールが連携して動く。dashboard.py 等は同居しているが今回は使われない。

どのコマンドが「何を動かす/動かさない」か

要素役割クラウドで動く?
daily-sync.yml手順書(いつ・何のコマンドを打つか)これが司令塔
--fetch(取得)元サービスから体組成を取得し data.db に保存動く
--push(登録)未送信ぶんを連携先サービスへ登録動く
dashboard.py手元で数値をグラフ表示(Streamlit)使わない(あなたの PC で見る用)
exporter.py など別機能別ジョブ用の付随機能使わない(今回の目的外)

要するに 「リポジトリ全体がコピーされて実行される」のではなく、「レシピが名指しした1本のコマンドだけが実行される」と理解すると正確です。

3なぜ「data ブランチ」でデータを保存できるのか

②で触れたとおり、ランナーは実行が終わると丸ごと消えるため、実行中に更新した data.db もそのままでは消えてしまいます。 これでは「前回どこまで連携先に送ったか」が毎回わからなくなり、同じ計測値を何度も二重登録してしまいます。

解決の発想: 消えるのは「ランナーのファイル」。一方 Git のブランチは GitHub 上に永続的に残る。 ならば data.db を Git ブランチに置いて、実行のたびに「取り出す→更新→戻す」を繰り返せばよい —— これが data ブランチの役割です。

コードは master ブランチ、データ(data.db)は data ブランチ と、置き場所を分けています。 こうすると、毎日更新される巨大な DB ファイルで コードの履歴(master)が汚れないという利点もあります。

GitHub: data ブランチ data.db を永続保存 1回の実行(使い捨てランナー) ① 取り出す ② 更新 取得&送信 ③ 戻す restore(読み出し) commit(書き戻し)
各実行は data ブランチから data.db を 取り出し、更新し、コミットして戻す。ランナーが消えても最新の DB は GitHub 上に残る。

実行をまたいで状態が引き継がれる様子

下は「昨日の実行」と「今日の実行」で、DB が data ブランチ経由でバトンのように受け渡される様子です。 この 連続性 があるおかげで、--push は「まだ送っていないぶん」だけを正しく判定でき、二重登録が起きません。

data ブランチ (GitHub 上・永続) DB(〜昨日) DB(〜今朝) DB(〜今夜) 今朝 9:00 の実行 昨日ぶんを取り出し 新しい計測を追記 連携先へ送信 今夜 21:30 の実行 今朝ぶんを取り出し 差分だけ追記・送信 (送信済みは飛ばす) 「送信済みかどうか」の印(synced_at)も DB に入っているので、次の実行に正しく伝わる=二重送信を防ぐ。
DB が実行から実行へ受け渡されることで「どこまで送ったか」が保たれる。だから重複なく、取りこぼしも次回で回収できる。

data ブランチへの「戻し方」(技術メモ)

戻す処理は、作業ツリーを切り替えずに data.db 1ファイルだけを載せたコミットを作って data ブランチへ push しています (git の低レベル命令 hash-object → mktree → commit-tree → push)。コードの履歴(master)には一切影響しません。

このコミットは 親を持たない「orphan(オーファン)コミット」として作り、--force で data ブランチの先端ごと差し替えています。 こうすると、1日2回の実行を何度くり返しても data ブランチは常に「最新の data.db が入った 1 コミットだけ」になり、 実行のたびにコミットが積み上がって .git が肥大化するのを防げます(DB はバイナリで差分圧縮が効きにくいため、履歴を溜めると特に膨らみやすい)。 差し替えで参照されなくなった古いコミットは、GitHub 側の GC でいずれ回収されます。実行は concurrency で直列化しているので、force-push が競合して状態を壊すこともありません。

data ブランチは触らなくてOK。 これは人が見るためのものではなく、 プログラムが「前回の続き」を思い出すための内部の保存領域です。過去の履歴を残す必要がないので、毎回まるごと置き換えて最新の1個だけを保っています。閲覧用ダッシュボードは従来どおり手元の PC で使えます。

旧:毎回コミットを積む(履歴が伸び続ける) DB① DB② DB③ DB④ ✗ 実行のたび +1 コミット。DB(バイナリ)が丸ごと溜まり .git が肥大化。 新:orphan+force-push(常に1コミット) 旧コミット (参照切れ→GC) 置き換え data.db 親なし・単一コミット ✓ 先端を毎回まるごと差し替え。履歴は積まず、常に最新DB 1個だけ。
orphan コミット(親を持たないコミット)で先端を置き換えるため、実行を重ねても data ブランチは常に1コミット。過去分は参照されなくなり GitHub 側で回収される。

まとめ(3行で)

  1. cron:GitHub のタイマーが 9:00 / 21:30 JST に、使い捨ての Linux を1台起動する。
  2. プログラム:起動したマシンは、リポジトリの Python(python -m synctool …)をそのまま実行する。レシピ本体にロジックは無い。
  3. data ブランチ:マシンは消えるので、DB を GitHub 上のブランチに「取り出し→更新→戻す」して状態を永続化し、二重送信を防いでいる。戻す際は orphan コミット+force-push で先端を差し替えるため、履歴は積み上がらず常に最新の1コミットだけを保つ。
サーバーレスな cron で SaaS 間データ同期を自動化する — GitHub Actions 実践メモ
タイトルとURLをコピーしました