Skip to content

Commit 89a9dfb

Browse files
authored
Remove JIRA in-progress transition on PR open (#182)
This has been troublesome in a couple different ways: - Sometimes PRs get opened accidentally against the wrong release branch, which results in lots of JIRA comments that are wrong and confusing. - Sometimes resolving a JIRA requires multiple PRs and having lots of these "submitted for review" comments quickly becomes noise. - Also check transition before changing it to avoid extra jira churn.
1 parent 08e9833 commit 89a9dfb

File tree

7 files changed

+223
-76
lines changed

7 files changed

+223
-76
lines changed

src/jira/api.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use jira::models::*;
1212
use version;
1313

1414
pub trait Session: Send + Sync {
15+
fn get_issue(&self, key: &str) -> Result<Issue>;
1516
fn get_transitions(&self, key: &str) -> Result<Vec<Transition>>;
1617

1718
fn transition_issue(&self, key: &str, transition: &TransitionRequest) -> Result<()>;
@@ -93,6 +94,12 @@ impl JiraSession {
9394
}
9495

9596
impl Session for JiraSession {
97+
fn get_issue(&self, key: &str) -> Result<Issue> {
98+
self.client
99+
.get::<Issue>(&format!("/issue/{}", key))
100+
.map_err(|e| Error::from(format!("Error creating getting issue [{}]: {}", key, e)))
101+
}
102+
96103
fn get_transitions(&self, key: &str) -> Result<Vec<Transition>> {
97104
#[derive(Deserialize)]
98105
struct TransitionsResp {

src/jira/models.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11

2+
#[derive(Deserialize, Serialize, Clone, Debug)]
3+
pub struct Issue {
4+
pub key: String,
5+
pub status: Option<Status>,
6+
}
7+
8+
#[derive(Deserialize, Serialize, Clone, Debug)]
9+
pub struct Status {
10+
pub name: String,
11+
}
212

313
#[derive(Deserialize, Serialize, Clone, Debug)]
414
pub struct Comment {

src/jira/workflow.rs

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use version;
55

66
use config::JiraConfig;
77
use errors::*;
8-
use github::{Commit, CommitLike, PullRequest, PushCommit};
8+
use github::{Commit, CommitLike, PushCommit};
99
use jira;
1010
use jira::Transition;
1111

@@ -67,47 +67,48 @@ fn get_jira_project(jira_key: &str) -> &str {
6767
}
6868
}
6969

70+
fn needs_transition(state: &Option<jira::Status>, target: &Vec<String>) -> bool {
71+
if let &Some(ref state) = state {
72+
return !target.contains(&state.name);
73+
} else {
74+
return true;
75+
}
76+
}
77+
7078
pub fn submit_for_review(
71-
pr: &PullRequest,
7279
commits: &Vec<Commit>,
7380
projects: &Vec<String>,
7481
jira: &jira::api::Session,
7582
config: &JiraConfig,
7683
) {
84+
let review_states = config.review_states();
85+
let progress_states = config.progress_states();
86+
7787
for key in get_fixed_jira_keys(commits, projects) {
78-
// add comment
79-
if let Err(e) = jira.comment_issue(
80-
&key,
81-
&format!("Review submitted for branch {}: {}", pr.base.ref_name, pr.html_url),
82-
)
83-
{
84-
error!("Error commenting on key [{}]: {}", key, e);
85-
continue; // give up on transitioning if we can't comment.
88+
let issue_state = try_get_issue_state(&key, jira);
89+
90+
if !needs_transition(&issue_state, &review_states) {
91+
continue;
8692
}
8793

8894
// try to transition to in-progress
89-
try_transition(&key, &config.progress_states(), jira);
95+
if needs_transition(&issue_state, &progress_states) {
96+
try_transition(&key, &progress_states, jira);
97+
}
98+
9099
// try transition to pending-review
91-
try_transition(&key, &config.review_states(), jira);
100+
try_transition(&key, &review_states, jira);
92101
}
93102

94103
for key in get_referenced_jira_keys(commits, projects) {
95-
// add comment
96-
if let Err(e) = jira.comment_issue(
97-
&key,
98-
&format!(
99-
"Referenced by review submitted for branch {}: {}",
100-
pr.base.ref_name,
101-
pr.html_url
102-
),
103-
)
104-
{
105-
error!("Error commenting on key [{}]: {}", key, e);
106-
continue; // give up on transitioning if we can't comment.
104+
let issue_state = try_get_issue_state(&key, jira);
105+
106+
if !needs_transition(&issue_state, &progress_states) {
107+
continue;
107108
}
108109

109110
// try to transition to in-progress
110-
try_transition(&key, &config.progress_states(), jira);
111+
try_transition(&key, &progress_states, jira);
111112
}
112113
}
113114

@@ -134,14 +135,19 @@ pub fn resolve_issue(
134135

135136
let fix_msg = format!("Merged into branch {}: {}{}", branch, desc, version_desc);
136137
let ref_msg = format!("Referenced by commit merged into branch {}: {}{}", branch, desc, version_desc);
138+
let resolved_states = config.resolved_states();
137139

138140
for key in get_fixed_jira_keys(&vec![commit], projects) {
139141
if let Err(e) = jira.comment_issue(&key, &fix_msg) {
140142
error!("Error commenting on key [{}]: {}", key, e);
141143
}
142144

143-
let to = config.resolved_states();
144-
match find_transition(&key, &to, jira) {
145+
let issue_state = try_get_issue_state(&key, jira);
146+
if !needs_transition(&issue_state, &resolved_states) {
147+
continue;
148+
}
149+
150+
match find_transition(&key, &resolved_states, jira) {
145151
Ok(Some(transition)) => {
146152
let mut req = transition.new_request();
147153

@@ -168,12 +174,12 @@ pub fn resolve_issue(
168174
}
169175

170176
if let Err(e) = jira.transition_issue(&key, &req) {
171-
error!("Error transitioning JIRA issue [{}] to one of [{:?}]: {}", key, to, e);
177+
error!("Error transitioning JIRA issue [{}] to one of [{:?}]: {}", key, resolved_states, e);
172178
} else {
173-
info!("Transitioned [{}] to one of [{:?}]", key, to);
179+
info!("Transitioned [{}] to one of [{:?}]", key, resolved_states);
174180
}
175181
}
176-
Ok(None) => info!("JIRA [{}] cannot be transitioned to any of [{:?}]", key, to),
182+
Ok(None) => info!("JIRA [{}] cannot be transitioned to any of [{:?}]", key, resolved_states),
177183
Err(e) => error!("{}", e),
178184
};
179185
}
@@ -308,6 +314,16 @@ fn find_relevant_versions(
308314
.collect::<Vec<_>>()
309315
}
310316

317+
fn try_get_issue_state(key: &str, jira: &jira::api::Session) -> Option<jira::Status> {
318+
match jira.get_issue(key) {
319+
Ok(issue) => issue.status,
320+
Err(e) => {
321+
error!("Error getting JIRA [{}] {}", key, e);
322+
None
323+
},
324+
}
325+
}
326+
311327
fn try_transition(key: &str, to: &Vec<String>, jira: &jira::api::Session) {
312328
match find_transition(&key, to, jira) {
313329
Ok(Some(transition)) => {

src/server/github_handler.rs

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ pub struct GithubEventHandler {
5757
const MAX_CONCURRENT_MERGES: usize = 20;
5858
const MAX_CONCURRENT_VERSIONS: usize = 20;
5959
const MAX_CONCURRENT_FORCE_PUSH: usize = 20;
60+
const MAX_COMMITS_FOR_JIRA_CONSIDERATION: usize = 20;
6061

6162
impl GithubHandlerState {
6263
pub fn new(
@@ -375,15 +376,15 @@ impl GithubEventHandler {
375376
if let Some(ref verb) = verb {
376377
let commits = self.pull_request_commits(&pull_request);
377378

379+
let attachments = vec![
380+
SlackAttachmentBuilder::new("")
381+
.title(format!("Pull Request #{}: \"{}\"", pull_request.number, pull_request.title.as_str()))
382+
.title_link(pull_request.html_url.as_str())
383+
.build(),
384+
];
385+
378386
if !pull_request.is_wip() {
379387
let msg = format!("Pull Request {}", verb);
380-
let attachments =
381-
vec![SlackAttachmentBuilder::new("")
382-
.title(format!("Pull Request #{}: \"{}\"",
383-
pull_request.number,
384-
pull_request.title.as_str()))
385-
.title_link(pull_request.html_url.as_str())
386-
.build()];
387388

388389
if notify_channel_only {
389390
self.messenger.send_to_channel(&msg, &attachments, &self.data.repository);
@@ -404,16 +405,26 @@ impl GithubEventHandler {
404405
if self.action == "opened" {
405406
if let Some(ref jira_config) = self.config.jira {
406407
if let Some(ref jira_session) = self.jira_session {
407-
let jira_projects =
408-
self.config.repos().jira_projects(&self.data.repository, &pull_request.base.ref_name);
409-
410-
jira::workflow::submit_for_review(
411-
&pull_request,
412-
&commits,
413-
&jira_projects,
414-
jira_session.deref(),
415-
jira_config,
416-
);
408+
if commits.len() > MAX_COMMITS_FOR_JIRA_CONSIDERATION {
409+
let msg = format!(
410+
"Too many commits on Pull Request #{}. Ignoring JIRAs.",
411+
pull_request.number
412+
);
413+
self.messenger.send_to_owner(&msg, &attachments, &pull_request.user, &self.data.repository);
414+
415+
} else {
416+
let jira_projects = self.config.repos().jira_projects(
417+
&self.data.repository,
418+
&pull_request.base.ref_name,
419+
);
420+
421+
jira::workflow::submit_for_review(
422+
&commits,
423+
&jira_projects,
424+
jira_session.deref(),
425+
jira_config,
426+
);
427+
}
417428
}
418429
}
419430
}

tests/github_handler_test.rs

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,13 @@ fn test_push_force_notify_ignored() {
13741374
expect_thread.join().unwrap();
13751375
}
13761376

1377+
fn new_issue(key: &str) -> jira::Issue {
1378+
jira::Issue {
1379+
key: key.into(),
1380+
status: None,
1381+
}
1382+
}
1383+
13771384
fn new_transition(id: &str, name: &str) -> jira::Transition {
13781385
jira::Transition {
13791386
id: id.into(),
@@ -1407,6 +1414,17 @@ fn some_jira_commits() -> Vec<Commit> {
14071414
]
14081415
}
14091416

1417+
fn many_jira_commits() -> Vec<Commit> {
1418+
let commit = Commit {
1419+
sha: "ffeedd00110011".into(),
1420+
html_url: "http://commit/ffeedd00110011".into(),
1421+
author: Some(User::new("bob-author")),
1422+
commit: CommitDetails { message: "Fix [SER-1] Add the feature\n\nThe body ([OTHER-123])".into() },
1423+
};
1424+
1425+
return (0..21).collect::<Vec<u32>>().into_iter().map(|_| commit.clone()).collect();
1426+
}
1427+
14101428
fn some_jira_push_commits() -> Vec<PushCommit> {
14111429
vec![
14121430
PushCommit {
@@ -1417,6 +1435,7 @@ fn some_jira_push_commits() -> Vec<PushCommit> {
14171435
},
14181436
]
14191437
}
1438+
14201439
#[test]
14211440
fn test_jira_pull_request_opened() {
14221441
let mut test = new_test_with_jira();
@@ -1449,11 +1468,7 @@ fn test_jira_pull_request_opened() {
14491468
]);
14501469

14511470
if let Some(ref jira) = test.jira {
1452-
jira.mock_comment_issue(
1453-
"SER-1",
1454-
"Review submitted for branch master: http://the-pr",
1455-
Ok(()),
1456-
);
1471+
jira.mock_get_issue("SER-1", Ok(new_issue("SER-1")));
14571472

14581473
jira.mock_get_transitions("SER-1", Ok(vec![new_transition("001", "the-progress")]));
14591474
jira.mock_transition_issue("SER-1", &new_transition_req("001"), Ok(()));
@@ -1466,6 +1481,52 @@ fn test_jira_pull_request_opened() {
14661481
assert_eq!((StatusCode::Ok, "pr".into()), resp);
14671482
}
14681483

1484+
#[test]
1485+
fn test_jira_pull_request_opened_too_many_commits() {
1486+
let mut test = new_test_with_jira();
1487+
test.handler.event = "pull_request".into();
1488+
test.handler.action = "opened".into();
1489+
test.handler.data.pull_request = some_pr();
1490+
test.handler.data.sender = User::new("the-pr-owner");
1491+
1492+
test.github.mock_get_pull_request_commits(
1493+
"some-user",
1494+
"some-repo",
1495+
32,
1496+
Ok(many_jira_commits()),
1497+
);
1498+
1499+
let attach = vec![
1500+
SlackAttachmentBuilder::new("")
1501+
.title("Pull Request #32: \"The PR\"")
1502+
.title_link("http://the-pr")
1503+
.build(),
1504+
];
1505+
1506+
test.slack.expect(vec![
1507+
slack::req(
1508+
"the-reviews-channel",
1509+
&format!("Pull Request opened by the.pr.owner {}", REPO_MSG),
1510+
attach.clone()
1511+
),
1512+
slack::req(
1513+
"the-reviews-channel",
1514+
&format!("Too many commits on Pull Request #32. Ignoring JIRAs. {}", REPO_MSG),
1515+
attach.clone()
1516+
),
1517+
slack::req(
1518+
"@the.pr.owner",
1519+
&format!("Too many commits on Pull Request #32. Ignoring JIRAs."),
1520+
attach.clone()
1521+
),
1522+
]);
1523+
1524+
// do not set jira expectations
1525+
1526+
let resp = test.handler.handle_event().unwrap();
1527+
assert_eq!((StatusCode::Ok, "pr".into()), resp);
1528+
}
1529+
14691530
#[test]
14701531
fn test_jira_push_master() {
14711532
let mut test = new_test_with_jira();
@@ -1476,6 +1537,7 @@ fn test_jira_push_master() {
14761537
test.handler.data.commits = Some(some_jira_push_commits());
14771538

14781539
if let Some(ref jira) = test.jira {
1540+
jira.mock_get_issue("SER-1", Ok(new_issue("SER-1")));
14791541
jira.mock_comment_issue("SER-1", "Merged into branch master: [ffeedd0|http://commit/ffeedd00110011]\n{quote}Fix [SER-1] Add the feature{quote}", Ok(()));
14801542

14811543
jira.mock_get_transitions("SER-1", Ok(vec![new_transition("003", "the-resolved")]));
@@ -1496,6 +1558,7 @@ fn test_jira_push_develop() {
14961558
test.handler.data.commits = Some(some_jira_push_commits());
14971559

14981560
if let Some(ref jira) = test.jira {
1561+
jira.mock_get_issue("SER-1", Ok(new_issue("SER-1")));
14991562
jira.mock_comment_issue("SER-1", "Merged into branch develop: [ffeedd0|http://commit/ffeedd00110011]\n{quote}Fix [SER-1] Add the feature{quote}", Ok(()));
15001563

15011564
jira.mock_get_transitions("SER-1", Ok(vec![new_transition("003", "the-resolved")]));
@@ -1516,6 +1579,7 @@ fn test_jira_push_release() {
15161579
test.handler.data.commits = Some(some_jira_push_commits());
15171580

15181581
if let Some(ref jira) = test.jira {
1582+
jira.mock_get_issue("SER-1", Ok(new_issue("SER-1")));
15191583
jira.mock_comment_issue("SER-1", "Merged into branch release/55: [ffeedd0|http://commit/ffeedd00110011]\n{quote}Fix [SER-1] Add the feature{quote}", Ok(()));
15201584

15211585
jira.mock_get_transitions("SER-1", Ok(vec![new_transition("003", "the-resolved")]));

0 commit comments

Comments
 (0)