2 # frozen_string_literal: true
4 require "abstract_command"
8 class CheckCiStatusCmd < AbstractCommand
11 Check the status of CI tests. Used to determine whether tests can be
12 cancelled, or whether a long-timeout label can be removed.
15 switch "--cancel", description: "Determine whether tests can be cancelled."
16 switch "--long-timeout-label", description: "Determine whether a long-timeout label can be removed."
18 named_args :pull_request_number, number: 1
23 GRAPHQL_WORKFLOW_RUN_QUERY = <<~GRAPHQL
24 query ($owner: String!, $name: String!, $pr: Int!) {
25 repository(owner: $owner, name: $name) {
26 pullRequest(number: $pr) {
30 checkSuites(last: 100) {
41 checkRuns(last: 100) {
58 ALLOWABLE_REMAINING_MACOS_RUNNERS = 1
60 def get_workflow_run_status(pull_request)
62 return @status_cache[pull_request] if @status_cache.include? pull_request
64 owner, name = ENV.fetch("GITHUB_REPOSITORY").split("/")
70 odebug "Checking CI status for #{owner}/#{name}##{pull_request}..."
72 response = GitHub::API.open_graphql(GRAPHQL_WORKFLOW_RUN_QUERY, variables:, scopes: ["repo"].freeze)
73 commit_node = response.dig("repository", "pullRequest", "commits", "nodes", 0)
74 check_suite_nodes = commit_node.dig("commit", "checkSuites", "nodes")
75 ci_nodes = check_suite_nodes.select do |node|
76 workflow_run = node.fetch("workflowRun")
77 next false if workflow_run.blank?
79 workflow_run.fetch("event") == "pull_request" && workflow_run.dig("workflow", "name") == "CI"
81 # There can be multiple CI nodes when a PR is closed and reopened.
82 # Make sure we use the latest one in this case.
83 ci_node = ci_nodes.max_by { |node| DateTime.parse(node.dig("workflowRun", "createdAt")) }
84 return [nil, nil] if ci_node.blank?
86 check_run_nodes = ci_node.dig("checkRuns", "nodes")
88 @status_cache[pull_request] = [ci_node, check_run_nodes]
89 [ci_node, check_run_nodes]
92 def run_id_if_cancellable(pull_request)
93 ci_node, = get_workflow_run_status(pull_request)
94 return if ci_node.nil?
96 # Possible values: COMPLETED, IN_PROGRESS, PENDING, QUEUED, REQUESTED, WAITING
97 # https://docs.github.com/en/graphql/reference/enums#checkstatusstateb
98 ci_status = ci_node.fetch("status")
99 odebug "CI status: #{ci_status}"
100 return if ci_status == "COMPLETED"
102 ci_run_id = ci_node.dig("workflowRun", "databaseId")
103 odebug "CI run ID: #{ci_run_id}"
107 def allow_long_timeout_label_removal?(pull_request)
108 ci_node, check_run_nodes = get_workflow_run_status(pull_request)
109 return false if ci_node.nil?
111 ci_status = ci_node.fetch("status")
112 odebug "CI status: #{ci_status}"
113 return true if ci_status == "COMPLETED"
115 # The `test_deps` job is still waiting to be processed.
116 return false if check_run_nodes.none? { |node| node.fetch("name").end_with?("(deps)") }
118 incomplete_macos_checks = check_run_nodes.select do |node|
119 check_run_status = node.fetch("status")
120 check_run_name = node.fetch("name")
121 odebug "#{check_run_name}: #{check_run_status}"
123 check_run_status != "COMPLETED" && check_run_name.start_with?("macOS")
126 incomplete_macos_checks.count <= ALLOWABLE_REMAINING_MACOS_RUNNERS
130 pr = args.named.first.to_i
132 if !args.cancel? && !args.long_timeout_label?
133 raise UsageError, "At least one of `--cancel` and `--long-timeout-label` is needed."
139 run_id = run_id_if_cancellable(pr)
140 outputs["cancellable-run-id"] = run_id.to_json
143 if args.long_timeout_label?
144 allow_removal = allow_long_timeout_label_removal?(pr)
145 outputs["allow-long-timeout-label-removal"] = allow_removal
148 github_output = ENV.fetch("GITHUB_OUTPUT")
149 File.open(github_output, "a") do |f|
150 outputs.each do |key, value|
151 odebug "#{key}: #{value}"
152 f.puts "#{key}=#{value}"