2022-02-17 06:06:36 +00:00
module . exports = {
friendlyName : 'Deliver estimation report' ,
description : 'Send estimation report to Slack.' ,
exits : {
success : {
description : 'It worked. The estimation report was sent to Slack.'
} ,
} ,
fn : async function ( ) {
// ██████╗ ███████╗████████╗ ██████╗ ███████╗██████╗ ██████╗ ██████╗ ████████╗
// ██╔════╝ ██╔════╝╚══██╔══╝ ██╔══██╗██╔════╝██╔══██╗██╔═══██╗██╔══██╗╚══██╔══╝
// ██║ ███╗█████╗ ██║ ██████╔╝█████╗ ██████╔╝██║ ██║██████╔╝ ██║
// ██║ ██║██╔══╝ ██║ ██╔══██╗██╔══╝ ██╔═══╝ ██║ ██║██╔══██╗ ██║
// ╚██████╔╝███████╗ ██║ ██║ ██║███████╗██║ ╚██████╔╝██║ ██║ ██║
// ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
//
sails . log ( 'Getting estimation report...' ) ;
if ( ! sails . config . custom . githubAccessToken ) {
throw new Error ( 'No GitHub access token configured! (Please set `sails.config.custom.githubAccessToken`.)' ) ;
} //•
let baseHeaders = {
'User-Agent' : 'fleet story points' ,
'Authorization' : ` token ${ sails . config . custom . githubAccessToken } `
} ;
let estimationReport = { } ;
// Fetch projects
2022-06-09 05:08:13 +00:00
let projects = await sails . helpers . http . get ( ` https://api.github.com/orgs/fleetdm/projects ` , { } , baseHeaders ) . retry ( ) ; // let projects = [];// « hack if you get rate limited and want to test beta projets
2022-02-17 06:06:36 +00:00
// This nasty little hack mixes in new "beta" projects that are part of Github Projects 2.0 (beta)
// but makes them look like normal projects from the actually-documented GitHub REST API.
// > [?] https://docs.github.com/en/enterprise-cloud@latest/issues/trying-out-the-new-projects-experience/using-the-api-to-manage-projects#finding-the-node-id-of-a-field
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// PS. In case you have to do anything with graphql ever again, try uncommenting this.
// ```
// console.log(
// require('util').inspect(
// await sails.helpers.http.post(`https://api.github.com/graphql`,{
// query:'{organization(login: "fleetdm") {projectsNext(first: 20) {nodes {id databaseId title fields(first: 20) {nodes {id name settings}} items(first: 20) {nodes{title id fieldValues(first: 8) {nodes{value projectField{name}}} content{...on Issue {repository{name} labels(first:20) {nodes{name}} assignees(first: 10) {nodes{login}}}}}} }}}}'
// }, baseHeaders),
// {depth:null}
// )
// );
// console.log();
// console.log();
// console.log('-0--------------');
// console.log();
// // return;
// ```
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
let graphqlHairball = await sails . helpers . http . post ( ` https://api.github.com/graphql ` , {
query : '{organization(login: "fleetdm") {projectsNext(first: 20) {nodes {id databaseId title fields(first: 20) {nodes {id name settings}} items(first: 20) {nodes{title id fieldValues(first: 8) {nodes{value projectField{name}}} content{...on Issue {repository{name} labels(first:20) {nodes{name}} assignees(first: 10) {nodes{login}}}}}} }}}}'
2022-06-09 05:08:13 +00:00
} , baseHeaders ) . retry ( ) ;
2022-02-17 06:06:36 +00:00
projects = projects . concat (
graphqlHairball . data . organization . projectsNext . nodes . map ( ( betaProject ) => ( {
_isBetaProject : true , // « we need this because some APIs only work for one kind of project or the other
_betaProjectColumns : JSON . parse ( _ . find ( betaProject . fields . nodes , { name : 'Status' } ) . settings ) . options . map ( ( betaColumn ) => ( {
_isBetaColumn : true ,
_betaStatusId : betaColumn . id ,
name : betaColumn . name ,
} ) ) ,
_betaProjectCards : betaProject . items . nodes . filter ( ( betaCard ) => (
betaCard . content && betaCard . content . labels && betaCard . content . labels . nodes &&
betaCard . fieldValues && _ . find ( betaCard . fieldValues . nodes , ( fieldValueNode ) => fieldValueNode . projectField . name === 'Status' )
) ) . map ( ( betaCard ) => ( {
_isBetaCard : true ,
_betaStatusId : _ . find ( betaCard . fieldValues . nodes , ( fieldValueNode ) => fieldValueNode . projectField . name === 'Status' ) . value ,
labels : betaCard . content . labels . nodes
} ) ) ,
name : betaProject . title , // « it's been renamed for some reason
node _id : betaProject . id , // eslint-disable-line camelcase
id : betaProject . databaseId // « the good ole ID for the rest of us ("node_id" is the graphql ID)
} ) )
) ; // </hack>
// console.log(require('util').inspect(projects, {depth:null}));
// return;
await sails . helpers . flow . simultaneouslyForEach ( projects , async ( project ) => {
estimationReport [ project . name ] = { } ;
let columns ;
if ( ! project . _isBetaProject ) {
2022-06-09 05:08:13 +00:00
columns = await sails . helpers . http . get ( ` https://api.github.com/projects/ ${ project . id } /columns ` , { } , baseHeaders ) . retry ( ) ;
2022-02-17 06:06:36 +00:00
} else {
columns = project . _betaProjectColumns ; // [?] https://docs.github.com/en/enterprise-cloud@latest/graphql/reference/objects#projectnextitem
}
await sails . helpers . flow . simultaneouslyForEach ( columns , async ( column ) => {
estimationReport [ project . name ] [ column . name ] = 0 ;
let cards ;
if ( ! project . _isBetaProject ) {
2022-06-09 05:08:13 +00:00
cards = await sails . helpers . http . get ( ` https://api.github.com/projects/columns/ ${ column . id } /cards ` , { } , baseHeaders ) . retry ( ) ;
2022-02-17 06:06:36 +00:00
} else {
cards = project . _betaProjectCards . filter ( ( betaCard ) => betaCard . _betaStatusId === column . _betaStatusId ) ;
}
await sails . helpers . flow . simultaneouslyForEach ( cards , async ( card ) => {
// Get the number of story points associated with this card.
let numPoints = 0 ;
let labels ;
if ( ! project . _isBetaProject ) {
if ( ! card . content _url ) {
// ignore "notes" (FUTURE: Maybe add some kind of sniffing for a prefix like "[5]")
labels = [ ] ;
} else {
2022-06-09 05:08:13 +00:00
let issue = await sails . helpers . http . get ( card . content _url , { } , baseHeaders ) . retry ( ) ;
2022-02-17 06:06:36 +00:00
labels = issue . labels ;
}
} else {
labels = card . labels ;
}
let pointLabels = labels . filter ( ( label ) => Number ( label . name ) >= 1 && Number ( label . name ) < Infinity ) ;
if ( pointLabels . length >= 2 ) { throw new Error ( ` Cannot have more than one story point label, but this card ${ require ( 'util' ) . inspect ( card , { depth : null } )} seems to have more than one: ${ _ . pluck ( pointLabels , 'name' ) } ` ) ; }
if ( pointLabels . length === 0 ) {
numPoints = 0 ;
} else {
numPoints = Number ( pointLabels [ 0 ] . name ) ;
}
estimationReport [ project . name ] [ column . name ] += numPoints ;
} ) ; //∞
} ) ; //∞
} ) ; //∞
// ██████╗ ██████╗ ███████╗████████╗ ████████╗ ██████╗
// ██╔══██╗██╔═══██╗██╔════╝╚══██╔══╝ ╚══██╔══╝██╔═══██╗
// ██████╔╝██║ ██║███████╗ ██║ ██║ ██║ ██║
// ██╔═══╝ ██║ ██║╚════██║ ██║ ██║ ██║ ██║
// ██║ ╚██████╔╝███████║ ██║ ██║ ╚██████╔╝
// ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝
//
// ███████╗██╗ █████╗ ██████╗██╗ ██╗
// ██╔════╝██║ ██╔══██╗██╔════╝██║ ██╔╝
// ███████╗██║ ███████║██║ █████╔╝
// ╚════██║██║ ██╔══██║██║ ██╔═██╗
// ███████║███████╗██║ ██║╚██████╗██║ ██╗
// ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
//
sails . log ( 'Delivering estimation report to Slack...' ) ;
2022-02-18 22:10:41 +00:00
if ( ! sails . config . custom . slackWebhookUrlForGithubEstimates ) {
2022-02-17 06:06:36 +00:00
throw new Error (
2022-02-18 22:10:41 +00:00
'Estimation report not delivered: slackWebhookUrlForGithubEstimates needs to be configured in sails.config.custom. Here\'s the undelivered report: ' +
2022-02-17 06:06:36 +00:00
` ${ require ( 'util' ) . inspect ( estimationReport , { depth : null } )} `
) ;
} else {
2022-02-18 22:10:41 +00:00
await sails . helpers . http . post ( sails . config . custom . slackWebhookUrlForGithubEstimates , {
2022-02-17 06:06:36 +00:00
text : ` New estimation report: \n ${ require ( 'util' ) . inspect ( estimationReport , { depth : null } )} `
2022-06-09 05:08:13 +00:00
} ) . retry ( ) ;
2022-02-17 06:06:36 +00:00
}
}
} ;