Merge pull request #206263 from Homebrew/bump-cdxgen-11.1.7
[homebrew-core.git] / cmd / check-ci-status.rb
blob965311477254daa0f140c4efe3718cbf6f6f31da
1 # typed: true
2 # frozen_string_literal: true
4 require "abstract_command"
6 module Homebrew
7   module Cmd
8     class CheckCiStatusCmd < AbstractCommand
9       cmd_args do
10         description <<~EOS
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.
13         EOS
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
20         hide_from_man_page!
21       end
23       GRAPHQL_WORKFLOW_RUN_QUERY = <<~GRAPHQL
24         query ($owner: String!, $name: String!, $pr: Int!) {
25           repository(owner: $owner, name: $name) {
26             pullRequest(number: $pr) {
27               commits(last: 1) {
28                 nodes {
29                   commit {
30                     checkSuites(last: 100) {
31                       nodes {
32                         status
33                         workflowRun {
34                           event
35                           databaseId
36                           createdAt
37                           workflow {
38                             name
39                           }
40                         }
41                         checkRuns(last: 100) {
42                           nodes {
43                             name
44                             status
45                             conclusion
46                             databaseId
47                           }
48                         }
49                       }
50                     }
51                   }
52                 }
53               }
54             }
55           }
56         }
57       GRAPHQL
58       ALLOWABLE_REMAINING_MACOS_RUNNERS = 1
60       def get_workflow_run_status(pull_request)
61         @status_cache ||= {}
62         return @status_cache[pull_request] if @status_cache.include? pull_request
64         owner, name = ENV.fetch("GITHUB_REPOSITORY").split("/")
65         variables = {
66           owner:,
67           name:,
68           pr:    pull_request,
69         }
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"
80         end
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]
90       end
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}"
104         ci_run_id
105       end
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")
124         end
126         incomplete_macos_checks.count <= ALLOWABLE_REMAINING_MACOS_RUNNERS
127       end
129       def run
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."
134         end
136         outputs = {}
138         if args.cancel?
139           run_id = run_id_if_cancellable(pr)
140           outputs["cancellable-run-id"] = run_id.to_json
141         end
143         if args.long_timeout_label?
144           allow_removal = allow_long_timeout_label_removal?(pr)
145           outputs["allow-long-timeout-label-removal"] = allow_removal
146         end
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}"
153           end
154         end
155       end
156     end
157   end