GitHub Projectsの見積もりから完了日を予測するマシンを作ったけど運用に乗らなかった話
nocoの取締役CPOの多田と申します。
プロダクトマネジメントからアーキテクチャー設計まで業務は多岐にわたりますが、今回はプロジェクト管理の試行錯誤について紹介します。
背景
nocoではNotionとGitHub Projectsを使って開発アイテムを整理しています。
開発着手前の設計の段階ではNotionで案件をまとめ、着手できる粒度まで煮詰まったらGitHubにIssueを作成する流れです。
もともと別プロジェクトではZenHubを利用していましたが、新しいプロジェクトが始まり、そういった管理ツールは何も入れていない状態です。
見積もりから未来を予測する
私たちの開発チームはスクラムを採用していますが、GItHub Projectsを使えば自動的にベロシティを計測できます。
これだけでは「このマイルストーンが終わるのはいつなのか」「12月までにどのあたりまでバックログを消化できるのか」は簡単にはわかりませが、手持ちの情報としては以下の3つがあります。
ベロシティの履歴
優先順位の付いたバックログ
各アイテムの見積もり
これらの情報があれば原理的には「このアイテムは◯◯月ごろには終わる」という予測が立てられるはずです。
そこで、GitHub Projectsのデータからアイテムごとの「完了日」を予測するGitHub Actionsスクリプトを作りました。
過去のスプリントからベロシティを計算し、バックログの見積もりと比較することで、「個々のアイテムの完了予測日」を計算し、GitHub Projectsに書き出すようになっています。
実際のコード
まずは、GitHub Projectsのデータを取得します。
GitHub Projectsのデータは GraphQL API を使わないと取得できないので、以下のようなクエリを組み立てます。
query ($projectId: ID!, $endCursor: String) {
node(id: $projectId) {
... on ProjectV2 {
items(first: 100, after: $endCursor) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
fieldValues(first: 50) {
nodes {
... on ProjectV2ItemFieldTextValue {
text
field {
... on ProjectV2FieldCommon {
id
name
}
}
}
... on ProjectV2ItemFieldNumberValue {
number
field {
... on ProjectV2FieldCommon {
id
name
}
}
}
... on ProjectV2ItemFieldIterationValue {
title
startDate
duration
iterationId
field {
... on ProjectV2FieldCommon {
id
name
}
}
}
... on ProjectV2ItemFieldDateValue {
date
field {
... on ProjectV2FieldCommon {
id
name
}
}
}
... on ProjectV2ItemFieldSingleSelectValue {
name
field {
... on ProjectV2FieldCommon {
id
name
}
}
}
}
}
}
}
}
}
}
これを jq をつかっていい感じに変換します。
フィールド名は環境に合わせで変更が必要です。
jq ".data.node.items.nodes[]
| {
id: .id,
title: ((.fieldValues.nodes[] | select(.field.name == \"Title\") | .text)),
status: ((.fieldValues.nodes[] | select(.field.name == \"Status\") | .name)//null),
estimation: ((.fieldValues.nodes[] | select(.field.name == \"Estimate\") | .number)//null),
iteration: ((.fieldValues.nodes[] | select(.field.name == \"Iteration\") | {
title: .title,
startDate: .startDate,
duration: .duration,
})//null),
}
"
処理のコアとなるのは、以下のコードです。
export type IIteraion = {
startDate: Date;
duration: number;
pointCompleted: number;
};
// Calculate the velocity per day of the project
// return mean / standard deviation of velocity
export const calcVelocityPerDay = (iterations: IIteraion[]): Velocity => {
const mean =
iterations.reduce((acc, iteration) => {
const businessDays = calcBusinessDays(
iteration.startDate,
iteration.duration
);
return acc + iteration.pointCompleted / businessDays;
}, 0) / iterations.length;
const std = Math.sqrt(
iterations.reduce((acc, iteration) => {
const businessDays = calcBusinessDays(
iteration.startDate,
iteration.duration
);
return acc + Math.pow(iteration.pointCompleted / businessDays - mean, 2);
}, 0) / iterations.length
);
return {
mean,
std,
};
};
export type IItem = {
estimation: number;
};
export type IVelocity = {
mean: number;
std: number;
};
export type IItemWithPrediction<T> = {
item: T;
prediction: Prediction;
};
// Calculate the number of days to complete the project
// using the worst velocity (mean - std).
export const predictNumDaysToComplete = <T extends IItem>(
items: T[],
velocity: IVelocity
): IItemWithPrediction<T>[] => {
let numDaysToComplete = 0;
const worstVelocity = velocity.mean - velocity.std;
const itemWithPredictions = items.map((item) => {
numDaysToComplete += item.estimation / worstVelocity;
const dateToComplete = addBusinessDays(new Date(), numDaysToComplete);
return {
item,
prediction: {
numDaysToComplete,
dateToComplete,
// Convert to YYYY-MM-DD
dateToCompleteString: dateToComplete.toISOString().split("T")[0],
},
};
});
return itemWithPredictions;
};
GitHub API から取得した、GitHub Projectsのデータを使い、calcVelocityPerDay によって「1日あたりの平均ベロシティ」を計算します。これはイテレーションの日数が祝日などの影響で変化しても、その分、分母となる稼働日を変えることでばらつきを抑える目的があります。
その後 predictNumDaysToComplete によってアイテムごとに完了日を加算していきます。ここで、「アイテムの予測完了期間=見積もり / 1日あたりの最悪ベロシティ」という計算をしますが、「1日あたりの最悪ベロシティ = 1日あたりの平均ベロシティ - ベロシティの標準偏差」という補正を加えることで、「開発スピードの不安定さ」を加味しています。
あとは、この計算結果をGItHub Projectsに書き込むだけです。
mutation ($projectId: ID!, $itemId: ID!, $fieldId: ID!, $date: Date!) {
updateProjectV2ItemFieldValue(
input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: { date: $date }
}
) {
projectV2Item {
id
}
}
}
運用を始めて半年経過したが、あまり活用されていない
と、このように色々工夫して、GitHub Actionsで定期実行するように設定し、半年ほど運用を続けましたが、いまいちチームで活用はされませんでした。
根本的な理由は、GitHubのUI上でタスクの整理(並び替えや統廃合)がしにくく、バックログの管理が難しくなっていることです。
GitHub Projects の View では、「担当者ごと」「イテレーションごと」とグループ化してタスクを並べる事ができますが、この状態だとグローバルな「並び順」を変更するのが面倒です。
また、Table Viewでは一つずつカードを並び替えることはできますが、複数個を一括で移動したり、キーボード操作で移動することもできません。
さらに、過去の経緯で「開発が未定だがメモのためにIssueにしておく」という運用があり、これによって「バックログの純度」が低い状態が続いています。
バックログは100件以下が望ましいと言われますが、現時点で300個以上あり、並び替えに対する心理的コストが大きくなってしまっているという理由もあります。
元々思いつきで休日プログラミングで作った仕組みではあるものの、バックログマネジメントをもう少し頑張ればうまく運用できそうな気配もあるので、どうにか改善したいところです。
まとめ
ベロシティから完了日を予測する仕組みを作りましたが、現時点ではうまくチームで活用はされていません。
しかもおそらく原因はバックログのメンテナンスをサボりがちな私にあります。
まずは、指針通り「バックログを100個以下にする」「タスクの並び順、粒度を整理し続ける」といった基本をこなしていきたいと考えています。
また、Linearといったモダンなプロジェクト管理ツールも出てきていて、近くチームで検討予定です。
今回のスクリプトは、TypeScryptとGraphQLとbashでできていて、コメントが多かったらGitHub Actionsとして切り出して公開します。