夜中にビルドを一本投げ、通るのを待って、通ったら提出コマンドを打ちます。個人開発でアプリを何本か抱えていると、この順番が指の癖になってしまいます。手順書に残すほどでもない、けれど間違えると三十分が消える——その程度の作業が、アプリの本数だけ並んでいるのです。
先日、壁紙アプリの更新をひとつ出したあとで、同じ順番をもう一度打ちながら考えました。この順番こそ、自分ではなく機械に持たせるべきものではないか、と。
Rork から書き出したリポジトリは、その時点でもう普通の Expo プロジェクトです。手元に eas.json があって GitHub に載っているなら、EAS Workflows の設定ファイルを一枚足すだけで、この順番はそのまま自動化の対象になります。
ただ、私が最初に書いた一本は失敗しておりました。書式としては正しく、eas も受け取ってくれる形です。それでも、いちばん通知が欲しい場面でだけ黙る作りになっていたのです。
置き場所と、最初に効く二つの制約
設定ファイルは .eas/workflows/ に置きます。.eas ディレクトリは eas.json と同じ階層です。ここを一段間違えると、コマンドはファイルを見つけてくれません。
my-app
.eas
workflows
pr-preview.yml
release.yml
eas.json
app.json
拡張子は .yml か .yaml のいずれかで、ファイルサイズは 16 KiB 以下という上限があります。16 KiB は普通に書いていれば当たらない数字ですが、ジョブを足し続けて一枚にまとめたくなったときに効いてきます。一本のファイルに詰め込まず、目的ごとに分けておくほうが後で楽になります。私は用途ごとに一枚ずつ置く形をお勧めします。
もう一つ、書き始める前に知っておくと迷わないことがあります。トリガーの既定値は「書かなかったほう」が空になる という点です。on.push に branches も tags も書かなければ branches は ['*']、tags は [] として扱われます。ところが片方だけを書くと、書かなかったほうは [] になります。タグだけを指定したつもりで branches の行を消すと、ブランチ側の発火は完全に止まります。狙いどおりならよいのですが、私は最初これを取り違えていました。
PR を開いたらプレビュー更新が出るところまで
一本目は、プルリクエストを開いたらプレビュー用の更新を配信するものにします。手で eas update を打っていた作業がそのまま置き換わりますし、失敗しても本番には届きませんので、最初の一本として安心して試せます。
# .eas/workflows/pr-preview.yml
name : PR preview
on :
pull_request :
branches : [ '*' ]
paths :
- app/**
- package.json
- '!**/*.md'
jobs :
publish_preview :
type : update
environment : preview
params :
branch : pr-${{ github.event.pull_request.number }}
message : ${{ github.event.pull_request.title }}
hooks :
before_update :
- name : 型を通してから配信します
run : npx tsc --noEmit
読み方を順に置いておきます。paths は監視するパスで、先頭に ! を付けると除外になります。README だけを直した PR でビルド枠を使わずに済むのは、地味ですが効きます。branches を省略すると ['*'] になりますので、ここは書かなくても動きます。あえて書いているのは、後から読む自分のためです。
type: update は eas update に対応するジョブで、branch と channel は同時に指定できません。今回は PR 番号からブランチ名を作っておりますので branch のほうを使います。
そして hooks です。ここが最初につまずいた場所でした。
steps を書けるジョブと、env が効かないジョブ
steps を書けるのは、カスタムジョブと type: build のジョブだけです。type: update や type: submit に steps を足しても通りません。配信の前後で何かを走らせたいときは、そのジョブが持つ hooks——before_update や after_update——に置きます。
同じ性質の落とし穴がもう一つあります。VM を持たないジョブには env が効きません。 具体的には次のジョブたちです。
ジョブの型 役割 env
build / update / submit / deploy実際にコードを取り出して走ります 効きます
slack / github-comment通知だけを出します 効きません
get-build / update-rollout / branch-deleteEAS 側の情報を読み書きします 効きません
require-approval / doc人の操作を待ち、文面を出します 効きません
通知ジョブに環境変数を渡そうとして黙って無視されると、原因の見当がつくまでに時間を取られます。私はここで一度、Webhook の URL が空のまま通知の飛ばない状態を作りました。本番運用に載せる前に気づけたのは運がよかっただけで、回避の手立ては後半に置いた検査に寄せてあります。
environment の既定値がジョブの型ごとに違うことも、覚えておくと得をします。build は eas.json のビルドプロファイルから推論し、submit は提出対象のビルドから引き継ぎ、maestro 系は preview、それ以外は production です。プレビュー配信のつもりで本番の環境変数を読んでいた、という取り違えは、ここを書かなかったときに起こります。一本目で environment: preview を明示しているのは、そのためです。
タグを打ったらビルドと TestFlight まで
二本目は、タグを打ったら本番ビルドを作り、TestFlight まで運ぶものにします。手で打っていた順番が、そのまま needs の鎖になります。
# .eas/workflows/release.yml(このまま置くと、こけた夜に通知が来ません)
name : Release
on :
push :
tags :
- v*
jobs :
build_ios :
type : build
params :
platform : ios
profile : production
ship_testflight :
needs : [ build_ios ]
type : testflight
params :
build_id : ${{ needs.build_ios.outputs.build_id }}
external_groups : [ Early ]
submit_beta_review : true
notify :
needs : [ ship_testflight ]
type : slack
params :
webhook_url : ${{ env.SLACK_HOOK_URL }}
message : "リリースの経路が最後まで通りました"
type: build は platform が必須で、profile を省くと production が使われます。出力として build_id を返しますので、次のジョブはそれを受け取ります。
type: testflight は build_id(これから上げるビルド)か asc_build_id(すでに上がっているビルド)のどちらか一方を取ります。両方は書けません。処理待ちの上限は wait_processing_timeout_seconds で、既定は 1800 秒です。三十分で戻ってこないビルドは、そこで打ち切られます。
ここまでは、手で打っていた順番の写しです。読み返しても、おかしなところは見当たりませんでした。
失敗した回にだけ通知が届かない
けれど、この notify は届きません。正確には、通ったときだけ届いて、こけたときには届きません。
needs は「先行ジョブが成功したら次へ進む」という意味です。ship_testflight がこけた回、notify は「先行が成功していない」ので走らないまま終わります。通知が欲しいのは成功した夜ではなく、こけた夜のほうなのに。
結果を成否によらず受け取りたいときは after を使います。
report :
after : [ build_ios , ship_testflight ]
type : slack
params :
webhook_url : ${{ env.SLACK_HOOK_URL }}
message : "build=${{ after.build_ios.status }} / testflight=${{ after.ship_testflight.status }}"
after で受けたジョブの状態は after.<ジョブID>.status から読みます。値は success / failure / skipped のいずれかです。ここで気をつけたいのは、after でつないだのに needs.<ジョブID>.status と書いてしまう取り違え です。参照する名前空間は、つないだキーと揃える必要があります。
成功を待つのは needs、結果を見届けるのは after。 言葉にしてしまえば一行ですが、書いている最中の頭は「順番に並べる」ことしか考えておりませんでした。手で打っていた順番をそのまま写すと、needs のほうを選んでしまうのです。手作業には「こけたら自分が見ている」という前提が含まれていて、その前提だけが自動化から抜け落ちます。
夜間の定期実行が立っている三つの前提
順番を機械に渡せると、次は「毎晩まわしておきたい」と考えたくなります。on.schedule はそのための仕組みですが、公式の説明には前提が三つ書かれています。読み飛ばすと痛い順に並べます。
前提 意味するところ
デフォルトブランチでのみ走ります 作業ブランチに置いた定期実行は、いつまで待っても発火しません
GMT で解釈されます 0 0 * * * は日本時間の午前 9 時です。夜間帯のつもりが業務時間帯になります
まれにスキップされ、まれに複数回走ります 公式がそう明記しています。同じジョブが二度走っても壊れない作りが要ります
三つめは、私にとって意外でした。定期実行というものを「必ず一度だけ来るもの」として設計していたからです。二度走っても困らない形——たとえばバージョン番号をその場で採番するのではなく、すでにあるものを見て判断する形——にしておく必要があります。
なお、コミットメッセージに [eas skip] や [skip eas] を含めると、push と pull_request によるワークフローの起動を見送れます。設定ファイル自体を直しているあいだ、無駄な起動を止めるのに使えます。
push する前に手元で見る検査
ここまでの落とし穴は、どれも「書式としては正しい」ものばかりです。YAML としては読めますし、コマンドも受け取ります。気づくのは、こけた夜に通知が来なかったときです。
ですので、push する前に手元で見る小さな検査を書きました。EAS 側の検証を置き換えるものではなく、自分が繰り返す取り違えだけを拾うものです。
#!/usr/bin/env python3
"""EAS Workflows の設定を、push する前に手元で見るための小さな検査。
使い方: python3 check_workflow.py .eas/workflows/*.yml"""
import sys, pathlib, yaml
SIZE_LIMIT = 16 * 1024
STEPS_ALLOWED = { "build" , None } # steps を書けるのは build とカスタムジョブだけ
NO_VM_JOBS = { # VM を持たない = env が効かない
"apple-device-registration-request" , "branch-delete" , "doc" , "get-build" ,
"github-comment" , "require-approval" , "slack" , "update-rollout" ,
}
NOTIFY_TYPES = { "slack" , "github-comment" }
def check (path):
p = pathlib.Path(path)
problems, notes = [], []
size = p.stat().st_size
notes.append( f "size= { size } B / limit= { SIZE_LIMIT } B" )
if size > SIZE_LIMIT :
problems.append( f "16 KiB を超えています( { size } B)" )
try :
doc = yaml.safe_load(p.read_text( encoding = "utf-8" )) or {}
except yaml.YAMLError as e:
return [ f "YAML として読めません: { e } " ], notes
jobs = doc.get( "jobs" ) or {}
notes.append( f "jobs= { len (jobs) } ( { ', ' .join(jobs) } )" )
for jid, job in jobs.items():
job = job or {}
jtype = job.get( "type" )
for key in ( "needs" , "after" ):
for dep in job.get(key) or []:
if dep not in jobs:
problems.append( f " { jid } . { key } : 存在しないジョブ ' { dep } ' を指しています" )
if "steps" in job and jtype not in STEPS_ALLOWED :
problems.append( f " { jid } : type= { jtype } では steps を書けません(hooks を使います)" )
if "env" in job and jtype in NO_VM_JOBS :
problems.append( f " { jid } : type= { jtype } は VM を持たないため env が効きません" )
if jtype in NOTIFY_TYPES and job.get( "needs" ) and not job.get( "after" ):
problems.append(
f " { jid } : 通知ジョブが needs にぶら下がっています。"
f "needs は先行ジョブの成功が条件なので、失敗した回にこの通知は届きません"
)
return problems, notes
if __name__ == "__main__" :
bad = 0
for arg in sys.argv[ 1 :]:
problems, notes = check(arg)
print ( f "== { arg } " )
for n in notes:
print ( f " - { n } " )
for pr in problems:
print ( f " NG { pr } " )
bad += 1
if not problems:
print ( " OK 気になる点はありませんでした" )
sys.exit( 1 if bad else 0 )
取り違えを含んだ一本に通します
先ほどの notify を含む release.yml に通すと、こう出ます。
== .eas/workflows/release.yml
- size=517B / limit=16384B
- jobs=3 (build_ios, ship_testflight, notify)
NG notify: 通知ジョブが needs にぶら下がっています。needs は先行ジョブの成功が条件なので、失敗した回にこの通知は届きません
わざと四つ間違えた一本に通します
わざと四つ取り違えた PR 用の設定も通してみました。type: update に steps を書き、github-comment に env を付け、after の参照先を一文字打ち間違えた形です。
== .eas/workflows/pr-preview.yml
- size=717B / limit=16384B
- jobs=3 (publish_preview, comment, tell_me_anyway)
NG publish_preview: type=update では steps を書けません(hooks を使います)
NG comment: type=github-comment は VM を持たないため env が効きません
NG comment: 通知ジョブが needs にぶら下がっています。needs は先行ジョブの成功が条件なので、失敗した回にこの通知は届きません
NG tell_me_anyway.after: 存在しないジョブ 'publsh_preview' を指しています
直したあとの二本に通します
四つとも拾えました。直したあとの二本は、こう返ります。
== .eas/workflows/pr-preview-fixed.yml
- size=599B / limit=16384B
- jobs=2 (publish_preview, comment)
OK 気になる点はありませんでした
== .eas/workflows/release-fixed.yml
- size=561B / limit=16384B
- jobs=3 (build_ios, ship_testflight, report)
OK 気になる点はありませんでした
ジョブ ID の打ち間違いを拾えるのが、思っていたよりありがたいところでした。EAS 側でも当然弾かれますが、そのために一度 push して待つ時間が要らなくなります。
書き上げた設定は eas workflow:run .eas/workflows/release.yml で手動でも起こせます。on に何を書いていても、このコマンドからは走ります。最初の一回は、タグを打つ前にこれで確かめておくと落ち着きます。
二本目を置く前に
順番を機械に渡すというのは、手順を書き写す作業ではありませんでした。手作業に含まれていた「こけたら自分が見ている」という前提を、どこかに置き直す作業だったのだと思います。after の一行は、その置き直しでした。
Rork から書き出したリポジトリを Git に載せるところは Rork エクスポート後、git init 前に .gitignore を用意する に書いております。まだ載せていない方は、そちらが先になります。パッケージの版ずれが気になる方は expo のパッチを1つ上げると、expo install の推奨が何個動くのかを数えました も併せてご覧ください。
次にやることを一つだけ挙げるとしたら、.eas/workflows/pr-preview.yml を一枚だけ置いて、README を直しただけの PR を開いてみることです。paths の除外が効いていれば何も起きませんし、効いていなければ更新が一本飛びます。どちらに転んでも、設定ファイルが自分の意図どおりに読まれているかが一目でわかります。本番の経路を組むのは、そのあとで十分です。
ここまでお読みくださって、ありがとうございました。私自身、まだ二本目を置いたところです。手で打つ回数が減ったぶんを、どこに戻すかはこれから考えます。