protected void UpdateStateOfExternalItem(Card card, List<string> states, BoardMapping mapping, bool runOnlyOnce) { if (string.IsNullOrEmpty(card.ExternalSystemName) || !card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) return; if (string.IsNullOrEmpty(card.ExternalCardID)) { Log.Debug("Ignoring card [{0}] with missing external id value.", card.Id); return; } if (states == null || states.Count == 0) return; int tries = 0; bool success = false; while (tries < 10 && !success && (!runOnlyOnce || tries == 0)) { if (tries > 0) { Log.Warn(string.Format("Attempting to update external work item [{0}] attempt number [{1}]", card.ExternalCardID, tries)); // wait 5 seconds before trying again Thread.Sleep(new TimeSpan(0, 0, 5)); } //https://yoursite.atlassian.net/rest/api/latest/issue/{issueIdOrKey} var request = CreateRequest(string.Format("rest/api/latest/issue/{0}", card.ExternalCardID), Method.GET); var jiraResp = ExecuteRequest(request); if (jiraResp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(jiraResp.Content); Log.Error( string.Format( "Unable to get issues from Jira, Error: {0}. Check your board/repo mapping configuration.", errorMessage.Message)); } else { var issueToUpdate = new JsonSerializer<Issue>().DeserializeFromString(jiraResp.Content); // Check for a workflow mapping to the closed state if (states != null && states.Count > 0 && states[0].Contains(">")) { var workflowStates = states[0].Split('>'); // check to see if the workitem is already in one of the workflow states var alreadyInState = workflowStates.FirstOrDefault( x => x.Trim().ToLowerInvariant() == issueToUpdate.Fields.Status.Name.ToLowerInvariant()); if (!string.IsNullOrEmpty(alreadyInState)) { // change workflowStates to only use the states after the currently set state var currentIndex = Array.IndexOf(workflowStates, alreadyInState); if (currentIndex < workflowStates.Length - 1) { var updatedWorkflowStates = new List<string>(); for (int i = currentIndex + 1; i < workflowStates.Length; i++) { updatedWorkflowStates.Add(workflowStates[i]); } workflowStates = updatedWorkflowStates.ToArray(); } } if (workflowStates.Length > 0) { foreach (string workflowState in workflowStates) { UpdateStateOfExternalItem(card, new List<string> {workflowState.Trim()}, mapping, runOnlyOnce); } return; } } foreach (var state in states) { if (issueToUpdate.Fields.Status.Name.ToLowerInvariant() == state.ToLowerInvariant()) { Log.Debug(string.Format("Issue [{0}] is already in state [{1}]", issueToUpdate.Key, state)); return; } } try { // first get a list of available transitions var transitionsRequest = CreateRequest( string.Format("rest/api/2/issue/{0}/transitions?expand=transitions.fields", card.ExternalCardID), Method.GET); var transitionsResponse = ExecuteRequest(transitionsRequest); if (transitionsResponse.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(jiraResp.Content); Log.Error(string.Format("Unable to get available transitions from Jira, Error: {0}.", errorMessage.Message)); } else { var availableTransitions = new JsonSerializer<TransitionsResponse>().DeserializeFromString( transitionsResponse.Content); if (availableTransitions != null && availableTransitions.Transitions != null && availableTransitions.Transitions.Any()) { // now find match from available transitions to states var valid = false; Transition validTransition = null; foreach (var st in states) { validTransition = availableTransitions.Transitions.FirstOrDefault( x => x.Name.ToLowerInvariant() == st.ToLowerInvariant() || x.To.Name.ToLowerInvariant() == st.ToLowerInvariant()); if (validTransition != null) { // if you find one then set it valid = true; break; } } if (!valid) { // if not then write an error message Log.Error( string.Format( "Unable to update Issue [{0}] to [{1}] because the status transition is invalid. Try adding additional states to the config.", card.ExternalCardID, states.Join(","))); } else { // go ahead and try to update the state of the issue in JIRA //https://yoursite.atlassian.net/rest/api/latest/issue/{issueIdOrKey}/transitions?expand=transitions.fields var updateRequest = CreateRequest( string.Format( "rest/api/latest/issue/{0}/transitions?expand=transitions.fields", card.ExternalCardID), Method.POST); updateRequest.AddParameter("application/json", "{ \"transition\": { \"id\": \"" + validTransition.Id + "\"}}", ParameterType.RequestBody); var resp = ExecuteRequest(updateRequest); if (resp.StatusCode != HttpStatusCode.OK && resp.StatusCode != HttpStatusCode.NoContent) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(resp.Content); Log.Error( string.Format( "Unable to update Issue [{0}] to [{1}], Description: {2}, Message: {3}", card.ExternalCardID, validTransition.To.Name, resp.StatusDescription, errorMessage.Message)); } else { success = true; Log.Debug(String.Format("Updated state for Issue [{0}] to [{1}]", card.ExternalCardID, validTransition.To.Name)); } } } else { Log.Error( string.Format( "Unable to update Issue [{0}] to [{1}] because no transitions were available from its current status [{2}]. The user account you are using to connect may not have proper privileges.", card.ExternalCardID, states.Join(","), issueToUpdate.Fields.Status.Name)); } } } catch (Exception ex) { Log.Error(string.Format("Unable to update Issue [{0}] to [{1}], Exception: {2}", card.ExternalCardID, states.Join(","), ex.Message)); } } tries++; } }
protected override void CardUpdated(Card updatedCard, List<string> updatedItems, BoardMapping boardMapping) { if (!updatedCard.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) return; if (string.IsNullOrEmpty(updatedCard.ExternalCardID)) return; if (string.IsNullOrEmpty(updatedCard.ExternalCardID)) { Log.Debug("Ignoring card [{0}] with missing external id value.", updatedCard.ExternalCardID); return; } //https://yoursite.atlassian.net/rest/api/latest/issue/{issueIdOrKey} var request = CreateRequest(string.Format("rest/api/latest/issue/{0}", updatedCard.ExternalCardID), Method.GET); var jiraResp = ExecuteRequest(request); if (jiraResp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(jiraResp.Content); Log.Error( string.Format( "Unable to get issues from Jira, Error: {0}. Check your board/repo mapping configuration.", errorMessage.Message)); } else { var issueToUpdate = new JsonSerializer<Issue>().DeserializeFromString(jiraResp.Content); if (issueToUpdate != null && issueToUpdate.Key == updatedCard.ExternalCardID) { bool isDirty = false; bool updateEpicName = false; var updateJson = "{ \"fields\": { "; if (updatedItems.Contains("Title") && issueToUpdate.Fields.Summary != updatedCard.Title) { issueToUpdate.Fields.Summary = updatedCard.Title; isDirty = true; updateEpicName = true; } updateJson += "\"summary\": \"" + issueToUpdate.Fields.Summary.Replace("\"", "\\\"") + "\""; if (updateEpicName) { if (issueToUpdate.Fields.IssueType.Name.ToLowerInvariant() == "epic") { if (CustomFields.Any()) { var epicNameField = CustomFields.FirstOrDefault(x => x.Name == "Epic Name"); if (epicNameField != null) { updateJson += ", \"" + epicNameField.Id + "\": \"" + updatedCard.Title.Replace("\"", "\\\"") + "\""; } } } } if (updatedItems.Contains("Description") && issueToUpdate.Fields.Description.SanitizeCardDescription().JiraPlainTextToLeanKitHtml() != updatedCard.Description) { var updatedDescription = updatedCard.Description.LeanKitHtmlToJiraPlainText(); updateJson += ", \"description\": \"" + updatedDescription + "\""; isDirty = true; } if (updatedItems.Contains("Priority")) { updateJson += ", \"priority\": { \"name\": \"" + GetPriority(updatedCard.Priority) + "\"}"; isDirty = true; } if (updatedItems.Contains("DueDate") && CurrentUser != null) { try { var dateFormat = CurrentUser.DateFormat ?? "MM/dd/yyyy"; var parsed = DateTime.ParseExact(updatedCard.DueDate, dateFormat, CultureInfo.InvariantCulture); updateJson += ", \"duedate\": \"" + parsed.ToString("o") + "\""; } catch (Exception ex) { Log.Warn(ex, "Could not parse due date: {0}", updatedCard.DueDate); } } if (updatedItems.Contains("Tags")) { var newLabels = updatedCard.Tags.Split(','); string updateLabels = ""; int ctr = 0; foreach (string newLabel in newLabels) { if (ctr > 0) updateLabels += ", "; updateLabels += "\"" + newLabel.Trim() + "\""; ctr++; } updateJson += ", \"labels\": [" + updateLabels + "]"; isDirty = true; } string comment = ""; if (updatedItems.Contains("Size")) { comment += "LeanKit card Size changed to " + updatedCard.Size + ". "; } if (updatedItems.Contains("Blocked")) { if (updatedCard.IsBlocked) comment += "LeanKit card is blocked: " + updatedCard.BlockReason + ". "; else comment += "LeanKit card is no longer blocked: " + updatedCard.BlockReason + ". "; } updateJson += "}}"; if (isDirty) { try { //https://yoursite.atlassian.net/rest/api/latest/issue/{issueIdOrKey} var updateRequest = CreateRequest(string.Format("rest/api/latest/issue/{0}", updatedCard.ExternalCardID), Method.PUT); updateRequest.AddParameter("application/json", updateJson, ParameterType.RequestBody); var resp = ExecuteRequest(updateRequest); if (resp.StatusCode != HttpStatusCode.OK && resp.StatusCode != HttpStatusCode.NoContent) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(resp.Content); Log.Error(string.Format("Unable to update Issue [{0}], Description: {1}, Message: {2}", updatedCard.ExternalCardID, resp.StatusDescription, errorMessage.Message)); } else { Log.Debug(String.Format("Updated Issue [{0}]", updatedCard.ExternalCardID)); } } catch (Exception ex) { Log.Error(string.Format("Unable to update Issue [{0}], Exception: {1}", updatedCard.ExternalCardID, ex.Message)); } } if (!string.IsNullOrEmpty(comment)) { try { //https://yoursite.atlassian.net/rest/api/latest/issue/{issueIdOrKey} var updateRequest = CreateRequest( string.Format("rest/api/latest/issue/{0}/comment", updatedCard.ExternalCardID), Method.POST); updateRequest.AddParameter( "application/json", "{ \"body\": \"" + comment + "\"}", ParameterType.RequestBody); var resp = ExecuteRequest(updateRequest); if (resp.StatusCode != HttpStatusCode.OK && resp.StatusCode != HttpStatusCode.NoContent && resp.StatusCode != HttpStatusCode.Created) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(resp.Content); Log.Error( string.Format( "Unable to create comment for updated Issue [{0}], Description: {1}, Message: {2}", updatedCard.ExternalCardID, resp.StatusDescription, errorMessage.Message)); } else { Log.Debug(String.Format("Created comment for updated Issue [{0}]", updatedCard.ExternalCardID)); } } catch (Exception ex) { Log.Error(string.Format("Unable to create comment for updated Issue [{0}], Exception: {1}", updatedCard.ExternalCardID, ex.Message)); } } } } }
protected override void Synchronize(BoardMapping project) { Log.Debug("Polling Jira for Issues"); var queryAsOfDate = QueryDate.AddMilliseconds(Configuration.PollingFrequency*-1.5); string jqlQuery; var formattedQueryDate = queryAsOfDate.ToString(QueryDateFormat, CultureInfo.InvariantCulture); if (!string.IsNullOrEmpty(project.Query)) { jqlQuery = string.Format(project.Query, formattedQueryDate); } else { var queryFilter = string.Format(" and ({0})", string.Join(" or ", project.QueryStates.Select(x => "status = '" + x.Trim() + "'").ToList())); if (!string.IsNullOrEmpty(project.ExcludedTypeQuery)) { queryFilter += project.ExcludedTypeQuery; } jqlQuery = string.Format("project=\"{0}\" {1} and updated > \"{2}\" order by created asc", project.Identity.Target, queryFilter, formattedQueryDate); } //https://yoursite.atlassian.net/rest/api/latest/search?jql=project=%22More+Tests%22+and+status=%22open%22+and+created+%3E+%222008/12/31+12:00%22+order+by+created+asc&fields=id,status,priority,summary,description var request = CreateRequest("rest/api/latest/search", Method.GET); request.AddParameter("jql", jqlQuery); request.AddParameter("fields", "id,status,priority,summary,description,issuetype,type,assignee,duedate,labels"); request.AddParameter("maxResults", "9999"); var jiraResp = ExecuteRequest(request); if (jiraResp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(jiraResp.Content); Log.Error( string.Format( "Unable to get issues from Jira, Error: {0}. Check your board/project mapping configuration.", errorMessage.Message)); return; } var resp = new JsonSerializer<IssuesResponse>().DeserializeFromString(jiraResp.Content); Log.Info("\nQueried [{0}] at {1} for changes after {2}", project.Identity.Target, QueryDate, queryAsOfDate.ToString("o")); if (resp != null && resp.Issues != null && resp.Issues.Any()) { var issues = resp.Issues; foreach (var issue in issues) { Log.Info("Issue [{0}]: {1}, {2}, {3}", issue.Key, issue.Fields.Summary, issue.Fields.Status.Name, issue.Fields.Priority.Name); // does this workitem have a corresponding card? var card = LeanKit.GetCardByExternalId(project.Identity.LeanKit, issue.Key); if (card == null || !card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) { Log.Debug("Create new card for Issue [{0}]", issue.Key); CreateCardFromItem(project, issue); } else { Log.Debug("Previously created a card for Issue [{0}]", issue.Key); if (project.UpdateCards) IssueUpdated(issue, card, project); else Log.Info("Skipped card update because 'UpdateCards' is disabled."); } } Log.Info("{0} item(s) queried.\n", issues.Count); } }
protected void UpdateStateOfExternalItem(Card card, List<string> states, BoardMapping boardMapping, bool runOnlyOnce) { if (!card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) return; if (string.IsNullOrEmpty(card.ExternalCardID)) return; if (states == null || states.Count == 0) return; long issueNumber; string target = boardMapping.Identity.Target; // use external card id to get the GitHub issue try { issueNumber = Convert.ToInt32(card.ExternalCardID.Split('|')[1]); } catch (Exception) { Log.Debug("Ignoring card [{0}] with missing external id value.", card.Id); return; } int tries = 0; bool success = false; while (tries < 10 && !success && (!runOnlyOnce || tries == 0)) { if (tries > 0) { Log.Error(string.Format("Attempting to update external issue [{0}] attempt number [{1}]",issueNumber, tries)); // wait 5 seconds before trying again Thread.Sleep(new TimeSpan(0, 0, 5)); } //"https://api.github.com/repos/{0}/{1}/issues/{2} var request = new RestRequest(string.Format("repos/{0}/{1}/issues/{2}", Configuration.Target.Host, target, issueNumber), Method.GET); var ghResp = _restClient.Execute(request); if (ghResp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(ghResp.Content); Log.Error(string.Format("Unable to get issue from GitHub, Error: {0}. Check your board mapping configuration.", errorMessage.Message)); } else { var issueToUpdate = new JsonSerializer<Issue>().DeserializeFromString(ghResp.Content); if (issueToUpdate != null && issueToUpdate.Number == issueNumber) { if (issueToUpdate.State.ToLowerInvariant() == states[0].ToLowerInvariant()) { Log.Debug(string.Format("Issue [{0}] is already in state [{1}]", issueToUpdate.Id, states[0])); return; } issueToUpdate.State = states[0]; try { //"https://api.github.com/repos/{0}/{1}/issues/{2} var updateRequest = new RestRequest(string.Format("repos/{0}/{1}/issues/{2}", Configuration.Target.Host, target, issueNumber), Method.PATCH); updateRequest.AddParameter("application/json", "{ \"state\": \"" + issueToUpdate.State + "\"}", ParameterType.RequestBody); var resp = _restClient.Execute(updateRequest); if (resp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(resp.Content); Log.Error(string.Format("Unable to update Issue [{0}] to [{1}], Description: {2}, Message: {3}", issueNumber, issueToUpdate.State, resp.StatusDescription, errorMessage.Message)); } else { success = true; Log.Debug(String.Format("Updated state for Issue [{0}] to [{1}]", issueNumber, issueToUpdate.State)); } } catch (Exception ex) { Log.Error(string.Format("Unable to update Issue [{0}] to [{1}], Exception: {2}", issueNumber, issueToUpdate.State, ex.Message)); } } else { Log.Debug(String.Format("Could not retrieve Issue [{0}] for updating state to [{1}]", issueNumber, issueToUpdate.State)); } } tries++; } }
protected override void CardUpdated(Card updatedCard, List<string> updatedItems, BoardMapping boardMapping) { if (!updatedCard.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) return; long issueNumber; string target = boardMapping.Identity.Target; // use external card id to get the GitHub Issue try { issueNumber = Convert.ToInt32(updatedCard.ExternalCardID.Split('|')[1]); } catch (Exception) { Log.Debug("Ignoring card [{0}] with missing external id value.", updatedCard.Id); return; } //"https://api.github.com/repos/{0}/{1}/issues/{2} var request = new RestRequest(string.Format("repos/{0}/{1}/issues/{2}", Configuration.Target.Host, target, issueNumber), Method.GET); var ghResp = _restClient.Execute(request); if (ghResp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(ghResp.Content); Log.Error(string.Format("Unable to get issue from GitHub, Error: {0}. Check your board mapping configuration.", errorMessage.Message)); } else { var issueToUpdate = new JsonSerializer<Issue>().DeserializeFromString(ghResp.Content); if (issueToUpdate != null && issueToUpdate.Number == issueNumber) { bool isDirty = false; if (updatedItems.Contains("Title") && issueToUpdate.Title != updatedCard.Title) { issueToUpdate.Title = updatedCard.Title; isDirty = true; } string updateJson = "{ \"title\": \"" + issueToUpdate.Title.Replace("\"", "\\\"") + "\""; if (updatedItems.Contains("Description") && issueToUpdate.Body.SanitizeCardDescription() != updatedCard.Description) { updateJson += ", \"body\": \"" + updatedCard.Description.Replace("\"", "\\\"") + "\""; isDirty = true; } if (updatedItems.Contains("Tags")) { var newLabels = updatedCard.Tags.Split(','); string updateLabels = ""; int ctr = 0; foreach (string newLabel in newLabels) { if (ctr > 0) updateLabels += ", "; updateLabels += "{ \"name\": \"" + newLabel.Trim() + "\"}"; ctr++; } updateJson += ", \"labels\": [" + updateLabels + "]"; isDirty = true; } updateJson += "}"; string comment = ""; if (updatedItems.Contains("Priority")) { comment += "LeanKit card Priority changed to " + updatedCard.Priority + ".<br />"; } if (updatedItems.Contains("DueDate")) { comment += "LeanKit card DueDate changed to " + updatedCard.DueDate + ".<br />"; } if (updatedItems.Contains("Size")) { comment += "LeanKit card Size changed to " + updatedCard.Size + ".<br />"; } if (updatedItems.Contains("Blocked")) { if (updatedCard.IsBlocked) comment += "LeanKit card is blocked: " + updatedCard.BlockReason + ".<br />"; else comment += "LeanKit card is no longer blocked: " + updatedCard.BlockReason + ".<br />"; } if (isDirty) { try { //"https://api.github.com/repos/{0}/{1}/issues/{2} var updateRequest = new RestRequest(string.Format("repos/{0}/{1}/issues/{2}", Configuration.Target.Host, target, issueNumber), Method.PATCH); updateRequest.AddParameter( "application/json", updateJson, ParameterType.RequestBody ); var resp = _restClient.Execute(updateRequest); if (resp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(resp.Content); Log.Error(string.Format("Unable to update Issue [{0}], Description: {1}, Message: {2}", issueNumber, resp.StatusDescription, errorMessage.Message)); } else { Log.Debug(String.Format("Updated Issue [{0}]", issueNumber)); } } catch (Exception ex) { Log.Error(string.Format("Unable to update Issue [{0}], Exception: {1}", issueNumber, ex.Message)); } } if (!string.IsNullOrEmpty(comment)) { try { //"https://api.github.com/repos/{0}/{1}/issues/{2}/comments var newCommentRequest = new RestRequest(string.Format("repos/{0}/{1}/issues/{2}/comments", Configuration.Target.Host, target, issueNumber), Method.POST); newCommentRequest.AddParameter( "application/json", "{ \"body\": \"" + comment + "\"}", ParameterType.RequestBody ); var resp = _restClient.Execute(newCommentRequest); if (resp.StatusCode != HttpStatusCode.OK || resp.StatusCode != HttpStatusCode.Created) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(resp.Content); Log.Error(string.Format("Unable to create comment on updated Issue [{0}], Description: {1}, Message: {2}", issueNumber, resp.StatusDescription, errorMessage.Message)); } else { Log.Debug(String.Format("Created comment on Updated Issue [{0}]", issueNumber)); } } catch (Exception ex) { Log.Error(string.Format("Unable to create comment on updated Issue [{0}], Exception: {1}", issueNumber, ex.Message)); } } } } }
protected override void CreateNewItem(Card card, BoardMapping boardMapping) { var jiraIssueType = GetJiraIssueType(boardMapping, card.TypeId); string json = "{ \"fields\": { "; json += "\"project\": { \"key\": \"" + boardMapping.Identity.Target + "\" }"; json += ", \"summary\": \"" + card.Title + "\" "; json += ", \"description\": \"" + (card.Description != null ? card.Description.Replace("</p>", "").Replace("<p>", "").Replace("\n","\\n") : "") + "\" "; json += ", \"issuetype\": { \"name\": \"" + jiraIssueType + "\" }"; json += ", \"priority\": { \"name\": \"" + GetPriority(card.Priority) + "\" }"; if (jiraIssueType.ToLowerInvariant() == "epic") { if (CustomFields.Any()) { var epicNameField = CustomFields.FirstOrDefault(x => x.Name == "Epic Name"); if (epicNameField != null) { json += ", \"" + epicNameField.Id + "\": \"" + card.Title + "\""; } } } if (!string.IsNullOrEmpty(card.DueDate)) { DateTime updatedDate; var isDate = DateTime.TryParse(card.DueDate, out updatedDate); if (isDate) { json += ", \"duedate\": \"" + updatedDate.ToString("o") + "\""; } } if (!string.IsNullOrEmpty(card.Tags)) { var newLabels = card.Tags.Split(','); string updateLabels = ""; int ctr = 0; foreach (string newLabel in newLabels) { if (ctr > 0) updateLabels += ", "; updateLabels += "\"" + newLabel.Trim() + "\""; ctr++; } json += ", \"labels\": [" + updateLabels + "]"; } json += "}}"; Issue newIssue = null; try { //https://yoursite.atlassian.net/rest/api/latest/issue var createRequest = new RestRequest("/rest/api/latest/issue", Method.POST); createRequest.AddParameter("application/json", json, ParameterType.RequestBody); var resp = _restClient.Execute(createRequest); if (resp.StatusCode != HttpStatusCode.OK && resp.StatusCode != HttpStatusCode.Created) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(resp.Content); Log.Error(string.Format("Unable to create Issue from card [{0}], Description: {1}, Message: {2}", card.ExternalCardID, resp.StatusDescription, errorMessage.Message)); } else { newIssue = new JsonSerializer<Issue>().DeserializeFromString(resp.Content); Log.Debug(String.Format("Created Issue [{0}]", newIssue.Key)); } } catch (Exception ex) { Log.Error(string.Format("Unable to create Issue from Card [{0}], Exception: {1}", card.ExternalCardID, ex.Message)); } if (newIssue != null) { try { card.ExternalCardID = newIssue.Key; card.ExternalSystemName = ServiceName; card.ExternalSystemUrl = string.Format(_externalUrlTemplate, newIssue.Key); // now that we've created the work item let's try to set it to any matching state defined by lane var states = boardMapping.LaneToStatesMap[card.LaneId]; if (states != null) { UpdateStateOfExternalItem(card, states, boardMapping, true); } LeanKit.UpdateCard(boardMapping.Identity.LeanKit, card); } catch (Exception ex) { Log.Error(string.Format("Error updating Card [{0}] after creating new Issue, Exception: {1}", card.ExternalCardID, ex.Message)); } } }
protected override void Synchronize(BoardMapping project) { Log.Debug("Polling GitHub for Issues"); var queryAsOfDate = QueryDate.AddMilliseconds(Configuration.PollingFrequency * -1.5); // GitHub will only let us query one state at a time :( foreach (var state in project.QueryStates) { //https://api.github.com/repos/{0}/{1}/issues?state=Open&since={2} var request = new RestRequest(string.Format("repos/{0}/{1}/issues", Configuration.Target.Host, project.Identity.Target), Method.GET); request.AddParameter("state", state); request.AddParameter("since", queryAsOfDate.ToString("o")); var resp = _restClient.Execute(request); if (resp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(resp.Content); Log.Error(string.Format("Unable to get issues from GitHub, Error: {0}. Check your board/repo mapping configuration.", errorMessage.Message)); return; } var issues = new JsonSerializer<List<Issue>>().DeserializeFromString(resp.Content); Log.Info("\nQueried [{0}] at {1} for changes after {2}", project.Identity.Target, QueryDate, queryAsOfDate.ToString("o")); if (issues == null || !issues.Any() || issues[0].Id <= 0) continue; foreach (var issue in issues.Where(issue => issue.Id > 0)) { Log.Info("Issue [{0}]: {1}, {2}, {3}", issue.Number, issue.Title, issue.User.Login, issue.State); // does this workitem have a corresponding card? var card = LeanKit.GetCardByExternalId(project.Identity.LeanKit, issue.Id + "|" + issue.Number.ToString()); if (card == null || !card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) { Log.Debug("Create new card for Issue [{0}]", issue.Number); CreateCardFromItem(project, issue); } else { Log.Debug("Previously created a card for Issue [{0}]", issue.Number); if (project.UpdateCards) IssueUpdated(issue, card, project); else Log.Info("Skipped card update because 'UpdateCards' is disabled."); } } Log.Info("{0} item(s) queried.\n", issues.Count); } }
protected override void CardUpdated(Card updatedCard, List<string> updatedItems, BoardMapping boardMapping) { if (!updatedCard.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) return; if (string.IsNullOrEmpty(updatedCard.ExternalCardID)) return; long issueNumber; string target = boardMapping.Identity.Target; // use external card id to get the GitHub Issue try { issueNumber = Convert.ToInt32(updatedCard.ExternalCardID.Split('|')[1]); } catch (Exception) { Log.Debug("Ignoring card [{0}] with missing external id value.", updatedCard.Id); return; } //"https://api.github.com/repos/{0}/{1}/pulls/{2} var request = new RestRequest(string.Format("repos/{0}/{1}/pulls/{2}", Configuration.Target.Host, target, issueNumber), Method.GET); var ghResp = _restClient.Execute(request); if (ghResp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(ghResp.Content); Log.Error(string.Format("Unable to get Pull Request from GitHub, Error: {0}. Check your board mapping configuration.", errorMessage.Message)); } else { var pullToUpdate = new JsonSerializer<Pull>().DeserializeFromString(ghResp.Content); if (pullToUpdate != null && pullToUpdate.Number == issueNumber) { bool isDirty = false; if (updatedItems.Contains("Title") && pullToUpdate.Title != updatedCard.Title) { pullToUpdate.Title = updatedCard.Title.Replace("\"", "\\\""); isDirty = true; } if (updatedItems.Contains("Description") && pullToUpdate.Body.SanitizeCardDescription() != updatedCard.Description) { pullToUpdate.Body = updatedCard.Description; isDirty = true; } // Do nothing with Priority, DueDate, Size, Blocked, or Tags because GitHub pull requests do not have comments //if (updatedItems.Contains("Priority")) //if (updatedItems.Contains("DueDate")) //if (updatedItems.Contains("Size")) //if (updatedItems.Contains("Blocked")) //if (updatedItems.Contains("Tags")) if (isDirty) { try { //"https://api.github.com/repos/{0}/{1}/pulls/{2} var updateRequest = new RestRequest(string.Format("repos/{0}/{1}/pulls/{2}", Configuration.Target.Host, target, issueNumber), Method.PATCH); updateRequest.AddParameter( "application/json", "{ \"title\": \"" + pullToUpdate.Title + "\", \"body\": \"" + pullToUpdate.Body + "\"}", ParameterType.RequestBody ); var resp = _restClient.Execute(updateRequest); if (resp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(resp.Content); Log.Error(string.Format("Unable to update Pull Request [{0}], Description: {1}, Message: {2}", issueNumber, resp.StatusDescription, errorMessage.Message)); } else { Log.Debug(String.Format("Updated Pull Request [{0}]", issueNumber)); } } catch (Exception ex) { Log.Error(string.Format("Unable to update Pull Request [{0}], Exception: {1}", issueNumber, ex.Message)); } } } } }
protected override void CardUpdated(Card updatedCard, List<string> updatedItems, BoardMapping boardMapping) { if (!updatedCard.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) return; var target = boardMapping.Identity.Target; if (string.IsNullOrEmpty(updatedCard.ExternalCardID)) { Log.Debug("Ignoring card [{0}] with missing external id value.", updatedCard.ExternalCardID); return; } //https://mysubdomain.unfuddle.com/api/v1/projects/{id}/tickets/{id} var request = new RestRequest(string.Format("/api/v1/projects/{0}/tickets/{1}", target, updatedCard.ExternalCardID), Method.GET); var unfuddleResp = _restClient.Execute(request); if (unfuddleResp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(unfuddleResp.Content); Log.Error(string.Format("Unable to get tickets from Unfuddle, Error: {0}. Check your board/repo mapping configuration.", errorMessage.Message)); } else { var ticketToUpdate = new JsonSerializer<Ticket>().DeserializeFromString(unfuddleResp.Content); if (ticketToUpdate != null && ticketToUpdate.Id.ToString() == updatedCard.ExternalCardID) { bool isDirty = false; string summaryXml = ""; string descriptionXml = ""; string priorityXml = ""; string dueDateXml = ""; if (updatedItems.Contains("Title") && ticketToUpdate.Summary != updatedCard.Title) { summaryXml = "<summary>" + updatedCard.Title + "</summary>"; isDirty = true; } if (updatedItems.Contains("Description") && ticketToUpdate.Description.SanitizeCardDescription() != updatedCard.Description) { var updatedDescription = updatedCard.Description; if (!string.IsNullOrEmpty(updatedDescription)) { updatedDescription = updatedDescription.Replace("<p>", "").Replace("</p>", ""); } descriptionXml = "<description>" + updatedDescription + "</description>"; isDirty = true; } if (updatedItems.Contains("Priority")) { priorityXml = "<priority>" + GetPriority(updatedCard.Priority) + "</priority>"; isDirty = true; } if (updatedItems.Contains("DueDate")) { DateTime updatedDate; var isDate = DateTime.TryParse(updatedCard.DueDate, out updatedDate); if (isDate) { dueDateXml = "<due-on>" + updatedDate.ToString("o") + "</due-on>"; isDirty = true; } } string comment = ""; if (updatedItems.Contains("Size")) { comment += "LeanKit card Size changed to " + updatedCard.Size + ". "; } if (updatedItems.Contains("Tags")) { comment += "LeanKit card Tags changed to " + updatedCard.Tags + ". "; } if (updatedItems.Contains("Blocked")) { if (updatedCard.IsBlocked) comment += "LeanKit card is blocked: " + updatedCard.BlockReason + ". "; else comment += "LeanKit card is no longer blocked: " + updatedCard.BlockReason + ". "; } if (isDirty) { try { //https://mysubdomain.unfuddle.com/api/v1/api/v1/projects/{id}/tickets/{id} var updateRequest = new RestRequest(string.Format("/api/v1/projects/{0}/tickets/{1}", target, updatedCard.ExternalCardID), Method.PUT); updateRequest.AddHeader("Accept", "application/json"); updateRequest.AddHeader("Content-type", "application/xml"); updateRequest.AddParameter( "application/xml", string.Format("<ticket>{0}{1}{2}{3}</ticket>", summaryXml, descriptionXml, priorityXml, dueDateXml), ParameterType.RequestBody ); var resp = _restClient.Execute(updateRequest); if (resp.StatusCode != HttpStatusCode.OK) { if (resp.Content != null && !string.IsNullOrEmpty(resp.Content.Trim())) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(resp.Content); Log.Error(string.Format("Unable to update Ticket [{0}], Description: {1}, Message: {2}", updatedCard.ExternalCardID, resp.StatusDescription, errorMessage.Message)); } else { Log.Error(string.Format("Unable to update Ticket [{0}], Description: {1}, Message: {2}", updatedCard.ExternalCardID, resp.StatusDescription, resp.StatusDescription)); } } else { Log.Debug(String.Format("Updated Ticket [{0}]", updatedCard.ExternalCardID, ticketToUpdate.Status)); } } catch (Exception ex) { Log.Error(string.Format("Unable to update Ticket [{0}], Exception: {1}", updatedCard.ExternalCardID, ex.Message)); } } if (!string.IsNullOrEmpty(comment)) { try { //https://mysubdomain.unfuddle.com/api/v1/api/v1/projects/{id}/tickets/{id}/comments var updateRequest = new RestRequest(string.Format("/api/v1/projects/{0}/tickets/{1}/comments", target, updatedCard.ExternalCardID), Method.POST); updateRequest.AddHeader("Accept", "application/json"); updateRequest.AddHeader("Content-type", "application/xml"); updateRequest.AddParameter( "application/xml", string.Format("<comment><body>{0}</body></comment>", comment), ParameterType.RequestBody ); var resp = _restClient.Execute(updateRequest); if (resp.StatusCode != HttpStatusCode.OK && resp.StatusCode != HttpStatusCode.Created) { if (resp.Content != null && !string.IsNullOrEmpty(resp.Content.Trim())) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(resp.Content); Log.Error(string.Format("Unable to create comment on updated Ticket [{0}], Description: {1}, Message: {2}", updatedCard.ExternalCardID, resp.StatusDescription, errorMessage.Message)); } else { Log.Error(string.Format("Unable to create comment on updated Ticket [{0}], Description: {1}, Message: {2}", updatedCard.ExternalCardID, resp.StatusDescription, resp.StatusDescription)); } } else { Log.Debug(String.Format("Created comment on updated Ticket [{0}]", updatedCard.ExternalCardID, ticketToUpdate.Status)); } } catch (Exception ex) { Log.Error(string.Format("Unable to create comment on updated Ticket [{0}], Exception: {1}", updatedCard.ExternalCardID, ex.Message)); } } } } }
protected void UpdateStateOfExternalItem(Card card, List<string> states, BoardMapping mapping, bool runOnlyOnce) { if (!card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) return; if (states == null || states.Count == 0) return; if (string.IsNullOrEmpty(card.ExternalCardID)) { Log.Debug("Ignoring card [{0}] with missing external id value.", card.Id); return; } int tries = 0; bool success = false; while (tries < 10 && !success && (!runOnlyOnce || tries == 0)) { if (tries > 0) { Log.Error(string.Format("Attempting to update external ticket [{0}] attempt number [{1}]", card.ExternalCardID, tries)); // wait 5 seconds before trying again Thread.Sleep(new TimeSpan(0, 0, 5)); } //https://mysubdomain.unfuddle.com/api/v1/projects/{id}/tickets/{id} var request = new RestRequest(string.Format("/api/v1/projects/{0}/tickets/{1}", mapping.Identity.Target, card.ExternalCardID), Method.GET); var unfuddleResp = _restClient.Execute(request); if (unfuddleResp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(unfuddleResp.Content); Log.Error(string.Format("Unable to get tickets from Unfuddle, Error: {0}. Check your board/repo mapping configuration.", errorMessage.Message)); } else { var ticketToUpdate = new JsonSerializer<Ticket>().DeserializeFromString(unfuddleResp.Content); if (ticketToUpdate != null && ticketToUpdate.Id.ToString() == card.ExternalCardID) { // Check for a workflow mapping to the closed state if (states[0].Contains(">")) { var workflowStates = states[0].Split('>'); // check to see if the workitem is already in one of the workflow states var alreadyInState = workflowStates.FirstOrDefault(x => x.Trim().ToLowerInvariant() == ticketToUpdate.Status.ToLowerInvariant()); if (!string.IsNullOrEmpty(alreadyInState)) { // change workflowStates to only use the states after the currently set state var currentIndex = Array.IndexOf(workflowStates, alreadyInState); if (currentIndex < workflowStates.Length - 1) { var updatedWorkflowStates = new List<string>(); for (int i = currentIndex + 1; i < workflowStates.Length; i++) { updatedWorkflowStates.Add(workflowStates[i]); } workflowStates = updatedWorkflowStates.ToArray(); } } if (workflowStates.Length > 0) { foreach (string workflowState in workflowStates) { //UpdateStateOfExternalItem(card, new LaneStateMap() { State = workflowState.Trim(), States = new List<string>() { workflowState.Trim() } }, mapping, runOnlyOnce); UpdateStateOfExternalItem(card, new List<string> {workflowState.Trim()}, mapping, runOnlyOnce); } return; } } if (ticketToUpdate.Status.ToLowerInvariant() == states[0].ToLowerInvariant()) { Log.Debug(string.Format("Ticket [{0}] is already in state [{1}]", ticketToUpdate.Id, states[0])); return; } ticketToUpdate.Status = states[0]; try { //https://mysubdomain.unfuddle.com/api/v1/api/v1/projects/{id}/tickets/{id} var updateRequest = new RestRequest(string.Format("/api/v1/projects/{0}/tickets/{1}", mapping.Identity.Target, card.ExternalCardID), Method.PUT); updateRequest.AddHeader("Accept", "application/json"); updateRequest.AddHeader("Content-type", "application/xml"); updateRequest.AddParameter("application/xml", "<ticket><status>" + ticketToUpdate.Status + "</status></ticket>", ParameterType.RequestBody); var resp = _restClient.Execute(updateRequest); if (resp.StatusCode != HttpStatusCode.OK) { if (resp.Content != null && !string.IsNullOrEmpty(resp.Content.Trim())) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(resp.Content); Log.Error(string.Format("Unable to update Ticket [{0}] to [{1}], Description: {2}, Message: {3}", card.ExternalCardID, ticketToUpdate.Status, resp.StatusDescription, errorMessage.Message)); } else { Log.Error(string.Format("Unable to update Ticket [{0}] to [{1}], Description: {2}, Message: {3}", card.ExternalCardID, ticketToUpdate.Status, resp.StatusDescription, resp.StatusDescription)); } } else { success = true; Log.Debug(String.Format("Updated state for Ticket [{0}] to [{1}]", card.ExternalCardID, ticketToUpdate.Status)); } } catch (Exception ex) { Log.Error(string.Format("Unable to update Ticket [{0}] to [{1}], Exception: {2}", card.ExternalCardID, ticketToUpdate.Status, ex.Message)); } } else { Log.Debug(String.Format("Could not retrieve Ticket [{0}] for updating state to [{1}]", card.ExternalCardID, ticketToUpdate.Status)); } } tries++; } }
public long? CalculateAssignedUserId(long boardId, Ticket ticket) { if (ticket == null) return null; if (ticket.Assignee_Id > 0) { // http://mysubdomain.unfuddle.com/api/v1/people/{id} var request = new RestRequest(string.Format("/api/v1/people/{0}", ticket.Assignee_Id), Method.GET); var unfuddleResp = _restClient.Execute(request); if (unfuddleResp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(unfuddleResp.Content); Log.Warn(string.Format("Unable to get user from Unfuddle, Error: {0}", errorMessage.Message)); } else { var user = new JsonSerializer<Person>().DeserializeFromString(unfuddleResp.Content); if (user != null) { var lkUser = LeanKit.GetBoard(boardId).BoardUsers.FirstOrDefault(x => x!= null && ((!string.IsNullOrEmpty(x.EmailAddress)) && (!string.IsNullOrEmpty(user.Email)) && x.EmailAddress.ToLowerInvariant() == user.Email.ToLowerInvariant()) || ((!string.IsNullOrEmpty(x.FullName)) && (!string.IsNullOrEmpty(user.Last_Name)) && x.FullName.ToLowerInvariant() == (user.First_Name + " " + user.Last_Name).ToLowerInvariant()) || ((!string.IsNullOrEmpty(x.UserName)) && (!string.IsNullOrEmpty(user.Username)) && x.UserName.ToLowerInvariant() == user.Username.ToLowerInvariant())); if (lkUser != null) return lkUser.Id; } } } return null; }
protected override void Synchronize(BoardMapping project) { Log.Debug("Polling Unfuddle for Tickets"); var queryAsOfDate = QueryDate.AddMilliseconds(Configuration.PollingFrequency * -1.5); var unfuddleQuery = !string.IsNullOrEmpty(project.Query) ? string.Format(project.Query, queryAsOfDate.ToString("yyyy/MM/dd hh:mm")) : string.Format("status-eq-{0},created_at-gt-{1}", project.QueryStates[0], queryAsOfDate.ToString("yyyy/MM/dd hh:mm")); //http://mysubdomain.unfuddle.com/api/v1/projects/{id}/ticket_reports/dynamic?sort_by=created_at&sort_direction=ASC&conditions_string=status-eq-new,created_at-gt-yyyy/MM/dd hh:mm var request = new RestRequest(string.Format("/api/v1/projects/{0}/ticket_reports/dynamic", project.Identity.Target), Method.GET); request.AddParameter("sort_by", "created_at"); request.AddParameter("sort_direction", "ASC"); request.AddParameter("conditions_string", unfuddleQuery); //, "id,status,priority,summary,description,type"); request.AddParameter("limit", "500"); var unfuddleResp = _restClient.Execute(request); if (unfuddleResp.StatusCode != HttpStatusCode.OK) { var serializer = new JsonSerializer<ErrorMessage>(); var errorMessage = serializer.DeserializeFromString(unfuddleResp.Content); Log.Error(string.Format("Unable to get tickets from Unfuddle, Error: {0}. Check your board/project mapping configuration.", errorMessage.Message)); return; } var resp = new JsonSerializer<TicketsResponse>().DeserializeFromString(unfuddleResp.Content); Log.Info("\nQueried [{0}] at {1} for changes after {2}", project.Identity.Target, QueryDate, queryAsOfDate.ToString("o")); if (resp != null && resp.Groups != null && resp.Groups.Any()) { foreach (var group in resp.Groups) { if (group != null && group.Tickets != null && group.Tickets.Any()) { var tickets = group.Tickets; foreach (var ticket in tickets) { Log.Info("Ticket [{0}]: {1}, {2}, {3}", ticket.Id, ticket.Summary, ticket.Status, ticket.Priority); // does this workitem have a corresponding card? var card = LeanKit.GetCardByExternalId(project.Identity.LeanKit, ticket.Id.ToString()); if (card == null || !card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) { Log.Debug("Create new card for Ticket [{0}]", ticket.Id); CreateCardFromItem(project, ticket); } else { Log.Debug("Previously created a card for Ticket [{0}]", ticket.Id); if (project.UpdateCards) TicketUpdated(ticket, card, project); else Log.Info("Skipped card update because 'UpdateCards' is disabled."); } } } } Log.Info("{0} item(s) queried.\n", resp.Count); } }