ワークスペースとブランチ
並列に動くタスクが、それぞれのワークスペースとブランチでどのように分離して実行されるのか、結果がどのように統合ブランチにマージされるのか、そしてこの分離が要らないのはどんなときか。
CodeDAG では、1 つのワークフローの中で互いに依存しないタスクが、同じリポジトリを同時に変更できます。デフォルトでは、実行中のタスクがそれぞれワークスペースを 1 つ占有します。ワークスペースとは、プライマリリポジトリ、またはそのレプリカの 1 つのことです。そのため、同時に実行できるタスクの数は、最大でもワークスペースの数までです。タスクの変更はそれぞれのタスクブランチにコミットされ、上流の結果は CodeDAG が自動で下流にマージします。すべてがレビューを通ると、結果は 1 本の統合ブランチにマージされ、メインブランチにコミットが増えることはありません。コンパイルもテストの実行もしないワークフローには、この分離は必要ないので、代わりに共有ワークスペースモードを使えます。
ワークスペース
複数のタスクが同時にコードを変更する場合、1 つのディレクトリを共有することはできません。1 つのディレクトリで同時にチェックアウトできるブランチは、1 本だけです。そのうえ、あるタスクが変更している途中のコードが、別のタスクのコンパイルやテストに紛れ込んでしまいます。そのため、同時に実行するタスクには、それぞれ専用のチェックアウトが必要です。プロジェクトのプライマリリポジトリと各レプリカは、ちょうどそれぞれが 1 つのチェックアウトです。CodeDAG はその 1 つひとつをワークスペース(workspace)と呼び、タスクの実行時にタスクへ割り当てます。
デフォルトの分離ブランチモードでは、各タスクがワークスペースを 1 つ占有し、実行の開始からレビューの終了まで使い続けます。そのため、ワークスペースの数が、同時に実行できるタスク数の上限になります。maxConcurrency でできるのは、この上限をさらに下げることだけです。詳しくは同時に実行されるタスクの数は何で決まるかを参照してください。
プライマリリポジトリもデフォルトではワークスペースの 1 つで、しかも最初にタスクが割り当てられます。そのため、ワークフローの実行中、プライマリリポジトリはタスクブランチに切り替えられます。その間もプライマリリポジトリで開発を続けたい場合は、ワークフローにレプリカだけを使わせることができます。詳しくはワークフローの実行中に自分でも開発を続けるを参照してください。ワークスペースに未コミットの変更がある場合、CodeDAG がそれを上書きすることはありません。タスクは on-hold で止まるので、変更をコミットするか stash で退避してから復旧します。
ワークスペースが分離するのは、ファイルだけです。データベース、ポート、キャッシュディレクトリのようなローカルのリソースは、複数のワークスペースで共有されたままです。タスクがこうしたリソースを使う場合は、ワークスペースごとに別のポートや別のテスト用データベースを使うなど、ぶつからないように自分で分けておく必要があります。
タスクブランチ、統合ブランチ、ベース
1 つのワークフローには、コードを変更するタスクがいくつもあり、同時に進むものもあります。それぞれの変更は、別々に確認でき、別々に巻き戻せなければなりません。それでいて最後には 1 つにまとめる必要があり、まとめた結果は、自分で確認するまでメインブランチに入るべきではありません。そこで、デフォルトの分離ブランチモードでは、各タスクの変更をそのタスク専用のタスクブランチにコミットします。すべてのタスクブランチは同じコミットから作られ、このコミットをベースと呼びます。レビューを通った結果は、最後に 1 本の統合ブランチにマージされます。CodeDAG がメインブランチにコミットすることはなく、結果をいつ、どのような方法でメインブランチにマージするかは、自分で決めます。
「CodeDAG の仕組み」で取り上げたダイヤモンド型のワークフロー spec-to-code を例にします。タスクと依存関係は次のとおりです。
flowchart LR t1["タスク 1<br/>インターフェース契約を起草"] --> t2["タスク 2<br/>契約に沿ってテストを書く"] t1 --> t3["タスク 3<br/>契約に沿って実装する"] t2 --> t4["タスク 4<br/>テストを通す"] t3 --> t4これに対応するブランチは次のようになります。どのタスクブランチもベースから作られ、まず承認済みの上流の結果をマージしてから、そのタスク自身の変更をコミットします。最後に統合ブランチにマージされ、main には何の変化もありません。
%%{init: {"themeVariables": {"commitLabelFontSize": "12px"}}}%%gitGraph commit id: "ベース" branch "cdg/spec-to-code/t1-インターフェース契約を起草" commit id: "タスク 1 の変更" checkout main branch "cdg/spec-to-code/t2-契約に沿ってテストを書く" merge "cdg/spec-to-code/t1-インターフェース契約を起草" id: "タスク 2 が上流をマージ" commit id: "タスク 2 の変更" checkout main branch "cdg/spec-to-code/t3-契約に沿って実装する" merge "cdg/spec-to-code/t1-インターフェース契約を起草" id: "タスク 3 が上流をマージ" commit id: "タスク 3 の変更" checkout main branch "cdg/spec-to-code/t4-テストを通す" merge "cdg/spec-to-code/t2-契約に沿ってテストを書く" id: "タスク 4 がタスク 2 をマージ" merge "cdg/spec-to-code/t3-契約に沿って実装する" id: "タスク 4 がタスク 3 をマージ" commit id: "タスク 4 の変更" checkout main branch "cdg/spec-to-code/integration" merge "cdg/spec-to-code/t4-テストを通す" id: "統合ブランチにマージ"ベースが決まるのは、ワークフローの投入時ではなく、実行が始まるときです。baseRef を書いていれば、その時点で baseRef が指しているコミットがベースになり、書いていなければ、その時点でプライマリリポジトリにチェックアウトされているコミットがベースになります。
すべてのタスクのレビューが終わると、CodeDAG はプライマリリポジトリで統合ブランチをチェックアウトし、そこに結果をマージします。そのため、ワークフローの終了後、プライマリリポジトリは統合ブランチをチェックアウトした状態のままで、元のブランチには戻りません。次のワークフローに baseRef が書かれておらず、自分でメインブランチに戻してもいなければ、次のワークフローはこの統合ブランチの結果の続きから始まります。
合流ノードでのマージとコンフリクト
あるタスクが並列に動く複数の上流に依存している場合、上流の結果はそれぞれ別のタスクブランチにあります。このタスクを始める前に、それらをマージし、コンフリクトがあれば先に解決しておかなければなりません。このようなタスクを合流ノードと呼びます。
この作業を誰が担うかについては、一般に 2 つの設計があります。1 つは手動で、ワークフローを書く人が、各合流ノードの前に、ブランチのマージとコンフリクトの解決だけをするノードを追加します。もう 1 つは自動で、合流ノードが始まる前に、オーケストレーションエンジンが済ませます。CodeDAG が採用しているのは後者です。同じダイヤモンド型のワークフローは、2 つの設計でそれぞれ次のようになります。
%%{init: {"flowchart": {"padding": 10, "rankSpacing": 32}}}%%flowchart TB subgraph manual["手動:マージノードを追加"] direction LR m1["タスク 1<br/>インターフェース<br/>契約を起草"] --> m2["タスク 2<br/>契約に沿って<br/>テストを書く"] m1 --> m3["タスク 3<br/>契約に沿って<br/>実装する"] m2 --> m4["タスク 4<br/>ブランチをマージし、<br/>コンフリクトを解決"] m3 --> m4 m4 --> m5["タスク 5<br/>テストを通す"] end subgraph auto["自動:CodeDAG の方式"] direction LR t1["タスク 1<br/>インターフェース<br/>契約を起草"] --> t2["タスク 2<br/>契約に沿って<br/>テストを書く"] t1 --> t3["タスク 3<br/>契約に沿って<br/>実装する"] subgraph t4["タスク 4"] direction LR tm(["CodeDAG が<br/>自動でマージし<br/>コンフリクトを解決"]) --> tw["テストを通す"] end t2 --> tm t3 --> tm end manual ~~~ auto style tm stroke-dasharray: 5 5手動の設計では、ワークフローに開発タスクのほかにマージ用のタスクも書かなければならず、分岐が増えるほど手間がかかります。CodeDAG は、この作業をエンジンに任せます。合流ノードが始まる前に、エンジンが各上流の承認済みの結果を、そのタスクのブランチに自動マージします。図のタスク 4 の中にある破線の枠がこれにあたります。そのため、CodeDAG のワークフローに書くのは、開発タスクとその間の依存関係だけで、マージ用のタスクを別に書く必要はありません。
マージでコンフリクトが起きた場合、CodeDAG はその解決だけを担当するエージェントセッションを別に起動し、最大 3 回まで解決を試みます。このセッションには各上流タスクのタイトルとプロンプトが見えており、それぞれの意図に沿ってマージします。合流ノード自体の作業はしません。3 回試みても解決しなかった場合、このタスクは on-hold で止まります。その場合は、タスクのワークスペースで自分でマージを完了させてから、タスクを復旧します。手順は、トラブルシューティングのマージコンフリクトを参照してください。
すべてのタスクのレビューが終わったあとの統合ブランチへのマージも、同じ自動マージで、コンフリクトの扱いも変わりません。違うのは、3 回試みても解決しなかったときに on-hold で止まるのが、ワークフロー全体だという点だけです。
結果に納得できないときの備え
あるタスクの結果が使えないとき、巻き戻したいのはそのタスクだけです。タスクの変更はすべて専用のタスクブランチにあるので、巻き戻してもほかのタスクには影響しません。決める必要があるのは、どこまで巻き戻すかと、元の結果を残すかどうかの 2 点だけです。CodeDAG は、この 2 点のそれぞれに対応する仕組みを用意しています。
- クリーンベースライン:上流の結果がすでにマージされていて、タスクがまだ何も手を付けていない時点のコミットです。タスクブランチをここまで巻き戻しても、失われるのはそのタスク自身の変更だけで、上流からマージされた結果は残ります。
- スナップショットブランチ:巻き戻す前に、記録として残しておくためのブランチです。タスクのその時点の変更が保存されます。この変更が下流に渡ることはなく、あとから取り出せます。
レビューで変更を処理する方法はいくつかありますが、どれもこの 2 つの組み合わせです。スナップショットを残すかどうか(--code snapshot か --code discard か)と、巻き戻したあとに新しいセッションを起動してやり直すかどうか(rework)で決まります。選び方は判断に応じてアクションを選ぶ、取り出す方法はスナップショットから一部の変更を取り出すを参照してください。
実行モード
タスクごとにワークスペースを 1 つ、タスクブランチを 1 本用意するこの分離は、同時にコードを変更するタスクのためのものです。あるタスクがコンパイルやテストを実行しても、別のタスクが変更している途中のコードが紛れ込むことはありません。一方、ドキュメントの作成、翻訳、調査のようなタスクは、コンパイルもテストの実行もしないので、こうした干渉を受けません。そのようなタスクのためにレプリカを用意し、ブランチを作り、マージやコミットをしても、余計な手間が増えるだけです。
そこで CodeDAG では、この分離を使うかどうかを、ワークフローごとに選べるようにしています。この選択を実行モードと呼び、ワークフローファイルの executionMode で設定します。デフォルトは分離ブランチモード(isolated-branches)で、ここまでの節で説明してきた方式です。もう 1 つが共有ワークスペースモード(shared-workspace)です。すべてのタスクが同じワークスペースでそのまま実行され、ブランチの作成も、マージも、コミットも行われません。クイックスタートで使ったのがこのモードです。
共有ワークスペースモードでは、すべてのタスクが、デフォルトではプライマリリポジトリの現在のブランチで実行されます。結果は未コミットの変更としてワークスペースに残るので、確認したうえで自分でコミットします。このモードにレプリカは必要なく、同時に実行されるタスクの数は maxConcurrency だけで決まります。
分離を省く代わりに、変更をタスク単位で追跡したり巻き戻したりすることはできなくなります。CodeDAG は、どの変更がどのタスクのものかを区別しなくなり、タスクブランチ、自動マージ、スナップショット、破棄、やり直しは、どれもこのモードにはありません。書き込みも分離されなくなります。同時に実行中のタスクが同じファイルを変更した場合にどうなるかは、並列に実行される 2 つのタスクが同じファイルを変更した場合はどうなりますかを参照してください。そのため、コードを変更してテストを実行する仕事には、やはりデフォルトの分離ブランチモードが向いています。
最終更新