Project as Codeは、プロジェクト管理ツールの設定(作業項目の種類・状態・ワークフロー・ラベル・機能の有効化)をYAMLファイルで定義し、Gitの差分とプルリクエストで変更していく手法です。プロジェクト管理ツールのPlaneが2026年1月5日に「Projects as Code」として提唱し、CLIの「Plane Compose」で実装しています。本記事は2026年10月30日時点の最新版plane-compose 0.5.2をインストールし、ログインなしで動くコマンドを実行した結果をもとに、定義、Infrastructure as Code(IaC)との違い、YAMLの書き方、CIでの適用までを説明します。なお「PaC」と略すとPolicy as Codeと区別がつかないため、本記事では略さずに書きます。
まとめ
- Project as Codeは、ツールの画面で設定していたプロジェクトの「構造」をファイルにし、Gitの履歴とレビューで管理する手法。
- この名前で公開されている実装はPlaneのCLI「Plane Compose」(PyPIのplane-compose 0.5.2・Python 3.10以上・AGPL-3.0)。
- 基本の流れは
plane auth login→plane init→ schemaを編集 →plane schema validate→plane push。リモートとの違いを見るのはplane diffで、plane statusはローカルの同期状態の表示。 - validateは日付の引用符漏れ・priorityの値・重複idをpush前に止める。一方、ワークフロー内の状態名のタイプミスは止めない。
- IaCが実行環境を作るのに対し、Project as Codeはチームの進め方を作る。リモート側の削除は、追加と更新だけの
plane schema pushと、ローカルに無い項目を消すplane schema syncで扱いが分かれる。 - 作業項目の種類はPro、ワークフローはBusiness、複数ワークフローと承認はEnterprise Gridの機能。Freeプランではコード化できる範囲が狭い。
Project as Codeの定義と対象範囲
Planeの発表ブログは、Project as Codeを「プロジェクトの運用モデルを第一級の成果物にする」ことと説明し、設定をファイルに置いてGitで版管理し、プルリクエストで変える運用を示しています。狙いはドリフト(設定の食い違い)の防止です。画面上の設定と担当者の記憶に頼っていると、同じワークフローのはずの2チームが別の動きをしたり、一時的に緩めた権限がそのまま残ったりする、とブログは例を挙げています。
4区分とPlane Composeのファイルの対応
ブログはコード化する要素をStructure・Flow・Control・Automationの4つに分けています。0.5.2の既定テンプレートで、それぞれがどのファイルに当たるかを並べました。
| 区分 | 中身 | 0.5.2での置き場所 |
|---|---|---|
| Structure | 作業項目の種類・カスタム項目 | schema/types.yaml |
| Flow | 状態・ワークフロー・遷移 | schema/states.yaml、schema/workflows.yaml |
| Control | メンバーのロール | schema/members.yaml(roleのみ) |
| Automation | イベント起点のルール | ワークスペース側のautomations.yaml |
ずれが大きいのはControlとAutomationです。members.yamlの冒頭コメントには、メンバーは通常PlaneのUIで管理し、pullで取得した内容は読み取り専用で、書き換えられるのはrole(guest・member・admin)だけとあります。自動化はプロジェクトのフォルダには入らず、plane ws でワークスペース設定を取り込んだとき、ワークスペースでautomations機能が有効な場合に限って書き出されます。ブログの構成例にあった automations/ ディレクトリは、プロジェクト側には作られません。
画面での設定変更との違い
Plane Composeは、送信済みの項目のリモートIDと内容のハッシュを .plane/state.json に記録し、ハッシュが変わっていない項目は送りません(Plane Composeの開発者ドキュメント)。Terraformの状態ファイルと同じ考え方で、YAMLの変更がそのままGitの差分になるため、状態を誰がいつ何のために足したかをコミット履歴で追えます。テンプレートからの複製でも初期状態はそろいますが、複製後に各プロジェクトが別々に変わっていくのは止められません。これを戻すのが plane upgrade で、テンプレートを既存プロジェクトに重ね直し、重なる項目はテンプレート側を優先し、プロジェクト固有の項目は残します。
「PaC」がPolicy as Codeを指す理由
検索で「PaC」や「iac pac」と入れたときに見つかるのは、多くがPolicy as Codeの話です。Policy as Codeはアクセス制御やコンプライアンスのルールを専用言語で書き、機械的に評価する手法で、代表例はOpen Policy Agent(OPA)とRegoです。IaCとPolicy as Codeは組み合わせて使うことが多く、Terraformで書いた変更計画をポリシーで検査してから適用する、という流れが典型です。HashiCorpもHCLでポリシーを書くTerraform Policyを提供しています。
Project as Codeはこれとは別物で、扱うのはプロジェクト管理ツールの設定です。Planeの公式資料も「Projects-as-code」と書き、略語を使っていません。
Infrastructure as Code(IaC)との違い
どちらも宣言的なファイルとGitを使いますが、反映先が違います。IaCはサーバーやネットワークを作り、Project as Codeは作業項目の種類や状態遷移を作ります。コマンドの役割はTerraformとよく似ており、ドキュメントも plane schema import を「Terraform-style」と説明しています。
| 観点 | IaC(Terraform) | Project as Code(Plane Compose) |
|---|---|---|
| 対象 | サーバー・ネットワーク・DB | 種類・状態・ワークフロー・ラベル |
| 初期化 | terraform init | plane init |
| 変更の事前確認 | terraform plan | plane push --dry-run |
| 適用 | terraform apply | plane push |
| 既存の取り込み | terraform import | plane schema import、plane clone |
| 状態の記録 | terraform.tfstate | .plane/state.json |
| リモート側の削除 | destroyで削除 | plane schema sync で削除 |
最後の行は注意が要ります。plane schema push は追加と更新だけで、ローカルに無いカスタム項目を消すには --prune を明示します。一方の plane schema sync は、リモートをローカルと完全に一致させるコマンドです。ヘルプには「Delete operations are not yet supported by the Plane API」「削除予定を表示するだけ」と書かれていますが、0.5.2の配布ソース(cli/schema.py)は状態・ラベル・種類の削除処理を呼び出しており、ヘルプの記述が実装と食い違っています。ソースのコメントによると、既定の種類、グループの既定状態、作業項目が割り当てられたままの状態は削除に失敗し、ワークスペース単位で種類を管理している場合は種類の削除を飛ばします。画面で足した状態やラベルをYAMLに取り込まないまま実行すると消える可能性があるため、syncの前に plane schema diff と plane schema sync --dry-run で削除対象を確かめてください。IaCの運用全体の設計はHCP Terraformの記事も参考になります。
Plane Composeのインストールと初期化
インストールとログイン
PyPIのplane-composeは0.1.0(2025年12月15日)から始まり、最新は0.5.2(2026年7月2日)です。要件はPython 3.10以上で、ドキュメントはpipxでの導入を案内しています。
pipx install plane-compose==0.5.2
plane --version # plane version 0.5.2
plane auth login # サーバーURL・認証方式・トークン・ワークスペースを順に聞かれる
plane auth list-connections
サーバーURLは空欄でPlane Cloud(api.plane.so)になり、セルフホストなら自社インスタンスのURLを入れます。認証方式は個人のトークン(pat)かワークスペース単位のトークン(workspace)で、接続情報は ~/.config/plane-compose/config.json に保存されます。ログイン前に plane init を実行すると「No workspaces connected.」と表示されて何も作られないので、最初に認証を済ませます。
plane initで作られるファイル
plane init CORE -w acme のように、プロジェクトキーとワークスペースを渡して実行します。ディレクトリ名はキーを小文字にしたもの(core/)になります。既定テンプレートから作られる主な構成は次のとおりです。
core/
plane.yaml # ワークスペース・プロジェクトキー・既定値
workspace.yaml # ワークスペースの機能設定(init時にPlaneから取得)
.plane/ # 同期状態(state.json)
schema/
states.yaml labels.yaml workflows.yaml types.yaml
features.yaml members.yaml workitem_templates.yaml page_templates.yaml
work/
workitems.yaml cycles.yaml modules.yaml milestones.yaml pages.yaml
schema/が構造、work/が日々の作業データという分け方です。テンプレートのstates.yamlにはIceboxからCancelledまで8つの状態、labels.yamlには5つのラベルが入っていますが、types.yamlとworkflows.yamlは例がコメントで書かれているだけで中身は空です。features.yamlではcycles・modules・pages・viewsがtrue、work_item_types・workflowsはfalseになっています。plane.yamlは次の形で生成されます。
type: project
workspace: acme
connection: connection-1
project:
key: CORE
name: Core
description: ""
network: public
timezone: UTC
defaults:
type: Story
workflow: default
template: default
nameはキーを先頭だけ大文字にした値(CORE→Core)で入るため、表示名は生成後に書き換えます。以降のコマンドは cd core でこのディレクトリに移ってから実行します。
schemaファイルの書き方
状態・ラベル・ワークフロー・種類の編集順
ドキュメントは「各ファイルは、それより前のファイルで定義した名前だけを参照できる」として、states.yaml → labels.yaml → workflows.yaml → types.yamlの順に編集するよう求めています。状態はbacklog・unstarted・started・completed・cancelledの5グループのどれかに属します。ドキュメントは is_default: true の状態をちょうど1つにするよう書いていますが、0.5.2の既定テンプレートは8つ全部がfalseで、plane schema validate もそのまま通ります。初期状態は自分で1つ指定してください。
承認付きワークフローのYAML
ワークフローの書式は2種類あります。initで作られるテンプレートのコメント例は、状態の一覧とtransitionsを分けて書く旧書式です。0.5.2のソースでは、clone・pull・schema pushが書き出すのは状態名をキーにした新書式(flow_type と to)に統一されています。旧書式で書き始めると、初回の同期でファイルが新書式に書き換わり、差分が大きく出ます。最初から新書式で書いておくのが無難です。
workflows:
Release Workflow:
is_active: true
work_item_types:
- Feature
states:
Ready:
flow_type: transition
to:
- In Review: {}
In Review:
flow_type: approval
to:
- Done:
approvers: [[email protected]]
required_approvals: 1
rejection_state: Ready
Done: {}
flow_type はtransition(承認なしで、to に並べた状態へ移る)かapproval(approversの承認を経て移る)の2値で、approve と書くと plane schema validate が「type must be ‘transition’ or ‘approval’」で失敗します。flow_type は状態単位の設定なので、同じ状態から出る遷移はすべて同じ種別になります。遷移を書かなければ、どの状態へも移動できます。
見落としやすいのは状態名のタイプミスです。上の例の Done を Shiped に変えても plane schema validate は成功し、状態の数が8から9に増えます。states.yamlに無い名前は、グループunstartedの新しい状態として扱われるためです。レビューでは、validateが通ったかだけでなく「States」の件数が想定どおりかも確認してください。
push前のYAML検証とエラー例
検証コマンドは2つあります。plane schema validate はschemaファイルだけを、Planeに接続せずに確認します。plane validate はwork/workitems.yamlの作業項目を検証し、既定ではPlaneからスキーマを取得して種類・状態・ラベルの実在まで確かめます。--offline を付けると接続せずに済みますが、その3つの確認は省かれます。0.5.2で plane validate --offline に通した結果は次のとおりです。
| workitems.yamlの記述 | 結果 | 終了コード |
|---|---|---|
| due_date: 2026-11-01(引用符なし) | エラー(Input should be a valid string) | 1 |
| due_date: 2026/11/01 | エラー(Date must be in YYYY-MM-DD format) | 1 |
| priority: urgentish | エラー(有効値はhigh・low・medium・none・urgent) | 1 |
| 同じidを2件 | エラー(Duplicate ID) | 1 |
| idを書かない | 警告のみ | 0 |
YAMLの日付と引用符
形式が正しいはずの 2026-11-01 が落ちるのは、YAMLの読み込みで日付型に変換され、文字列を期待する検証に合わなくなるためです。テンプレートのcycles.yamlの例も start_date: '2024-01-01' と引用符付きで書かれています。due_date・start_date・cycleの日付は、すべて引用符で囲んでください。
作業項目の固定idと重複作成の防止
idの無い作業項目は検証を通りますが、「Editing this item’s title will create a duplicate on next push」という警告が出ます。idが無いとタイトルで照合するため、タイトルを直すと別の作業項目として新規作成されます。Gitで管理する以上、作業項目には変わらない識別子を付けておきます。次の例のFeatureとChoreは、types.yamlのコメントにあるとおりPlaneが自動で作る基本の種類(Bug・Feature・Task・Chore・Improvement)です。
workitems:
- id: auth-oauth2
title: OAuth2 login
type: Feature
state: Ready
priority: high
labels: [Backend]
- id: ci-setup
title: Setup CI
type: Chore
state: Ready
priority: urgent
due_date: '2026-11-01'
CIでの構成変更の確認と適用
Plane ComposeはCI向けに、非対話のログインと結果別の終了コードを用意しています。plane push --exit-code は0=変更なし、1=エラー、2=変更を適用(--dry-run 時は変更を検出)を返し、plane diff --exit-code は0=一致、1=エラー、2=差分ありを返します。GitHub Actionsのbashは終了コード0以外を失敗として止めるので、2を成功として扱う書き方が要ります。プルリクエストでは事前確認だけを行い、mainへのマージ後に適用する構成の例です。前提は、プロジェクトが一度 plane schema push 済みでplane.yamlにUUIDが入っていること、リポジトリのSecretsにPLANE_TOKENを登録していることの2つです。フォークからのプルリクエストにはSecretsが渡らないため、ログインの段階で失敗します。
name: plane-compose
on:
pull_request:
paths: ['projects/core/**']
push:
branches: [main]
paths: ['projects/core/**']
jobs:
plane:
runs-on: ubuntu-latest
defaults:
run:
working-directory: projects/core
steps:
- uses: actions/checkout@v4
- run: pipx install plane-compose==0.5.2
- run: plane auth login --server-url https://api.plane.so --auth-type pat --token "${{ secrets.PLANE_TOKEN }}" --workspace acme
- run: plane schema validate
- run: plane validate --offline
- if: github.event_name == 'pull_request'
run: plane push --dry-run --exit-code || [ $? -eq 2 ]
- if: github.event_name == 'push'
run: plane push --force --no-conflict-check --exit-code || [ $? -eq 2 ]
作業項目の検証を --offline にしているのは、接続したときの plane validate が変更前のPlane側スキーマを参照し、同じプルリクエストで追加した種類や状態を未定義として扱うためです。参照名の整合はレビューと plane push --dry-run の結果で確かめます。版は ==0.5.2 のように固定します。0.x系で、0.4.0(2026年5月11日)から0.5.2(7月2日)までの2か月足らずに、rc版を除いて8回リリースされているためです。--no-conflict-check はpush前にリモートとの衝突を確かめるAPI呼び出しを省くオプションで、ドキュメントは衝突が起きない前提のCI向けとしています。画面からの直接編集を許している組織では外してください。
画面で直接変えられた設定を見つけるには、定期実行のジョブで、スキーマは plane schema diff --exit-code、作業項目は plane diff --exit-code を回します。plane diff は作業項目を、new_local・modified_local・modified_remote・conflicts・in_sync・deleted_remoteの6区分に分けて表示します。modified_remoteが出たら、plane pull で取り込んでコミットするか、YAMLの内容でpushし直すかを決めます。Gitを正とする運用はGitOpsと同じ考え方です。
発表時の説明と0.5.2の実装の違い
ブログやLPの説明だけで手順を組むと、0.5.2では動かない箇所があります。ヘルプとソースで確認できた違いは次の4点です。
| 項目 | 発表ブログ・LP | 0.5.2の実装 |
|---|---|---|
| plane status | pushで何が変わるかを表示 | ローカルの同期状態を表示 |
| リモートとの比較 | 記載なし | plane diff |
| 自動化の置き場所 | プロジェクト内のautomations/ | ワークスペース側(plane ws) |
| plane schema pull | LPに掲載 | 存在しない(No such command) |
plane status はPlaneに接続せず、.plane/state.jsonと手元のファイルを比べます。未ログインで実行しても「Never synced」「Pending local changes 2」のように表示されるため、リモートの最新状態を反映していません。Planeとの違いを確かめるのは plane diff(作業項目)と plane schema diff(スキーマ)です。LPにある plane schema pull の代わりには、plane pull --schema-only か plane schema import --merge を使います。
採用を見送るべき条件
Plane ComposeはPlane専用で、JiraやBacklogなど他のツールの設定は扱えません。Jiraのプロジェクトを同じ考え方で管理したい場合、この記事の手順はそのまま使えません。Planeを使っている場合でも、次のどれかに当てはまるなら、全面的なコード化は見送るか、状態とラベルだけに絞って始めるほうが割に合います。
- Freeプランで使っている:Planeの料金ページでは、作業項目の種類とプロパティ(Work Item Types and Properties)がPro、ワークフロー(Single Workflow)がBusiness、作業項目の種類ごとの複数ワークフローと遷移ごとの承認者(Multiple Workflows + Approvals)がEnterprise Gridの機能です。Freeでコード化できるのは、状態・ラベル・機能の切り替え・作業項目が中心になります。
- 権限をコードで縛りたい:メンバーの追加や削除は画面で管理する前提で、YAMLで変えられるのはroleだけです。権限の監査が目的なら、Plane側の監査ログと併用する必要があります。
- 単発の小さなプロジェクト:同じ構成を繰り返し立ち上げる予定が無ければ、テンプレート化の手間を回収できません。
- メンバーの多くがGitに不慣れ:変更のたびにプルリクエストを要求すると、画面で直す運用に戻りがちで、かえってドリフトが増えます。
ライセンスはAGPL-3.0-or-laterです。AGPL-3.0は、改変版を配布する場合に加え、改変版をネットワーク越しに他者へ使わせる場合にもソースの提供を求めます(第13条)。改変して組み込むときは、この条件を確認してください。また、PyPIがHomepageとして示すGitHubリポジトリ(makeplane/compose)は2026年10月30日の取得で404を返し、ソースを確認できませんでした。挙動を読むときは、PyPIのwheelに同梱されたソースを見ることになります。ツールを入れる前に、チームの進め方そのものを標準化したい段階であれば、要件定義・RFP作成・PMO支援のように運用ルールを先に決める進め方もあります。
よくある質問
Project as Codeとは何ですか?
プロジェクト管理ツールの設定(作業項目の種類・状態・ワークフロー・ラベルなど)をYAMLで定義し、Gitで版管理してプルリクエストで変更する手法です。Planeが2026年1月に提唱し、CLIのPlane Composeで実装しています。
PaCは何の略ですか?
一般にはPolicy as Code(ポリシーのコード化)を指し、OPAやRegoの文脈で使われます。Project as Codeを指して略すこともありますが、Planeの公式資料は略語を使っていないため、フルスペルで書くほうが誤解がありません。
IaCとPaCの違いは何ですか?
PaCをPolicy as Codeと読む場合、IaCはインフラを作るコード、Policy as Codeはその変更を検査するルールで、組み合わせて使います。Project as Codeと読む場合は対象が違い、IaCはサーバーやネットワーク、Project as Codeはプロジェクト管理ツールの設定を扱います。
セルフホスト版のPlaneでも使えますか?
使えます。plane auth login でサーバーURLを聞かれたときに、自社インスタンスのURLを入れます。CIでは --server-url オプションで指定します。
既存のPlaneプロジェクトをコード管理に移すには?
plane clone acme/CORE のようにワークスペースとプロジェクトキーを指定すると、plane.yaml・schema/・work/がまとめて作られます。作業項目が多いプロジェクトでは --schema-only を付けて構造だけを取り込み、Gitにコミットしてから運用を始めると扱いやすくなります。