From b732545e466b2d8d3046542fd2a40eb6ba887296 Mon Sep 17 00:00:00 2001 From: jpklein Date: Sat, 6 Sep 2014 10:43:01 -0400 Subject: [PATCH 01/13] Add settings for rally cron job --- scripts/config/rallycron.conf.php | 4 ++++ scripts/include/rally.php | 21 +++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 scripts/config/rallycron.conf.php diff --git a/scripts/config/rallycron.conf.php b/scripts/config/rallycron.conf.php new file mode 100644 index 0000000..bbd2816 --- /dev/null +++ b/scripts/config/rallycron.conf.php @@ -0,0 +1,4 @@ +text, $payload->attachments); - + if($result=='Invalid channel specified'){ die("Sorry, the rallyme command can't post messages to your private chat.\n"); } - + if($result!="ok"){ print_r($result."\n"); print_r(json_encode($payload)); @@ -71,11 +72,11 @@ function postit($channel_name, $payload, $attachments){ global $config, $slackCommand; return slack_incoming_hook_post_with_attachments( - $config['slack']['hook'], - $config['rally']['botname'], - $slackCommand->ChannelName, - $config['rally']['boticon'], - $payload, + $config['slack']['hook'], + $config['rally']['botname'], + $slackCommand->ChannelName, + $config['rally']['boticon'], + $payload, $attachments); } @@ -169,7 +170,7 @@ function GetDefectPayload($ref) array_push($fields,$firstattachment); global $slackCommand; - + $userlink = BuildUserLink($slackCommand->UserName); $user_message = "Ok, {$userlink}, here's the defect you requested."; @@ -265,7 +266,7 @@ function GetRequirementPayload($ref) if($blocked) array_push($fields, MakeField("blocked",$blockedreason,true)); - + array_push($fields, MakeField("description",$short_description,false)); if($firstattachment!=null) From cbd19baeaa8115682ecfbe515a50b500cf3cb355 Mon Sep 17 00:00:00 2001 From: jpklein Date: Sat, 6 Sep 2014 10:50:02 -0400 Subject: [PATCH 02/13] Add Slack utility functions --- scripts/include/slack.php | 54 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/scripts/include/slack.php b/scripts/include/slack.php index 38639a2..7a1dc21 100644 --- a/scripts/include/slack.php +++ b/scripts/include/slack.php @@ -29,11 +29,35 @@ function BuildSlashCommand($request) return $cmd; } +//text-formatting functions + +function SanitizeText($text) +{ + $text = strtr($text, array('
' => '\n', '
' => '\n', '

' => '\n')); + return html_entity_decode(strip_tags($text), ENT_HTML401 | ENT_COMPAT, 'UTF-8'); +} + +function l($text, $url) +{ + return '<' . $url . '|' . $text . '>'; +} + +function em($text) +{ + return '_' . $text . '_'; +} + +function b($text) +{ + return '*' . $text . '*'; +} + +//posting functions function slack_incoming_hook_post($uri, $user, $channel, $icon, $emoji, $payload){ - + $data = array( - "text" => $payload, + "text" => $payload, "channel" => "#".$channel, "username"=>$user ); @@ -53,18 +77,40 @@ function slack_incoming_hook_post($uri, $user, $channel, $icon, $emoji, $payload return curl_post($uri, $data_string); } - +function SendIncomingWebHookMessage($channel, $payload, $attachments) +{ + global $config; + + //allow bot to display formatted attachment text + $attachments->mrkdwn_in = ['pretext', 'text', 'title', 'fields']; + + $reply = slack_incoming_hook_post_with_attachments( + $config['slack']['hook'], + $config['rally']['botname'], + $channel, + $config['rally']['boticon'], + $payload, + $attachments + ); + + $success = ($reply == 'ok'); + if (!$success) { + trigger_error('Unable to send Incoming WebHook message: ' . $reply); + } + return $success; +} function slack_incoming_hook_post_with_attachments($uri, $user, $channel, $icon, $payload, $attachments){ $data = array( - "text" => $payload, + "text" => $payload, "channel" => "#".$channel, "username"=>$user, "icon_url"=>$icon, "attachments"=>array($attachments)); $data_string = "payload=" . json_encode($data, JSON_HEX_AMP|JSON_HEX_APOS|JSON_NUMERIC_CHECK|JSON_PRETTY_PRINT); + $data_string = strtr($data_string, array('\\\\n' => '\n')); //unescape slashes in newline characters mylog('sent.txt',$data_string); return curl_post($uri, $data_string); } From 22fdf5e30327859ea4c29f485f4465f67f02a41e Mon Sep 17 00:00:00 2001 From: jpklein Date: Sat, 6 Sep 2014 11:03:19 -0400 Subject: [PATCH 03/13] Define rally cron job's main routine --- scripts/include/rallycron.inc.php | 6 ++++++ scripts/rallycron.php | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 scripts/include/rallycron.inc.php create mode 100644 scripts/rallycron.php diff --git a/scripts/include/rallycron.inc.php b/scripts/include/rallycron.inc.php new file mode 100644 index 0000000..c87d906 --- /dev/null +++ b/scripts/include/rallycron.inc.php @@ -0,0 +1,6 @@ + Date: Sat, 6 Sep 2014 15:03:19 -0400 Subject: [PATCH 04/13] WIP! Fetch new and significantly-updated artifacts from Rally --- scripts/include/rallycron.inc.php | 112 ++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/scripts/include/rallycron.inc.php b/scripts/include/rallycron.inc.php index c87d906..e63586d 100644 --- a/scripts/include/rallycron.inc.php +++ b/scripts/include/rallycron.inc.php @@ -4,3 +4,115 @@ require('slack.php'); require('rallyme.config.php'); require('rally.php'); + +function FetchUpdatedRallyArtifacts($since) +{ + global $RALLY_URL, $RALLY_PROJECT_ID, $RALLY_TIMESTAMP_FORMAT; + + $api_url = $RALLY_URL . 'slm/webservice/v2.0/'; + $query_url = $api_url . 'artifact?query=((Project.ObjectID+%3D+' . $RALLY_PROJECT_ID . ')AND(LastUpdateDate+>+' . $since . '))&fetch=CreationDate,FormattedID,LastUpdateDate,Owner,Ready,RevisionHistory,ScheduleState,SubmittedBy&order=LastUpdateDate+asc&pagesize=200'; +var_dump($query_url); + $results = CallAPI($query_url); + $results = $results->QueryResult->Results; + + $project_url = $RALLY_URL . '#/' . $RALLY_PROJECT_ID; + + $items = array(); + foreach ($results as $Result) { +var_dump('processing ' . $Result->FormattedID . ' ' . $Result->_refObjectName); + $user = ''; + switch ($type = $Result->_type) { + case 'Defect': + $path = '/detail/defect/'; + $user = $Result->SubmittedBy->_refObjectName; + break; + case 'HierarchicalRequirement': + $type = 'User Story'; + $path = '/detail/userstory/'; + break; + case 'TestCase': + $type = 'Test Case'; + $path = '/detail/testcase/'; + break; + default: +var_dump('-> skipping unimportant new artifact'); + continue 2; //don't display other artifact types + } + if (empty($user) && isset($Result->Owner)) { + $user = $Result->Owner->_refObjectName; + } + + //was the artifact just created? + $lastUpDate = date_create_from_format($RALLY_TIMESTAMP_FORMAT, $Result->LastUpdateDate)->getTimestamp(); + $creationDate = date_create_from_format($RALLY_TIMESTAMP_FORMAT, $Result->CreationDate)->getTimestamp(); + + if (($lastUpDate - $creationDate) < 2) { //assume items updated within 1 sec haven't changed state +var_dump('-> reporting newly-created artifact'); + $items[] = array( //report newly-created artifacts + 'type' => $type, + 'title' => $Result->_refObjectName, + 'url' => $project_url . $path . basename($Result->_ref), + 'user' => $user, + 'id' => $Result->FormattedID + ); + + } elseif ($type == 'User Story') { //track progress of user stories + switch ($Result->ScheduleState) { + case 'Completed': + $fact_table = array(1 => 'SCHEDULE STATE changed'); + $state = 'acceptance-ready'; + break; + case 'In-Progress': + if ($Result->Ready) { + $fact_table = array(1 => 'READY changed from [false] to [true]'); + $state = 'verification-ready'; + } else { + $fact_table = array( + 0 => 'SCHEDULE STATE changed', + 1 => 'READY changed from [true] to [false]' + ); + $state = 'needs-work'; + } + break; + default: +var_dump('-> skipping unimportant status'); + continue 2; //don't parse other state changes + } + + //parse latest revision messages to verify state change + $query2_url = $Result->RevisionHistory->_ref . '/Revisions?query=(CreationDate+>+' . $since . ')&fetch=CreationDate,Description,User'; + $statusResults = CallAPI($query2_url); + + $is_verified = FALSE; + foreach ($statusResults->QueryResult->Results as $Revision) { + if (isset($fact_table[0]) && (strpos($Revision->Description, $fact_table[0]) !== FALSE)){ +var_dump('-> poisoning the well'); + continue 2; //stop parsing if the negative fact has appeared + } + if (strpos($Revision->Description, $fact_table[1]) !== FALSE) { + $is_verified = TRUE; + $user = $Revision->User->_refObjectName; + } + } + if (!$is_verified) { +var_dump('-> skipping unconfirmed state change'); + continue; //skip artifacts with unconfirmed state change + } +var_dump('-> reporting artifact state change'); + $items[] = array( //report stories that have changed state + 'type' => $type, + 'title' => $Result->_refObjectName, + 'url' => $project_url . $path . basename($Result->_ref), + 'user' => $user, + 'id' => $Result->FormattedID, + 'state' => $state //presence of this key indicates state-change notification + ); + } + } + return $items; +} + +function SendRallyUpdateNotifications($items) +{ + +} From 76721732ac66d580647424ca1b1d27edee1568bd Mon Sep 17 00:00:00 2001 From: jpklein Date: Sat, 6 Sep 2014 20:56:47 -0400 Subject: [PATCH 05/13] Remove troubleshooting code from rally fetch --- scripts/include/rallycron.inc.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/scripts/include/rallycron.inc.php b/scripts/include/rallycron.inc.php index e63586d..ab8e3ef 100644 --- a/scripts/include/rallycron.inc.php +++ b/scripts/include/rallycron.inc.php @@ -11,7 +11,7 @@ function FetchUpdatedRallyArtifacts($since) $api_url = $RALLY_URL . 'slm/webservice/v2.0/'; $query_url = $api_url . 'artifact?query=((Project.ObjectID+%3D+' . $RALLY_PROJECT_ID . ')AND(LastUpdateDate+>+' . $since . '))&fetch=CreationDate,FormattedID,LastUpdateDate,Owner,Ready,RevisionHistory,ScheduleState,SubmittedBy&order=LastUpdateDate+asc&pagesize=200'; -var_dump($query_url); + $results = CallAPI($query_url); $results = $results->QueryResult->Results; @@ -19,7 +19,7 @@ function FetchUpdatedRallyArtifacts($since) $items = array(); foreach ($results as $Result) { -var_dump('processing ' . $Result->FormattedID . ' ' . $Result->_refObjectName); + $user = ''; switch ($type = $Result->_type) { case 'Defect': @@ -35,7 +35,6 @@ function FetchUpdatedRallyArtifacts($since) $path = '/detail/testcase/'; break; default: -var_dump('-> skipping unimportant new artifact'); continue 2; //don't display other artifact types } if (empty($user) && isset($Result->Owner)) { @@ -47,7 +46,6 @@ function FetchUpdatedRallyArtifacts($since) $creationDate = date_create_from_format($RALLY_TIMESTAMP_FORMAT, $Result->CreationDate)->getTimestamp(); if (($lastUpDate - $creationDate) < 2) { //assume items updated within 1 sec haven't changed state -var_dump('-> reporting newly-created artifact'); $items[] = array( //report newly-created artifacts 'type' => $type, 'title' => $Result->_refObjectName, @@ -75,7 +73,6 @@ function FetchUpdatedRallyArtifacts($since) } break; default: -var_dump('-> skipping unimportant status'); continue 2; //don't parse other state changes } @@ -86,7 +83,6 @@ function FetchUpdatedRallyArtifacts($since) $is_verified = FALSE; foreach ($statusResults->QueryResult->Results as $Revision) { if (isset($fact_table[0]) && (strpos($Revision->Description, $fact_table[0]) !== FALSE)){ -var_dump('-> poisoning the well'); continue 2; //stop parsing if the negative fact has appeared } if (strpos($Revision->Description, $fact_table[1]) !== FALSE) { @@ -95,10 +91,8 @@ function FetchUpdatedRallyArtifacts($since) } } if (!$is_verified) { -var_dump('-> skipping unconfirmed state change'); continue; //skip artifacts with unconfirmed state change } -var_dump('-> reporting artifact state change'); $items[] = array( //report stories that have changed state 'type' => $type, 'title' => $Result->_refObjectName, From d160f9f823b00aaed8d78f49f6e85803c9bf790e Mon Sep 17 00:00:00 2001 From: jpklein Date: Sat, 6 Sep 2014 22:09:17 -0400 Subject: [PATCH 06/13] Post state-change notifications to Slack --- scripts/include/rallycron.inc.php | 81 ++++++++++++++++++++++++------- scripts/rallycron.php | 5 +- 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/scripts/include/rallycron.inc.php b/scripts/include/rallycron.inc.php index ab8e3ef..6fcb918 100644 --- a/scripts/include/rallycron.inc.php +++ b/scripts/include/rallycron.inc.php @@ -18,13 +18,13 @@ function FetchUpdatedRallyArtifacts($since) $project_url = $RALLY_URL . '#/' . $RALLY_PROJECT_ID; $items = array(); - foreach ($results as $Result) { + foreach ($results as $Artifact) { $user = ''; - switch ($type = $Result->_type) { + switch ($type = $Artifact->_type) { case 'Defect': $path = '/detail/defect/'; - $user = $Result->SubmittedBy->_refObjectName; + $user = $Artifact->SubmittedBy->_refObjectName; break; case 'HierarchicalRequirement': $type = 'User Story'; @@ -37,31 +37,31 @@ function FetchUpdatedRallyArtifacts($since) default: continue 2; //don't display other artifact types } - if (empty($user) && isset($Result->Owner)) { - $user = $Result->Owner->_refObjectName; + if (empty($user) && isset($Artifact->Owner)) { + $user = $Artifact->Owner->_refObjectName; } //was the artifact just created? - $lastUpDate = date_create_from_format($RALLY_TIMESTAMP_FORMAT, $Result->LastUpdateDate)->getTimestamp(); - $creationDate = date_create_from_format($RALLY_TIMESTAMP_FORMAT, $Result->CreationDate)->getTimestamp(); + $lastUpDate = date_create_from_format($RALLY_TIMESTAMP_FORMAT, $Artifact->LastUpdateDate)->getTimestamp(); + $creationDate = date_create_from_format($RALLY_TIMESTAMP_FORMAT, $Artifact->CreationDate)->getTimestamp(); if (($lastUpDate - $creationDate) < 2) { //assume items updated within 1 sec haven't changed state $items[] = array( //report newly-created artifacts 'type' => $type, - 'title' => $Result->_refObjectName, - 'url' => $project_url . $path . basename($Result->_ref), + 'title' => $Artifact->_refObjectName, + 'url' => $project_url . $path . basename($Artifact->_ref), 'user' => $user, - 'id' => $Result->FormattedID + 'id' => $Artifact->FormattedID ); } elseif ($type == 'User Story') { //track progress of user stories - switch ($Result->ScheduleState) { + switch ($Artifact->ScheduleState) { case 'Completed': $fact_table = array(1 => 'SCHEDULE STATE changed'); $state = 'acceptance-ready'; break; case 'In-Progress': - if ($Result->Ready) { + if ($Artifact->Ready) { $fact_table = array(1 => 'READY changed from [false] to [true]'); $state = 'verification-ready'; } else { @@ -77,11 +77,13 @@ function FetchUpdatedRallyArtifacts($since) } //parse latest revision messages to verify state change - $query2_url = $Result->RevisionHistory->_ref . '/Revisions?query=(CreationDate+>+' . $since . ')&fetch=CreationDate,Description,User'; - $statusResults = CallAPI($query2_url); + $query2_url = $Artifact->RevisionHistory->_ref . '/Revisions?query=(CreationDate+>+' . $since . ')&fetch=CreationDate,Description,User'; + + $results2 = CallAPI($query2_url); + $results2 = $results2->QueryResult->Results; $is_verified = FALSE; - foreach ($statusResults->QueryResult->Results as $Revision) { + foreach ($results2 as $Revision) { if (isset($fact_table[0]) && (strpos($Revision->Description, $fact_table[0]) !== FALSE)){ continue 2; //stop parsing if the negative fact has appeared } @@ -93,12 +95,13 @@ function FetchUpdatedRallyArtifacts($since) if (!$is_verified) { continue; //skip artifacts with unconfirmed state change } + $items[] = array( //report stories that have changed state 'type' => $type, - 'title' => $Result->_refObjectName, - 'url' => $project_url . $path . basename($Result->_ref), + 'title' => $Artifact->_refObjectName, + 'url' => $project_url . $path . basename($Artifact->_ref), 'user' => $user, - 'id' => $Result->FormattedID, + 'id' => $Artifact->FormattedID, 'state' => $state //presence of this key indicates state-change notification ); } @@ -108,5 +111,47 @@ function FetchUpdatedRallyArtifacts($since) function SendRallyUpdateNotifications($items) { + global $SLACK_CHANNEL_FOR_RALLY_PROJECT; + $success = TRUE; + + foreach ($items as $item) { + $item['title'] = SanitizeText($item['title']); + $item['title'] = TruncateText($item['title'], 300); + $slug = l($item['title'], $item['url']); + + //display a state-change notification as a message attachment + if (isset($item['state'])) { + switch ($item['state']) { + case 'verification-ready': + $item['state'] = ' is ready for QA'; + $color = '#F29513'; //github orange + break; + case 'needs-work': + $item['state'] = ' needs additional work'; + $color = '#D84A63'; //paletton-suggested red + break; + case 'acceptance-ready': + $item['state'] = ' is ready for acceptance'; + $color = '#6CC644'; //github green + } + + $pretext = em($item['type'] . ' updated by ' . $item['user']); + $text = b($item['id']) . $item['state']; + $fields = array(MakeField('', $slug)); + $fallback = $item['type'] . ' ' . $item['id'] . $item['state']; + + //display a link to the new artifact as a message attachment + } else { + $pretext = em('New ' . $item['type'] . ' added by ' . $item['user']); + $text = ''; + $color = '#6CC644'; //github green + $fields = array(MakeField($item['id'], $slug)); + $fallback = $item['type'] . ' ' . $item['id'] . ' added by ' . $item['user']; + } + + $message = MakeAttachment($pretext, $text, $color, $fields, $fallback); + $success = sendIncomingWebHookMessage($SLACK_CHANNEL_FOR_RALLY_PROJECT, '', $message) && $success; + } + return $success; } diff --git a/scripts/rallycron.php b/scripts/rallycron.php index 7fa7541..87e9c8a 100644 --- a/scripts/rallycron.php +++ b/scripts/rallycron.php @@ -5,5 +5,6 @@ date_default_timezone_set('UTC'); $since = date($RALLY_TIMESTAMP_FORMAT, time() - $CRON_INTERVAL); -$items = FetchUpdatedRallyArtifacts($since); -$result = SendRallyUpdateNotifications($items); +if ($items = FetchUpdatedRallyArtifacts($since)) { + $result = SendRallyUpdateNotifications($items); +} From 907eed9ecadd192da9592fb1ccb2342217604388 Mon Sep 17 00:00:00 2001 From: jpklein Date: Mon, 8 Sep 2014 02:13:23 -0400 Subject: [PATCH 07/13] Refactor rally cron posting function --- scripts/include/rallycron.inc.php | 24 ++++++++++++++++++++++++ scripts/include/slack.php | 23 ----------------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/scripts/include/rallycron.inc.php b/scripts/include/rallycron.inc.php index 6fcb918..981f500 100644 --- a/scripts/include/rallycron.inc.php +++ b/scripts/include/rallycron.inc.php @@ -155,3 +155,27 @@ function SendRallyUpdateNotifications($items) return $success; } + +function SendIncomingWebHookMessage($channel, $payload, $attachments) +{ + global $config; + + //allow bot to display formatted attachment text + $attachments->mrkdwn_in = ['pretext', 'text', 'title', 'fields']; + + $reply = slack_incoming_hook_post_with_attachments( + $config['slack']['hook'], + $config['rally']['botname'], + $channel, + $config['rally']['boticon'], + $payload, + $attachments + ); + + $success = ($reply == 'ok'); + if (!$success) { + trigger_error('Unable to send Incoming WebHook message: ' . $reply); + } + return $success; +} + diff --git a/scripts/include/slack.php b/scripts/include/slack.php index 7a1dc21..c8d1be3 100644 --- a/scripts/include/slack.php +++ b/scripts/include/slack.php @@ -77,29 +77,6 @@ function slack_incoming_hook_post($uri, $user, $channel, $icon, $emoji, $payload return curl_post($uri, $data_string); } -function SendIncomingWebHookMessage($channel, $payload, $attachments) -{ - global $config; - - //allow bot to display formatted attachment text - $attachments->mrkdwn_in = ['pretext', 'text', 'title', 'fields']; - - $reply = slack_incoming_hook_post_with_attachments( - $config['slack']['hook'], - $config['rally']['botname'], - $channel, - $config['rally']['boticon'], - $payload, - $attachments - ); - - $success = ($reply == 'ok'); - if (!$success) { - trigger_error('Unable to send Incoming WebHook message: ' . $reply); - } - return $success; -} - function slack_incoming_hook_post_with_attachments($uri, $user, $channel, $icon, $payload, $attachments){ $data = array( From 4559f6ae2d857f0d00da3db53bbd4eb05a583f90 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Mon, 22 Sep 2014 12:12:47 -0400 Subject: [PATCH 08/13] Add an overview of rallybot notifications --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c01006e..6a4c638 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,17 @@ A place where I put some integration scripts for the popular Slack messaging pla #Rally Bot -We use this to query our Rally instance for defects, tasks and user stories. I have it configured to respond to a /rallyme slash command in Slack. +Pushes notifications from Rally and allows users to fetch ticket details. Notifications are sent whenever: + +1. a comment is added to a ticket +2. a new defect, user story, or test case is created +3. a user story changes state + +The bot uses a combination of Rally's _State_ and _Ready_ fields to track the progress of user stories. When a story's state is set to "In-Progress" and ready is checked, rallybot will announce that the story is ready for testing. After testing is completed by a member of the QA team, they may either: 1) uncheck the Ready flag to have rallybot announce that the story needs work, or 2) set the story to "Completed" to notify the Product Owner that it is ready for review. + + **Note**: As soon as all of a story's tasks are set to "Complete", Rally will automatically set the story's State to "Complete". So be sure to leave at least one task open in order to correctly track stories with defects. + +The bot can also be configured to respond to a /rallyme slash command to query our Rally instance for defects, tasks and user stories. E.g.: /rallyme DE12345 From 202d9e52178b857cc7cde65d32464b886dffff94 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Mon, 22 Sep 2014 12:20:09 -0400 Subject: [PATCH 09/13] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 6a4c638..465f1ba 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ slack-integration A place where I put some integration scripts for the popular Slack messaging platform. - #Rally Bot Pushes notifications from Rally and allows users to fetch ticket details. Notifications are sent whenever: @@ -14,7 +13,7 @@ Pushes notifications from Rally and allows users to fetch ticket details. Notifi The bot uses a combination of Rally's _State_ and _Ready_ fields to track the progress of user stories. When a story's state is set to "In-Progress" and ready is checked, rallybot will announce that the story is ready for testing. After testing is completed by a member of the QA team, they may either: 1) uncheck the Ready flag to have rallybot announce that the story needs work, or 2) set the story to "Completed" to notify the Product Owner that it is ready for review. - **Note**: As soon as all of a story's tasks are set to "Complete", Rally will automatically set the story's State to "Complete". So be sure to leave at least one task open in order to correctly track stories with defects. + **Note**: As soon as all of a story's tasks are completed, Rally will automatically set the story's state to "Completed". So be sure to leave at least one task open in order to correctly track stories with defects. The bot can also be configured to respond to a /rallyme slash command to query our Rally instance for defects, tasks and user stories. From 553c952607db4b9b8d3f6549bf48bec8e18bd043 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Mon, 22 Sep 2014 12:25:41 -0400 Subject: [PATCH 10/13] Remove mention of unimplemented functionality from README.md --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 465f1ba..9b9cb97 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,12 @@ A place where I put some integration scripts for the popular Slack messaging pla Pushes notifications from Rally and allows users to fetch ticket details. Notifications are sent whenever: -1. a comment is added to a ticket -2. a new defect, user story, or test case is created -3. a user story changes state +1. a new defect, user story, or test case is created +2. a user story changes state -The bot uses a combination of Rally's _State_ and _Ready_ fields to track the progress of user stories. When a story's state is set to "In-Progress" and ready is checked, rallybot will announce that the story is ready for testing. After testing is completed by a member of the QA team, they may either: 1) uncheck the Ready flag to have rallybot announce that the story needs work, or 2) set the story to "Completed" to notify the Product Owner that it is ready for review. +The bot uses a combination of Rally's _State_ and _Ready_ fields to track the progress of user stories. When a story's state is set to "In-Progress" and the ready field is checked, rallybot will announce that the story is ready for testing. After testing is completed by a member of the QA team, they may either: 1) uncheck the Ready flag to have rallybot announce that the story needs work, or 2) set the story to "Completed" to notify the Product Owner that it is ready for review. - **Note**: As soon as all of a story's tasks are completed, Rally will automatically set the story's state to "Completed". So be sure to leave at least one task open in order to correctly track stories with defects. +> **Note**: As soon as all of a story's tasks are completed, Rally will automatically set the story's state to "Completed". So be sure to leave at least one task open in order to correctly track stories with defects. The bot can also be configured to respond to a /rallyme slash command to query our Rally instance for defects, tasks and user stories. From 84b5cf05a4d1ba3ea8efed291121d70979471094 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Mon, 22 Sep 2014 12:29:05 -0400 Subject: [PATCH 11/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b9cb97..2da0286 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Pushes notifications from Rally and allows users to fetch ticket details. Notifi 1. a new defect, user story, or test case is created 2. a user story changes state -The bot uses a combination of Rally's _State_ and _Ready_ fields to track the progress of user stories. When a story's state is set to "In-Progress" and the ready field is checked, rallybot will announce that the story is ready for testing. After testing is completed by a member of the QA team, they may either: 1) uncheck the Ready flag to have rallybot announce that the story needs work, or 2) set the story to "Completed" to notify the Product Owner that it is ready for review. +The bot uses a combination of Rally's _state_ and _ready_ fields to track the progress of user stories. When a story's state is set to "In-Progress" and the ready field is checked, rallybot will announce that the story is ready for testing. When the QA team has completed testing, they may either: 1) uncheck the Ready flag to have rallybot announce that the story needs work, or 2) set the story to "Completed" to notify the Product Owner that it is ready for review. > **Note**: As soon as all of a story's tasks are completed, Rally will automatically set the story's state to "Completed". So be sure to leave at least one task open in order to correctly track stories with defects. From 6461ba9e74039040008d7cace7ea71298432304c Mon Sep 17 00:00:00 2001 From: JP Klein Date: Mon, 22 Sep 2014 15:21:35 -0400 Subject: [PATCH 12/13] Update styling of state-change notifications (for mobile users) --- scripts/include/rallycron.inc.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/include/rallycron.inc.php b/scripts/include/rallycron.inc.php index 981f500..af951db 100644 --- a/scripts/include/rallycron.inc.php +++ b/scripts/include/rallycron.inc.php @@ -134,11 +134,12 @@ function SendRallyUpdateNotifications($items) $item['state'] = ' is ready for acceptance'; $color = '#6CC644'; //github green } + $item['state'] = $item['id'] . $item['state']; $pretext = em($item['type'] . ' updated by ' . $item['user']); - $text = b($item['id']) . $item['state']; - $fields = array(MakeField('', $slug)); - $fallback = $item['type'] . ' ' . $item['id'] . $item['state']; + $text = ''; + $fields = array(MakeField($item['state'], $slug)); + $fallback = $item['type'] . ' ' . $item['state']; //display a link to the new artifact as a message attachment } else { From 6858de516659dd40c0e19fb32ec4ab47b7e1839b Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 7 Oct 2014 18:47:28 -0400 Subject: [PATCH 13/13] Extract story-state-change-detection algorithm Placing it in rally library file for easy comparison with future function that will determine story state without the benefit of looking in a discrete time window of revision messages --- scripts/include/rally.php | 60 +++++++++++++++++++++++++++++++ scripts/include/rallycron.inc.php | 47 ++++-------------------- 2 files changed, 66 insertions(+), 41 deletions(-) diff --git a/scripts/include/rally.php b/scripts/include/rally.php index bc311b3..1f287eb 100644 --- a/scripts/include/rally.php +++ b/scripts/include/rally.php @@ -311,6 +311,66 @@ function CallAPI($uri) return $object; } +/** + * Returns an abstract notion of a story's current state based on a combination + * of its ScheduleState and Ready fields and revision history. + * + * @param object $Story + * @param object $lastCronTime + * + * @return string[] Array containing story's state label and name of the user + * that updated the story to this state + */ +function FetchStoryStateChangeInfo($Story, $lastCronTime) +{ + $state = ''; + $fact_table = array(); //revision messages that must appear/not appear since last cron run to verify state + + switch ($Story->ScheduleState) { //make hypothesis about state based on story's ScheduleState/Ready fields + + case 'Completed': + $state = 'acceptance-ready'; + $fact_table[1] = 'SCHEDULE STATE changed'; + break; + + case 'In-Progress': + if ($Story->Ready) { + $state = 'verification-ready'; + $fact_table[1] = 'READY changed from [false] to [true]'; + + } else { + $state = 'needs-work'; + $fact_table[0] = 'SCHEDULE STATE changed'; //array index 0 indicates fact that must not appear + $fact_table[1] = 'READY changed from [true] to [false]'; + } + break; + + default: + return NULL; //don't report other states + } + + $query_url = $Story->RevisionHistory->_ref . '/Revisions?query=(CreationDate+>+' . $lastCronTime . ')&fetch=CreationDate,Description,User'; + $results = CallAPI($query_url); + $results = $results->QueryResult->Results; + + $is_verified = FALSE; //parse latest revision messages to verify state + $updated_by = '' //return the name of the user that made the change + + foreach ($results as $Revision) { + if (isset($fact_table[0]) && (strpos($Revision->Description, $fact_table[0]) !== FALSE)){ + return NULL; //stop parsing if the negative fact has appeared + } + if (strpos($Revision->Description, $fact_table[1]) !== FALSE) { //keep looking for negative fact + $is_verified = TRUE; + $updated_by = $Revision->User->_refObjectName; + } + } + + if (!$is_verified) { + return NULL; //don't report stories with unconfirmed state changes + } + return array($state, $updated_by); +} function GetProjectID($projectref) { diff --git a/scripts/include/rallycron.inc.php b/scripts/include/rallycron.inc.php index af951db..a1f59d8 100644 --- a/scripts/include/rallycron.inc.php +++ b/scripts/include/rallycron.inc.php @@ -55,54 +55,19 @@ function FetchUpdatedRallyArtifacts($since) ); } elseif ($type == 'User Story') { //track progress of user stories - switch ($Artifact->ScheduleState) { - case 'Completed': - $fact_table = array(1 => 'SCHEDULE STATE changed'); - $state = 'acceptance-ready'; - break; - case 'In-Progress': - if ($Artifact->Ready) { - $fact_table = array(1 => 'READY changed from [false] to [true]'); - $state = 'verification-ready'; - } else { - $fact_table = array( - 0 => 'SCHEDULE STATE changed', - 1 => 'READY changed from [true] to [false]' - ); - $state = 'needs-work'; - } - break; - default: - continue 2; //don't parse other state changes - } - //parse latest revision messages to verify state change - $query2_url = $Artifact->RevisionHistory->_ref . '/Revisions?query=(CreationDate+>+' . $since . ')&fetch=CreationDate,Description,User'; - - $results2 = CallAPI($query2_url); - $results2 = $results2->QueryResult->Results; - - $is_verified = FALSE; - foreach ($results2 as $Revision) { - if (isset($fact_table[0]) && (strpos($Revision->Description, $fact_table[0]) !== FALSE)){ - continue 2; //stop parsing if the negative fact has appeared - } - if (strpos($Revision->Description, $fact_table[1]) !== FALSE) { - $is_verified = TRUE; - $user = $Revision->User->_refObjectName; - } - } - if (!$is_verified) { - continue; //skip artifacts with unconfirmed state change + $state_info = FetchStoryStateChangeInfo($Artifact, $since); //see rally library file + if (is_null($state_info)) { + continue; //skip stories that haven't changed state } - $items[] = array( //report stories that have changed state + $items[] = array( 'type' => $type, 'title' => $Artifact->_refObjectName, 'url' => $project_url . $path . basename($Artifact->_ref), - 'user' => $user, + 'user' => $state_info[1], 'id' => $Artifact->FormattedID, - 'state' => $state //presence of this key indicates state-change notification + 'state' => $state_info[0] //presence of this key indicates state-change notification ); } }