【第4回】GitHub Actions × Python 自動化入門 — ワークフロー設計のベストプラクティスとよくあるハマりどころ

プログラミング

前回はスケジュール実行とSlack通知の組み合わせを紹介しました。今回は「動くワークフローを作れるようになってきたけど、なんとなく設計が雑なまま運用してる気がする…」という状態から一歩進むための話をまとめます。

正直、自分もまだ試行錯誤中なんですが、ここ数ヶ月で「これは最初から知っておきたかった」と感じたことを中心に書いていきます。

この記事でわかること

  • Pythonパッケージインストールのキャッシュ設定方法(pipとuv)
  • permissionsの設定で権限を最小化するやり方
  • YAMLインデント、トリガー発火、Secretsなどよくあるハマりどころ
  • concurrencyで重複実行を防ぐ方法
  • 環境ごとのワークフロー分割設計

キャッシュをちゃんと使うと世界が変わる

Pythonのワークフローでいちばん時間を食うのって、依存パッケージのインストールなんですよね。毎回 pip install -r requirements.txt を走らせてたころ、ジョブが終わるまで2〜3分待つのが普通でした。

actions/cache を使うとこれをかなり短縮できます。キャッシュキーに requirements.txt のハッシュを含めておくと、ファイルが変わったときにキーが変わるので、結果的にキャッシュが作り直される仕組みになります。

- name: pip キャッシュ
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
    restore-keys: |
      ${{ runner.os }}-pip-

最近は uv を使うとさらに速くなることがあります。uvの公式ドキュメントでも、GitHub Actions向けに astral-sh/setup-uv が official action として案内されていて、キャッシュ設定もオプションで内包できるので設定が楽です。

- uses: astral-sh/setup-uv@v5
  with:
    enable-cache: true

- run: uv sync

uvはpipより依存解決がかなり速いケースがあるようなので、試してみる価値はあると思います。ただ、既存プロジェクトへの移行コストはゼロじゃないので、新規プロジェクトから使い始めるのが現実的かなと。

permissionsの設定、最初から意識するべきだった

これは自分がかなり長いこと見落としてたやつです。

GitHub Actionsのデフォルト権限はリポジトリ設定(デフォルト)に依存するので、セキュリティを重視するなら read-only をベースにして、必要なものだけ書き込み権限を付与する 設計にした方がいいです。

たとえば、スクリプトが自動でPRを作ったりコミットをプッシュするワークフローを書くとき、権限が足りずに Resource not accessible by integration で詰まることがあります。これ、リポジトリ全体の設定を緩めるんじゃなくて、ワークフローのYAMLで明示的に指定するのが正しい対処です。

name: Auto Commit

on:
  schedule:
    - cron: '0 9 * * 1'

permissions:
  contents: write   # コミット・プッシュに必要
  pull-requests: write  # PR作成が必要なら

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Python スクリプト実行
        run: python scripts/update_data.py
      - name: 変更をコミット
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add .
          git diff --cached --quiet || git commit -m "auto: データ更新"
          git push

git diff --cached --quiet || git commit -m "..." の部分、変更がなかったときにコミットコマンドが失敗するのを防ぐためのワンライナーです。ここ結構ハマるポイントなので書き留めておきます。

よくあるハマりどころ集

シリーズを通じて自分がぶつかったり、調べていて「あ〜これ引っかかりそう」と思ったパターンをまとめます。

① YAMLのインデントミスで動かない

GitHub ActionsのYAMLはインデントのズレに容赦がないです。エラーメッセージが「Invalid workflow file」とざっくりしてることも多くて、どこが悪いのか一瞬わからない。

Cursorだと構文エラーを拾ってくれることが多いようですが、念のため yamllint.com のようなオンラインツールで事前確認するのが確実です。特にマルチラインの run| を使うときは要注意。

② トリガーが思ったように発火しない

push トリガーで branches を指定しているのに、指定ブランチにプッシュしても動かない……というパターンがあります。よくある原因はこのあたりです。

  • branches のパターン指定が間違っている(例: mainmaster の混在)
  • paths フィルターを使っているのに、変更ファイルがパターンに一致していない
  • workflow_dispatch を追加するのを忘れて、手動トリガーでテストできない

開発中は workflow_dispatch を必ず入れておくと、任意のタイミングで手動実行できて楽です。

on:
  push:
    branches: [main]
  workflow_dispatch:  # これを入れておくと開発が楽

③ Secretsが空になってスクリプトがエラー

フォークされたリポジトリからのPRでは、Secretsはランナーに渡されません。これはセキュリティ上の仕様なので意図的なものですが、「自分のリポジトリなのになぜ?」と焦ることがある。

また、Secretsの名前はそのままログに出ないようにマスキングされますが、Secretsを加工した値(Base64エンコードした文字列など)はマスキングされない場合があるようです。ログへの出力には注意が必要です。

④ スクリプトが途中で失敗してもワークフローが成功扱いになる

Pythonスクリプト内で例外をキャッチして握りつぶしていたり、subprocess の戻り値を確認していないと、スクリプトが実質失敗していてもワークフローのジョブが success になります。

Pythonスクリプト側で sys.exit(1) を使って明示的に終了コードを返す習慣をつけると安心です。

import sys

def main():
    try:
        # メイン処理
        process()
    except Exception as e:
        print(f"エラー: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()

concurrencyで重複実行を防ぐ

これ、知らないと地味に困ります。

たとえばプッシュのたびにワークフローが動く設定にしていると、短時間に複数回プッシュしたとき、前のジョブが終わる前に次のジョブが走り始めます。データを更新するスクリプトだと競合が起きたり、外部APIのレート制限に引っかかったりします。

concurrency を設定すると、同じグループ内でワークフローが重複して走るのを防げます。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true  # 古いジョブをキャンセルして新しいものを優先

cancel-in-progress: true にすると、新しいジョブが来たときに古い実行中のジョブをキャンセルします。デプロイ系のジョブには向いてないこともあるので、用途によって使い分けるといいです。

環境ごとにワークフローを分けるか、一本にまとめるか

これは正直まだ自分の中でも答えが出てないんですが、経験上こういう感じかなと思っています。

  • テスト・リント → 単独のワークフローファイルに切り出す(頻度が高いので軽く保ちたい)
  • デプロイ → 別ファイルに分けて、テストワークフローの成功を条件にする
  • 定期バッチ → 完全に独立したファイルにする

フェイルファスト・ムダの最小化・セキュリティ・メンテナンス性といった観点を設計時に意識しておくと、長期運用で効いてきます。全部を1ファイルに詰め込むとYAMLが肥大化して読めなくなっていきます。「1ファイル1つの目的」くらいの粒度にしておくと、後から見たときに何をしているワークフローなのか判断しやすいです。

余談ですが、ワークフローファイルが増えてきたら .github/workflows/ の中を整理する機会を作るといいと思います。半年後に見ても意味がわかるファイル名をつけておくと本当に助かります。

このシリーズで作ったものの全体像

全4回を通じて、こんなワークフローを作ってきました。

  • 第1回:GitHub Actionsの基本構造とPythonスクリプトの実行
  • 第2回:テスト・リントの自動化(pytest, flake8)
  • 第3回:スケジュール実行とSlack通知
  • 第4回(今回):設計のベストプラクティスとハマりどころの整理

シリーズを通じて感じたのは、GitHub Actionsは「始めること」のハードルが低い反面、ちゃんと設計しないと後でじわじわ苦労するツールだなということ。特にpermissionsとキャッシュは最初から意識しておくと後が楽です。

※この記事にはプロモーションが含まれます

ちなみに、お名前.com レンタルサーバー(WordPressに特化した高速レンタルサーバー。月額990円〜、独自ドメイン実質0円)も気になっています。お名前.com レンタルサーバー

まとめ

GitHub Actions × Pythonの実践的な運用で大切なのは、以下の5つです。

  • キャッシュ設定:pipやuvで依存パッケージをキャッシュしてビルド時間を削減
  • permissions管理:デフォルトread-onlyで、必要な権限だけを付与
  • YAMLバリデーション:インデントエラーは事前チェックツールで防止
  • トリガー確認:workflow_dispatchを入れて手動実行でテスト
  • concurrency設定:重複実行を防いでレート制限や競合を回避

「ちゃんと動く」から「ちゃんと設計された」ワークフローへ進むのは、こういった細かい配慮の積み重ねです。自分も完璧ではありませんが、振り返りながら少しずつ改善していく感じで進めています。

📚 シリーズ「GitHub Actions × Python 自動化入門」(第4回 / 全4回)

← 前回の記事: 前回の記事はこちら

🎉 このシリーズは今回で完結です!

参考になったらクリックしてもらえると嬉しいです!

Blogmura ProgrammingProgramming Ranking
タイトルとURLをコピーしました