仕組み解説
PC を起動しなくても、体組成データが別サービスへ自動同期される仕組み
「GitHub Actions の cron」「実際に動くプログラム」「data ブランチでのデータ保存」を、図解つきで順に説明します。
0まず全体像:何が「PC の代わり」になったのか
やりたいことはシンプルで、体組成計側のクラウドにたまった計測値(体重・体脂肪など)を取得し、別のヘルスサービスへ日次で登録するという同期処理です。 これまでは 手元の PC が「タイマー・実行するプログラム・データ置き場」の3役をすべて担っていました。だから PC を消すと動きません。今回はこの3役を、24時間動いている GitHub のサーバー に肩代わりさせただけです。 やっていること(Python コード)は同じで、動く場所と、動かすタイマーが変わったのがポイントです。
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 JST、30 12(UTC) = 21:30 JST になります。
さらにプログラム内部の時刻計算がズレないよう、ジョブに TZ: Asia/Tokyo を設定して実行環境自体も日本時間に合わせています。
「動くたびに新品のPCが立ち上がる」=ランナー
cron の時刻になると、GitHub は ランナー(runner)と呼ばれるまっさらな仮想マシン(使い捨ての Linux PC)を1台起動します。 そこでレシピの手順を上から順に実行し、終わったらマシンごと破棄します。毎回まっさらなので速くて安全ですが、 「実行中に作ったファイルは、終了とともに消える」という重要な性質があります(これが④の data ブランチが必要な理由です)。
2どのプログラムが動く? リポジトリのコードとの関係
ここが一番の勘どころです。ワークフローファイル自体には、同期のロジックは1行も入っていません。
ワークフローは「手順書(レシピ)」にすぎず、実際の処理は リポジトリに置いてある Python コード
(src/synctool/…)をそのまま呼び出して実行しています。
つまりランナーは、あなたが PC のコマンドプロンプトで打っていたのとまったく同じコマンドを、GitHub のサーバー上で打っているだけです。
# ワークフローがランナー上で実行している中身(抜粋)
pip install -e . # リポジトリの Python パッケージを入れる
python -m synctool --fetch # 元サービスから体組成を取得して DB へ
python -m synctool --push # 未送信ぶんを連携先サービスへ登録
__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)が汚れないという利点もあります。
実行をまたいで状態が引き継がれる様子
下は「昨日の実行」と「今日の実行」で、DB が data ブランチ経由でバトンのように受け渡される様子です。
この 連続性 があるおかげで、--push は「まだ送っていないぶん」だけを正しく判定でき、二重登録が起きません。
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 で使えます。
まとめ(3行で)
- cron:GitHub のタイマーが 9:00 / 21:30 JST に、使い捨ての Linux を1台起動する。
- プログラム:起動したマシンは、リポジトリの Python(
python -m synctool …)をそのまま実行する。レシピ本体にロジックは無い。 - data ブランチ:マシンは消えるので、DB を GitHub 上のブランチに「取り出し→更新→戻す」して状態を永続化し、二重送信を防いでいる。戻す際は orphan コミット+force-push で先端を差し替えるため、履歴は積み上がらず常に最新の1コミットだけを保つ。
