From 39af89226f916c58368a7e7d6c53fbda47bbfee1 Mon Sep 17 00:00:00 2001 From: David Neal Date: Fri, 6 Nov 2015 08:11:16 -0800 Subject: [PATCH 01/20] Adding fix on service start to not check for card moves when UpdateTargetItems is turned off for a board mapping --- .../Targets/TargetBase.cs | 82 ++++++++++--------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/IntegrationService.Library/Targets/TargetBase.cs b/IntegrationService.Library/Targets/TargetBase.cs index 485ba53..9ab616f 100644 --- a/IntegrationService.Library/Targets/TargetBase.cs +++ b/IntegrationService.Library/Targets/TargetBase.cs @@ -123,65 +123,67 @@ protected TargetBase(IBoardSubscriptionManager subscriptions, IConfigurationProv public virtual void Process() { - if (Configuration != null && Configuration.Mappings != null) - { - var pollingInSeconds = Configuration.PollingFrequency / 1000; + if (Configuration == null || Configuration.Mappings == null) return; + + var pollingInSeconds = Configuration.PollingFrequency / 1000; - // Add 3 seconds to reduce risk of race conditions - // between LeanKit and Target system - pollingInSeconds += 3; + // Add 3 seconds to reduce risk of race conditions + // between LeanKit and Target system + pollingInSeconds += 3; - foreach (var mapping in Configuration.Mappings) + foreach (var mapping in Configuration.Mappings) + { + // pickup any changes since the last time the service ran + // start subscription to each board in board mappings + CheckForMissedCardMoves(mapping); + try { - // pickup any changes since the last time the service ran - // start subscription to each board in board mappings - CheckForMissedCardMoves(mapping); - try - { - Subscriptions.Subscribe(LeanKitAccount, mapping.Identity.LeanKit, pollingInSeconds, BoardUpdate); - } - catch (Exception ex) - { - Log.Error(string.Format("An error occured: {0} - {1} - {2}", ex.GetType(), ex.Message, ex.StackTrace)); - } + Subscriptions.Subscribe(LeanKitAccount, mapping.Identity.LeanKit, pollingInSeconds, BoardUpdate); + } + catch (Exception ex) + { + Log.Error(string.Format("An error occured: {0} - {1} - {2}", ex.GetType(), ex.Message, ex.StackTrace)); } } - if (Configuration != null && Configuration.Mappings != null) + QueryDate = Configuration.EarliestSyncDate.ToUniversalTime(); + + var i = 0; + while (true) { - QueryDate = Configuration.EarliestSyncDate.ToUniversalTime(); + i++; + if (i%10 == 0) + SaveRecentQueryDate(QueryDate); - int i = 0; - while (true) + foreach (var project in Configuration.Mappings) { - i++; - if (i%10 == 0) - SaveRecentQueryDate(QueryDate); - - foreach (var project in Configuration.Mappings) + try { - try - { - Synchronize(project); - } - catch (Exception ex) - { - Log.Error("An error occurred: {0} - {1} - {2}", ex.GetType(), ex.Message, ex.StackTrace); - } + Synchronize(project); + } + catch (Exception ex) + { + Log.Error("An error occurred: {0} - {1} - {2}", ex.GetType(), ex.Message, ex.StackTrace); } + } - if (StopEvent.WaitOne(Configuration.PollingFrequency)) - break; + if (StopEvent.WaitOne(Configuration.PollingFrequency)) + break; - QueryDate = DateTime.Now; - } + QueryDate = DateTime.Now; } } private void CheckForMissedCardMoves(BoardMapping mapping) { + if (!mapping.UpdateTargetItems) + { + Log.Info("Skipped check for missed card moves because 'UpdateTargetItems' is disabled."); + return; + } + // if we have local storage, we have saved board versions and we have one for this board - long boardId = mapping.Identity.LeanKit; + long boardId = mapping.Identity.LeanKit; if (AppSettings != null && AppSettings.BoardVersions != null && AppSettings.BoardVersions.Any() && From d8dbdab4592d5de2ba532bd2d35a35b7277c0e14 Mon Sep 17 00:00:00 2001 From: David Neal Date: Thu, 21 Jan 2016 14:17:05 -0500 Subject: [PATCH 02/20] Fixed scenario where board update could trigger... ... a target item to be updated even when UpdateTargetItems is disabled. --- .../Targets/TargetBase.cs | 180 +++++++++--------- .../JiraSubsystem.cs | 27 +-- IntegrationService.Targets.TFS/Tfs.cs | 3 +- 3 files changed, 98 insertions(+), 112 deletions(-) diff --git a/IntegrationService.Library/Targets/TargetBase.cs b/IntegrationService.Library/Targets/TargetBase.cs index 9ab616f..b9a93c2 100644 --- a/IntegrationService.Library/Targets/TargetBase.cs +++ b/IntegrationService.Library/Targets/TargetBase.cs @@ -183,72 +183,68 @@ private void CheckForMissedCardMoves(BoardMapping mapping) } // if we have local storage, we have saved board versions and we have one for this board - long boardId = mapping.Identity.LeanKit; - if (AppSettings != null && - AppSettings.BoardVersions != null && - AppSettings.BoardVersions.Any() && - AppSettings.BoardVersions.ContainsKey(boardId)) + var boardId = mapping.Identity.LeanKit; + if (AppSettings == null || AppSettings.BoardVersions == null || !AppSettings.BoardVersions.Any() || + !AppSettings.BoardVersions.ContainsKey(boardId)) return; + + var version = AppSettings.BoardVersions[boardId]; + Log.Debug(string.Format("Checking for any cards moved to mapped lanes on board [{0}] since service last ran, version [{1}].", boardId, version)); + try { - var version = AppSettings.BoardVersions[boardId]; - Log.Debug(string.Format("Checking for any cards moved to mapped lanes on board [{0}] since service last ran, version [{1}].", boardId, version)); - try + var events = LeanKit.GetBoardHistorySince(boardId, version); + var board = LeanKit.GetBoard(boardId); + if (board == null || events == null) return; + + foreach (var ev in events) { - var events = LeanKit.GetBoardHistorySince(boardId, version); - var board = LeanKit.GetBoard(boardId); - if (board != null && events != null) + // check for created cards + if (ev.EventType == "CardCreation") { - foreach (var ev in events) + var card = LeanKit.GetCard(board.Id, ev.CardId); + if (card != null && string.IsNullOrEmpty(card.ExternalCardID)) { - // check for created cards - if (ev.EventType == "CardCreation") - { - var card = LeanKit.GetCard(board.Id, ev.CardId); - if (card != null && string.IsNullOrEmpty(card.ExternalCardID)) - { - try - { - CreateNewItem(card.ToCard(), mapping); - } - catch (Exception e) - { - Log.Error("Exception for CreateNewItem: " + e.Message); - } - } - } - // only look for moved cards - else if (ev.ToLaneId != 0) + try { - var lane = board.GetLaneById(ev.ToLaneId); - if (lane != null) + CreateNewItem(card.ToCard(), mapping); + } + catch (Exception e) + { + Log.Error("Exception for CreateNewItem: " + e.Message); + } + } + } + // only look for moved cards + else if (ev.ToLaneId != 0) + { + var lane = board.GetLaneById(ev.ToLaneId); + if (lane != null) + { + if (lane.Id.HasValue && mapping.LaneToStatesMap.Any() && mapping.LaneToStatesMap.ContainsKey(lane.Id.Value)) + { + if (mapping.LaneToStatesMap[lane.Id.Value] != null && mapping.LaneToStatesMap[lane.Id.Value].Count > 0) { - if (lane.Id.HasValue && mapping.LaneToStatesMap.Any() && mapping.LaneToStatesMap.ContainsKey(lane.Id.Value)) + // board.GetCard() only seems to get cards in active lanes + // using LeanKitApi.GetCard() instead because it will get + // cards in archive lanes + var card = LeanKit.GetCard(board.Id, ev.CardId); + if (card != null && !string.IsNullOrEmpty(card.ExternalCardID)) { - if (mapping.LaneToStatesMap[lane.Id.Value] != null && mapping.LaneToStatesMap[lane.Id.Value].Count > 0) - { - // board.GetCard() only seems to get cards in active lanes - // using LeanKitApi.GetCard() instead because it will get - // cards in archive lanes - var card = LeanKit.GetCard(board.Id, ev.CardId); - if (card != null && !string.IsNullOrEmpty(card.ExternalCardID)) - { - try { - UpdateStateOfExternalItem(card.ToCard(), mapping.LaneToStatesMap[lane.Id.Value], mapping); - } catch (Exception e) { - Log.Error("Exception for UpdateStateOfExternalItem: " + e.Message); - } - } + try { + UpdateStateOfExternalItem(card.ToCard(), mapping.LaneToStatesMap[lane.Id.Value], mapping); + } catch (Exception e) { + Log.Error("Exception for UpdateStateOfExternalItem: " + e.Message); } } } - } + } } - UpdateBoardVersion(board.Id, board.Version); - } - } - catch (Exception ex) - { - Log.Error(string.Format("An error occured: {0} - {1} - {2}", ex.GetType(), ex.Message, ex.StackTrace)); + } } + UpdateBoardVersion(board.Id, board.Version); + } + catch (Exception ex) + { + Log.Error(string.Format("An error occured: {0} - {1} - {2}", ex.GetType(), ex.Message, ex.StackTrace)); } } @@ -637,7 +633,7 @@ protected virtual void BoardUpdate(long boardId, BoardChangedEventArgs eventArgs // check for content change events if (!boardConfig.CreateTargetItems) { - Log.Info("Skipped adding target items because 'AddTargetItems' is disabled."); + Log.Info("Skipped checking for newly added cards because 'CreateTargetItems' is disabled."); } else { @@ -659,54 +655,66 @@ protected virtual void BoardUpdate(long boardId, BoardChangedEventArgs eventArgs } } - //Ignore all other events except for MovedCardEvents - if (!eventArgs.MovedCards.Any()) + if (!boardConfig.UpdateTargetItems && !boardConfig.CreateTargetItems) { - Log.Debug(String.Format("No Card Move Events detected event for board [{0}], exiting method", boardId)); + Log.Info("Skipped checking moved cards because 'UpdateTargetItems' and 'CreateTargetItems' are disabled."); + UpdateBoardVersion(boardId); return; } - UpdateBoardVersion(boardId); - - Log.Debug("Checking for cards moved to mapped lanes."); - foreach (var movedCardEvent in eventArgs.MovedCards.Where(x => x != null && x.ToLane != null && x.MovedCard != null)) + if (eventArgs.MovedCards.Any()) { - try + Log.Debug("Checking for cards moved to mapped lanes."); + foreach (var movedCardEvent in eventArgs.MovedCards.Where(x => x != null && x.ToLane != null && x.MovedCard != null)) { - if (!movedCardEvent.ToLane.Id.HasValue) continue; - - if (boardConfig.LaneToStatesMap.Any() && - boardConfig.LaneToStatesMap.ContainsKey(movedCardEvent.ToLane.Id.Value)) + try { - var states = boardConfig.LaneToStatesMap[movedCardEvent.ToLane.Id.Value]; - if (states != null && states.Count > 0) + if (!movedCardEvent.ToLane.Id.HasValue) continue; + + if (boardConfig.LaneToStatesMap.Any() && + boardConfig.LaneToStatesMap.ContainsKey(movedCardEvent.ToLane.Id.Value)) { - try - { - if (!string.IsNullOrEmpty(movedCardEvent.MovedCard.ExternalCardID)) - UpdateStateOfExternalItem(movedCardEvent.MovedCard, states, boardConfig); - else if (boardConfig.CreateTargetItems) - // This may be a task card being moved to the parent board, or card being moved from another board - CreateNewItem(movedCardEvent.MovedCard, boardConfig); - } - catch (Exception e) + var states = boardConfig.LaneToStatesMap[movedCardEvent.ToLane.Id.Value]; + if (states != null && states.Count > 0) { - Log.Error("Exception for UpdateStateOfExternalItem: " + e.Message); + try + { + if (!string.IsNullOrEmpty(movedCardEvent.MovedCard.ExternalCardID) && boardConfig.UpdateTargetItems) + { + UpdateStateOfExternalItem(movedCardEvent.MovedCard, states, boardConfig); + } + else if (string.IsNullOrEmpty(movedCardEvent.MovedCard.ExternalCardID) && boardConfig.CreateTargetItems) + { + // This may be a task card being moved to the parent board, or card being moved from another board + CreateNewItem(movedCardEvent.MovedCard, boardConfig); + } + } + catch (Exception e) + { + Log.Error("Exception for UpdateStateOfExternalItem: " + e.Message); + } } + else + Log.Debug(string.Format("No states are mapped to the Lane [{0}]", movedCardEvent.ToLane.Id.Value)); } else - Log.Debug(String.Format("No states are mapped to the Lane [{0}]", movedCardEvent.ToLane.Id.Value)); + { + Log.Debug(string.Format("No states are mapped to the Lane [{0}]", movedCardEvent.ToLane.Id.Value)); + } } - else + catch (Exception e) { - Log.Debug(String.Format("No states are mapped to the Lane [{0}]", movedCardEvent.ToLane.Id.Value)); + string.Format("Error processing moved card, [{0}]: {1}", movedCardEvent.MovedCard.Id, e.Message).Error(e); } } - catch (Exception e) - { - string.Format("Error processing moved card, [{0}]: {1}", movedCardEvent.MovedCard.Id, e.Message).Error(e); - } } + else + { + Log.Debug(string.Format("No Card Move Events detected event for board [{0}], exiting method", boardId)); + } + + UpdateBoardVersion(boardId); + } protected void SaveRecentQueryDate(DateTime queryDate) diff --git a/IntegrationService.Targets.JIRA/JiraSubsystem.cs b/IntegrationService.Targets.JIRA/JiraSubsystem.cs index 21bed2f..a69b67b 100644 --- a/IntegrationService.Targets.JIRA/JiraSubsystem.cs +++ b/IntegrationService.Targets.JIRA/JiraSubsystem.cs @@ -159,31 +159,6 @@ private IRestResponse ExecuteRequest(RestRequest request) return _restClient.Execute(request); } - //private string GetSessionCookie(string host, string user, string password) - //{ - // string sessionCookie = null; - // try - // { - // _restClient.BaseUrl = new Uri(host); - // var request = new RestRequest("rest/auth/1/session", Method.POST); - // request.AddJsonBody(new { username = user, password = password }); - // var response = ExecuteRequest(request); - // if (response.StatusCode != HttpStatusCode.OK) - // { - // string.Format("Error connecting to {0}{1}", _restClient.BaseUrl, request.Resource).Error(); - // if (response.Content != null) response.Content.Error(); - // return null; - // }; - // var cookie = response.Cookies.FirstOrDefault(c => c.Name.Equals("JSESSIONID", StringComparison.OrdinalIgnoreCase)); - // if (cookie != null) sessionCookie = cookie.Value; - // } - // catch (Exception ex) - // { - // "Error getting session using rest/auth/1/session.".Error(ex); - // } - // return sessionCookie; - //} - public override void Init() { if (Configuration == null) return; @@ -744,6 +719,8 @@ protected override void UpdateStateOfExternalItem(Card card, List states protected void UpdateStateOfExternalItem(Card card, List states, BoardMapping mapping, bool runOnlyOnce) { + if (!mapping.UpdateTargetItems) return; + if (string.IsNullOrEmpty(card.ExternalSystemName) || !card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) return; diff --git a/IntegrationService.Targets.TFS/Tfs.cs b/IntegrationService.Targets.TFS/Tfs.cs index f7e21a3..dbf91b7 100644 --- a/IntegrationService.Targets.TFS/Tfs.cs +++ b/IntegrationService.Targets.TFS/Tfs.cs @@ -325,7 +325,8 @@ protected override void UpdateStateOfExternalItem(Card card, List states } protected void UpdateStateOfExternalItem(Card card, List states, BoardMapping mapping, bool runOnlyOnce) - { + { + if (!mapping.UpdateTargetItems) return; if (!card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) return; if (string.IsNullOrEmpty(card.ExternalCardID)) return; From 5c610cdf21c96a32c30d79a83884cc749626c95a Mon Sep 17 00:00:00 2001 From: Sharp Jon Date: Tue, 26 Jan 2016 15:23:07 -0600 Subject: [PATCH 03/20] Added support for NTLM/Windows auth using standard "DOMAIN\user" input convention --- IntegrationService.Targets.TFS/Tfs.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/IntegrationService.Targets.TFS/Tfs.cs b/IntegrationService.Targets.TFS/Tfs.cs index dbf91b7..f2a2b8b 100644 --- a/IntegrationService.Targets.TFS/Tfs.cs +++ b/IntegrationService.Targets.TFS/Tfs.cs @@ -877,7 +877,24 @@ public override void Init() } else { - _projectCollectionNetworkCredentials = new NetworkCredential(Configuration.Target.User, Configuration.Target.Password); + // Check for common NTLM/Windows Auth convention + // ("DOMAIN\Username") in User input and separate + // domain from username: + string domain = null; + if (Configuration.Target.User.Contains("\\")) + { + string[] domainUser = Configuration.Target.User.Split('\\'); + domain = domainUser[0]; + Configuration.Target.User.Replace(domain + "\\", ""); + } + if (domain != null) + { + _projectCollectionNetworkCredentials = new NetworkCredential(Configuration.Target.User, Configuration.Target.Password, domain); + } + else + { + _projectCollectionNetworkCredentials = new NetworkCredential(Configuration.Target.User, Configuration.Target.Password); + } if (_projectCollection == null) { _projectCollection = new TfsTeamProjectCollection(_projectCollectionUri, _projectCollectionNetworkCredentials); From 5efab986bc720995f49456a68bb14604f85f9a1b Mon Sep 17 00:00:00 2001 From: Sharp Jon Date: Wed, 27 Jan 2016 08:32:57 -0600 Subject: [PATCH 04/20] Fixed (stupid) issue with username --- IntegrationService.Targets.TFS/Tfs.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/IntegrationService.Targets.TFS/Tfs.cs b/IntegrationService.Targets.TFS/Tfs.cs index f2a2b8b..623e7af 100644 --- a/IntegrationService.Targets.TFS/Tfs.cs +++ b/IntegrationService.Targets.TFS/Tfs.cs @@ -3,6 +3,7 @@ // Copyright (c) LeanKit Inc. All rights reserved. // //------------------------------------------------------------------------------ +// This is my file now! using System; using System.Collections.Generic; @@ -881,15 +882,17 @@ public override void Init() // ("DOMAIN\Username") in User input and separate // domain from username: string domain = null; + string username = null; if (Configuration.Target.User.Contains("\\")) { string[] domainUser = Configuration.Target.User.Split('\\'); domain = domainUser[0]; - Configuration.Target.User.Replace(domain + "\\", ""); + username = domainUser[1]; } if (domain != null) { - _projectCollectionNetworkCredentials = new NetworkCredential(Configuration.Target.User, Configuration.Target.Password, domain); + Log.Debug("Logging in using NTLM auth (using domain: {0}, username: {1})", domain, username); + _projectCollectionNetworkCredentials = new NetworkCredential(username, Configuration.Target.Password, domain); } else { From f654d09dea713525ba3b614d1c0c10c21593196a Mon Sep 17 00:00:00 2001 From: David Neal Date: Mon, 1 Feb 2016 17:35:24 -0500 Subject: [PATCH 05/20] Fix for issue types that contain a slash --- .gitignore | 1 + IntegrationService/Site/lib/niceTools.js | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index dafd4f9..2c5bccd 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ node_modules packages *.sln.DotSettings *.bak +.vs \ No newline at end of file diff --git a/IntegrationService/Site/lib/niceTools.js b/IntegrationService/Site/lib/niceTools.js index a3fed2c..57fe500 100644 --- a/IntegrationService/Site/lib/niceTools.js +++ b/IntegrationService/Site/lib/niceTools.js @@ -21,6 +21,8 @@ String.prototype.toId = function (includeHashSymbol) { str = str.toLowerCase(); // replace spaces with double underscores str = str.replace(/ /g, "__"); + // replace slash with triple dash + str = str.replace(/\//g, "---"); if (includeHashSymbol) str = "#" + str; @@ -29,9 +31,12 @@ String.prototype.toId = function (includeHashSymbol) { String.prototype.fromId = function () { var str = this; + // replace double pluses with slash + str = str.replace(/---/g, "/"); // replace double underscores with spaces str = str.replace(/__/g, " "); + // trim and convert to Uppercase first letter str = str.replace(/\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1); From 0f1ee360130bd205e9490d1346c326209bf0473e Mon Sep 17 00:00:00 2001 From: David Neal Date: Fri, 4 Mar 2016 10:11:41 -0600 Subject: [PATCH 06/20] Refactor to better support multiple mappings to the same board --- .../BoardSubscriptionManager.cs | 14 +- .../Targets/TargetBase.cs | 243 +++++++++--------- .../JiraSubsystem.cs | 45 ++-- 3 files changed, 154 insertions(+), 148 deletions(-) diff --git a/IntegrationService.Library/BoardSubscriptionManager.cs b/IntegrationService.Library/BoardSubscriptionManager.cs index 11db5d9..108f9e3 100644 --- a/IntegrationService.Library/BoardSubscriptionManager.cs +++ b/IntegrationService.Library/BoardSubscriptionManager.cs @@ -90,14 +90,12 @@ public ILeanKitApi Subscribe(ILeanKitAccountAuth auth, long boardId, int polling } lock (BoardSubscriptions) { - if (!BoardSubscriptions.ContainsKey(boardId)) - { - BoardSubscriptions[boardId] = new BoardSubscription(auth, boardId, pollingFrequency); - } - - BoardSubscriptions[boardId].Notifications.Add(notification); - - return BoardSubscriptions[boardId].LkClientApi; + if (BoardSubscriptions.ContainsKey(boardId)) return BoardSubscriptions[boardId].LkClientApi; + + BoardSubscriptions[boardId] = new BoardSubscription(auth, boardId, pollingFrequency); + BoardSubscriptions[boardId].Notifications.Add(notification); + + return BoardSubscriptions[boardId].LkClientApi; } } diff --git a/IntegrationService.Library/Targets/TargetBase.cs b/IntegrationService.Library/Targets/TargetBase.cs index b9a93c2..7aeea37 100644 --- a/IntegrationService.Library/Targets/TargetBase.cs +++ b/IntegrationService.Library/Targets/TargetBase.cs @@ -495,79 +495,80 @@ protected virtual void BoardUpdate(long boardId, BoardChangedEventArgs eventArgs { if (eventArgs.BoardStructureChanged) { - Log.Debug(String.Format("Received BoardStructureChanged event for [{0}], reloading Configuration", boardId)); + Log.Debug(string.Format("Received BoardStructureChanged event for [{0}], reloading Configuration", boardId)); // TODO: Ideally this would be ReloadConfiguration(boardId); ReloadConfiguration(); } - var boardConfig = Configuration.Mappings.FirstOrDefault(x => x.Identity.LeanKit == boardId); - if (boardConfig == null) - { - Log.Debug(String.Format("Expected a configuration for board [{0}].", boardId)); - return; - } + Log.Debug(string.Format("Received board changed event for board [{0}]", boardId)); - Log.Debug(String.Format("Received board changed event for board [{0}]", boardId)); + // var boardConfig = Configuration.Mappings.FirstOrDefault(x => x.Identity.LeanKit == boardId); + var boardConfigs = Configuration.Mappings.Where(x => x.Identity.LeanKit == boardId).ToList(); + if (boardConfigs.Count == 0) + { + Log.Debug(string.Format("Expected a configuration for board [{0}].", boardId)); + return; + } - // check for content change events - if (!boardConfig.UpdateTargetItems) - { - Log.Info("Skipped target item update because 'UpdateTargetItems' is disabled."); - } - else - { - Log.Info("Checking for updated cards."); - if (eventArgs.UpdatedCards.Any()) - { - var itemsUpdated = new List(); - foreach (var updatedCardEvent in eventArgs.UpdatedCards) - { - try - { - if (updatedCardEvent.UpdatedCard == null) throw new Exception("Updated card is null"); - if (updatedCardEvent.OriginalCard == null) throw new Exception("Original card is null"); + // check for content change events + if (boardConfigs.Any(c => c.UpdateTargetItems)) + { + var boardConfig = + Configuration.Mappings.FirstOrDefault(x => x.Identity.LeanKit == boardId && x.UpdateTargetItems == true); - var card = updatedCardEvent.UpdatedCard; + Log.Info("Checking for updated cards."); - if (string.IsNullOrEmpty(card.ExternalCardID) && !string.IsNullOrEmpty(card.ExternalSystemUrl)) - { - // try to grab id from url - var pos = card.ExternalSystemUrl.LastIndexOf('='); - if (pos > 0) - card.ExternalCardID = card.ExternalSystemUrl.Substring(pos + 1); - } + if (eventArgs.UpdatedCards.Any()) + { + var itemsUpdated = new List(); + foreach (var updatedCardEvent in eventArgs.UpdatedCards) + { + try + { + if (updatedCardEvent.UpdatedCard == null) throw new Exception("Updated card is null"); + if (updatedCardEvent.OriginalCard == null) throw new Exception("Original card is null"); - if (string.IsNullOrEmpty(card.ExternalCardID)) continue; // still invalid; skip this card - - if (card.Title != updatedCardEvent.OriginalCard.Title) - itemsUpdated.Add("Title"); - if (card.Description != updatedCardEvent.OriginalCard.Description) - itemsUpdated.Add("Description"); - if (card.Tags != updatedCardEvent.OriginalCard.Tags) - itemsUpdated.Add("Tags"); - if (card.Priority != updatedCardEvent.OriginalCard.Priority) - itemsUpdated.Add("Priority"); - if (card.DueDate != updatedCardEvent.OriginalCard.DueDate) - itemsUpdated.Add("DueDate"); - if (card.Size != updatedCardEvent.OriginalCard.Size) - itemsUpdated.Add("Size"); - if (card.IsBlocked != updatedCardEvent.OriginalCard.IsBlocked) - itemsUpdated.Add("Blocked"); - - if (itemsUpdated.Count <= 0) continue; - - CardUpdated(card, itemsUpdated, boardConfig); - } - catch (Exception e) - { + var card = updatedCardEvent.UpdatedCard; + + if (string.IsNullOrEmpty(card.ExternalCardID) && !string.IsNullOrEmpty(card.ExternalSystemUrl)) + { + // try to grab id from url + var pos = card.ExternalSystemUrl.LastIndexOf('='); + if (pos > 0) + card.ExternalCardID = card.ExternalSystemUrl.Substring(pos + 1); + } + + if (string.IsNullOrEmpty(card.ExternalCardID)) continue; // still invalid; skip this card + + if (card.Title != updatedCardEvent.OriginalCard.Title) + itemsUpdated.Add("Title"); + if (card.Description != updatedCardEvent.OriginalCard.Description) + itemsUpdated.Add("Description"); + if (card.Tags != updatedCardEvent.OriginalCard.Tags) + itemsUpdated.Add("Tags"); + if (card.Priority != updatedCardEvent.OriginalCard.Priority) + itemsUpdated.Add("Priority"); + if (card.DueDate != updatedCardEvent.OriginalCard.DueDate) + itemsUpdated.Add("DueDate"); + if (card.Size != updatedCardEvent.OriginalCard.Size) + itemsUpdated.Add("Size"); + if (card.IsBlocked != updatedCardEvent.OriginalCard.IsBlocked) + itemsUpdated.Add("Blocked"); + + if (itemsUpdated.Count <= 0) continue; + + CardUpdated(card, itemsUpdated, boardConfig); + } + catch (Exception e) + { var card = updatedCardEvent.UpdatedCard ?? updatedCardEvent.OriginalCard ?? new Card(); string.Format("Error processing blocked card, [{0}]: {1}", card.Id, e.Message).Error(e); - } - } - } - if (eventArgs.BlockedCards.Any()) + } + } + } + if (eventArgs.BlockedCards.Any()) { - var itemsUpdated = new List(); + var itemsUpdated = new List(); foreach (var cardBlockedEvent in eventArgs.BlockedCards) { try @@ -599,7 +600,7 @@ protected virtual void BoardUpdate(long boardId, BoardChangedEventArgs eventArgs if (eventArgs.UnBlockedCards.Any()) { var itemsUpdated = new List(); - foreach (var cardUnblockedEvent in eventArgs.UnBlockedCards) + foreach (var cardUnblockedEvent in eventArgs.UnBlockedCards) { try { @@ -625,92 +626,98 @@ protected virtual void BoardUpdate(long boardId, BoardChangedEventArgs eventArgs var card = cardUnblockedEvent.UnBlockedCard ?? new Card(); string.Format("Error processing unblocked card, [{0}]: {1}", card.Id, e.Message).Error(e); } - } + } } - } - - - // check for content change events - if (!boardConfig.CreateTargetItems) - { - Log.Info("Skipped checking for newly added cards because 'CreateTargetItems' is disabled."); } else { - Log.Info("Checking for added cards."); - if (eventArgs.AddedCards.Any()) + Log.Info("Skipped target item update because 'UpdateTargetItems' is disabled."); + } + + foreach (var boardConfig in boardConfigs) + { + // check for content change events + if (!boardConfig.CreateTargetItems) { - foreach (var newCard in eventArgs.AddedCards.Select(cardAddEvent => cardAddEvent.AddedCard) - .Where(newCard => newCard != null && string.IsNullOrEmpty(newCard.ExternalCardID))) + Log.Info("Skipped checking for newly added cards because 'CreateTargetItems' is disabled."); + } + else + { + Log.Info("Checking for added cards."); + if (eventArgs.AddedCards.Any()) { - try - { - CreateNewItem(newCard, boardConfig); - } - catch (Exception e) + foreach (var newCard in eventArgs.AddedCards.Select(cardAddEvent => cardAddEvent.AddedCard) + .Where(newCard => newCard != null && string.IsNullOrEmpty(newCard.ExternalCardID))) { - string.Format("Error processing newly created card, [{0}]: {1}", newCard.Id, e.Message).Error(e); + try + { + CreateNewItem(newCard, boardConfig); + } + catch (Exception e) + { + string.Format("Error processing newly created card, [{0}]: {1}", newCard.Id, e.Message).Error(e); + } } } } - } - if (!boardConfig.UpdateTargetItems && !boardConfig.CreateTargetItems) - { - Log.Info("Skipped checking moved cards because 'UpdateTargetItems' and 'CreateTargetItems' are disabled."); - UpdateBoardVersion(boardId); - return; - } + if (!boardConfig.UpdateTargetItems && !boardConfig.CreateTargetItems) + { + Log.Info("Skipped checking moved cards because 'UpdateTargetItems' and 'CreateTargetItems' are disabled."); + // UpdateBoardVersion(boardId); + continue; + } - if (eventArgs.MovedCards.Any()) - { - Log.Debug("Checking for cards moved to mapped lanes."); - foreach (var movedCardEvent in eventArgs.MovedCards.Where(x => x != null && x.ToLane != null && x.MovedCard != null)) + if (eventArgs.MovedCards.Any()) { - try + Log.Debug("Checking for cards moved to mapped lanes."); + foreach (var movedCardEvent in eventArgs.MovedCards.Where(x => x != null && x.ToLane != null && x.MovedCard != null)) { - if (!movedCardEvent.ToLane.Id.HasValue) continue; - - if (boardConfig.LaneToStatesMap.Any() && - boardConfig.LaneToStatesMap.ContainsKey(movedCardEvent.ToLane.Id.Value)) + try { - var states = boardConfig.LaneToStatesMap[movedCardEvent.ToLane.Id.Value]; - if (states != null && states.Count > 0) + if (!movedCardEvent.ToLane.Id.HasValue) continue; + + if (boardConfig.LaneToStatesMap.Any() && + boardConfig.LaneToStatesMap.ContainsKey(movedCardEvent.ToLane.Id.Value)) { - try + var states = boardConfig.LaneToStatesMap[movedCardEvent.ToLane.Id.Value]; + if (states != null && states.Count > 0) { - if (!string.IsNullOrEmpty(movedCardEvent.MovedCard.ExternalCardID) && boardConfig.UpdateTargetItems) + try { - UpdateStateOfExternalItem(movedCardEvent.MovedCard, states, boardConfig); + if (!string.IsNullOrEmpty(movedCardEvent.MovedCard.ExternalCardID) && boardConfig.UpdateTargetItems) + { + UpdateStateOfExternalItem(movedCardEvent.MovedCard, states, boardConfig); + } + else if (string.IsNullOrEmpty(movedCardEvent.MovedCard.ExternalCardID) && boardConfig.CreateTargetItems) + { + // This may be a task card being moved to the parent board, or card being moved from another board + CreateNewItem(movedCardEvent.MovedCard, boardConfig); + } } - else if (string.IsNullOrEmpty(movedCardEvent.MovedCard.ExternalCardID) && boardConfig.CreateTargetItems) + catch (Exception e) { - // This may be a task card being moved to the parent board, or card being moved from another board - CreateNewItem(movedCardEvent.MovedCard, boardConfig); + Log.Error("Exception for UpdateStateOfExternalItem: " + e.Message); } } - catch (Exception e) - { - Log.Error("Exception for UpdateStateOfExternalItem: " + e.Message); - } + else + Log.Debug(string.Format("No states are mapped to the Lane [{0}]", movedCardEvent.ToLane.Id.Value)); } else + { Log.Debug(string.Format("No states are mapped to the Lane [{0}]", movedCardEvent.ToLane.Id.Value)); + } } - else + catch (Exception e) { - Log.Debug(string.Format("No states are mapped to the Lane [{0}]", movedCardEvent.ToLane.Id.Value)); + string.Format("Error processing moved card, [{0}]: {1}", movedCardEvent.MovedCard.Id, e.Message).Error(e); } } - catch (Exception e) - { - string.Format("Error processing moved card, [{0}]: {1}", movedCardEvent.MovedCard.Id, e.Message).Error(e); - } } - } - else - { - Log.Debug(string.Format("No Card Move Events detected event for board [{0}], exiting method", boardId)); + else + { + Log.Debug(string.Format("No Card Move Events detected event for board [{0}], exiting method", boardId)); + } } UpdateBoardVersion(boardId); diff --git a/IntegrationService.Targets.JIRA/JiraSubsystem.cs b/IntegrationService.Targets.JIRA/JiraSubsystem.cs index a69b67b..feff194 100644 --- a/IntegrationService.Targets.JIRA/JiraSubsystem.cs +++ b/IntegrationService.Targets.JIRA/JiraSubsystem.cs @@ -465,30 +465,30 @@ private void IssueUpdated(Issue issue, Card card, BoardMapping boardMapping) !string.IsNullOrEmpty(issue.Fields.Status.Name)) { var laneIds = boardMapping.LanesFromState(issue.Fields.Status.Name); - if (laneIds.Any()) + if (!laneIds.Any()) return; + if (laneIds.Contains(card.LaneId)) { - if (!laneIds.Contains(card.LaneId)) - { - // first let's see if any of the lanes are sibling lanes, if so then - // we should be using one of them. So we'll limit the results to just siblings - if (boardMapping.ValidLanes != null) - { - var siblingLaneIds = (from siblingLaneId in laneIds - let parentLane = - boardMapping.ValidLanes.FirstOrDefault(x => - x.HasChildLanes && - x.ChildLaneIds.Contains(siblingLaneId) && - x.ChildLaneIds.Contains(card.LaneId)) - where parentLane != null - select siblingLaneId).ToList(); - if (siblingLaneIds.Any()) - laneIds = siblingLaneIds; - } - - LeanKit.MoveCard(boardMapping.Identity.LeanKit, card.Id, laneIds.First(), 0, - "Moved Lane From Jira Issue"); - } + Log.Debug("Card [{0}] is already in mapped Lane [{1}]", card.Id, card.LaneId); + return; + } + // first let's see if any of the lanes are sibling lanes, if so then + // we should be using one of them. So we'll limit the results to just siblings + if (boardMapping.ValidLanes != null) + { + var siblingLaneIds = (from siblingLaneId in laneIds + let parentLane = + boardMapping.ValidLanes.FirstOrDefault(x => + x.HasChildLanes && + x.ChildLaneIds.Contains(siblingLaneId) && + x.ChildLaneIds.Contains(card.LaneId)) + where parentLane != null + select siblingLaneId).ToList(); + if (siblingLaneIds.Any()) + laneIds = siblingLaneIds; } + var laneId = laneIds.First(); + Log.Info("Moving card [{0}] to Lane [{1}]", card.Id, laneId); + LeanKit.MoveCard(boardMapping.Identity.LeanKit, card.Id, laneId, 0, "Moved Lane From Jira Issue"); } } @@ -497,6 +497,7 @@ protected override void Synchronize(BoardMapping project) Log.Debug("Polling Jira for Issues"); var queryAsOfDate = QueryDate.AddMilliseconds(Configuration.PollingFrequency*-1.5); + //var queryAsOfDate = QueryDate.AddMinutes(-5); string jqlQuery; var formattedQueryDate = queryAsOfDate.ToString(QueryDateFormat, CultureInfo.InvariantCulture); From 61035d1661149a13c755b684d06bcb025bbda58f Mon Sep 17 00:00:00 2001 From: David Neal Date: Fri, 4 Mar 2016 16:27:40 -0600 Subject: [PATCH 07/20] Change LK polling, logging info/debug --- .../Targets/TargetBase.cs | 11 +++++----- .../JiraSubsystem.cs | 20 +++++++++---------- IntegrationService.Targets.TFS/Tfs.cs | 4 ++-- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/IntegrationService.Library/Targets/TargetBase.cs b/IntegrationService.Library/Targets/TargetBase.cs index 7aeea37..3c84d82 100644 --- a/IntegrationService.Library/Targets/TargetBase.cs +++ b/IntegrationService.Library/Targets/TargetBase.cs @@ -125,11 +125,12 @@ public virtual void Process() { if (Configuration == null || Configuration.Mappings == null) return; - var pollingInSeconds = Configuration.PollingFrequency / 1000; + const int pollingInSeconds = 5; + //var pollingInSeconds = Configuration.PollingFrequency / 1000; - // Add 3 seconds to reduce risk of race conditions - // between LeanKit and Target system - pollingInSeconds += 3; + //// Add 3 seconds to reduce risk of race conditions + //// between LeanKit and Target system + //pollingInSeconds += 3; foreach (var mapping in Configuration.Mappings) { @@ -664,7 +665,7 @@ protected virtual void BoardUpdate(long boardId, BoardChangedEventArgs eventArgs if (!boardConfig.UpdateTargetItems && !boardConfig.CreateTargetItems) { Log.Info("Skipped checking moved cards because 'UpdateTargetItems' and 'CreateTargetItems' are disabled."); - // UpdateBoardVersion(boardId); + UpdateBoardVersion(boardId); continue; } diff --git a/IntegrationService.Targets.JIRA/JiraSubsystem.cs b/IntegrationService.Targets.JIRA/JiraSubsystem.cs index feff194..3787aba 100644 --- a/IntegrationService.Targets.JIRA/JiraSubsystem.cs +++ b/IntegrationService.Targets.JIRA/JiraSubsystem.cs @@ -540,12 +540,13 @@ protected override void Synchronize(BoardMapping project) var resp = new JsonSerializer().DeserializeFromString(jiraResp.Content); - Log.Info("\nQueried [{0}] at {1} for changes after {2}", project.Identity.Target, QueryDate, + Log.Info("Queried [{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; + Log.Info("{0} item(s) queried.", issues.Count); foreach (var issue in issues) { Log.Info("Issue [{0}]: {1}, {2}, {3}", issue.Key, issue.Fields.Summary, issue.Fields.Status.Name, @@ -565,10 +566,9 @@ protected override void Synchronize(BoardMapping project) if (project.UpdateCards) IssueUpdated(issue, card, project); else - Log.Info("Skipped card update because 'UpdateCards' is disabled."); + Log.Debug("Skipped card update because 'UpdateCards' is disabled."); } } - Log.Info("{0} item(s) queried.\n", issues.Count); } } @@ -622,14 +622,14 @@ private void CreateCardFromItem(BoardMapping project, Issue issue) // TODO: Add size from the custom story points field. - Log.Info("Creating a card of type [{0}] for issue [{1}] on Board [{2}] on Lane [{3}]", mappedCardType.Name, + Log.Debug("Creating a card of type [{0}] for issue [{1}] on Board [{2}] on Lane [{3}]", mappedCardType.Name, issue.Key, boardId, laneId); CardAddResult cardAddResult = null; - int tries = 0; - bool success = false; - while (tries < 10 && !success) + var tries = 0; + var success = false; + while (tries < 3 && !success) { if (tries > 0) { @@ -736,9 +736,9 @@ protected void UpdateStateOfExternalItem(Card card, List states, BoardMa if (states == null || states.Count == 0) return; - int tries = 0; - bool success = false; - while (tries < 10 && !success && (!runOnlyOnce || tries == 0)) + var tries = 0; + var success = false; + while (tries < 3 && !success && (!runOnlyOnce || tries == 0)) { if (tries > 0) { diff --git a/IntegrationService.Targets.TFS/Tfs.cs b/IntegrationService.Targets.TFS/Tfs.cs index 623e7af..3e7c660 100644 --- a/IntegrationService.Targets.TFS/Tfs.cs +++ b/IntegrationService.Targets.TFS/Tfs.cs @@ -109,7 +109,7 @@ protected override void Synchronize(BoardMapping project) var changedItems = query.EndQuery(cancelableAsyncResult); - Log.Info("\nQuery [{0}] for changes after {1}", project.Identity.Target, queryAsOfDate); + Log.Info("Query [{0}] for changes after {1}", project.Identity.Target, queryAsOfDate); Log.Debug(queryStr); foreach (WorkItem item in changedItems) @@ -136,7 +136,7 @@ protected override void Synchronize(BoardMapping project) Log.Info("Skipped card update because 'UpdateCards' is disabled."); } } - Log.Info("{0} item(s) queried.\n", changedItems.Count); + Log.Info("{0} item(s) queried.", changedItems.Count); } From 0b54e64d8039c3e0a1a93a9f7c65887d6e188c9f Mon Sep 17 00:00:00 2001 From: David Neal Date: Mon, 21 Mar 2016 14:08:54 -0400 Subject: [PATCH 08/20] Caching of LeanKit and JIRA updates --- .../IntegrationService.Library.csproj | 4 ++ .../Targets/TargetBase.cs | 63 ++++++++++++++++++- IntegrationService.Library/packages.config | 1 + .../JiraSubsystem.cs | 48 +++++++++++--- 4 files changed, 107 insertions(+), 9 deletions(-) diff --git a/IntegrationService.Library/IntegrationService.Library.csproj b/IntegrationService.Library/IntegrationService.Library.csproj index 5b1f180..2dc2fa5 100644 --- a/IntegrationService.Library/IntegrationService.Library.csproj +++ b/IntegrationService.Library/IntegrationService.Library.csproj @@ -51,6 +51,10 @@ False ..\packages\log4net.2.0.3\lib\net40-full\log4net.dll + + ..\packages\MemoryCache.1.2.0\lib\MemoryCache.dll + True + False ..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll diff --git a/IntegrationService.Library/Targets/TargetBase.cs b/IntegrationService.Library/Targets/TargetBase.cs index 3c84d82..1dc703c 100644 --- a/IntegrationService.Library/Targets/TargetBase.cs +++ b/IntegrationService.Library/Targets/TargetBase.cs @@ -10,6 +10,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using System.Threading; using IntegrationService.Util; using LeanKit.API.Client.Library; @@ -57,6 +58,60 @@ protected AppSettings AppSettings } } + protected void TargetSetCacheVersion(string key, string value) + { + const int cacheExpiration = 60 * 60 * 1000; + var targetKey = GetTargetCacheKey(key); + if (MemoryCache.Cache.Exists(targetKey)) + { + MemoryCache.Cache.Update(targetKey, value); + } + else + { + MemoryCache.Cache.Store(targetKey, value, cacheExpiration); + } + } + + protected bool TargetCacheCheckForVersion(string key, string value) + { + var targetKey = GetTargetCacheKey(key); + if (!MemoryCache.Cache.Exists(targetKey)) return false; + var val = (string) MemoryCache.Cache.Get(targetKey); + return val.Equals(value, StringComparison.OrdinalIgnoreCase); + } + + private static string GetTargetCacheKey(string key) + { + var rgx = new Regex("[^a-zA-Z0-9-]"); + return "t-" + rgx.Replace(key, ""); + } + + private static string GetCardCacheKey(long cardId, bool isStateChange) + { + return string.Format("{0}-{1}", cardId, isStateChange); + } + + protected void CacheCardVersion(long cardId, bool isStateChange, long version) + { + const int cardCacheExpiration = 60*60*1000; + var key = GetCardCacheKey(cardId, isStateChange); + if (MemoryCache.Cache.Exists(key)) + { + MemoryCache.Cache.Update(key, version); + } + else + { + MemoryCache.Cache.Store(key, version, cardCacheExpiration); + } + } + + protected long GetCachedCardVersion(long cardId, bool isStateChange) + { + var key = GetCardCacheKey(cardId, isStateChange); + if (!MemoryCache.Cache.Exists(key)) return 0; + return (long) MemoryCache.Cache.Get(key); + } + private User _currentUser; protected User CurrentUser { @@ -530,6 +585,12 @@ protected virtual void BoardUpdate(long boardId, BoardChangedEventArgs eventArgs if (updatedCardEvent.OriginalCard == null) throw new Exception("Original card is null"); var card = updatedCardEvent.UpdatedCard; + var version = GetCachedCardVersion(card.Id, false); + if (version >= card.Version) + { + Log.Debug("Processing UpdatedCards events, Card [{0}] with version [{1}] has already been processed. Skipping comparison.", card.Id, card.Version); + continue; + } if (string.IsNullOrEmpty(card.ExternalCardID) && !string.IsNullOrEmpty(card.ExternalSystemUrl)) { @@ -744,7 +805,7 @@ private void CheckLastQueryDate() Configuration.EarliestSyncDate = AppSettings.RecentQueryDate; } - private void UpdateBoardVersion(long boardId, long? version = null) + protected void UpdateBoardVersion(long boardId, long? version = null) { if (!version.HasValue) { var board = LeanKit.GetBoard(boardId); diff --git a/IntegrationService.Library/packages.config b/IntegrationService.Library/packages.config index 2862596..c4a32e6 100644 --- a/IntegrationService.Library/packages.config +++ b/IntegrationService.Library/packages.config @@ -3,6 +3,7 @@ + diff --git a/IntegrationService.Targets.JIRA/JiraSubsystem.cs b/IntegrationService.Targets.JIRA/JiraSubsystem.cs index 3787aba..609d9a0 100644 --- a/IntegrationService.Targets.JIRA/JiraSubsystem.cs +++ b/IntegrationService.Targets.JIRA/JiraSubsystem.cs @@ -192,6 +192,13 @@ protected override void CardUpdated(Card updatedCard, List updatedItems, return; } + var version = GetCachedCardVersion(updatedCard.Id, false); + if (version >= updatedCard.Version) + { + Log.Debug("CardUpdated, Card [{0}] with version [{1}] has already been processed. Skipping comparison.", updatedCard.Id, updatedCard.Version); + return; + } + //https://yoursite.atlassian.net/rest/api/latest/issue/{issueIdOrKey} var request = CreateRequest(string.Format("rest/api/latest/issue/{0}", updatedCard.ExternalCardID), Method.GET); @@ -328,6 +335,7 @@ protected override void CardUpdated(Card updatedCard, List updatedItems, else { Log.Debug(String.Format("Updated Issue [{0}]", updatedCard.ExternalCardID)); + CacheCardVersion(updatedCard.Id, false, updatedCard.Version); } } catch (Exception ex) @@ -366,8 +374,8 @@ protected override void CardUpdated(Card updatedCard, List updatedItems, } else { - Log.Debug(String.Format("Created comment for updated Issue [{0}]", - updatedCard.ExternalCardID)); + Log.Debug(string.Format("Created comment for updated Issue [{0}]", updatedCard.ExternalCardID)); + CacheCardVersion(updatedCard.Id, false, updatedCard.Version); } } catch (Exception ex) @@ -398,7 +406,7 @@ private void IssueUpdated(Issue issue, Card card, BoardMapping boardMapping) } if (issue.Fields.Description != null && - issue.Fields.Description.SanitizeCardDescription().JiraPlainTextToLeanKitHtml() != card.Description) + issue.Fields.Description.SanitizeCardDescription().JiraPlainTextToLeanKitHtml() != card.Description) { card.Description = issue.Fields.Description.SanitizeCardDescription().JiraPlainTextToLeanKitHtml(); saveCard = true; @@ -455,7 +463,9 @@ private void IssueUpdated(Issue issue, Card card, BoardMapping boardMapping) if (saveCard) { Log.Info("Updating card [{0}]", card.Id); - LeanKit.UpdateCard(boardId, card); + var result = LeanKit.UpdateCard(boardId, card); + CacheCardVersion(result.CardDTO.Id, false, result.CardDTO.Version); + TargetSetCacheVersion(issue.Key, issue.Fields.Updated); } // check the state of the work item @@ -469,6 +479,7 @@ private void IssueUpdated(Issue issue, Card card, BoardMapping boardMapping) if (laneIds.Contains(card.LaneId)) { Log.Debug("Card [{0}] is already in mapped Lane [{1}]", card.Id, card.LaneId); + TargetSetCacheVersion(issue.Key, issue.Fields.Updated); return; } // first let's see if any of the lanes are sibling lanes, if so then @@ -489,6 +500,9 @@ private void IssueUpdated(Issue issue, Card card, BoardMapping boardMapping) var laneId = laneIds.First(); Log.Info("Moving card [{0}] to Lane [{1}]", card.Id, laneId); LeanKit.MoveCard(boardMapping.Identity.LeanKit, card.Id, laneId, 0, "Moved Lane From Jira Issue"); + var updatedCard = LeanKit.GetCard(boardMapping.Identity.LeanKit, card.Id); + CacheCardVersion(updatedCard.Id, true, updatedCard.Version); + TargetSetCacheVersion(issue.Key, issue.Fields.Updated); } } @@ -513,7 +527,7 @@ protected override void Synchronize(BoardMapping project) { queryFilter += project.ExcludedTypeQuery; } - jqlQuery = string.Format("project=\"{0}\" {1} and updated > \"{2}\" order by created asc", + jqlQuery = string.Format("project=\"{0}\" {1} and updated > \"{2}\" order by updated asc", project.Identity.Target, queryFilter, formattedQueryDate); } @@ -522,7 +536,7 @@ protected override void Synchronize(BoardMapping project) request.AddParameter("jql", jqlQuery); request.AddParameter("fields", - "id,status,priority,summary,description,issuetype,type,assignee,duedate,labels"); + "id,status,priority,summary,description,issuetype,type,assignee,duedate,labels,updated"); request.AddParameter("maxResults", "9999"); var jiraResp = ExecuteRequest(request); @@ -549,6 +563,11 @@ protected override void Synchronize(BoardMapping project) Log.Info("{0} item(s) queried.", issues.Count); foreach (var issue in issues) { + if (TargetCacheCheckForVersion(issue.Key, issue.Fields.Updated)) + { + Log.Info("Issue [{0}] already processed, skipping.", issue.Key); + continue; + } Log.Info("Issue [{0}]: {1}, {2}, {3}", issue.Key, issue.Fields.Summary, issue.Fields.Status.Name, issue.Fields.Priority.Name); @@ -643,6 +662,9 @@ private void CreateCardFromItem(BoardMapping project, Issue issue) { cardAddResult = LeanKit.AddCard(boardId, card, "New Card From Jira Issue"); success = true; + CacheCardVersion(cardAddResult.CardId, false, 1); + CacheCardVersion(cardAddResult.CardId, true, 1); + TargetSetCacheVersion(issue.Key, issue.Fields.Updated); } catch (Exception ex) { @@ -732,6 +754,13 @@ protected void UpdateStateOfExternalItem(Card card, List states, BoardMa return; } + var version = GetCachedCardVersion(card.Id, true); + if (version >= card.Version) + { + Log.Debug("UpdateStateOfExternalItem, Card [{0}] with version [{1}] has already been processed. Skipping comparison.", card.Id, card.Version); + return; + } + if (states == null || states.Count == 0) return; @@ -888,8 +917,9 @@ protected void UpdateStateOfExternalItem(Card card, List states, BoardMa else { success = true; - Log.Debug(String.Format("Updated state for Issue [{0}] to [{1}]", + Log.Debug(string.Format("Updated state for Issue [{0}] to [{1}]", card.ExternalCardID, validTransition.To.Name)); + CacheCardVersion(card.Id, true, card.Version); } } } @@ -1009,7 +1039,9 @@ protected override void CreateNewItem(Card card, BoardMapping boardMapping) UpdateStateOfExternalItem(card, states, boardMapping, true); } - LeanKit.UpdateCard(boardMapping.Identity.LeanKit, card); + var result = LeanKit.UpdateCard(boardMapping.Identity.LeanKit, card); + CacheCardVersion(card.Id, false, result.CardDTO.Version); + CacheCardVersion(card.Id, true, result.CardDTO.Version); } catch (Exception ex) { From 0687e16f363e4a67a14721d1512e16ce837548d5 Mon Sep 17 00:00:00 2001 From: David Neal Date: Tue, 22 Mar 2016 14:54:37 -0400 Subject: [PATCH 09/20] Caching of changes with TFS --- .../JiraSubsystem.cs | 61 +++++---- IntegrationService.Targets.TFS/Tfs.cs | 118 ++++++++++++------ 2 files changed, 108 insertions(+), 71 deletions(-) diff --git a/IntegrationService.Targets.JIRA/JiraSubsystem.cs b/IntegrationService.Targets.JIRA/JiraSubsystem.cs index 609d9a0..e0e48d9 100644 --- a/IntegrationService.Targets.JIRA/JiraSubsystem.cs +++ b/IntegrationService.Targets.JIRA/JiraSubsystem.cs @@ -334,7 +334,7 @@ protected override void CardUpdated(Card updatedCard, List updatedItems, } else { - Log.Debug(String.Format("Updated Issue [{0}]", updatedCard.ExternalCardID)); + Log.Debug(string.Format("Updated Issue [{0}]", updatedCard.ExternalCardID)); CacheCardVersion(updatedCard.Id, false, updatedCard.Version); } } @@ -471,39 +471,38 @@ private void IssueUpdated(Issue issue, Card card, BoardMapping boardMapping) // check the state of the work item // if we have the state mapped to a lane then check to see if the card is in that lane // if it is not in that lane then move it to that lane - if (boardMapping.UpdateCardLanes && issue.Fields != null && issue.Fields.Status != null && - !string.IsNullOrEmpty(issue.Fields.Status.Name)) + if (!boardMapping.UpdateCardLanes || issue.Fields == null || issue.Fields.Status == null || + string.IsNullOrEmpty(issue.Fields.Status.Name)) return; + + var laneIds = boardMapping.LanesFromState(issue.Fields.Status.Name); + if (!laneIds.Any()) return; + if (laneIds.Contains(card.LaneId)) { - var laneIds = boardMapping.LanesFromState(issue.Fields.Status.Name); - if (!laneIds.Any()) return; - if (laneIds.Contains(card.LaneId)) - { - Log.Debug("Card [{0}] is already in mapped Lane [{1}]", card.Id, card.LaneId); - TargetSetCacheVersion(issue.Key, issue.Fields.Updated); - return; - } - // first let's see if any of the lanes are sibling lanes, if so then - // we should be using one of them. So we'll limit the results to just siblings - if (boardMapping.ValidLanes != null) - { - var siblingLaneIds = (from siblingLaneId in laneIds - let parentLane = - boardMapping.ValidLanes.FirstOrDefault(x => - x.HasChildLanes && - x.ChildLaneIds.Contains(siblingLaneId) && - x.ChildLaneIds.Contains(card.LaneId)) - where parentLane != null - select siblingLaneId).ToList(); - if (siblingLaneIds.Any()) - laneIds = siblingLaneIds; - } - var laneId = laneIds.First(); - Log.Info("Moving card [{0}] to Lane [{1}]", card.Id, laneId); - LeanKit.MoveCard(boardMapping.Identity.LeanKit, card.Id, laneId, 0, "Moved Lane From Jira Issue"); - var updatedCard = LeanKit.GetCard(boardMapping.Identity.LeanKit, card.Id); - CacheCardVersion(updatedCard.Id, true, updatedCard.Version); + Log.Debug("Card [{0}] is already in mapped Lane [{1}]", card.Id, card.LaneId); TargetSetCacheVersion(issue.Key, issue.Fields.Updated); + return; + } + // first let's see if any of the lanes are sibling lanes, if so then + // we should be using one of them. So we'll limit the results to just siblings + if (boardMapping.ValidLanes != null) + { + var siblingLaneIds = (from siblingLaneId in laneIds + let parentLane = + boardMapping.ValidLanes.FirstOrDefault(x => + x.HasChildLanes && + x.ChildLaneIds.Contains(siblingLaneId) && + x.ChildLaneIds.Contains(card.LaneId)) + where parentLane != null + select siblingLaneId).ToList(); + if (siblingLaneIds.Any()) + laneIds = siblingLaneIds; } + var laneId = laneIds.First(); + Log.Info("Moving card [{0}] to Lane [{1}]", card.Id, laneId); + LeanKit.MoveCard(boardMapping.Identity.LeanKit, card.Id, laneId, 0, "Moved Lane From Jira Issue"); + var updatedCard = LeanKit.GetCard(boardMapping.Identity.LeanKit, card.Id); + CacheCardVersion(updatedCard.Id, true, updatedCard.Version); + TargetSetCacheVersion(issue.Key, issue.Fields.Updated); } protected override void Synchronize(BoardMapping project) diff --git a/IntegrationService.Targets.TFS/Tfs.cs b/IntegrationService.Targets.TFS/Tfs.cs index 3e7c660..bcc65f2 100644 --- a/IntegrationService.Targets.TFS/Tfs.cs +++ b/IntegrationService.Targets.TFS/Tfs.cs @@ -63,7 +63,7 @@ protected override void Synchronize(BoardMapping project) Log.Debug("Polling TFS [{0}] for Work Items", project.Identity.TargetName); //query a project for new items - var stateQuery = string.Format(" AND ({0})", String.Join(" or ", project.QueryStates.Select(x => "[System.State] = '" + x.Trim() + "'").ToList())); + var stateQuery = string.Format(" AND ({0})", string.Join(" or ", project.QueryStates.Select(x => "[System.State] = '" + x.Trim() + "'").ToList())); var iterationQuery = ""; if (!string.IsNullOrEmpty(project.IterationPath)) { @@ -79,7 +79,7 @@ protected override void Synchronize(BoardMapping project) } else { - tfsQuery = String.Format( + tfsQuery = string.Format( "[System.TeamProject] = '{0}' {1} {2} {3} and [System.ChangedDate] > '{4}'", project.Identity.TargetName, iterationQuery, stateQuery, project.ExcludedTypeQuery, queryAsOfDate); } @@ -114,7 +114,13 @@ protected override void Synchronize(BoardMapping project) foreach (WorkItem item in changedItems) { - Log.Info("Work Item [{0}]: {1}, {2}, {3}", + if (CheckWorkItemCache(item)) + { + Log.Info("Work Item [{0}] already processed, skipping.", item.Id); + continue; + } + + Log.Info("Work Item [{0}]: {1}, {2}, {3}", item.Id, item.Title, item.Fields["System.AssignedTo"].Value, item.State); // does this workitem have a corresponding card? @@ -245,8 +251,11 @@ private void CreateCardFromWorkItem(BoardMapping project, WorkItem workItem) { cardAddResult = LeanKit.AddCard(boardId, card, "New Card From TFS Work Item"); success = true; - } - catch (Exception ex) + CacheCardVersion(cardAddResult.CardId, false, 1); + CacheCardVersion(cardAddResult.CardId, true, 1); + CacheWorkItem(workItem); + } + catch (Exception ex) { Log.Error(ex, string.Format("An error occurred creating a new card for work item [{0}]", workItem.Id)); } @@ -257,8 +266,18 @@ private void CreateCardFromWorkItem(BoardMapping project, WorkItem workItem) Log.Info("Created a card [{0}] of type [{1}] for work item [{2}] on Board [{3}] on Lane [{4}]", card.Id, mappedCardType.Name, workItem.Id, boardId, laneId); } + private void CacheWorkItem(WorkItem workItem) + { + TargetSetCacheVersion(workItem.Id.ToString(), workItem.ChangedDate.ToString("s")); + } - public void SetWorkItemPriority(WorkItem workItem, int newPriority) + private bool CheckWorkItemCache(WorkItem workItem) + { + return TargetCacheCheckForVersion(workItem.Id.ToString(), workItem.ChangedDate.ToString("s")); + } + + + public void SetWorkItemPriority(WorkItem workItem, int newPriority) { //LK Priority: 0 = Low, 1 = Normal, 2 = High, 3 = Critical //TFS Priority: 1-4 @@ -331,6 +350,13 @@ protected void UpdateStateOfExternalItem(Card card, List states, BoardMa if (!card.ExternalSystemName.Equals(ServiceName, StringComparison.OrdinalIgnoreCase)) return; if (string.IsNullOrEmpty(card.ExternalCardID)) return; + var version = GetCachedCardVersion(card.Id, true); + if (version >= card.Version) + { + Log.Debug("UpdateStateOfExternalItem, Card [{0}] with version [{1}] has already been processed. Skipping comparison.", card.Id, card.Version); + return; + } + int workItemId; // use external card id to get the TFS work item @@ -482,6 +508,7 @@ protected void UpdateStateOfExternalItem(Card card, List states, BoardMa workItemToUpdate.Save(); success = true; Log.Debug("Updated state for mapped WorkItem [{0}] to [{1}]", workItemId, workItemToUpdate.State); + CacheCardVersion(card.Id, true, card.Version); } catch (ValidationException ex) { @@ -595,13 +622,15 @@ private void WorkItemUpdated(WorkItem workItem, Card card, BoardMapping project) if(saveCard) { Log.Info("Updating card [{0}]", card.Id); - LeanKit.UpdateCard(boardId, card); - } + var result = LeanKit.UpdateCard(boardId, card); + CacheCardVersion(result.CardDTO.Id, false, result.CardDTO.Version); + } + CacheWorkItem(workItem); // check the state of the work item // if we have the state mapped to a lane then check to see if the card is in that lane // if it is not in that lane then move it to that lane - if (!project.UpdateCardLanes || string.IsNullOrEmpty(workItem.State)) return; + if (!project.UpdateCardLanes || string.IsNullOrEmpty(workItem.State)) return; // if card is already in archive lane then we do not want to move it to the end lane // because it is effectively the same thing with respect to integrating with TFS @@ -612,28 +641,28 @@ private void WorkItemUpdated(WorkItem workItem, Card card, BoardMapping project) var laneIds = project.LanesFromState(workItem.State); - if (laneIds.Any()) - { - if (!laneIds.Contains(card.LaneId)) - { - // first let's see if any of the lanes are sibling lanes, if so then - // we should be using one of them. So we'll limit the results to just siblings - if (project.ValidLanes != null) { - var siblingLaneIds = (from siblingLaneId in laneIds - let parentLane = - project.ValidLanes.FirstOrDefault(x => - x.HasChildLanes && - x.ChildLaneIds.Contains(siblingLaneId) && - x.ChildLaneIds.Contains(card.LaneId)) - where parentLane != null - select siblingLaneId).ToList(); - if (siblingLaneIds.Any()) - laneIds = siblingLaneIds; - } - - LeanKit.MoveCard(project.Identity.LeanKit, card.Id, laneIds.First(), 0, "Moved Lane From TFS Work Item"); - } + if (!laneIds.Any()) return; + if (laneIds.Contains(card.LaneId)) return; + + // first let's see if any of the lanes are sibling lanes, if so then + // we should be using one of them. So we'll limit the results to just siblings + if (project.ValidLanes != null) { + var siblingLaneIds = (from siblingLaneId in laneIds + let parentLane = + project.ValidLanes.FirstOrDefault(x => + x.HasChildLanes && + x.ChildLaneIds.Contains(siblingLaneId) && + x.ChildLaneIds.Contains(card.LaneId)) + where parentLane != null + select siblingLaneId).ToList(); + if (siblingLaneIds.Any()) + laneIds = siblingLaneIds; } + + LeanKit.MoveCard(project.Identity.LeanKit, card.Id, laneIds.First(), 0, "Moved Lane From TFS Work Item"); + var updatedCard = LeanKit.GetCard(project.Identity.LeanKit, card.Id); + CacheCardVersion(updatedCard.Id, true, updatedCard.Version); + CacheWorkItem(workItem); } protected override void CardUpdated(Card card, List updatedItems, BoardMapping boardMapping) @@ -644,7 +673,14 @@ protected override void CardUpdated(Card card, List updatedItems, BoardM if (string.IsNullOrEmpty(card.ExternalCardID)) return; - Log.Info("Card [{0}] updated.", card.Id); + var version = GetCachedCardVersion(card.Id, false); + if (version >= card.Version) + { + Log.Debug("CardUpdated, Card [{0}] with version [{1}] has already been processed. Skipping comparison.", card.Id, card.Version); + return; + } + + Log.Info("Card [{0}] updated.", card.Id); int workItemId; try @@ -707,7 +743,8 @@ protected override void CardUpdated(Card card, List updatedItems, BoardM { Log.Info("Updating corresponding work item [{0}]", workItem.Id); workItem.Save(); - } + CacheCardVersion(card.Id, false, card.Version); + } // unsupported properties; append changes to history @@ -726,12 +763,10 @@ protected override void CardUpdated(Card card, List updatedItems, BoardM workItem.Save(); } - if (updatedItems.Contains("Tags")) - { - workItem.History += "Tags in LeanKit changed to " + card.Tags + "\r"; - workItem.Save(); - } + if (!updatedItems.Contains("Tags")) return; + workItem.History += "Tags in LeanKit changed to " + card.Tags + "\r"; + workItem.Save(); } private void SetDueDate(WorkItem workItem, string date) @@ -819,10 +854,13 @@ protected override void CreateNewItem(Card card, BoardMapping boardMapping) if (states != null) { UpdateStateOfExternalItem(card, states, boardMapping, true); - } + } - LeanKit.UpdateCard(boardMapping.Identity.LeanKit, card); - } + var result = LeanKit.UpdateCard(boardMapping.Identity.LeanKit, card); + CacheCardVersion(card.Id, false, result.CardDTO.Version); + CacheCardVersion(card.Id, true, result.CardDTO.Version); + + } catch (ValidationException ex) { Log.Error("Unable to create WorkItem from Card [{0}]. ValidationException: {1}", card.Id, ex.Message); From bc4995edb93bab71747c3c69f744e447e50ef62e Mon Sep 17 00:00:00 2001 From: David Neal Date: Fri, 20 May 2016 15:41:38 -0400 Subject: [PATCH 10/20] Fix to honor CreateCards=false --- .../GitHubIssuesSubsystem.cs | 9 +++++++-- .../GitHubPullsSubsystem.cs | 9 +++++++-- IntegrationService.Targets.JIRA/JiraSubsystem.cs | 5 +++++ IntegrationService.Targets.TFS/Tfs.cs | 6 ++++++ IntegrationService.Targets.Unfuddle/UnfuddleSubsystem.cs | 7 ++++++- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/IntegrationService.Targets.GitHub/GitHubIssuesSubsystem.cs b/IntegrationService.Targets.GitHub/GitHubIssuesSubsystem.cs index cf83331..0b38c11 100644 --- a/IntegrationService.Targets.GitHub/GitHubIssuesSubsystem.cs +++ b/IntegrationService.Targets.GitHub/GitHubIssuesSubsystem.cs @@ -346,8 +346,13 @@ protected override void Synchronize(BoardMapping project) private void CreateCardFromItem(BoardMapping project, Issue issue) { if (issue == null) return; - - var boardId = project.Identity.LeanKit; + if (!project.CreateCards) + { + Log.Debug("CreateCards is disabled, skipping card creation."); + return; + } + + var boardId = project.Identity.LeanKit; var mappedCardType = issue.LeanKitCardType(project); var laneId = project.LanesFromState(issue.State).First(); diff --git a/IntegrationService.Targets.GitHub/GitHubPullsSubsystem.cs b/IntegrationService.Targets.GitHub/GitHubPullsSubsystem.cs index f388e43..6770f9b 100644 --- a/IntegrationService.Targets.GitHub/GitHubPullsSubsystem.cs +++ b/IntegrationService.Targets.GitHub/GitHubPullsSubsystem.cs @@ -242,8 +242,13 @@ protected override void Synchronize(BoardMapping project) private void CreateCardFromItem(BoardMapping project, Pull pull) { if (pull == null) return; - - var boardId = project.Identity.LeanKit; + if (!project.CreateCards) + { + Log.Debug("CreateCards is disabled, skipping card creation."); + return; + } + + var boardId = project.Identity.LeanKit; var mappedCardType = pull.LeanKitCardType(project); var laneId = project.LanesFromState(pull.State).First(); diff --git a/IntegrationService.Targets.JIRA/JiraSubsystem.cs b/IntegrationService.Targets.JIRA/JiraSubsystem.cs index e0e48d9..9222b0c 100644 --- a/IntegrationService.Targets.JIRA/JiraSubsystem.cs +++ b/IntegrationService.Targets.JIRA/JiraSubsystem.cs @@ -593,6 +593,11 @@ protected override void Synchronize(BoardMapping project) private void CreateCardFromItem(BoardMapping project, Issue issue) { if (issue == null) return; + if (!project.CreateCards) + { + Log.Debug("CreateCards is disabled, skipping card creation."); + return; + } var boardId = project.Identity.LeanKit; diff --git a/IntegrationService.Targets.TFS/Tfs.cs b/IntegrationService.Targets.TFS/Tfs.cs index bcc65f2..7ad63c1 100644 --- a/IntegrationService.Targets.TFS/Tfs.cs +++ b/IntegrationService.Targets.TFS/Tfs.cs @@ -150,6 +150,12 @@ private void CreateCardFromWorkItem(BoardMapping project, WorkItem workItem) { if (workItem == null) return; + if (!project.CreateCards) + { + Log.Debug("CreateCards is disabled, skipping card creation."); + return; + } + var boardId = project.Identity.LeanKit; var mappedCardType = workItem.LeanKitCardType(project); diff --git a/IntegrationService.Targets.Unfuddle/UnfuddleSubsystem.cs b/IntegrationService.Targets.Unfuddle/UnfuddleSubsystem.cs index f4b5aad..8e9d7e8 100644 --- a/IntegrationService.Targets.Unfuddle/UnfuddleSubsystem.cs +++ b/IntegrationService.Targets.Unfuddle/UnfuddleSubsystem.cs @@ -354,8 +354,13 @@ protected override void Synchronize(BoardMapping project) private void CreateCardFromItem(BoardMapping project, Ticket ticket) { if (ticket == null) return; + if (!project.CreateCards) + { + Log.Debug("CreateCards is disabled, skipping card creation."); + return; + } - var boardId = project.Identity.LeanKit; + var boardId = project.Identity.LeanKit; var mappedCardType = ticket.LeanKitCardType(project); From a1b5023768e2c6287ad2f9c2cffae5668e35e33f Mon Sep 17 00:00:00 2001 From: David Neal Date: Wed, 25 May 2016 11:21:18 -0400 Subject: [PATCH 11/20] Improvements to activity caching --- .../IntegrationService.Library.csproj | 5 +- .../Targets/TargetBase.cs | 88 +++++++++++++------ IntegrationService.Library/packages.config | 1 - IntegrationService/App.config | 1 + 4 files changed, 64 insertions(+), 31 deletions(-) diff --git a/IntegrationService.Library/IntegrationService.Library.csproj b/IntegrationService.Library/IntegrationService.Library.csproj index 2dc2fa5..3144a7c 100644 --- a/IntegrationService.Library/IntegrationService.Library.csproj +++ b/IntegrationService.Library/IntegrationService.Library.csproj @@ -51,10 +51,6 @@ False ..\packages\log4net.2.0.3\lib\net40-full\log4net.dll - - ..\packages\MemoryCache.1.2.0\lib\MemoryCache.dll - True - False ..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll @@ -78,6 +74,7 @@ + diff --git a/IntegrationService.Library/Targets/TargetBase.cs b/IntegrationService.Library/Targets/TargetBase.cs index 1dc703c..85e6364 100644 --- a/IntegrationService.Library/Targets/TargetBase.cs +++ b/IntegrationService.Library/Targets/TargetBase.cs @@ -10,6 +10,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.Caching; using System.Text.RegularExpressions; using System.Threading; using IntegrationService.Util; @@ -58,26 +59,60 @@ protected AppSettings AppSettings } } + private double? _cacheMinutes; + protected double CacheMinutes + { + get + { + if (_cacheMinutes.HasValue) return _cacheMinutes.Value; + var configMinutes = ConfigurationManager.AppSettings["CacheExpirationMinutes"]; + const double defaultCacheMinutes = 60*6; + if (configMinutes == null) + { + _cacheMinutes = defaultCacheMinutes; + Log.Debug(string.Format("Cache: Caching is set to {0} minutes", _cacheMinutes.Value)); + return _cacheMinutes.Value; + } + double minutes; + if (double.TryParse(configMinutes, out minutes)) + { + if (minutes <= 0) minutes = defaultCacheMinutes; + } + else + { + minutes = defaultCacheMinutes; + } + _cacheMinutes = minutes; + Log.Debug(string.Format("Cache: Caching is set to {0} minutes", _cacheMinutes.Value)); + return _cacheMinutes.Value; + } + } + + protected CacheItemPolicy GetCacheItemPolicy() + { + return new CacheItemPolicy {SlidingExpiration = TimeSpan.FromMinutes(CacheMinutes) }; + } + protected void TargetSetCacheVersion(string key, string value) { - const int cacheExpiration = 60 * 60 * 1000; + var cache = MemoryCache.Default; var targetKey = GetTargetCacheKey(key); - if (MemoryCache.Cache.Exists(targetKey)) - { - MemoryCache.Cache.Update(targetKey, value); - } - else - { - MemoryCache.Cache.Store(targetKey, value, cacheExpiration); - } + Log.Debug(string.Format("Cache: Setting Target Version [{0}]: {1}", targetKey, value)); + cache.Set(targetKey, value, GetCacheItemPolicy()); } protected bool TargetCacheCheckForVersion(string key, string value) { + var cache = MemoryCache.Default; var targetKey = GetTargetCacheKey(key); - if (!MemoryCache.Cache.Exists(targetKey)) return false; - var val = (string) MemoryCache.Cache.Get(targetKey); - return val.Equals(value, StringComparison.OrdinalIgnoreCase); + if (!cache.Contains(targetKey)) + { + Log.Debug(string.Format("Cache: Target Version not found [{0}]: {1}", targetKey, value)); + return false; + } + var val = (string) cache.Get(targetKey); + Log.Debug(string.Format("Cache: Returning Target Version [{0}]: {1}", targetKey, val)); + return val.Equals(value, StringComparison.OrdinalIgnoreCase); } private static string GetTargetCacheKey(string key) @@ -93,23 +128,24 @@ private static string GetCardCacheKey(long cardId, bool isStateChange) protected void CacheCardVersion(long cardId, bool isStateChange, long version) { - const int cardCacheExpiration = 60*60*1000; - var key = GetCardCacheKey(cardId, isStateChange); - if (MemoryCache.Cache.Exists(key)) - { - MemoryCache.Cache.Update(key, version); - } - else - { - MemoryCache.Cache.Store(key, version, cardCacheExpiration); - } - } + var targetKey = GetCardCacheKey(cardId, isStateChange); + var cache = MemoryCache.Default; + Log.Debug(string.Format("Cache: Setting Card Version [{0}]: {1}", targetKey, version)); + cache.Set(targetKey, version, GetCacheItemPolicy()); + } protected long GetCachedCardVersion(long cardId, bool isStateChange) { - var key = GetCardCacheKey(cardId, isStateChange); - if (!MemoryCache.Cache.Exists(key)) return 0; - return (long) MemoryCache.Cache.Get(key); + var targetKey = GetCardCacheKey(cardId, isStateChange); + var cache = MemoryCache.Default; + if (!cache.Contains(targetKey)) + { + Log.Debug(string.Format("Cache: Card Version not found [{0}]", targetKey)); + return 0; + } + var version = (long)cache.Get(targetKey); + Log.Debug(string.Format("Cache: Returning Cached Card Version [{0}]: {1}", targetKey, version)); + return version; } private User _currentUser; diff --git a/IntegrationService.Library/packages.config b/IntegrationService.Library/packages.config index c4a32e6..2862596 100644 --- a/IntegrationService.Library/packages.config +++ b/IntegrationService.Library/packages.config @@ -3,7 +3,6 @@ - diff --git a/IntegrationService/App.config b/IntegrationService/App.config index c0e1443..1ca5339 100644 --- a/IntegrationService/App.config +++ b/IntegrationService/App.config @@ -3,6 +3,7 @@ + From af54364c9a92be15d45d0463db9cb28db6aa14d3 Mon Sep 17 00:00:00 2001 From: David Neal Date: Wed, 25 May 2016 12:07:28 -0400 Subject: [PATCH 12/20] Improvements to JIRA error handling --- .../JiraSubsystem.cs | 85 +++++++------------ 1 file changed, 33 insertions(+), 52 deletions(-) diff --git a/IntegrationService.Targets.JIRA/JiraSubsystem.cs b/IntegrationService.Targets.JIRA/JiraSubsystem.cs index 9222b0c..4d7e69d 100644 --- a/IntegrationService.Targets.JIRA/JiraSubsystem.cs +++ b/IntegrationService.Targets.JIRA/JiraSubsystem.cs @@ -48,11 +48,7 @@ protected List CustomFields if (jiraResp.StatusCode != HttpStatusCode.OK) { - var serializer = new JsonSerializer(); - var errorMessage = serializer.DeserializeFromString(jiraResp.Content); - Log.Error(string.Format( - "Unable to get custom fields from JIRA, Error: {0}. Check your JIRA connection configuration.", - errorMessage.Message)); + ProcessJiraError(jiraResp, "Unable to get custom fields from JIRA."); } else { @@ -87,11 +83,7 @@ protected List Priorities if (jiraResp.StatusCode != HttpStatusCode.OK) { - var serializer = new JsonSerializer(); - var errorMessage = serializer.DeserializeFromString(jiraResp.Content); - Log.Error(string.Format( - "Unable to get priorities from JIRA, Error: {0}. Check your JIRA connection configuration.", - errorMessage.Message)); + ProcessJiraError(jiraResp, "Unable to get priorities from JIRA."); } else { @@ -133,6 +125,11 @@ public Jira(IBoardSubscriptionManager subscriptions, } + private void ClearSessionCookies() + { + _sessionCookies?.Clear(); + } + public void AddSessionCookieToRequest(RestRequest request) { if (_sessionCookies == null || _sessionCookies.Count == 0) @@ -206,12 +203,7 @@ protected override void CardUpdated(Card updatedCard, List updatedItems, if (jiraResp.StatusCode != HttpStatusCode.OK) { - var serializer = new JsonSerializer(); - 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)); + ProcessJiraError(jiraResp, string.Format("Unable to get issue [{0}] from JIRA.", updatedCard.ExternalCardID)); } else { @@ -327,10 +319,7 @@ protected override void CardUpdated(Card updatedCard, List updatedItems, if (resp.StatusCode != HttpStatusCode.OK && resp.StatusCode != HttpStatusCode.NoContent) { - var serializer = new JsonSerializer(); - 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)); + ProcessJiraError(resp, string.Format("Unable to update JIRA Issue [{0}].", updatedCard.ExternalCardID)); } else { @@ -365,12 +354,7 @@ protected override void CardUpdated(Card updatedCard, List updatedItems, resp.StatusCode != HttpStatusCode.NoContent && resp.StatusCode != HttpStatusCode.Created) { - var serializer = new JsonSerializer(); - 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)); + ProcessJiraError(resp, string.Format("Unable to create comment for updated Issue [{0}].", updatedCard.ExternalCardID)); } else { @@ -542,12 +526,7 @@ protected override void Synchronize(BoardMapping project) if (jiraResp.StatusCode != HttpStatusCode.OK) { - var serializer = new JsonSerializer(); - 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)); + ProcessJiraError(jiraResp, "Unable to get issues from Jira."); return; } @@ -590,6 +569,23 @@ protected override void Synchronize(BoardMapping project) } } + private void ProcessJiraError(IRestResponse response, string errMessage) + { + try + { + var serializer = new JsonSerializer(); + var errorMessage = serializer.DeserializeFromString(response.Content); + var err = string.Format(" Status: {0}, Message: {1}", response.StatusDescription, errorMessage.Message); + Log.Error( errMessage + err ); + } + catch (Exception) + { + var err = string.Format(" Status: {0}, Message: {1}", response.StatusDescription, response.Content); + Log.Error(errMessage + err); + ClearSessionCookies(); + } + } + private void CreateCardFromItem(BoardMapping project, Issue issue) { if (issue == null) return; @@ -788,12 +784,7 @@ protected void UpdateStateOfExternalItem(Card card, List states, BoardMa if (jiraResp.StatusCode != HttpStatusCode.OK) { - var serializer = new JsonSerializer(); - 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)); + ProcessJiraError(jiraResp, string.Format("Unable to get issue [{0}] from Jira.", card.ExternalCardID)); } else { @@ -853,10 +844,7 @@ protected void UpdateStateOfExternalItem(Card card, List states, BoardMa if (transitionsResponse.StatusCode != HttpStatusCode.OK) { - var serializer = new JsonSerializer(); - var errorMessage = serializer.DeserializeFromString(jiraResp.Content); - Log.Error(string.Format("Unable to get available transitions from Jira, Error: {0}.", - errorMessage.Message)); + ProcessJiraError(jiraResp, "Unable to get available transitions from Jira."); } else { @@ -910,13 +898,7 @@ protected void UpdateStateOfExternalItem(Card card, List states, BoardMa if (resp.StatusCode != HttpStatusCode.OK && resp.StatusCode != HttpStatusCode.NoContent) { - var serializer = new JsonSerializer(); - 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)); + ProcessJiraError(jiraResp, string.Format("Unable to update Issue [{0}] to [{1}].", card.ExternalCardID, validTransition.To.Name)); } else { @@ -1013,13 +995,12 @@ protected override void CreateNewItem(Card card, BoardMapping boardMapping) if (resp.StatusCode != HttpStatusCode.OK && resp.StatusCode != HttpStatusCode.Created) { - Log.Error(string.Format("Unable to create Issue from card [{0}], Description: {1}, Message: {2}", - card.ExternalCardID, resp.StatusDescription, resp.Content)); + ProcessJiraError(resp, string.Format("Unable to create Issue from card [{0}].", card.ExternalCardID)); } else { newIssue = new JsonSerializer().DeserializeFromString(resp.Content); - Log.Debug(String.Format("Created Issue [{0}]", newIssue.Key)); + Log.Debug(string.Format("Created Issue [{0}]", newIssue.Key)); } } catch (Exception ex) From 3d217c9d56ca404d3f35e60b344818ee9a8f6275 Mon Sep 17 00:00:00 2001 From: David Neal Date: Thu, 23 Jun 2016 12:00:27 -0400 Subject: [PATCH 13/20] New Cards in LeanKit in unmapped lanes should not trigger creating a new item in the target system --- .../Targets/TargetBase.cs | 64 ++++++++++++++----- .../JiraSubsystem.cs | 9 ++- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/IntegrationService.Library/Targets/TargetBase.cs b/IntegrationService.Library/Targets/TargetBase.cs index 85e6364..7c61111 100644 --- a/IntegrationService.Library/Targets/TargetBase.cs +++ b/IntegrationService.Library/Targets/TargetBase.cs @@ -293,16 +293,31 @@ private void CheckForMissedCardMoves(BoardMapping mapping) if (ev.EventType == "CardCreation") { var card = LeanKit.GetCard(board.Id, ev.CardId); - if (card != null && string.IsNullOrEmpty(card.ExternalCardID)) + if (card != null && string.IsNullOrEmpty(card.ExternalCardID) && mapping.CreateTargetItems) { - try - { - CreateNewItem(card.ToCard(), mapping); - } - catch (Exception e) - { - Log.Error("Exception for CreateNewItem: " + e.Message); - } + if (mapping.LaneToStatesMap.Any() && + mapping.LaneToStatesMap.ContainsKey(card.LaneId)) + { + var states = mapping.LaneToStatesMap[card.LaneId]; + if (states != null && states.Count > 0) + { + try + { + CreateNewItem(card.ToCard(), mapping); + } + catch (Exception e) + { + Log.Error("Exception for CreateNewItem: " + e.Message); + } + + } + else + Log.Debug(string.Format("No states are mapped to the Lane [{0}]", card.LaneId)); + } + else + { + Log.Debug(string.Format("No states are mapped to the Lane [{0}]", card.LaneId)); + } } } // only look for moved cards @@ -747,14 +762,29 @@ protected virtual void BoardUpdate(long boardId, BoardChangedEventArgs eventArgs foreach (var newCard in eventArgs.AddedCards.Select(cardAddEvent => cardAddEvent.AddedCard) .Where(newCard => newCard != null && string.IsNullOrEmpty(newCard.ExternalCardID))) { - try - { - CreateNewItem(newCard, boardConfig); - } - catch (Exception e) - { - string.Format("Error processing newly created card, [{0}]: {1}", newCard.Id, e.Message).Error(e); - } + if (boardConfig.LaneToStatesMap.Any() && + boardConfig.LaneToStatesMap.ContainsKey(newCard.LaneId)) + { + var states = boardConfig.LaneToStatesMap[newCard.LaneId]; + if (states != null && states.Count > 0) + { + try + { + CreateNewItem(newCard, boardConfig); + } + catch (Exception e) + { + Log.Error("Exception for CreateNewItem: " + e.Message); + } + + } + else + Log.Debug(string.Format("No states are mapped to the Lane [{0}]", newCard.LaneId)); + } + else + { + Log.Debug(string.Format("No states are mapped to the Lane [{0}]", newCard.LaneId)); + } } } } diff --git a/IntegrationService.Targets.JIRA/JiraSubsystem.cs b/IntegrationService.Targets.JIRA/JiraSubsystem.cs index 4d7e69d..7154920 100644 --- a/IntegrationService.Targets.JIRA/JiraSubsystem.cs +++ b/IntegrationService.Targets.JIRA/JiraSubsystem.cs @@ -575,7 +575,7 @@ private void ProcessJiraError(IRestResponse response, string errMessage) { var serializer = new JsonSerializer(); var errorMessage = serializer.DeserializeFromString(response.Content); - var err = string.Format(" Status: {0}, Message: {1}", response.StatusDescription, errorMessage.Message); + var err = errorMessage.Message != null ? string.Format(" Status: {0}, Message: {1}", response.StatusDescription, errorMessage.Message) : string.Format(" Status: {0}, Message: {1}", response.StatusDescription, response.Content); Log.Error( errMessage + err ); } catch (Exception) @@ -995,7 +995,7 @@ protected override void CreateNewItem(Card card, BoardMapping boardMapping) if (resp.StatusCode != HttpStatusCode.OK && resp.StatusCode != HttpStatusCode.Created) { - ProcessJiraError(resp, string.Format("Unable to create Issue from card [{0}].", card.ExternalCardID)); + ProcessJiraError(resp, string.Format("Unable to create Issue from card [{0}].", card.Id)); } else { @@ -1005,8 +1005,7 @@ protected override void CreateNewItem(Card card, BoardMapping boardMapping) } catch (Exception ex) { - Log.Error(string.Format("Unable to create Issue from Card [{0}], Exception: {1}", card.ExternalCardID, - ex.Message)); + Log.Error(string.Format("Unable to create Issue from Card [{0}], Exception: {1}", card.Id, ex.Message)); } if (newIssue != null) @@ -1031,7 +1030,7 @@ protected override void CreateNewItem(Card card, BoardMapping boardMapping) catch (Exception ex) { Log.Error(string.Format("Error updating Card [{0}] after creating new Issue, Exception: {1}", - card.ExternalCardID, + card.Id, ex.Message)); } } From c3711d16998ed3611db1531e440f782cad1446bf Mon Sep 17 00:00:00 2001 From: David Neal Date: Wed, 29 Jun 2016 13:05:43 -0400 Subject: [PATCH 14/20] * fix issue when JIRA priority is null * add perf optimization when creating new cards, always appending to the bottom of the Lane --- IntegrationService.Targets.GitHub/GitHubIssuesSubsystem.cs | 3 ++- IntegrationService.Targets.GitHub/GitHubPullsSubsystem.cs | 3 ++- IntegrationService.Targets.JIRA/JiraSubsystem.cs | 6 +++--- IntegrationService.Targets.TFS/Tfs.cs | 3 ++- IntegrationService.Targets.Unfuddle/UnfuddleSubsystem.cs | 3 ++- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/IntegrationService.Targets.GitHub/GitHubIssuesSubsystem.cs b/IntegrationService.Targets.GitHub/GitHubIssuesSubsystem.cs index 0b38c11..a89c874 100644 --- a/IntegrationService.Targets.GitHub/GitHubIssuesSubsystem.cs +++ b/IntegrationService.Targets.GitHub/GitHubIssuesSubsystem.cs @@ -367,7 +367,8 @@ private void CreateCardFromItem(BoardMapping project, Issue issue) LaneId = laneId, ExternalCardID = issue.Id + "|" + issue.Number, ExternalSystemName = ServiceName, - ExternalSystemUrl = string.Format(_externalUrlTemplate, project.Identity.Target, issue.Number) + ExternalSystemUrl = string.Format(_externalUrlTemplate, project.Identity.Target, issue.Number), + Index = 9999 }; var assignedUserId = issue.LeanKitAssignedUserId(boardId, LeanKit); diff --git a/IntegrationService.Targets.GitHub/GitHubPullsSubsystem.cs b/IntegrationService.Targets.GitHub/GitHubPullsSubsystem.cs index 6770f9b..a13ab5b 100644 --- a/IntegrationService.Targets.GitHub/GitHubPullsSubsystem.cs +++ b/IntegrationService.Targets.GitHub/GitHubPullsSubsystem.cs @@ -263,7 +263,8 @@ private void CreateCardFromItem(BoardMapping project, Pull pull) LaneId = laneId, ExternalCardID = pull.Id.ToString() + "|" + pull.Number.ToString(), ExternalSystemName = ServiceName, - ExternalSystemUrl = string.Format(_externalUrlTemplate, project.Identity.Target, pull.Number) + ExternalSystemUrl = string.Format(_externalUrlTemplate, project.Identity.Target, pull.Number), + Index = 9999 }; var assignedUserId = pull.LeanKitAssignedUser(boardId, LeanKit); diff --git a/IntegrationService.Targets.JIRA/JiraSubsystem.cs b/IntegrationService.Targets.JIRA/JiraSubsystem.cs index 7154920..05cb767 100644 --- a/IntegrationService.Targets.JIRA/JiraSubsystem.cs +++ b/IntegrationService.Targets.JIRA/JiraSubsystem.cs @@ -546,8 +546,7 @@ protected override void Synchronize(BoardMapping project) Log.Info("Issue [{0}] already processed, skipping.", issue.Key); continue; } - Log.Info("Issue [{0}]: {1}, {2}, {3}", issue.Key, issue.Fields.Summary, issue.Fields.Status.Name, - issue.Fields.Priority.Name); + Log.Info("Issue [{0}]: {1}, {2}", issue.Key, issue.Fields.Summary, issue.Fields.Status.Name); // does this workitem have a corresponding card? var card = LeanKit.GetCardByExternalId(project.Identity.LeanKit, issue.Key); @@ -613,7 +612,8 @@ private void CreateCardFromItem(BoardMapping project, Issue issue) LaneId = laneId, ExternalCardID = issue.Key, ExternalSystemName = ServiceName, - ExternalSystemUrl = string.Format(_externalUrlTemplate, issue.Key) + ExternalSystemUrl = string.Format(_externalUrlTemplate, issue.Key), + Index = 9999 }; var assignedUserId = issue.LeanKitAssignedUserId(boardId, LeanKit); diff --git a/IntegrationService.Targets.TFS/Tfs.cs b/IntegrationService.Targets.TFS/Tfs.cs index 7ad63c1..2fda404 100644 --- a/IntegrationService.Targets.TFS/Tfs.cs +++ b/IntegrationService.Targets.TFS/Tfs.cs @@ -171,7 +171,8 @@ private void CreateCardFromWorkItem(BoardMapping project, WorkItem workItem) TypeName = mappedCardType.Name, LaneId = laneId, ExternalCardID = workItem.Id.ToString(CultureInfo.InvariantCulture), - ExternalSystemName = ServiceName + ExternalSystemName = ServiceName, + Index = 9999 }; if (workItem.Fields.Contains("Tags") && workItem.Fields["Tags"] != null && workItem.Fields["Tags"].Value != null) diff --git a/IntegrationService.Targets.Unfuddle/UnfuddleSubsystem.cs b/IntegrationService.Targets.Unfuddle/UnfuddleSubsystem.cs index 8e9d7e8..8b6196f 100644 --- a/IntegrationService.Targets.Unfuddle/UnfuddleSubsystem.cs +++ b/IntegrationService.Targets.Unfuddle/UnfuddleSubsystem.cs @@ -376,7 +376,8 @@ private void CreateCardFromItem(BoardMapping project, Ticket ticket) LaneId = laneId, ExternalCardID = ticket.Id.ToString(), ExternalSystemName = ServiceName, - ExternalSystemUrl = string.Format(_externalUrlTemplate, ticket.Id, project.Identity.Target) + ExternalSystemUrl = string.Format(_externalUrlTemplate, ticket.Id, project.Identity.Target), + Index = 9999 }; var assignedUserId = CalculateAssignedUserId(boardId, ticket); From be861b5574c46eee7c856eeafb81caca950442e3 Mon Sep 17 00:00:00 2001 From: David Neal Date: Mon, 12 Sep 2016 12:44:00 -0400 Subject: [PATCH 15/20] Adding documentation and builds --- builds/.gitattributes | 1 + .../LeanKit.IntegrationService-20160629.zip | 3 + docs/.gitattributes | 2 + docs/changelog.md | 111 +++++ docs/downloads.md | 19 + .../images/LeanKitIntegrationOverview_150.png | 3 + docs/images/image00.gif | 3 + docs/images/image01.gif | 3 + docs/images/image02.gif | 3 + docs/images/image04.gif | 3 + docs/images/image05.gif | 3 + docs/images/image07.gif | 3 + docs/images/image08.gif | 3 + docs/images/image09.gif | 3 + docs/images/image10.gif | 3 + docs/images/image11.gif | 3 + docs/images/image12.gif | 3 + docs/images/image13.gif | 3 + docs/images/image14.gif | 3 + docs/images/image15.gif | 3 + docs/images/image16.gif | 3 + docs/images/image17.gif | 3 + docs/images/image18.gif | 3 + docs/images/image19.gif | 3 + docs/images/image21.gif | 3 + docs/images/image22.gif | 3 + docs/images/image24.gif | 3 + docs/images/leankit-card-id-settings.gif | 3 + docs/overview.md | 431 ++++++++++++++++++ 29 files changed, 636 insertions(+) create mode 100644 builds/.gitattributes create mode 100644 builds/LeanKit.IntegrationService-20160629.zip create mode 100644 docs/.gitattributes create mode 100644 docs/changelog.md create mode 100644 docs/downloads.md create mode 100644 docs/images/LeanKitIntegrationOverview_150.png create mode 100644 docs/images/image00.gif create mode 100644 docs/images/image01.gif create mode 100644 docs/images/image02.gif create mode 100644 docs/images/image04.gif create mode 100644 docs/images/image05.gif create mode 100644 docs/images/image07.gif create mode 100644 docs/images/image08.gif create mode 100644 docs/images/image09.gif create mode 100644 docs/images/image10.gif create mode 100644 docs/images/image11.gif create mode 100644 docs/images/image12.gif create mode 100644 docs/images/image13.gif create mode 100644 docs/images/image14.gif create mode 100644 docs/images/image15.gif create mode 100644 docs/images/image16.gif create mode 100644 docs/images/image17.gif create mode 100644 docs/images/image18.gif create mode 100644 docs/images/image19.gif create mode 100644 docs/images/image21.gif create mode 100644 docs/images/image22.gif create mode 100644 docs/images/image24.gif create mode 100644 docs/images/leankit-card-id-settings.gif create mode 100644 docs/overview.md diff --git a/builds/.gitattributes b/builds/.gitattributes new file mode 100644 index 0000000..486a232 --- /dev/null +++ b/builds/.gitattributes @@ -0,0 +1 @@ +*.zip filter=lfs diff=lfs merge=lfs -text diff --git a/builds/LeanKit.IntegrationService-20160629.zip b/builds/LeanKit.IntegrationService-20160629.zip new file mode 100644 index 0000000..7e57a51 --- /dev/null +++ b/builds/LeanKit.IntegrationService-20160629.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd1f09b10dbaea3f1b9432da971c4073bf70dc58906b5f9aa8f025261d7bf137 +size 4764667 diff --git a/docs/.gitattributes b/docs/.gitattributes new file mode 100644 index 0000000..4a9d8e8 --- /dev/null +++ b/docs/.gitattributes @@ -0,0 +1,2 @@ +*.png filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..bd5fa63 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,111 @@ +### June 29, 2016 + +* Fixed issue caused by missing Priority field in JIRA issue + +### June 23, 2016 + +* Creating a LeanKit card in an unmapped lane should not trigger creating a new item in the target system. + +### May 25, 2016 + +* Synchronization improvements and fixes. + +### May 20, 2016 + +* Synchronization improvements and fixes. + +### March 22, 2016 + +* Synchronization improvements and fixes. + +### March 9, 2016 + +* Fix for synchronization conflicts. +* Improvements to logging. + +### February 1, 2016 + +* Fix for issue states or issue types that contain a slash (/) causing problems in the Configuration UI. + +### January 28, 2016 + +* Additional support for NTLM authentication for TFS. + +### January 21, 2016 + +* Fixed issue where in some scenarios a board update could trigger updating a target item, even when "Update Target Items" is disabled. + +### November 6, 2015 + +* Fixed issue with "Update Target Items" setting being ignored when service is restarted, potentially causing the states of target items being updated for cards that were moved while the service was stopped.  + +### July 23, 2015 + +* Fixed JIRA formatting issues of descriptions synchronized between JIRA and LeanKit. + +### June 29, 2015 + +* Fixed JIRA issue where reopening an issue did not move LeanKit card back to appropriate lane. + +### June 11, 2015 + +* Added support for additional JIRA priorities + +### May 7, 2015 + +* Updated LeanKit polling to reduce risk of race condition + +### March 30, 2015 + +* JIRA integration now uses cookie authentication, to be more compatible with proxy servers. +* Now supports alternative LeanKit domains, such as leankit.co. Must supply the full URL of the domain, e.g. https://company.leankit.co + +### February 9, 2015 + +* Fixed issue affecting JIRA integration running on non-US cultures + +### December 8, 2014 + +* Fixed issue affecting JIRA and GitHub synchronizing title and descriptions that contain double quotes +* Updated to the latest LeanKit.API.Client + +### September 3, 2014 + +* Fixed synchronization of tags with TFS +* Fixed issue with adding cards to default drop lane from JIRA + +### August 6, 2014 + +* Official support for Visual Studio Online and TFS 2013\. Requires the updated [TFS 2013 Object Model](http://visualstudiogallery.msdn.microsoft.com/3278bfa7-64a7-4a75-b0da-ec4ccb8d21b6) +* Optimized loading of VSO/TFS project members +* Requires .NET Framework 4.5 + +### July 15, 2014 + +* Bug fixes and improvements. + +### April 21, 2014 + +* Fixed issue with task card updates and attachments. + +### March 31, 2014 + +* Fixed issue with moving a task card onto the parent board and create target items (2-way sync) is enabled. +* Fixed issue with deleting a task card or its parent. +* Updated to the latest version of the LeanKit API Client Library. +* Better exception handling for board updates. + +### March 17, 2014 + +* TFS: Updated exception handling around updating and creating LeanKit cards. +* JIRA: Added support for updating epics. + +### March 5, 2014 + +* JIRA: Fixed issue with creating cards from new JIRA issues. +* JIRA: Fixed issue causing the service to attempt to create cards that already exist. +* JIRA: Fixed issue with creating a new card in LeanKit would create a "bug" issue type in JIRA, regardless of card type mapping. +* GitHub: Fixed issue retrieving all available repositories. +* GitHub: Fixed bad URL link to original GitHub issue. +* GitHub: Fixed state-to-lane mapping issue. +* GitHub: Fixed issue with card not moving if state of issue is updated in GitHub. \ No newline at end of file diff --git a/docs/downloads.md b/docs/downloads.md new file mode 100644 index 0000000..3b67c0a --- /dev/null +++ b/docs/downloads.md @@ -0,0 +1,19 @@ +## Download latest + +[LeanKit.IntegrationService-20160629.zip](../builds/LeanKit.IntegrationService-20160629.zip) + +## Installing + +Download the **.zip** file below, and follow the installation directions in the **[LeanKit Integration Service Overview & Guide](/entries/28393486 "LeanKit Integration Service Overview & Guide")**. + +## Upgrading + +1. Download and extract latest version of the LeanKit Integration Service into a separate folder. +2. Make a backup of the following files, for safe keeping: **config-live.json**, **config-edit.json**, and **IntegrationService.exe.config**. +3. Stop the existing Integration Service, if it is running. +4. Copy and overwrite all the files from the latest LeanKit Integration Service. +5. Restart the LeanKit Integration Service. + +## Change Log + +View [most recent changes](./changelog.md) to the LeanKit Integration Service. \ No newline at end of file diff --git a/docs/images/LeanKitIntegrationOverview_150.png b/docs/images/LeanKitIntegrationOverview_150.png new file mode 100644 index 0000000..a0e611f --- /dev/null +++ b/docs/images/LeanKitIntegrationOverview_150.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebd10af9f623228e8e9886f2c917db817fabb37b37dba1ab02a7a873777bda6e +size 195266 diff --git a/docs/images/image00.gif b/docs/images/image00.gif new file mode 100644 index 0000000..4c0378b --- /dev/null +++ b/docs/images/image00.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35b946dd41cc3c4b1ef887c7e086099eefd37ee4d03d272e5bade98e5a6e559e +size 20538 diff --git a/docs/images/image01.gif b/docs/images/image01.gif new file mode 100644 index 0000000..75976b4 --- /dev/null +++ b/docs/images/image01.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:013cf4433f9ee25cdbee809f837bd5fe6a36a38b4e6306693f6d54bdf0c91136 +size 13274 diff --git a/docs/images/image02.gif b/docs/images/image02.gif new file mode 100644 index 0000000..bfc9319 --- /dev/null +++ b/docs/images/image02.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b30a087aaae4844fbf1733b9fbdaeca8bbecd31ba9237327411f3091e26430f +size 19685 diff --git a/docs/images/image04.gif b/docs/images/image04.gif new file mode 100644 index 0000000..1ae4f49 --- /dev/null +++ b/docs/images/image04.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b982c78bde1976d1da98d5b2a458773e017d6449b3b0bcec05a832ecb748f33 +size 5691 diff --git a/docs/images/image05.gif b/docs/images/image05.gif new file mode 100644 index 0000000..fb8970e --- /dev/null +++ b/docs/images/image05.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c24a20f1f12c146b24ac0bc9c638d76e1938ac31e47390f66b0681928d6caa4c +size 34952 diff --git a/docs/images/image07.gif b/docs/images/image07.gif new file mode 100644 index 0000000..e2e682d --- /dev/null +++ b/docs/images/image07.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5339b173651617bcc1b077dec9103f3d5bc030ae03709cd1b66dc4ec9e875a1e +size 18774 diff --git a/docs/images/image08.gif b/docs/images/image08.gif new file mode 100644 index 0000000..f5d68f5 --- /dev/null +++ b/docs/images/image08.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1eb0d62865a420c417a4f1d39d34e4ed94f22cb6df11eae606ae366dba8b2c6e +size 24219 diff --git a/docs/images/image09.gif b/docs/images/image09.gif new file mode 100644 index 0000000..891d61b --- /dev/null +++ b/docs/images/image09.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f439fd0802ac4461f2e6915664986511bbb8ddc52df8b033b2f88f2a5a28cdb +size 21887 diff --git a/docs/images/image10.gif b/docs/images/image10.gif new file mode 100644 index 0000000..aa17ebc --- /dev/null +++ b/docs/images/image10.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e7cdaab0145759e3f30c845bb79d618373b31763246e8d2b678f840c5ee66c7 +size 26410 diff --git a/docs/images/image11.gif b/docs/images/image11.gif new file mode 100644 index 0000000..0bbefe6 --- /dev/null +++ b/docs/images/image11.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f6bd5e1586d5a447476276a81d89cad09bc25f261a94850be726f2fc611af94 +size 9804 diff --git a/docs/images/image12.gif b/docs/images/image12.gif new file mode 100644 index 0000000..d5d1098 --- /dev/null +++ b/docs/images/image12.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa3453c57076203d1355b3d9cb6b7ea8fd0b403c12bda4172c96f221be1e21d8 +size 15445 diff --git a/docs/images/image13.gif b/docs/images/image13.gif new file mode 100644 index 0000000..4a3e406 --- /dev/null +++ b/docs/images/image13.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dff40b5b4230e0599b8eed2305cf76f18c2c8f32524e633792d53695b4d6d27c +size 27528 diff --git a/docs/images/image14.gif b/docs/images/image14.gif new file mode 100644 index 0000000..48decf0 --- /dev/null +++ b/docs/images/image14.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cb5ab71e12d0906c37da6eb4fe7b95f14760f8468eafe216b5bd0d65730876d +size 34337 diff --git a/docs/images/image15.gif b/docs/images/image15.gif new file mode 100644 index 0000000..71f3c89 --- /dev/null +++ b/docs/images/image15.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d436764a55f81a29888dfd46059d38d2567f713308ef4a78b191274808c500d0 +size 14558 diff --git a/docs/images/image16.gif b/docs/images/image16.gif new file mode 100644 index 0000000..ad345a6 --- /dev/null +++ b/docs/images/image16.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acb1d9b1f13fe6550f5fe04d229c3279a44f468b35df09063c78431775b0881f +size 32748 diff --git a/docs/images/image17.gif b/docs/images/image17.gif new file mode 100644 index 0000000..4763c14 --- /dev/null +++ b/docs/images/image17.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fba11f189e7ca1b340638f9233f780a9f6fb13429cbdb35ec2607a891d3db6e5 +size 49840 diff --git a/docs/images/image18.gif b/docs/images/image18.gif new file mode 100644 index 0000000..b57dfc4 --- /dev/null +++ b/docs/images/image18.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45f0130da967fb1c1dc3329b1ca2264fd336b1942ea89f66637c561710b731c0 +size 41766 diff --git a/docs/images/image19.gif b/docs/images/image19.gif new file mode 100644 index 0000000..6c0422b --- /dev/null +++ b/docs/images/image19.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ff1db5a7923f3593a9a43cd58e2a3724d9e0f3963a9d015564595ca0b5a1a75 +size 31762 diff --git a/docs/images/image21.gif b/docs/images/image21.gif new file mode 100644 index 0000000..06df62e --- /dev/null +++ b/docs/images/image21.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:510fe83f59688128518a6c94e48ae7c8b0836d124b0dbb221cee98ab1920cc58 +size 48546 diff --git a/docs/images/image22.gif b/docs/images/image22.gif new file mode 100644 index 0000000..46d787c --- /dev/null +++ b/docs/images/image22.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e46f90dc5076c8e3e08bac3c44b765d639a5474f1a131794f7a9487900b49ca +size 9785 diff --git a/docs/images/image24.gif b/docs/images/image24.gif new file mode 100644 index 0000000..a3582f1 --- /dev/null +++ b/docs/images/image24.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a25054d1bea0d984afeb719c1659bf6287fb558f4ea9a091278b7641d713c758 +size 15728 diff --git a/docs/images/leankit-card-id-settings.gif b/docs/images/leankit-card-id-settings.gif new file mode 100644 index 0000000..281f213 --- /dev/null +++ b/docs/images/leankit-card-id-settings.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2a7e0a76d6fdde987023f3f76db4e0d7ef175d0433a470b88a404107535c7b9 +size 47341 diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000..fdb7a31 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,431 @@ +# Table of Contents + +* [Overview](#overview) +* [Requirements](#requirements) + * [Requirements for Visual Studio Team Services (VSTS) and Microsoft Team Foundation Server (TFS)](#requirements-vso-tfs) + * [Enable alternate credentials for VSTS](#vso-alternate-credentials) + * [Requirements for JIRA](#requirements-jira) + * [Update Card ID Settings in LeanKit](#card-id-settings) +* [Installation](#installation) + * [Running from the Command-Line](#command-line) + * [Installing as a service](#installing-as-service) + * [Upgrading](#upgrading) + * [Uninstalling](#uninstalling) + * [Changing the default configuration port number](#default-port-number) +* [Configuration](#configuration) + * [Launching the configuration management application](#config-mgmt) + * [Connecting to your LeanKit account](#connecting-to-leankit) + * [Connecting to your Target system](#connecting-to-target) + * [Global settings](#global-settings) + * [Mapping a Target project to a LeanKit board](#mapping) + * [Removing a Target system project to LeanKit board mapping](#remove-mapping) +* [Troubleshooting](#troubleshooting) + * [Common Issues](#common-issues) + * [Viewing the logs](#logs) + * [Customize logging](#customize-logging) + +# Overview + +The LeanKit Integration Service synchronizes items between a Target system and LeanKit. + +* Supported Target systems: Visual Studio Team Services (VSTS), Microsoft Team Foundation Server (TFS), Atlassian JIRA, and GitHub +* Runs as a Windows service, or from command line for testing +* One Target + one LeanKit account per instance +* Multiple Target projects / LeanKit boards per instance +* Map any status in the Target system to any lane on a LeanKit board +* Monitors the Target system, looking for new items and updates +* When new items are added to the Target system, new cards are added to LeanKit +* Monitors LeanKit boards for card moves and updates +* When new cards are added to LeanKit, new items are added to the Target system (optional) +* Supports state workflow (e.g. Active > Resolved > Closed) +* Target system query to check for new/updated items can be fully customized +* Synchronizes the following equivalent fields: title, description, priority, card type, tags, assigned to, due date, and card size. + +![LeanKitIntegrationOverview_150.png](./images/LeanKitIntegrationOverview_150.png) + +# Requirements + +Before installing and configuring the Integration Service, make sure the computer meets minimum requirements. The Integration Service requires a PC running Windows 7, Windows Server 2008, or later, and the [Microsoft .NET Framework 4.5](http://www.microsoft.com/en-us/download/details.aspx?id=30653). + +## Requirements for Visual Studio Team Services (VSTS) and Microsoft Team Foundation Server (TFS) + +### Install the TFS 2013 Object Model Client + +The LeanKit Integration Service supports VSTS and TFS versions 2010, 2012, 2013, and 2015\. Regardless of the version used as the Target, the [Team Foundation Server 2013 Object Model client](http://visualstudiogallery.msdn.microsoft.com/3278bfa7-64a7-4a75-b0da-ec4ccb8d21b6) must be installed. + +### Enable alternate credentials for Visual Studio Team Services + +To connect to VSTS, you will need to create and enable "alternate credentials" for the VSTS account used to query and update work items in VSTS. + +* Sign in to the VSTS account you are integrating, such as https://your-account.visualstudio.com +* In the top-right corner, click on your account name and then select _My Profile_ +* Select the _Credentials_ tab +* Click the _Enable alternate credentials and set password_ link +* Enter a password. It is suggested that you choose a unique password here (not associated with any other accounts) +* Click _Save Changes_ + +## Requirements for JIRA + +The minimum version of JIRA supported is 5.0. + +## Update Card ID Settings in LeanKit + +To function properly, the LeanKit boards that are synchronized with the LeanKit Integration Service need to have “Card ID” support enabled. The LeanKit Integration Service uses this feature to store the correlating item ID from the Target system. + +1. Sign in to your LeanKit account. +2. Navigate to the board you will be synchronizing with your Target system. +3. Click the **Configure Board Settings** icon (see the following image). +4. Click the **Card ID Settings** tab. +5. Click **Display external card ID field in card edit dialog**. +6. Click **Enable header** (optional) and choose **Display text from external card ID field**. +7. Click **Save and Close**. + +![image21.gif](./images/image21.gif) + +![leankit-card-id-settings.gif](./images/leankit-card-id-settings.gif) + +# Installation + +1. [Download](./downloads.md) the LeanKit Integration Service .zip file. +1. Extract the LeanKit Integration Service into a folder on the computer where the service will be installed. +1. Open an **Administrator** command prompt, and change the current directory to where the Integration Service is extracted. + +## Running from the Command-Line + +To run the Integration Service as a command-line application, simply run the executable. Type `IntegrationService.exe` and press ENTER. + +![image22.gif](./images/image22.gif) + +Running the LeanKit Integration Service from the command-line is useful for watching the activity between the Target system and LeanKit. + +![image00.gif](./images/image00.gif) + +## Installing as a service + +Open a command prompt as an administrator, and type the following: + +`> IntegrationService.exe install` + +The service will be installed and started immediately. By default, the Integration Service runs as the local service account. + +![image17.gif](./images/image17.gif) + +### Installing more than one instance of the service + +First, create separate folders for each instance, and copy all the files for the Integration Service into each folder. Next, open a command prompt as an administrator, and type the following for each instance, replacing [instance_name] with the desired instance name: + +`> IntegrationService.exe install /instance:[instance_name]` + +The service will be installed using the service name plus the instance name, and started immediately. + +## Upgrading + +1. Download and extract latest version of the LeanKit Integration Service into a separate folder. +2. Make a backup of the following files, for safe keeping: **config-live.json**, **config-edit.json**, and **IntegrationService.exe.config**. +3. Stop the existing Integration Service, if it is running. +4. Copy and overwrite all the files from the latest LeanKit Integration Service. +5. Restart the LeanKit Integration Service. + +## Uninstalling + +Open a command prompt as an administrator, and type the following: + +`> IntegrationService.exe uninstall` + +![image18.gif](./images/image18.gif) + +### Uninstalling a named instance + +Open a command prompt as an administrator, and type the following: + +`> IntegrationService.exe uninstall /instance:[instance_name]` + +## Changing the default configuration port number + +Locate and open the file IntegrationService.exe.config in a text editor. Find the application setting “ConfigurationSitePort” and change the value to the desired port number. Save the file, and restart the service. + +

+ 
+   
+    
+   
+  ... 
+
+
+ +# Configuration + +The LeanKit Integration Service includes a web-based management application for configuring the connections and mappings between one or more projects in a Target system and one or more LeanKit boards. + +## Launching the configuration management application + +1. Open a web browser on the machine where the LeanKit Integration Service is installed. +2. Browse to [http://localhost:8090/](http://localhost:8090/) + +By default, the web interface for managing the LeanKit Integration Service is available at [http://localhost:8090/](http://localhost:8090/). To change the port number (8090), see [Changing the default configuration port number](#default-port-number). + +## Connecting to your LeanKit account + +The first time you access the LeanKit Integration Service configuration application, you will be prompted to connect to your Leankit account. The LeanKit account user must have permission to access, create, and update cards with the boards you wish to integrate with your Target system. + +1. Enter your account name. Your account name can be found in the URL you use to access your LeanKit account. +2. Enter your account email address. +3. Enter your password. +4. Click **Connect**. + +![image14.gif](./images/image14.gif) + +Once you successfully connect to your LeanKit account, click **Next: Connect To Target** to continue. + +![image09.gif](./images/image09.gif) + +## Connecting to your Target system + +1. Choose the type of Target system you are connecting to. Choices are TFS, JIRA, GitHub Issues, and GitHub Pull Requests. + _ + Note: If you are connecting to Visual Studio Team Services, choose **TFS**. You must also [enable alternate credentials](#vso-alternate-credentials). + + _ +2. Enter your host address. + + If required for self-hosted TFS or JIRA, change the host prefix from https:// to http:// + + For Visual Studio Team Services or TFS, also include the name of the project collection. + + For GitHub, enter only the name of the account or organization associated with the desired repository. + +3. Enter your account user name. + + _Note: For **JIRA**, you must use your account **username** instead of your email address.Your username can be found by going to your account Profile._ + +4. Enter your password. +5. Click **Connect**. + +![image12.gif](./images/image12.gif) + +Once you successfully connect to your Target system, click **Next: Check Global Settings** to continue. + +![image02.gif](./images/image02.gif) + +## Global settings + +**Check Target for updates every [____] milliseconds**. By default, the LeanKit Integration Service will check the Target system every 60,000 milliseconds (60 seconds) for new changes. + +**Synchronize items created after: [____]**. By default, the LeanKit Integration Service will check only items created after January 1, 2013. + +Once you updated or verified these global settings, click **Next: Configure Boards and Projects** to continue. + +![image15.gif](./images/image15.gif) + +## Mapping a Target project to a LeanKit board + +1. On the left, select a LeanKit board. +2. On the right, select a Target project from the drop-down list. +3. Click **Configure…** to continue. + +![image01.gif](./images/image01.gif) + +![image24.gif](./images/image24.gif) + +### Choose how LeanKit cards are created + +Under the **Selection** tab, choose the project statuses and Target item types that you wish to synchronize with your LeanKit board. + +_Note: For Visual Studio Team Services or TFS accounts, you may also select a project Iteration Path to further refine the items that are selected for synchronization._ + +![image16.gif](./images/image16.gif) + +#### Using a custom Target query + +The LeanKit Integration Service supports custom queries for selecting items from the Target system to synchronize with your LeanKit board. When you select **Custom Query**, the **Simple Selection** area will be hidden, and a text box will appear where you may enter a custom query. + +![image07.gif](./images/image07.gif) + +#### Using a custom query for Visual Studio Team Services or TFS + +The LeanKit Integration Service supports custom Work Item queries. You may enter any valid Work Item query. Your Work Item query must include the following: + +`[System.ChangedDate] > '{0}'` + +This is a placeholder used to filter items in the query to only those that have changed since the last date and time Work Items were found for synchronization. + +The default Work Item query used by the LeanKit Integration Service is: + +
[System.TeamProject] = '' 
+  AND [System.IterationPath] UNDER '\\' 
+  AND ([System.State] = '' 
+       OR [System.State] = '' OR ... ) 
+  AND ([System.WorkItemType] <> '' 
+       AND [System.WorkItemType] <> '' AND ...) 
+  AND [System.ChangedDate] > '{0}'
+
+ +A full explanation of the query capabilities available for Visual Studio Team Services and TFS is beyond the scope of this documentation. Please consult your Work Item query documentation. + +#### Using a custom query for JIRA + +The LeanKit Integration Service supports custom queries with JIRA using the JIRA Query Language (JQL). You may enter any valid JQL. Your custom JIRA query must include the following: + +`updated > '{0}'` + +This is a placeholder used to filter items in the query to only those that have changed since the last date and time JIRA issues were found for synchronization. + +The default JIRA query used by the LeanKit Integration Service is: + +
project='' 
+and (status='' or status='' [or ]) 
+and updated > '{0}'   
+order by created asc
+
+ +A full explanation of the query capabilities available for JIRA is beyond the scope of this documentation. Please consult your JIRA Query Language documentation. + +#### Using a custom query for GitHub + +The LeanKit Integration Service does not support custom queries for GitHub. Please use the **Simple Selection** option. + +### Assign Target project item statuses to LeanKit board lanes + +Under the **Lanes and States** tab, click on a LeanKit board lane, and then select the Target project item statuses (states) that are appropriate for that lane. When new Target items are created or updated, a LeanKit card will be created in, or optionally moved to, the equivalent lane on the board. As cards are moved to different lanes, the associated Target item will be updated to the equivalent status. + +In the following example, we are choosing Target item states “New,” “Ready,” and “To Do” to be assigned (mapped) to the LeanKit “To Do” lane. + +![image19.gif](./images/image19.gif) + +_Note: All statuses checked on the Selection tab must be mapped to a lane. The required statuses will remain highlighted until they have been added._ + +To remove an assigned status from a lane, click the ‘X’ next to the status. + +#### Adding a custom state workflow + +In some cases, a Target project may not allow an item’s state to move directly from state to another in one step. These projects enforce a “state workflow” that an item must progress through. You can add one or more workflows to each LeanKit lane to express the order of states an item must go through to match the equivalent state of the lane. + +To add a new workflow: + +1. Select the appropriate lane. +2. Check the box for **Build Workflow**. +3. Select each state, in the correct order. +4. Click the **check mark icon** when finished. + +![image05.gif](./images/image05.gif) + +_Note: The order of states and workflows added to a lane will be the same order the LeanKit Integration Service will attempt to set a Target system’s item state when updating._ + +### Map Target item types to LeanKit card types + +Under the **Card Types** tab, you can customize how the Target project’s item types are mapped to LeanKit card types. + +To create a new card type mapping: + +1. Click the **Add** button. +2. Choose a LeanKit card type from the first drop-down list. +3. Choose a Target item type from the second drop-down list. +4. Click the **check mark icon** to save. + +![image10.gif](./images/image10.gif) + +To remove a card type mapping, click the ‘X’ icon next to the listed mapping. + +### Synchronization options + +Under the **Options** tab, you can customize how the LeanKit Integration Service will keep the Target project and LeanKit board synchronized. + +**Create a new corresponding LeanKit card:** When enabled, a new LeanKit card will be created each time a new item is created in the Target system that matches the selection criteria. + +**Create a new corresponding Target item:** When enabled, a new Target item will be created each time a new LeanKit card is added to the board being monitored. + +**Update Cards:** When enabled and a Target item is updated, the associated LeanKit card will be updated (e.g. Title, Description, or Due Date). This does not apply to changes to Target item state (see next). + +**Move Cards when State changes:** When enabled and a Target item’s state is updated, the associated LeanKit card will be moved to the equivalent lane (if mapped). + +**Update Target Items:** When enabled and a LeanKit card is updated or moved, the associated Target item will be updated (e.g. Title, Description, or Due Date). + +**When creating LeanKit cards, tag the card with the Target system:** When enabled and a new card is created, the card will be tagged with the name of the Target system (e.g. TFS or JIRA). This is useful when integrating more than one Target system with a single LeanKit board. + +![image13.gif](./images/image13.gif) + +### Saving a configuration + +Whenever you make changes to a LeanKit board to Target project mapping, please be sure to click the **Save** button to save your changes. + +![image11.gif](./images/image11.gif) + +### Activating and restarting the LeanKit Integration Service + +The **Activate…** tab is used to review and enable changes to your configuration. After creating or updating a Target system and LeanKit board configuration, you must activate the new configuration before the LeanKit Integration Service will recognize those changes. A summary report is displayed of all the Target system project to LeanKit board mappings and configured options. After reviewing this report, click the **Activate Now** button to restart the service and enable the new or updated configuration. + +![image08.gif](./images/image08.gif) + +## Removing a Target system project to LeanKit board mapping + +1. If not already selected, click on the **Board Configuration** tab. +2. On the left, click the LeanKit board and Target project mapping you wish to remove. +3. On the right, click on the **Options** tab. +4. At the bottom, click on **Remove Mapping…**. +5. Click **Remove** to confirm. +6. Click on the **Activate…** tab. +7. Click **Activate Now** to restart the service with the new configuration. + +_Note: Removing a mapping will stop items from being synchronized. However, it will **not** remove any items that have been created in LeanKit or the Target system as a result of the previous configuration._ + +![image04.gif](./images/image04.gif) + +# Troubleshooting + +## Common Issues + +When running the LeanKit Integration Service the first time, you see: + +**IntegrationService has Stopped Working** + +-or- + +**System.BadImageFormatException** + +This could be caused by not having the Microsoft .NET Framework 4.5 installed. Please see [Requirements](#requirements). + +When trying to access the management application with your browser, you see: + +**ERR_CONNECTION_RESET** + +This may be due to a local firewall issue. You may need to either allow port 8090, or change the port number to something that is allowed by your local firewall, e.g. 8080\. See [Changing the default configuration port number](#default-port-number). + +When trying to connect to LeanKit, you see: + +**Could not connect to LeanKit. Please verify your credentials.** + +This could be due to the Integration Service being installed behind a proxy server. To configure the Integration Service to use a proxy server, edit IntegrationService.exe.config, and update the following configuration section just before the closing tag. It should look something like: + +
  
+    
+      
+    
+
+ +Add a "useDefaultCredentials" setting. + +
  
+    
+      
+    
+
+ +Or, if more granular control of the proxy is required, it could look something like: + +
  
+    
+      
+    
+
+ +For more proxy help, please review the documentation on [.NET proxy configuration](http://msdn.microsoft.com/en-us/library/kd3cf2ex(v=vs.110).aspx). + +## Viewing the logs + +The LeanKit Integration Service logs all informational and error messages to the `[InstallPath]/Log` folder. A log file can assist in troubleshooting connection or synchronization issues. Each day’s logs will be stored in a file with the name `[yyyyMMdd].txt`. + +## Customize logging + +The LeanKit Integration Service uses [log4net](http://logging.apache.org/log4net/), a very flexible open-source logging utility that can log activity to a variety of outputs. For example, log4net can be configured to log to a database such as SQL Server or Oracle, or send an email whenever an error occurs. + +To customize how logs are generated, locate and edit the `logging.config` file found in the folder where the LeanKit Integration Service is installed. There are many examples provided in the [log4net documentation](http://logging.apache.org/log4net/release/config-examples.html). \ No newline at end of file From ec0cee7bf334b4a5f6e1c4cf95e8e6221c71932c Mon Sep 17 00:00:00 2001 From: David Neal Date: Mon, 12 Sep 2016 12:49:52 -0400 Subject: [PATCH 16/20] updating docs --- docs/overview.md | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index fdb7a31..1ccae0a 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -144,14 +144,15 @@ Open a command prompt as an administrator, and type the following: Locate and open the file IntegrationService.exe.config in a text editor. Find the application setting “ConfigurationSitePort” and change the value to the desired port number. Save the file, and restart the service. -

+```
+
  
    
     
    
   ... 
 
-
+``` # Configuration @@ -251,14 +252,15 @@ This is a placeholder used to filter items in the query to only those that have The default Work Item query used by the LeanKit Integration Service is: -
[System.TeamProject] = '' 
+```
+[System.TeamProject] = '' 
   AND [System.IterationPath] UNDER '\\' 
   AND ([System.State] = '' 
        OR [System.State] = '' OR ... ) 
   AND ([System.WorkItemType] <> '' 
        AND [System.WorkItemType] <> '' AND ...) 
   AND [System.ChangedDate] > '{0}'
-
+``` A full explanation of the query capabilities available for Visual Studio Team Services and TFS is beyond the scope of this documentation. Please consult your Work Item query documentation. @@ -272,11 +274,12 @@ This is a placeholder used to filter items in the query to only those that have The default JIRA query used by the LeanKit Integration Service is: -
project='' 
+```
+project='' 
 and (status='' or status='' [or ]) 
 and updated > '{0}'   
 order by created asc
-
+``` A full explanation of the query capabilities available for JIRA is beyond the scope of this documentation. Please consult your JIRA Query Language documentation. @@ -396,27 +399,33 @@ When trying to connect to LeanKit, you see: This could be due to the Integration Service being installed behind a proxy server. To configure the Integration Service to use a proxy server, edit IntegrationService.exe.config, and update the following configuration section just before the closing tag. It should look something like: -
  
+```
+  
     
       
     
-
+ +``` Add a "useDefaultCredentials" setting. -
  
+```
+  
     
       
     
-
+ +``` Or, if more granular control of the proxy is required, it could look something like: -
  
+```
+  
     
       
     
-
+ +``` For more proxy help, please review the documentation on [.NET proxy configuration](http://msdn.microsoft.com/en-us/library/kd3cf2ex(v=vs.110).aspx). From 1ec77ccc45d1d3f8895ca8756a74768048fd2151 Mon Sep 17 00:00:00 2001 From: David Neal Date: Mon, 12 Sep 2016 14:25:26 -0400 Subject: [PATCH 17/20] Update downloads.md --- docs/downloads.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/downloads.md b/docs/downloads.md index 3b67c0a..7173704 100644 --- a/docs/downloads.md +++ b/docs/downloads.md @@ -4,7 +4,7 @@ ## Installing -Download the **.zip** file below, and follow the installation directions in the **[LeanKit Integration Service Overview & Guide](/entries/28393486 "LeanKit Integration Service Overview & Guide")**. +Download the **.zip** file, and follow the installation directions in the **[Installation and Overview Guide](./overview.md)**. ## Upgrading @@ -16,4 +16,4 @@ Download the **.zip** file below, and follow the installation directions in the ## Change Log -View [most recent changes](./changelog.md) to the LeanKit Integration Service. \ No newline at end of file +View [most recent changes](./changelog.md) to the LeanKit Integration Service. From 27e21ebdeb70d4137c10c5a89c1ca1404aa3eeae Mon Sep 17 00:00:00 2001 From: David Neal Date: Mon, 12 Sep 2016 14:42:33 -0400 Subject: [PATCH 18/20] updating readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 98b021e..02e75f3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -Read the [LeanKit Integration Service Guide](https://support.leankit.com/entries/28393486-LeanKit-Integration-Service-Guide) +* [Installation and Overview Guide](./docs/overview.md) +* [Change log](./docs/changelog.md) +* [Downloads](./docs/download.md) The **LeanKit Integration Service** synchronizes items between a Target system and LeanKit. From 556dcff5b632cb5e0f2b6afb005df3f25abb844a Mon Sep 17 00:00:00 2001 From: David Neal Date: Mon, 12 Sep 2016 14:43:39 -0400 Subject: [PATCH 19/20] updating readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 02e75f3..1831a8b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# LeanKit Integration Service + * [Installation and Overview Guide](./docs/overview.md) * [Change log](./docs/changelog.md) * [Downloads](./docs/download.md) From 2eff3a964e098444e5557a887861e51158d1a98d Mon Sep 17 00:00:00 2001 From: David Neal Date: Mon, 12 Sep 2016 14:44:28 -0400 Subject: [PATCH 20/20] updating readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1831a8b..91d7f82 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ * [Installation and Overview Guide](./docs/overview.md) * [Change log](./docs/changelog.md) -* [Downloads](./docs/download.md) +* [Downloads](./docs/downloads.md) The **LeanKit Integration Service** synchronizes items between a Target system and LeanKit.