1+ //
2+ // Work item cost estimation logic.
3+ // See https://github.com/macrogreg/ProjectTrackingTemplate for info.
4+ //
5+
6+
7+ // Module imports:
18const axios = require ( 'axios' ) ;
29
10+
11+ // Configure the lookup table for the work item cost estimates.
12+ // This is a 2D table `ESTIMATED_COST_IN_DAYS[size][risk]` with cells containing the estimated effort in
13+ // working days, given the item's Size and Risk.
14+ // This doc explains how the numbers are determined:
15+ // https://github.com/macrogreg/ProjectTrackingTemplate#estimating-work-items-and-computing-time-effort
16+ function createEstimatedCostLookupTable ( ) {
17+
18+ function estimatedCostTableRow ( low , mid , high , severe ) {
19+ return {
20+ [ "LOW" ] : low ,
21+ [ "MID" ] : mid ,
22+ [ "HIGH" ] : high ,
23+ [ "SEVERE" ] : severe
24+ } ;
25+ }
26+
27+ const EstimatedCostInDays = {
28+ [ "XS" ] : estimatedCostTableRow ( 0.5 , 1 , 1.5 , 4 ) ,
29+ [ "S" ] : estimatedCostTableRow ( 2 , 3 , 4.5 , 12 ) ,
30+ [ "M" ] : estimatedCostTableRow ( 4 , 5 , 7.5 , 20 ) ,
31+ [ "L" ] : estimatedCostTableRow ( 7.5 , 10 , 15 , 40 ) ,
32+ [ "XL" ] : estimatedCostTableRow ( 15 , 20 , 30 , 80 ) ,
33+ } ;
34+
35+ return EstimatedCostInDays ;
36+ }
37+
38+ const ESTIMATED_COST_IN_DAYS = createEstimatedCostLookupTable ( ) ;
39+
40+
41+ // Read Env parameters. GH workflow calling this script should set them up.
342function loadEnvParameters ( ) {
443
544 const tokenTargetProjectRW = process . env . TOKEN_TARGET_PROJECT_RW ;
@@ -44,36 +83,23 @@ function loadEnvParameters() {
4483}
4584
4685
47- function EstimatedCostTableRow ( low , mid , high , severe ) {
48- return {
49- [ "LOW" ] : low ,
50- [ "MID" ] : mid ,
51- [ "HIGH" ] : high ,
52- [ "SEVERE" ] : severe
53- } ;
54- }
55-
56-
57- function computeEstimatedCost ( size , risk ) {
58-
59- const EstimatedCostInDays = {
60- [ "XS" ] : EstimatedCostTableRow ( 0.5 , 1 , 1.5 , 4 ) ,
61- [ "S" ] : EstimatedCostTableRow ( 2 , 3 , 4.5 , 12 ) ,
62- [ "M" ] : EstimatedCostTableRow ( 4 , 5 , 7.5 , 20 ) ,
63- [ "L" ] : EstimatedCostTableRow ( 7.5 , 10 , 15 , 40 ) ,
64- [ "XL" ] : EstimatedCostTableRow ( 15 , 20 , 30 , 80 ) ,
65- } ;
86+ // Lookup the estimated work item cost in days from the `ESTIMATED_COST_IN_DAYS` table
87+ // using the specified `size` and `risk`.
88+ // `size` comes in form: "Code (explanation)", e.g. "XS (1 ≤ day)"
89+ // `risk` comes in form: "Code: explanation", e.g. "Low: well-understood".
90+ // Codes are parsed out and used for look-ups.
91+ function lookupEstimatedCost ( size , risk ) {
6692
6793 const sizeKey = size ?. split ( '(' ) [ 0 ] . trim ( ) . toUpperCase ( ) ;
6894 const riskKey = risk ?. split ( ':' ) [ 0 ] . trim ( ) . toUpperCase ( ) ;
6995
70- if ( ! ( sizeKey in EstimatedCostInDays ) ) {
96+ if ( ! ( sizeKey in ESTIMATED_COST_IN_DAYS ) ) {
7197 const errMsg = `Invalid Size specifier: original value: "${ size } "; key: "${ sizeKey } ".` ;
7298 console . log ( errMsg ) ;
7399 throw new Error ( errMsg ) ;
74100 }
75101
76- const costTableRow = EstimatedCostInDays [ sizeKey ] ;
102+ const costTableRow = ESTIMATED_COST_IN_DAYS [ sizeKey ] ;
77103
78104 if ( ! ( riskKey in costTableRow ) ) {
79105 const errMsg = `Invalid Risk specifier: original value: "${ risk } "; key: "${ riskKey } ".` ;
@@ -82,11 +108,12 @@ function computeEstimatedCost(size, risk) {
82108 }
83109
84110 const estimate = costTableRow [ riskKey ] ;
85- // console.log(`EstimatedCostInDays [${sizeKey}][${riskKey}]: '${estimate}'`);
111+ // console.log(`ESTIMATED_COST_IN_DAYS [${sizeKey}][${riskKey}]: '${estimate}'`);
86112 return estimate ;
87113}
88114
89115
116+ // Execute a GraphGL call against the GitHub API.
90117async function graphql ( query , variables , bearerToken ) {
91118
92119 const EndpointUrl = "https://api.github.com/graphql" ;
@@ -115,6 +142,8 @@ async function graphql(query, variables, bearerToken) {
115142} ;
116143
117144
145+ // GitHub GraphGL uses entity IDs to refer to items. This function looks up the respective ID needed to run later
146+ // queries. E.g. the ID of the target project, and the ID of the estimate field.
118147async function getFieldIds ( envParams ) {
119148
120149 const ownerType = envParams . varTargetProjectOwnerType . toLowerCase ( ) ;
@@ -191,6 +220,9 @@ async function getFieldIds(envParams) {
191220}
192221
193222
223+ // Gets all project work items. Uses paging (100 batches).
224+ // This should work for projects with several 1000s of items, but we have not perf-tested it for projects with
225+ // several tens of thousands, let alone several hundreds of thousands of items.
194226async function getProjectItems ( projectId , tokenTargetProjectRW ) {
195227 const query = `
196228 query($projectId: ID!, $cursor: String) {
@@ -263,6 +295,7 @@ async function getProjectItems(projectId, tokenTargetProjectRW) {
263295}
264296
265297
298+ // Sets the estimate field of the specified item to the specified value.
266299async function updateDaysEstimate ( projectId , itemId , fieldId , newValue , tokenTargetProjectRW ) {
267300
268301 const mutation = `
@@ -292,6 +325,11 @@ async function updateDaysEstimate(projectId, itemId, fieldId, newValue, tokenTar
292325}
293326
294327
328+ // Read Env params, get IDs to use for GraphQL lookups, get all work items from the target project.
329+ // For each work item:
330+ // Get `Size` and `Risk`, compute the corresponding `Days Estimate` that the item should have;
331+ // Get the actual `Days Estimate` set on the item. If it differs from what it should be, update accordingly;
332+ // Handle counts, totals, errors.
295333async function main ( ) {
296334
297335 const envParams = loadEnvParameters ( ) ;
@@ -331,7 +369,7 @@ async function main() {
331369 continue ;
332370 }
333371
334- const estimate = computeEstimatedCost ( size , risk ) ;
372+ const estimate = lookupEstimatedCost ( size , risk ) ;
335373 console . log ( `Computed Estimate = '${ estimate ?? "<not available>" } '.` ) ;
336374
337375 const existingEstimate = fields . find ( f => f . field ?. name . toLowerCase ( ) === "days estimate" ) ?. number ;
@@ -366,6 +404,7 @@ async function main() {
366404}
367405
368406
407+ // Top-scope main function invocation:
369408main ( ) . catch ( err => {
370409 console . error ( "Script failed:" , err ) ;
371410 process . exit ( 1 ) ;
0 commit comments