diff --git a/openbis_all/source/ruby/dashboard/jira b/openbis_all/source/ruby/dashboard/jira index 70f8f74f529989c72ee060a4f8c993350091079b..1c153bb7b43fff6750b70c443e605b8811b4a17b 100755 --- a/openbis_all/source/ruby/dashboard/jira +++ b/openbis_all/source/ruby/dashboard/jira @@ -100,8 +100,22 @@ class Issue def fix_version fix_versions = self.fields["fixVersions"] return "Unscheduled" if fix_versions.nil? + return "" if fix_versions[0].nil? return fix_versions[0]["name"] end + + def transitions + @issue["transitions"] + end + + def next_sprint + self.fields["customfield_10550"] + end + + def resolved_or_closed? + status = self.status + (status == "Resolved" || status == "Closed") + end end @@ -111,13 +125,13 @@ end module JiraHelpers def JiraHelpers.search(query, limit=nil) - search_cmd = "curl -s --get --cookie #{$jira_cookie_path} '#{$jira_api_url}/search' --data-urlencode 'jql=#{query}' --data-urlencode 'fields=*all,-comment'" + search_cmd = "curl -s --get --cookie #{$jira_cookie_path} '#{$jira_api_url}/search' --data-urlencode 'os_authType=cookie' --data-urlencode 'jql=#{query}' --data-urlencode 'fields=*all,-comment'" search_cmd = search_cmd + " --data-urlencode 'maxResults=#{limit}'" unless limit.nil? return `#{search_cmd}` end def JiraHelpers.issue(issue_number, limit=nil) - issue_cmd = "curl -s --get --cookie #{$jira_cookie_path} '#{$jira_api_url}/issue/#{issue_number}'" + issue_cmd = "curl -s --get --cookie #{$jira_cookie_path} '#{$jira_api_url}/issue/#{issue_number}' --data-urlencode 'os_authType=cookie' --data-urlencode 'expand=transitions'" data = `#{issue_cmd}` issue_data = JSON.load(data) return Issue.new(issue_data) @@ -155,13 +169,50 @@ module JiraHelpers } link_cmd = "curl -s --cookie #{$jira_cookie_path} -H 'Content-Type: application/json' '#{$jira_api_url}/issueLink' -d '#{JSON.generate(link_data)}'" return `#{link_cmd}` - end + end + + def JiraHelpers.remove_next_sprint(bis_key) + update_data = { + "fields" => { + "customfield_10550" => nil + } + } + update_cmd = "curl -s --cookie #{$jira_cookie_path} -H 'Content-Type: application/json' '#{$jira_api_url}/issue/#{bis_key}' -X PUT -d '#{JSON.generate(update_data)}'" + return `#{update_cmd}` + end + + def JiraHelpers.set_next_sprint(bis_key) + update_data = { + "fields" => { + "customfield_10550" => [ + {"value"=>"Yes", "id"=>"10240", "self"=>"https://jira-bsse.ethz.ch/rest/api/2/customFieldOption/10240"} + ] + } + } + update_cmd = "curl -s --cookie #{$jira_cookie_path} -H 'Content-Type: application/json' '#{$jira_api_url}/issue/#{bis_key}' -X PUT -d '#{JSON.generate(update_data)}'" + return `#{update_cmd}` + end + + def JiraHelpers.set_fix_version(bis_key, fixversion) + update_data = { "fields" => { "fixVersions" => [ {"name" => fixversion } ] } } + update_cmd = "curl -s --cookie #{$jira_cookie_path} -H 'Content-Type: application/json' '#{$jira_api_url}/issue/#{bis_key}' -X PUT -d '#{JSON.generate(update_data)}'" + return `#{update_cmd}` + end + + def JiraHelpers.transition(bis_key, trans_id) + transition_data = { + "transition" => { "id" => "#{trans_id}"} + } + transition_cmd = "curl -s --cookie #{$jira_cookie_path} -H 'Content-Type: application/json' '#{$jira_api_url}/issue/#{bis_key}/transitions' -d '#{JSON.generate(transition_data)}'" + return `#{transition_cmd}` + end # Search and return the full data for the found objects def JiraHelpers.search_full(query, silent) print "Retrieving issues" unless silent ans = JiraHelpers.search(query) data = JSON.load(ans) + full_issues_data = data["issues"] full_issues = full_issues_data.collect { | issue_data | Issue.new(issue_data) } print "\n" unless silent @@ -176,14 +227,20 @@ module JiraHelpers print "." unless silent implementors.concat issue.implemented_by end - + print "\n" unless silent return implementors + end + + def JiraHelpers.session_valid_raw + # Just get the response code, don't care about the rest + ans = `curl -s -w %{http_code} -o /dev/null --head --get --cookie #{$jira_cookie_path} --data-urlencode 'os_authType=cookie' '#{$jira_url}/rest/auth/1/session'` + return ans end def JiraHelpers.session_valid? # Just get the response code, don't care about the rest - ans = `curl -s -w %{http_code} -o /dev/null --head --get --cookie #{$jira_cookie_path} '#{$jira_url}/rest/auth/1/session'` + ans = JiraHelpers.session_valid_raw return false if $?.to_i != 0 return ans == "200" end @@ -266,15 +323,37 @@ SYNOPSIS DESCRIPTION A simple api for jira. Login as jira user and get some info. The available commands are: - login asks for login and password to jira and stores a session cookie + login asks for login and password to jira and stores a session cookie sprint S133 get the information about the given sprint dump url print given url in pure json - plan S133 shows planned BIS issues - - cust 10 shows an overview of the N highest-priority issues from customer projects (OBP, SOB) + plan 133 shows planned BIS issues + + cust 10 shows an overview of the N highest-priority issues from customer projects (OBP, SOB) + + search 'project=SP AND fixVersion = S139 ORDER BY \"Global Rank\"' + runs a jira query and shows the results + + rank 133 ranks all issues in S133 in accordance with the ranks of the BIS issues + rank SP-121 SP-122 + ranks SP-121 after SP-122 + + kanban 133 returns a Kanban list of the issues in S133 + + create 133 create issues for all BIS issues designated as next sprint + create 133 BIS-111 + create an issue in S133 for BIS-111 + + start 133 mark all BIS issues in S133 as "Active in sprint" and clear the "Next Sprint" flag + + finish 133 Show whether the parent issues to those in S133 are resolved or not + finish 133 exec mark all BIS issues in S133 that have no other open issues as resolved. + Set "Next Sprint" for those with open issues. + + move 133 Set the fix version to 133 for all issues that are connected to the BIS issues + move 133 SP-452 Set the fix version of SP-452 to 133 eos end end @@ -303,6 +382,24 @@ class Login < JiraCommand end end +# +# Check if the session is valid +# +class SessionValid < JiraCommand + + def initialize + super + end + + def description + return "session" + end + + def run + return JiraHelpers.session_valid_raw + end +end + # # Lists the issues in the sprint # @@ -339,7 +436,7 @@ class DumpIssue < LoggedInCommand @issueUrl = JSON.load(ans)["issues"][0]["self"] end - ans = `curl -s --get --cookie #{$jira_cookie_path} '#{@issueUrl}'` + ans = `curl -s --get --cookie #{$jira_cookie_path} '#{@issueUrl}' --data-urlencode 'expand=transitions'` data = JSON.load(ans) return JSON.pretty_generate(data) end @@ -654,7 +751,7 @@ class PlanSprint < LoggedInCommand next if @sp_issue_dict[sp] status = issue.status - @sp_issue_dict[sp] = issue unless (status == "Resolved" || status == "Closed") + @sp_issue_dict[sp] = issue unless issue.resolved_or_closed? end end @@ -770,7 +867,6 @@ class CreateSprint < LoggedInCommand def create_explicit_issue issue = JiraHelpers.issue(@parent_issue) - puts "issue ", issue create_issues(@parent_issue, [issue]) return "1 issue" end @@ -890,7 +986,7 @@ class CustIssues < LoggedInCommand def init_implementors_dict(implementors) @implementors_dict = {} implementors.each do | issue | - key = issue["key"] + key = issue.key @implementors_dict[key] = issue end end @@ -902,17 +998,10 @@ class CustIssues < LoggedInCommand header = "%12s\t%12s\t%s" % ["Key", "BIS", "Summary"] puts header full_issues.each do | issue | - key = issue["key"] - fields = issue["fields"] - summary = fields["summary"]["value"] + key = issue.key + summary = issue.summary - implementedby = [] - fields["links"]["value"].each do | link | - implementor = link["issueKey"] - # We are only interested in links to issues in the specified sprint - next unless @implementors_dict[implementor] - implementedby << implementor if "is implemented by" == link["type"]["description"] - end + implementedby = issue.implemented_by if implementedby.length < 1 row = "%12s\t%12s\t%s" % [key, "----", summary] @@ -924,7 +1013,7 @@ class CustIssues < LoggedInCommand # print one row for each implemented by implementorissue = @implementors_dict[implementor] next if implementorissue.nil? - implementorfields = implementorissue["fields"] + implementorfields = implementorissue.fields time = 0 time = implementorfields["timetracking"]["value"]["timeestimate"] if implementorfields["timetracking"]["value"] != nil if index < 1 @@ -941,7 +1030,304 @@ class CustIssues < LoggedInCommand end end +# +# Starts the sprint by marking the issues as active and setting the next sprint flag to false +# +class StartSprint < LoggedInCommand + def initialize + super + @sprintNumber = InputHelpers.sprint_name(ARGV[1]) + end + + def description + return "start #{@sprintNumber}" + end + + + def run_logged_in + query = "project=SP AND fixVersion = #{@sprintNumber} ORDER BY \"Global Rank\" ASC" + full_issues = JiraHelpers.search_full(query, @silent) + self.set_active(full_issues) + # Nothing to show + return "#{full_issues.length} issues" + end + + def set_active(full_issues) + header = "Processing...\n" + puts header + processed = Set.new + full_issues.each do | issue | + next if issue.implements.nil? + implements_key = issue.implements.key + + next if processed.include? implements_key + processed << implements_key + + print "\t#{implements_key}" + + issue = JiraHelpers.issue(implements_key) + + if issue.next_sprint + JiraHelpers.remove_next_sprint(implements_key) + end + print " Next Sprint -> nil" + + start = issue.transitions.detect { | trans | "Start Sprint" == trans["name"] } + unless start + print " \tActive in sprint\n" + # already started + next + end + + JiraHelpers.transition(implements_key, start["id"]) + print " \tActive in sprint\n" + end + end +end + +# +# Finishes the sprint by marking the issues as resolved if there are no open issues associated with it, and setting the next sprint flag to true if there +# are still open issues +# +class FinishSprint < LoggedInCommand + def initialize + super + @sprintNumber = InputHelpers.sprint_name(ARGV[1]) + @execute = ARGV[2] == "exec" + end + + def description + exec_status = "print" + exec_status = "exec" if @execute + return "finish #{@sprintNumber} #{exec_status}" + end + + + def run_logged_in + query = "project=SP AND fixVersion = #{@sprintNumber} ORDER BY \"Global Rank\" ASC" + full_issues = JiraHelpers.search_full(query, @silent) + if @execute + self.resolve_or_move_to_next_sprint(full_issues) + else + self.print_parent_status(full_issues) + end + # Nothing to show + return "#{full_issues.length} issues" + end + + def print_parent_status(full_issues) + header = "Status...\n" + puts header + processed = Set.new + full_issues.each do | issue | + next if issue.implements.nil? + implements_key = issue.implements.key + + next if processed.include? implements_key + processed << implements_key + + key_to_print = "%12s" % [implements_key] + print "#{key_to_print}" + + issue = JiraHelpers.issue(implements_key) + + can_resolve = issue.transitions.detect { | trans | "Resolve Issue" == trans["name"] } + if can_resolve + status_string = "%9s %8s" % ["Active", ""] + else + status_string = "%9s %8s" % ["Resolved", "#{issue.fix_version}"] + end + + print status_string, " " + + open_issue = issue.implemented_by.detect { | implementor | !implementor.resolved_or_closed? } + if open_issue + open_string = "%9s" % open_issue.key + else + open_string = "%9s" % "Finished" + end + + print open_string, " " + + print issue.summary + print "\n" + end + print "finish #{@sprintNumber} exec to change status\n" + end + + def resolve_or_move_to_next_sprint(full_issues) + header = "Processing...\n" + puts header + processed = Set.new + full_issues.each do | issue | + next if issue.implements.nil? + implements_key = issue.implements.key + + next if processed.include? implements_key + processed << implements_key + + print "\t#{implements_key}" + + issue = JiraHelpers.issue(implements_key) + + open_issue = issue.implemented_by.detect { | implementor | !implementor.resolved_or_closed? } + if open_issue + self.move_to_next_sprint(issue) + else + self.resolve(issue) + end + + print "\n" + end + end + + def resolve(issue) + resolve = issue.transitions.detect { | trans | "Resolve Issue" == trans["name"] } + if resolve + JiraHelpers.transition(issue.key, resolve["id"]) + print " Resolved" + else + print " Already Resolved" + end + + JiraHelpers.set_fix_version(issue.key, @sprintNumber) + print ", Fix version #{@sprintNumber}" + + end + + def move_to_next_sprint(issue) + if issue.next_sprint + print " Next Sprint" + else + JiraHelpers.set_next_sprint(issue.key) + print " Moved to Next Sprint" + end + end +end + +# +# Move an issue into a particular sprint +# +class MoveIssue < LoggedInCommand + def initialize + super + @sprintNumber = ARGV[1] + @sprintNumber = "S" if @sprintNumber.nil? + @sprintNumber = "S" + @sprintNumber unless @sprintNumber.match("^[S|s].*") + @issue = ARGV[2] + end + + def description + return "move #{@sprintNumber} #{@issue}" + end + + + def run_logged_in + return move_issue + end + + def move_issue + issue = JiraHelpers.issue(@issue) + move_issues([issue]) + return "1 issue" + end +end + +# +# Move an issue into a particular sprint +# +class MoveSprint < LoggedInCommand + def initialize + super + @sprintNumber = ARGV[1] + @sprintNumber = "S" if @sprintNumber.nil? + @sprintNumber = "S" + @sprintNumber unless @sprintNumber.match("^[S|s].*") + @issue = ARGV[2] + end + + def description + return "move #{@sprintNumber} #{@issue}" + end + + + def run_logged_in + return search_and_move_issues + end + + def search_and_move_issues + sp_query = "project=SP AND fixVersion = #{@sprintNumber} ORDER BY \"Global Rank\" ASC" + sp_issues = JiraHelpers.search_full(sp_query, @silent) + init_sp_issue_dict(sp_issues) + bis_query= "project = BIS AND status not in (Resolved, Closed) AND \"Next Sprint\" = YES ORDER BY \"Global Rank\" ASC" + bis_issues = JiraHelpers.search_full(bis_query, @silent) + + ccs_query= "project = CCS AND status not in (Resolved, Closed) AND \"Next Sprint\" = YES ORDER BY \"Global Rank\" ASC" + ccs_issues = JiraHelpers.search_full(ccs_query, @silent) + + swe_query= "project = SWE AND status not in (Resolved, Closed) AND \"Next Sprint\" = YES ORDER BY \"Global Rank\" ASC" + swe_issues = JiraHelpers.search_full(swe_query, @silent) + + move_necessary_issues("BIS", bis_issues) + # print_issues_table("CCS", ccs_issues) + # print_issues_table("SWE", swe_issues) + + # Nothing to show + issue_count = bis_issues.length + ccs_issues.length + swe_issues.length + return "#{issue_count} issues" + end + + def init_sp_issue_dict(sp_issues) + @sp_issue_dict = {} + sp_issues.each do | issue | + key = issue.key + @sp_issue_dict[key] = issue + end + @seen_sp_issues = [].to_set + end + + # + # Take those issues that are not yet resolved / closed and add them to the sp issues dict + # + def enrich_sp_issue_dict(implementors) + implementors.each do | issue | + sp = issue.key + next if @sp_issue_dict[sp] + + status = issue.status + @sp_issue_dict[sp] = issue unless (status == "Resolved" || status == "Closed") + end + end + + def move_necessary_issues(title, full_issues) + issues_to_move = [] + full_issues.each do | issue | + is_umbrella = !issue.fields["sub-tasks"].nil? + next if is_umbrella + implementedby = [] + issue.implemented_by.each do | sp_issue | + sp = sp_issue.key + # Skip if the issue is already in this sprint + next if @sp_issue_dict[sp] + issues_to_move << sp_issue unless sp_issue.resolved_or_closed? + end + + end + move_issues(title, issues_to_move) + end + + def move_issues(title, issues_to_move) + puts ("=" * 12) + puts title + puts ("-" * 12) + + issues_to_move.each do | issue | + JiraHelpers.set_fix_version(issue.key, @sprintNumber) + row = "%12s\t%s" % [issue.key, @sprintNumber] + puts row + end + end +end def get_command return Help.new if ARGV.length < 1 @@ -956,10 +1342,14 @@ def get_command when "rank" then ARGV.length > 2 ? RankIssue.new : RankSprint.new when "kanban" then ListKanban.new when "create" then CreateSprint.new + when "start" then StartSprint.new + when "finish" then FinishSprint.new + when "move" then ARGV.length > 2 ? MoveIssue.new : MoveSprint.new + when "session" then SessionValid.new else Help.new end - return ans + return ans end