Supercherenkoスマレジアプリと解説メディアの個人開発

CI を YAML で書くのをやめる。Cloudflare の CI SDK が変えたのは実行の単位

CIの設定ファイルは、どこの現場でも同じ育ち方をします。最初は10行だったものが、キャッシュを足し、ジョブを分け、条件分岐を足して、半年後には誰も全体を把握していない。私も受託の案件でこれを何度も見ています。

Cloudflare が2026年8月4日に発表した CI SDK は、その設定ファイルを TypeScript で書けるようにしたもの、と紹介されています。ただ、言語が変わったのは結果であって、原因ではありません。変わったのは実行の単位です。ここを押さえると、なぜ TypeScript で書けるようになったのかも見えてきます。

YAML が膨らむのは、状態を持てないから

GitHub Actions の設定が育ってしまう理由は、書き手の怠慢ではありません。仕組みの側に、状態を持つ場所がないからです。

Actions では、1つのジョブの中のステップは同じランナーの上で動くので、ファイルもディレクトリも共有できます。ところがジョブをまたぐと共有できません。別のマシンで動くので、前のジョブが作ったものは残っていません。だから actions/upload-artifact で固めて、次のジョブで download-artifact して展開する、という往復を自分で書くことになります。並列にしたければ needs: で依存を書き、キャッシュが欲しければ actions/cache にキーを書きます。

この往復は、状態を保存する場所がないぶんを設定ファイルで埋める作業です。埋めるべき箇所が増えるほど設定は伸びます。YAMLが読みにくいのは記法のせいだと言われがちですが、記法をTypeScriptに変えただけでは、この往復は消えません。

失敗したときも同じところに戻ります。GitHub Actions では、再実行の単位がジョブです。5つのステップの4番目で落ちたとき、そのジョブをもう一度動かすと1番目から走り直します。依存のインストールに5分かかるなら、その5分をもう一度払います。

制限の数字も、この前提の上に乗っています。GitHub ホステッドランナーのジョブは6時間まで、ワークフロー実行全体は35日まで、再実行は50回まで。マシンを1台占有し続ける形なので、時間で切るしかありません。

ステップを Workflows の step にすると何が変わるか

Cloudflare の CI SDK は、CI のステップを Cloudflare Workflows の step として動かします。発表記事には、各ステップが「own Workflow step」になると書かれています。実行の単位が変わった、というのはこのことです。

Workflows は durable execution の仕組みです。公式ドキュメントの言葉では「Durable multi-step execution without timeouts」「automatically retry failed tasks, and persist state」。ステップの出力が保存され、失敗すれば自動で再試行され、状態が残る。これが基盤の側に最初から備わっています。

基盤が状態を持つと、3つのことが自動で付いてきます。

失敗したステップから再開できます。 インストールが終わっていれば、その結果は保存されています。テストで落ちたときに走り直すのはテストからで、インストールの5分は払い直しません。記事にも「restart from a specific step」とあります。

並列を自分で書かなくてよくなります。 ステップが独立していて、それぞれの入力と出力がはっきりしているので、依存していないものは同時に始められます。発表記事のコード例はこうです。

const deps = await ci.runner({
  name: 'install',
  command: 'bun install --frozen-lockfile',
  cache: { inputs: ['package.json', 'bun.lock'] }
});

await Promise.all([
  deps.runner({ name: 'lint', command: 'bun run lint' }),
  deps.runner({ name: 'test', command: 'bun run test' }),
  deps.runner({ name: 'typecheck', command: 'bun run typecheck' }),
  deps.runner({ name: 'build', command: 'bun run build' })
]);

needs: に相当するものは deps という変数です。依存関係を言語の変数として表現できるので、設定ファイルに書き写す必要がありません。これが「TypeScriptで書ける」の中身です。順番に書けるようになったのではなく、依存を値として持てるようになったから、言語で書く意味が出たという順序です。

時間の制限が消えます。 Workflows の制限を見ると、ステップごとの wall time は「Unlimited」です。1つのマシンを占有し続けるのではなく、ステップごとに実行して結果を保存する形なので、全体の所要時間で切る必要がありません。

代わりに別の制限が付きます。1つの Workflow のステップ数は既定10,000・最大25,000、ステップの出力は1MiBまで、保持期間は Workers Paid で30日。ステップの出力サイズに上限があるのは、出力を保存する仕組みだからです。ビルド成果物をステップの戻り値で渡す書き方はできません。

独立して動くための条件

ステップが独立して動くには、実行環境も独立している必要があります。CI SDK はコマンドの実行に Sandbox SDK を使い、記事の表現では「safe, isolated environment」で動かします。実行基盤は Containers です。

ここでトレードオフが出ます。Actions の同一ジョブ内のステップは、同じディスクを共有していました。npm install した node_modules は、次のステップにそのまま残っています。ステップを独立させると、この暗黙の共有が使えません。

だから CI SDK では、共有したいものを明示します。上のコードの cache: { inputs: ['package.json', 'bun.lock'] } がそれです。このファイルが変わらなければキャッシュを使う、という条件を値として書きます。キャッシュの実体は R2 に置かれます。

actions/cache にキーを書くのと、やっていることは同じです。違うのは、Actions では「共有されないから明示する」だったものが、CI SDK では「独立させたから明示する」になっている点です。同じ手間に見えて、前者は仕組みの穴を埋める作業、後者は仕組みの前提を書く作業です。後者は忘れると動かないので、「忘れたまま動いて、後で壊れる」という事故が起きません。

トリガーの書き方も値です。wrangler の設定に events フィールドが入り、リポジトリへの push をイベントとして受けます。

{
  "triggers": {
    "events": [{
      "type": "cf.artifacts.repo.pushed",
      "filter": { "namespace": "CI", "repoName": "my-repo" },
      "target": { "type": "workflow", "workflow_name": "ci-workflow" }
    }]
  }
}

記事は、これまで同じことをするには「イベントサブスクリプション、Queue、キューコンシューマの設定が必要」だったと書いています。部品を3つ繋ぐ作業が、宣言1つになったという差です。

なお自己修復エージェントの機能も入っています。Durable Objects と Workers AI を使い、失敗したステップを LLM に直させるものです。ただし、これは今回の変化の本体ではなく、上に載せられる機能の一例です。Workflows の上でステップが独立しているから、失敗したステップを名指しして差し替えられる、という順序で読むのが正しいと思います。

GitHub Actions と、どちらを選ぶか

数字と挙動を並べます。

  • 再実行の単位: Actions はジョブ単位で、ジョブの先頭から走り直す。CI SDK はステップ単位で、落ちたステップから再開する
  • 状態の共有: Actions は同一ジョブ内はディスク共有、ジョブ間は artifact で明示。CI SDK は全ステップが独立していて、キャッシュで明示
  • 並列: Actions は needs: とマトリックスで宣言。CI SDK は依存が変数なので、依存していないものは自動で並列に始まる
  • 時間の上限: Actions は GitHub ホステッドのジョブが6時間、ワークフロー実行が35日。CI SDK はステップの wall time が Unlimited
  • 出力の上限: Actions は artifact のサイズで管理。CI SDK はステップの出力が1MiBまで
  • 記述: Actions は YAML。CI SDK は TypeScript

いま乗り換える理由があるのは、限られた現場だと思います。

CI SDK が private beta で、料金の記載がない、という段階の問題もあります。ただそれ以上に、Actions の弱点が本当に痛いのはどこかを見たほうがよいです。ジョブの先頭から走り直すのが痛いのは、インストールやビルドに数分以上かかっていて、しかも後段でよく落ちるときです。1回のCIが3分で終わるなら、再開できても取り返すのは数十秒です。

記事が挙げている想定も、そこを外していません。狙いは「数百万のリポジトリに対して統一的なCI/CDを実行したい」プラットフォーム企業です。リポジトリごとに設定ファイルを配って回るのではなく、CIそのものをコードとして書いて配りたいという規模の話で、自分の案件1本のCIを速くしたい、という話ではありません。

私の受託案件で使うかというと、いまは使いません。スマレジ連携アプリはどれも Cloudflare Workers の上に載っているので基盤としては近いのですが、CIでやっているのは lint とテストとデプロイで、1回数分です。ここに durable execution を持ち込む理由がありません。まだ触っていないので、「向いていない」という判断ではありません。解こうとしている問題が自分のものではない、という判断です。

選ぶ基準は1行に絞れます。CIを速くしたいのか、CIを配りたいのか。前者なら Actions のキャッシュとジョブ分割を詰めるほうが早い。後者、つまり多数のリポジトリに同じCIを行き渡らせたいなら、コードで書ける仕組みに意味が出ます。この線引きは CI に限らず、途中で落ちる前提の処理を状態を持つ基盤に載せるかどうか、という判断としてそのまま使えます。

この記事の内容で判断がつかないところがあれば、そのまま聞いてください。相談と見積もりまでは無料です。

相談する

記事の一覧へ