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 Actionsの定期実行が9時間ずれる5つの落とし穴|timezone指定とUTC換算でJSTに合わせる

0
Posted at

TL;DR

  • timezone を指定しない schedule のcronはUTCで評価されます。JSTの平日9時のつもりで 0 9 * * 1-5 と書くと、UTCの9時として評価され、評価器の結果ではJST 18時の発火時刻になります。
  • timezone: "Asia/Tokyo" を指定するか、時刻と曜日の両方をUTCに換算します。JST平日6時をUTCで表す場合は、曜日も1つ戻した 0 21 * * 0-4 です。
  • 正時を避ける公式案内とYAMLの読み込みにも注意します。換算後のUTC cronと Asia/Tokyo 相当の評価は、2026年の288通りで発火時刻の集合が一致しました。

日次で動くAIエージェントを、GitHub Actionsの schedule で定期実行する場面を考えます。「平日の朝に動かす」と決めても、cronの時刻をJSTのつもりで書くと、指定した時刻とは異なる時刻で評価されます。早朝の実行では、UTCに換算したときの曜日も確認が必要です。

この記事では、JSTの時刻を指定するときに押さえたい5つの落とし穴を、再現、原因、直し方、確認ログの順に見ていきます。発火時刻の確認には、公式ドキュメントの構文(5フィールドと演算子 * , - /)に沿って自作したcron評価器を使います。この記事の発火時刻はすべて、この評価器で計算した結果です。

実行環境

Python 3.13.5、PyYAML 6.0.3、Linuxを使用しました。cronの構文はGitHubの公式ドキュメントに沿って、手元の評価器で発火時刻を計算しました。

以下は、公式のcron構文に沿って自作した評価器です。演算子は *、,、-、/ のみ、曜日は 0〜6 を扱います。夏時間の繰り下げは扱いません(日本は夏時間がないためです)。

cron_next.py(全文)
cron_next.py
from datetime import datetime, time, timedelta, timezone
from zoneinfo import ZoneInfo


def _parse_field(field: str, minimum: int, maximum: int) -> set[int]:
    values: set[int] = set()

    def number(text: str) -> int:
        if not text.isascii() or not text.isdigit():
            raise ValueError(f"不正な数値: {text!r}")
        value = int(text)
        if not minimum <= value <= maximum:
            raise ValueError(f"範囲外の値: {value}")
        return value

    for part in field.split(","):
        if not part:
            raise ValueError("空のフィールド要素です")

        pieces = part.split("/")
        if len(pieces) > 2:
            raise ValueError(f"不正な間隔: {part!r}")

        base = pieces[0]
        step = 1
        if len(pieces) == 2:
            if not pieces[1].isascii() or not pieces[1].isdigit():
                raise ValueError(f"不正な間隔: {part!r}")
            step = int(pieces[1])
            if step < 1:
                raise ValueError("間隔は1以上で指定してください")

        if base == "*":
            first, last = minimum, maximum
        elif "-" in base:
            bounds = base.split("-")
            if len(bounds) != 2:
                raise ValueError(f"不正な範囲: {base!r}")
            first, last = number(bounds[0]), number(bounds[1])
            if first > last:
                raise ValueError(f"逆順の範囲: {base!r}")
        else:
            first = number(base)
            last = maximum if len(pieces) == 2 else first

        values.update(range(first, last + 1, step))

    return values


def fires(
    cron: str, tz: str, start: datetime, end: datetime
) -> list[datetime]:
    fields = cron.split()
    if len(fields) != 5:
        raise ValueError("cronは5フィールドで指定してください")
    if fields[2] != "*" and fields[4] != "*":
        raise ValueError("日と曜日の両方を制限するcronは扱えません")
    if (
        start.tzinfo is None
        or start.utcoffset() is None
        or end.tzinfo is None
        or end.utcoffset() is None
    ):
        raise ValueError("開始時刻と終了時刻にはタイムゾーンが必要です")

    start_utc = start.astimezone(timezone.utc)
    end_utc = end.astimezone(timezone.utc)
    if start_utc > end_utc:
        raise ValueError("開始時刻は終了時刻以前にしてください")
    if start_utc == end_utc:
        return []

    minutes = _parse_field(fields[0], 0, 59)
    hours = _parse_field(fields[1], 0, 23)
    days = _parse_field(fields[2], 1, 31)
    months = _parse_field(fields[3], 1, 12)
    weekdays = _parse_field(fields[4], 0, 6)
    zone = ZoneInfo(tz)
    clock_times = [(hour, minute) for hour in sorted(hours) for minute in sorted(minutes)]

    # 時差の変化で端の日付を取りこぼさないよう、前後の日も調べる。
    day = start_utc.astimezone(zone).date() - timedelta(days=1)
    last_day = end_utc.astimezone(zone).date() + timedelta(days=1)
    instants: dict[datetime, datetime] = {}

    while day <= last_day:
        cron_weekday = (day.weekday() + 1) % 7
        if day.month in months and day.day in days and cron_weekday in weekdays:
            for hour, minute in clock_times:
                wall = datetime.combine(day, time(hour, minute))
                for fold in (0, 1):
                    local = wall.replace(tzinfo=zone, fold=fold)
                    instant = local.astimezone(timezone.utc)
                    # 往復して壁時計が変わる時刻は、夏時間の切り替えで存在しない。
                    restored = instant.astimezone(zone)
                    if restored.replace(tzinfo=None) != wall:
                        continue
                    if start_utc <= instant < end_utc:
                        instants[instant] = restored
        day += timedelta(days=1)

    return [instants[instant] for instant in sorted(instants)]

この評価器を使い、cronとタイムゾーンから発火時刻を計算します。後半では、Asia/Tokyo 相当の評価結果と、UTCに換算したcronの評価結果を比較します。

全体像:JSTの指定から発火時刻まで

on.schedule のcronは、分、時、日、月、曜日の5フィールド形式です。timezone を省略した場合はUTCで評価されます。2026年3月19日(GitHub Changelog)から、各scheduleエントリに timezone を指定でき、指定したタイムゾーンの壁時計の時刻でcronを評価できます。

曜日は 0〜6 または SUN〜SAT で指定し、日曜が 0 です。使える演算子は *、,、-、/ です。@daily のような省略記法は使えず、最短間隔は5分です。

落とし穴1:JSTのつもりで書いたcronが9時間ずれる

再現

平日9時に実行するつもりで、cronを 0 9 * * 1-5 と書きます。5フィールドのうち、分は 0、時は 9、曜日は月曜から金曜を表す 1-5 です。

JSTの予定として読むと「平日9時」ですが、timezone を指定しなければUTCの9時として評価されます。評価器が返す発火時刻をJSTで見ると18時です。朝に動かしたかったエージェントが、同じ日の夕方に動く指定になっています。

次のスクリプトは、JSTのつもりで書いたcronと、JST平日9時に対応するUTC cronの発火時刻を比較します。

case1_misread.py
from datetime import datetime
from zoneinfo import ZoneInfo

from cron_next import fires


def show(label: str, cron: str) -> None:
    jst = ZoneInfo("Asia/Tokyo")
    start = datetime(2026, 11, 2, tzinfo=jst)
    end = datetime(2026, 11, 9, tzinfo=jst)

    print(label)
    for instant in fires(cron, "UTC", start, end)[:3]:
        print(instant.astimezone(jst).strftime("%Y-%m-%d %H:%M JST (%a)"))


if __name__ == "__main__":
    show("JSTのつもりで指定したcronをUTCで評価:", "0 9 * * 1-5")
    print()
    show("JST平日09:00に対応するUTCのcron:", "0 0 * * 1-5")

両方のcronをUTCで評価し、結果をJSTで表示することで、時刻のずれと換算後の時刻を確認します。

原因

原因は、cronの 9 がJSTの9時ではなく、UTCの9時として評価されることです。timezone を省略したときはUTCが既定なので、利用者が想定したJSTの時刻はcron文字列だけでは伝わりません。

曜日の 1-5 は平日を表していますが、時刻の評価に使われるタイムゾーンも合わせて確認する必要があります。この例では9時間の差がそのまま表示され、JSTの18時になります。

直し方

直し方は2つあります。

  1. scheduleエントリに timezone: "Asia/Tokyo" を指定し、cronを 0 9 * * 1-5 のまま使います。Asia/Tokyo の壁時計では平日9時です。
  2. timezone を省略する場合は、JST平日9時をUTCに換算し、0 0 * * 1-5 とします。

ワークフローの on は次のように書きます。

on:
  schedule:
    # 方法1: timezone を指定する(JSTの壁時計で平日9時)
    - cron: '0 9 * * 1-5'
      timezone: "Asia/Tokyo"
    # 方法2: timezone を省略してUTCに換算する(JST平日9時 = UTC平日0時)
    # - cron: '0 0 * * 1-5'

どちらの場合も、まず「何曜日の何時をJSTで期待するか」を決めてから指定を確認します。cron_next.py の fires("0 9 * * 1-5", "Asia/Tokyo", ...) による評価が、換算後のUTC cronによる評価と一致することは、落とし穴5の verify_roundtrip.py で確認します。

確認ログ

次のログでは、JSTのつもりで指定した 0 9 * * 1-5 が18時、換算後のUTC cronが9時として表示されます。

$ python3 case1_misread.py
JSTのつもりで指定したcronをUTCで評価:
2026-11-02 18:00 JST (Mon)
2026-11-03 18:00 JST (Tue)
2026-11-04 18:00 JST (Wed)

JST平日09:00に対応するUTCのcron:
2026-11-02 09:00 JST (Mon)
2026-11-03 09:00 JST (Tue)
2026-11-04 09:00 JST (Wed)

2026年11月2日から4日までの3件はいずれも、換算前の指定が18時、換算後の指定が9時です。同じ「9」という数字でも、UTCの時刻として評価されるか、JSTの壁時計の時刻として評価されるかで結果が変わります。

落とし穴2:時刻だけ直して曜日を直さない

再現

JSTの平日6時をUTCに換算すると、時刻は前日の21時です。ここで時刻だけを直し、曜日を平日の 1-5 のままにして 0 21 * * 1-5 とすると、評価器の結果では、JSTの火曜から土曜の6時が発火時刻になり、期待する月曜6時は含まれません。

月曜6時のJSTは、UTCでは前日の日曜21時に当たります。一方、UTCの金曜21時は、JSTでは土曜6時です。時刻を21時に変えるだけでは、平日の範囲が意図したJSTの日付に揃いません。

次のスクリプトは、時刻も曜日も換算していない例、時刻だけ換算した例、時刻と曜日を換算した例を比較します。

case2_dow_shift.py
from datetime import datetime
from zoneinfo import ZoneInfo

from cron_next import fires


if __name__ == "__main__":
    jst = ZoneInfo("Asia/Tokyo")
    start = datetime(2026, 11, 2, tzinfo=jst)
    end = datetime(2026, 11, 9, tzinfo=jst)
    cases = (
        ("(a) 0 6 * * 1-5", "0 6 * * 1-5"),
        ("(b) 0 21 * * 1-5", "0 21 * * 1-5"),
        ("(c) 0 21 * * 0-4", "0 21 * * 0-4"),
    )
    weekdays = "月火水木金土日"
    by_case = []

    for _, cron in cases:
        by_date: dict[object, list[str]] = {}
        for instant in fires(cron, "UTC", start, end):
            local = instant.astimezone(jst)
            by_date.setdefault(local.date(), []).append(
                f"{weekdays[local.weekday()]}曜 {local:%H:%M}"
            )
        by_case.append(by_date)

    print("各cronはUTCで評価し、発火時刻をJSTで表示します。")
    print("JSTの日付    | (a) 0 6 * * 1-5  | (b) 0 21 * * 1-5 | (c) 0 21 * * 0-4")
    print("-" * 72)
    day = start.date()
    while day < end.date():
        cells = [
            "、".join(case.get(day, [])) or "なし"
            for case in by_case
        ]
        print(f"{day} | {cells[0]:<16} | {cells[1]:<16} | {cells[2]}")
        day = day.fromordinal(day.toordinal() + 1)

3つのcronはいずれもUTCで評価し、発火時刻をJSTの日付ごとに表示します。月曜と土曜の行を見ると、曜日を戻す必要が分かります。

原因

JSTの時刻が9時より前なら、UTCでは前日になります。そのため、UTCに換算する際は時刻だけでなく曜日も1つ戻します。

JST平日6時の場合、JSTの月曜から金曜に対応するUTCの曜日は、日曜から木曜です。日曜が 0 なので、UTCの曜日フィールドは 0-4 になります。

直し方

JST平日6時に対応するUTC cronは 0 21 * * 0-4 です。曜日を1つ戻したことで、UTCの日曜21時から木曜21時が、JSTの月曜6時から金曜6時に対応します。

次のスクリプトは、JSTの平日時刻をUTCのcronに換算します。9時より前と、9時以降の例を並べて確認します。

jst_to_utc.py
def jst_to_utc_cron(minute: int, hour: int, dow: set[int]) -> str:
    if type(minute) is not int or not 0 <= minute <= 59:
        raise ValueError("分は0から59の整数で指定してください")
    if type(hour) is not int or not 0 <= hour <= 23:
        raise ValueError("時は0から23の整数で指定してください")
    if not dow or any(type(day) is not int or not 0 <= day <= 6 for day in dow):
        raise ValueError("曜日は0から6の整数の空でない集合で指定してください")

    utc_hour = (hour - 9) % 24
    utc_days = sorted((day - (hour < 9)) % 7 for day in dow)

    if len(utc_days) > 1 and utc_days == list(range(utc_days[0], utc_days[-1] + 1)):
        weekday_field = f"{utc_days[0]}-{utc_days[-1]}"
    else:
        weekday_field = ",".join(str(day) for day in utc_days)

    return f"{minute} {utc_hour} * * {weekday_field}"


if __name__ == "__main__":
    weekdays = {1, 2, 3, 4, 5}
    for hour, minute in ((9, 0), (6, 0), (8, 30), (9, 7)):
        print(
            f"JST 平日 {hour:02d}:{minute:02d} → "
            f"UTC {jst_to_utc_cron(minute, hour, weekdays)}"
        )

9時より前の6時と8時30分ではUTCの日付が前日になるため、出力の曜日は 0-4 です。9時と9時7分では同じ日のUTC時刻になるため、曜日は 1-5 のままです。

確認ログ

まず、3つのcronを同じ週で比較したログです。

$ python3 case2_dow_shift.py
各cronはUTCで評価し、発火時刻をJSTで表示します。
JSTの日付    | (a) 0 6 * * 1-5  | (b) 0 21 * * 1-5 | (c) 0 21 * * 0-4
------------------------------------------------------------------------
2026-11-02 | 月曜 15:00         | なし               | 月曜 06:00
2026-11-03 | 火曜 15:00         | 火曜 06:00         | 火曜 06:00
2026-11-04 | 水曜 15:00         | 水曜 06:00         | 水曜 06:00
2026-11-05 | 木曜 15:00         | 木曜 06:00         | 木曜 06:00
2026-11-06 | 金曜 15:00         | 金曜 06:00         | 金曜 06:00
2026-11-07 | なし               | 土曜 06:00         | なし
2026-11-08 | なし               | なし               | なし

時刻だけ直した 0 21 * * 1-5 は、月曜が「なし」で土曜に6時が表示されています。時刻と曜日を直した 0 21 * * 0-4 は、月曜から金曜まで6時で、土曜と日曜は「なし」です。

続いて、JSTの平日時刻をUTC cronに換算した結果です。

$ python3 jst_to_utc.py
JST 平日 09:00 → UTC 0 0 * * 1-5
JST 平日 06:00 → UTC 0 21 * * 0-4
JST 平日 08:30 → UTC 30 23 * * 0-4
JST 平日 09:07 → UTC 7 0 * * 1-5

例えば、JST平日8時30分は 30 23 * * 0-4 です。時刻は前日の23時30分となり、曜日も日曜から木曜に戻っています。JST平日9時7分は 7 0 * * 1-5 となり、曜日はそのままです。

落とし穴3:正時に実行を集中させる

再現

平日9時にエージェントを動かしたい場合、UTCに換算すると 0 0 * * 1-5 です。この指定は毎回、分が 0 の正時に当たります。

一方、処理の要件が「9時ちょうど」でなくてもよければ、9時7分という指定も考えられます。落とし穴2の換算結果では、JST平日9時7分はUTCの 7 0 * * 1-5 です。

原因

公式ドキュメントは、負荷が高いとき、特に毎時0分付近にはscheduleの実行が遅延することがあり、負荷が十分高いと一部のキュー済みジョブが破棄されることもあると説明しています。そのため、毎時の別の時刻にずらすよう案内しています。

直し方

9時ちょうどである必要がない仕事なら、例えばJSTの9時7分にずらします。timezone を省略してUTCで指定する場合、落とし穴2の換算結果を使うと 7 0 * * 1-5 です。

timezone: "Asia/Tokyo" を指定する場合は、JSTの壁時計の時刻として分を 7、時を 9 にします。いずれも、正時を避けるという公式の案内に沿った指定です。

確認ログ

落とし穴2の jst_to_utc.py のログでは、JST平日9時7分が 7 0 * * 1-5 と出力されています。分フィールドが 0 から 7 に変わり、UTCの正時を避ける指定です。

落とし穴4:cronと on のYAML読み込みでつまずく

再現

5分ごとのcronとして */5 * * * * を書くとき、クォートを付けないと、YAMLの読み込みでエラーになります。先頭の * がエイリアスとして解釈されるためです。cron文字列をクォートすると読み込めます。

もう1つ、PyYAMLでワークフローを読むと、on: が文字列の "on" ではなく、真偽値 True のキーになる例があります。その場合、doc.get("on") だけではscheduleを取得できません。

次のスクリプトは、on のキー、クォートの有無、cron と timezone、そして list_schedules によるscheduleの取得結果を確認します。

case4_yaml.py
import yaml


def list_schedules(text: str) -> list[dict[str, object]]:
    doc = yaml.safe_load(text)
    if not isinstance(doc, dict):
        return []

    result = []
    for key in ("on", True):
        events = doc.get(key)
        if not isinstance(events, dict):
            continue
        schedules = events.get("schedule", [])
        if not isinstance(schedules, list):
            continue
        for schedule in schedules:
            if isinstance(schedule, dict):
                result.append(
                    {
                        "cron": schedule.get("cron"),
                        "timezone": schedule.get("timezone"),
                    }
                )
    return result


if __name__ == "__main__":
    example_a = "on:\n  schedule:\n    - cron: '0 0 * * 1-5'"
    example_c = (
        "on:\n"
        "  schedule:\n"
        "    - cron: '0 0 * * 1-5'\n"
        '      timezone: "Asia/Tokyo"'
    )

    doc = yaml.safe_load(example_a)
    print("(a) トップレベルのキー:", list(doc.keys()))
    print('(a) doc.get("on"):', doc.get("on"))
    print("(a) doc.get(True):", doc.get(True))

    print()
    try:
        yaml.safe_load("cron: */5 * * * *")
    except yaml.YAMLError as error:
        print("(b) クォートなしのエラー:", str(error).splitlines()[0])
    print("(b) クォートあり:", yaml.safe_load("cron: '*/5 * * * *'"))

    print()
    print("(c) cronとtimezone:", yaml.safe_load(example_c)[True]["schedule"][0])

    print()
    print("(d) (a)のスケジュール:", list_schedules(example_a))
    print("(d) (c)のスケジュール:", list_schedules(example_c))

list_schedules は、ワークフローをスクリプトで読む際に、文字列の "on" に加えて True のキーも見ます。これにより、このログの例ではscheduleの一覧を取り出せます。

原因

クォートなしの */5 * * * * は、先頭の * がYAMLのエイリアスとして扱われ、読み込みエラーになります。cronとして意味を確認する前に、YAMLとして読み込める形にする必要があります。

また、PyYAMLではYAML 1.1の扱いにより、on: が真偽値 True のキーになる場合があります。文字列キーだけを探す doc.get("on") は、この例では None を返します。

直し方

*/5 * * * * のようなcron文字列はクォートします。これで、ログの例では cron の値を文字列として読み込めます。

PyYAMLでワークフローを読むスクリプトでは、doc.get("on") だけでなく True のキーも確認します。case4_yaml.py の list_schedules は、scheduleの一覧を取り出すときにその両方を見ます。

確認ログ

次のログは、トップレベルのキーと、クォートの有無による読み込み結果を示しています。

$ python3 case4_yaml.py
(a) トップレベルのキー: [True]
(a) doc.get("on"): None
(a) doc.get(True): {'schedule': [{'cron': '0 0 * * 1-5'}]}

(b) クォートなしのエラー: while scanning an alias
(b) クォートあり: {'cron': '*/5 * * * *'}

(c) cronとtimezone: {'cron': '0 0 * * 1-5', 'timezone': 'Asia/Tokyo'}

(d) (a)のスケジュール: [{'cron': '0 0 * * 1-5', 'timezone': None}]
(d) (c)のスケジュール: [{'cron': '0 0 * * 1-5', 'timezone': 'Asia/Tokyo'}]

doc.get("on") は None ですが、doc.get(True) ではscheduleを含む値を取得しています。また、クォートなしの */5 * * * * はエラーとなり、クォートありではcron文字列として読み込めています。

list_schedules の結果では、timezone のない例が None、Asia/Tokyo を指定した例がその文字列として表示されています。ワークフローを読む側でも、cronとタイムゾーンの両方を確認できます。

落とし穴5:換算したcronを発火時刻で検証しない

再現

JSTの時刻と曜日をUTCに換算するとき、時刻の引き算だけを見ていると、落とし穴2のように曜日のずれを見落とします。換算後のcronが意図したJSTの時刻に対応するかは、発火時刻を比較して確認します。

次のスクリプトは、UTCに換算したcronの発火時刻の集合と、timezone: Asia/Tokyo 相当の評価による発火時刻の集合を比較します。

verify_roundtrip.py
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

from cron_next import fires
from jst_to_utc import jst_to_utc_cron


def main() -> int:
    jst = ZoneInfo("Asia/Tokyo")
    start = datetime(2026, 1, 1, tzinfo=jst)
    end = datetime(2027, 1, 1, tzinfo=jst)
    weekday_sets = (
        {1, 2, 3, 4, 5},
        {0},
        {6},
        {0, 6},
        {1},
        {0, 1, 2, 3, 4, 5, 6},
    )
    combinations = 0
    matches = 0

    for hour in range(24):
        for minute in (0, 7):
            for weekdays in weekday_sets:
                combinations += 1
                weekday_field = ",".join(map(str, sorted(weekdays)))
                local_cron = f"{minute} {hour} * * {weekday_field}"
                utc_cron = jst_to_utc_cron(minute, hour, weekdays)

                local_instants = {
                    instant.astimezone(timezone.utc)
                    for instant in fires(local_cron, "Asia/Tokyo", start, end)
                }
                utc_instants = {
                    instant.astimezone(timezone.utc)
                    for instant in fires(utc_cron, "UTC", start, end)
                }

                if local_instants == utc_instants:
                    matches += 1
                else:
                    print(f"不一致: JST {local_cron}、UTC {utc_cron}")
                    print("JST側のみ:", sorted(local_instants - utc_instants))
                    print("UTC側のみ:", sorted(utc_instants - local_instants))

    print(f"組み合わせ数: {combinations}")
    print(f"一致数: {matches}")
    return 0 if matches == combinations else 1


if __name__ == "__main__":
    raise SystemExit(main())

対象は、時が 0〜23、分が 0 と 7、曜日が6パターンの組み合わせです。2026年の1年間について、各組み合わせの発火時刻の集合が一致するかを確認します。

原因

JSTとUTCでは時刻の差により日付が変わる場合があります。時刻の数字だけを換算しても、曜日フィールドがJSTの意図に対応しているとは限りません。

timezone: "Asia/Tokyo" を指定する場合は、その壁時計の時刻でcronが評価されます。一方、timezone を省略する場合はUTCで評価されるため、JSTで表したかった時刻と曜日をUTC側の指定に反映する必要があります。

直し方

換算後のUTC cronについて、期待するタイムゾーンで評価した発火時刻と比較します。時刻と曜日の両方を含めて発火時刻の集合を比べることで、表示上のcron文字列だけでは見落としやすい日付のずれを確認できます。

この検証では、落とし穴1の fires("0 9 * * 1-5", "Asia/Tokyo", ...) のような評価と、対応するUTC cronの評価を比較します。検証範囲は、指定した時刻と曜日の288通り、2026年の1年間です。

確認ログ

次のログは、比較した組み合わせ数と、発火時刻の集合が一致した数です。

$ python3 verify_roundtrip.py
組み合わせ数: 288
一致数: 288

288通りすべてで一致しました。この結果は、検証した2026年の範囲と288通りの組み合わせについて、換算後のUTC cronと Asia/Tokyo 相当の評価が同じ発火時刻の集合を返したことを示します。

なお、timezone に夏時間のあるタイムゾーンを指定した場合、夏時間によってスキップされる時刻の実行は、次の有効な時刻に繰り下がります。日本には夏時間がありません。

ここまでの5つのスクリプトは、次の run_all.sh でまとめて実行し、run.log に保存しました。

run_all.sh
#!/bin/bash
# 各スクリプトを順に実行し、コマンドと出力を run.log に残す
cd "$(dirname "$0")" || exit 1
for f in jst_to_utc.py case1_misread.py case2_dow_shift.py verify_roundtrip.py case4_yaml.py; do
  echo "\$ python3 $f"
  python3 "$f" 2>&1
  echo
done

運用メモ

公式ドキュメントには、発火時刻以外にもscheduleの運用に関わる事項があります。

  • scheduleワークフローは、既定ブランチの最新コミットでのみ動きます。
  • 公開リポジトリでは、60日間リポジトリの活動がないとscheduleワークフローが自動で無効化されます。
  • github.event.schedule には、その回を起動したcron文字列が入ります。複数のcronを設定したとき、その値を分岐に使えます。

まとめ

JSTで定期実行したい場合は、timezone: "Asia/Tokyo" を指定する方法と、UTCのcronに換算する方法があります。UTCに換算する場合、JSTの時刻が9時より前なら前日になるため、曜日も1つ戻します。

また、正時を避ける公式案内を確認し、YAMLをスクリプトで読む場合はcronのクォートと on キーの扱いに注意します。今回の2026年・288通りの検証では、換算後のUTC cronと Asia/Tokyo 相当の評価で、発火時刻の集合がすべて一致しました。

出典

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?