/// <summary> /// Executes the "configure" action /// </summary> /// <param name="opts">command-line options</param> /// <returns>exit code</returns> private static int Configure(ConfigureOptions opts) { var config = File.Exists(opts.ConfigFile) ? JiraKanbanConfig.ParseXml(opts.ConfigFile) : new JiraKanbanConfig(); if (opts.JiraUsername != null) { config.JiraUsername = opts.JiraUsername; } if (opts.JiraInstanceBaseAddress != null) { config.JiraInstanceBaseAddress = opts.JiraInstanceBaseAddress; } if (opts.BoardId != null) { config.BoardId = opts.BoardId.Value; } if (!opts.NoStorePassword && !string.IsNullOrWhiteSpace(config.JiraUsername)) { Console.WriteLine($"Enter the Jira password for user '{config.JiraUsername}':"); config.JiraPassword = GetPassword(); } var xml = config.ToXml(); using (var f = File.OpenWrite(opts.ConfigFile)) xml.Save(f); Console.WriteLine($"Configuration file generated at: {opts.ConfigFile}"); return(0); }
/// <summary> /// Executes the "generate" action /// </summary> /// <param name="opts">command-line options</param> /// <returns>exit code</returns> private static int Generate(GenerateOptions opts) { if (!File.Exists(opts.ConfigFile)) { var color = Console.ForegroundColor; Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"ERROR: Configuration file does not exist: {opts.ConfigFile}"); Console.ForegroundColor = color; return(1); } var config = JiraKanbanConfig.ParseXml(opts.ConfigFile); if (opts.BoardId != null) { config.BoardId = opts.BoardId.Value; } var issues = GetIssues(opts, config); var charts = KanbanCharts.Create(config, issues); SaveChart(opts, charts.FlowEfficiencyChart, "FlowEfficiencyChart.png"); SaveChart(opts, charts.LeadTimeHistogramChart, "LeadTimeHistogramChart.png"); SaveChart(opts, charts.WeeklyThroughputHistogramChart, "WeeklyThroughputHistogramChart.png"); SaveChart(opts, charts.WeeklyThroughputChart, "WeeklyThroughputChart.png"); SaveChart(opts, charts.LeadTimeControlChart, "LeadTimeControlChart.png"); SaveChart(opts, charts.CumulativeFlowDiagramChart, "CumulativeFlowDiagramChart.png"); Console.WriteLine("Success !!!"); return(0); }
/// <summary> /// Calculates the "queue time" for this issue, considering the given configuration /// </summary> /// <param name="config">kanban configuration</param> /// <returns>issue queue time, in days</returns> public int QueueTime(JiraKanbanConfig config) { // ReSharper disable PossibleInvalidOperationException return((int)Math.Round(Columns .Where(c => c.Entered.HasValue && c.Exited.HasValue) .Where(c => config.QueueColumns.Any(queue => c.Name.Equals(queue, StringComparison.InvariantCultureIgnoreCase))) .Sum(c => (c.Exited.Value - c.Entered.Value).TotalDays))); // ReSharper restore PossibleInvalidOperationException }
/// <summary> /// Calculates the "touch time" for this issue, considering the given configuration /// </summary> /// <param name="config">kanban configuration</param> /// <returns>issue touch time, in days</returns> public int TouchTime(JiraKanbanConfig config) { var leadTime = LeadTime(config); if (leadTime == 0) { return(0); } var queueTime = Math.Min(leadTime, QueueTime(config)); return(leadTime - queueTime); }
/// <summary> /// Calculates the lead time for this issue, considering the given configuration /// </summary> /// <param name="config">kanban configuration</param> /// <returns>issue lead time, in days</returns> public int LeadTime(JiraKanbanConfig config) { var start = Entered(config.CommitmentStartColumns); var end = FirstEntered(config.DoneColumns); if (!start.HasValue || !end.HasValue) { return(0); } if (end < start) { end = Entered(config.DoneColumns); } if (!end.HasValue) { return(0); } return((int)Math.Round((end.Value - start.Value).TotalDays)); }
/// <summary> /// Retrieves issue metrics from the local cache or from Jira /// </summary> /// <param name="opts">command-line options</param> /// <param name="config">Kanban configuration</param> /// <returns>list of issue metrics</returns> private static Issue[] GetIssues(GenerateOptions opts, JiraKanbanConfig config) { var cacheFile = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) ?? "./", ".cache"); var version = Assembly.GetEntryAssembly().GetName().Version.ToString(); var cache = new Cache(); var quickFilters = config.QuickFilters ?? new int[0]; Issue[] issues = null; if (!opts.NoCache && File.Exists(cacheFile)) { // Attempt to retrieve data from the cache try { cache = JsonConvert.DeserializeObject <Cache>(File.ReadAllText(cacheFile, new UTF8Encoding(false))); } catch (Exception e) { Console.WriteLine("Error reading cache: " + e.Message); } if (cache.Version != version) { cache = new Cache(); } // Delete old cached items var timeLimit = DateTime.UtcNow.AddHours(opts.CacheHours * -1); cache.Items.RemoveAll(c => c.TimestampUtc < timeLimit); // Try to find a matching cached item issues = cache.Items .Where(c => c.BoardId == config.BoardId) .Where(c => c.QuickFilters.SequenceEqual(quickFilters)) .Select(c => c.Issues) .FirstOrDefault(); } if (issues != null) { Console.WriteLine($"Retrieved data from local cache ({issues.Length} issues)"); } else { // Data is not cached, retrieve it from Jira if (config.JiraPassword == null && !string.IsNullOrWhiteSpace(config.JiraUsername)) { Console.WriteLine($"Enter the Jira password for user '{config.JiraUsername}':"); config.JiraPassword = GetPassword(); } using (var extractor = new MetricsExtractor(config, Console.WriteLine)) { issues = extractor.GetBoardDataAsync(config.BoardId, quickFilters).Result; } // Add to the cache cache.Items.Add(new CacheItem { BoardId = config.BoardId, QuickFilters = quickFilters, Issues = issues, }); } // Save the new cache try { if (!opts.NoCache) { using (var f = File.OpenWrite(cacheFile)) { var data = new UTF8Encoding(false).GetBytes(JsonConvert.SerializeObject(cache)); f.Write(data, 0, data.Length); } } } catch (Exception e) { Console.WriteLine("Error writing disk cache: " + e.Message); } return(issues); }