gist-logs.rb 4.27 KB
Newer Older
Markus Reiter's avatar
Markus Reiter committed
1
# typed: true
2
3
# frozen_string_literal: true

BrewTestBot's avatar
BrewTestBot committed
4
require "formula"
5
require "install"
6
require "system_config"
BrewTestBot's avatar
BrewTestBot committed
7
require "stringio"
8
require "socket"
9
require "cli/parser"
10
11

module Homebrew
Markus Reiter's avatar
Markus Reiter committed
12
13
  extend T::Sig

14
15
  extend Install

16
17
  module_function

Markus Reiter's avatar
Markus Reiter committed
18
  sig { returns(CLI::Parser) }
19
20
21
  def gist_logs_args
    Homebrew::CLI::Parser.new do
      usage_banner <<~EOS
22
        `gist-logs` [<options>] <formula>
23

24
25
        Upload logs for a failed build of <formula> to a new Gist. Presents an
        error message if no logs are found.
26
27
      EOS
      switch "--with-hostname",
Mike McQuaid's avatar
Mike McQuaid committed
28
             description: "Include the hostname in the Gist."
29
      switch "-n", "--new-issue",
30
31
             description: "Automatically create a new issue in the appropriate GitHub repository "\
                          "after creating the Gist."
32
      switch "-p", "--private",
Mike McQuaid's avatar
Mike McQuaid committed
33
             description: "The Gist will be marked private and will not appear in listings but will "\
34
                          "be accessible with its link."
Markus Reiter's avatar
Markus Reiter committed
35

Mike McQuaid's avatar
Mike McQuaid committed
36
      named :formula
37
38
39
    end
  end

40
  def gistify_logs(f, args:)
Jack Nagel's avatar
Jack Nagel committed
41
    files = load_logs(f.logs)
42
43
    build_time = f.logs.ctime
    timestamp = build_time.strftime("%Y-%m-%d_%H-%M-%S")
44
45

    s = StringIO.new
46
    SystemConfig.dump_verbose_config s
47
    # Dummy summary file, asciibetically first, to control display title of gist
48
    files["# #{f.name} - #{timestamp}.txt"] = { content: brief_build_info(f, with_hostname: args.with_hostname?) }
49
    files["00.config.out"] = { content: s.string }
50
    files["00.doctor.out"] = { content: Utils.popen_read("#{HOMEBREW_PREFIX}/bin/brew", "doctor", err: :out) }
51
    unless f.core_formula?
Markus Reiter's avatar
Markus Reiter committed
52
      tap = <<~EOS
53
        Formula: #{f.name}
EricFromCanada's avatar
EricFromCanada committed
54
55
            Tap: #{f.tap}
           Path: #{f.path}
56
      EOS
57
      files["00.tap.out"] = { content: tap }
58
    end
59

Mike McQuaid's avatar
Mike McQuaid committed
60
    odisabled "`brew gist-logs` with a password", "HOMEBREW_GITHUB_API_TOKEN" if GitHub.api_credentials_type == :none
61

62
    # Description formatted to work well as page title when viewing gist
Mike McQuaid's avatar
Mike McQuaid committed
63
64
    descr = if f.core_formula?
      "#{f.name} on #{OS_VERSION} - Homebrew build logs"
65
    else
Mike McQuaid's avatar
Mike McQuaid committed
66
      "#{f.name} (#{f.full_name}) on #{OS_VERSION} - Homebrew build logs"
67
    end
68
    url = create_gist(files, descr, private: args.private?)
69

70
    url = create_issue(f.tap, "#{f.name} failed to build on #{MacOS.full_version}", url) if args.new_issue?
71

Stefan's avatar
Stefan committed
72
73
74
    puts url if url
  end

75
  def brief_build_info(f, with_hostname:)
76
    build_time_str = f.logs.ctime.strftime("%Y-%m-%d %H:%M:%S")
Markus Reiter's avatar
Markus Reiter committed
77
    s = +<<~EOS
78
79
      Homebrew build logs for #{f.full_name} on #{OS_VERSION}
    EOS
80
    if with_hostname
81
82
83
84
      hostname = Socket.gethostname
      s << "Host: #{hostname}\n"
    end
    s << "Build date: #{build_time_str}\n"
Mike McQuaid's avatar
Mike McQuaid committed
85
    s.freeze
86
87
  end

88
  # Causes some terminals to display secure password entry indicators.
Stefan's avatar
Stefan committed
89
  def noecho_gets
BrewTestBot's avatar
BrewTestBot committed
90
    system "stty -echo"
Stefan's avatar
Stefan committed
91
    result = $stdin.gets
BrewTestBot's avatar
BrewTestBot committed
92
    system "stty echo"
Stefan's avatar
Stefan committed
93
94
95
96
    puts
    result
  end

97
  def login!
BrewTestBot's avatar
BrewTestBot committed
98
    print "GitHub User: "
99
    ENV["HOMEBREW_GITHUB_API_USERNAME"] = $stdin.gets.chomp
BrewTestBot's avatar
BrewTestBot committed
100
    print "Password: "
101
    ENV["HOMEBREW_GITHUB_API_PASSWORD"] = noecho_gets.chomp
Stefan's avatar
Stefan committed
102
    puts
103
104
  end

Jack Nagel's avatar
Jack Nagel committed
105
  def load_logs(dir)
106
    logs = {}
107
108
109
110
111
112
113
114
115
    if dir.exist?
      dir.children.sort.each do |file|
        contents = file.size? ? file.read : "empty log"
        # small enough to avoid GitHub "unicorn" page-load-timeout errors
        max_file_size = 1_000_000
        contents = truncate_text_to_approximate_size(contents, max_file_size, front_weight: 0.2)
        logs[file.basename.to_s] = { content: contents }
      end
    end
BrewTestBot's avatar
BrewTestBot committed
116
    raise "No logs." if logs.empty?
Markus Reiter's avatar
Markus Reiter committed
117

118
119
120
    logs
  end

121
  def create_gist(files, description, private:)
122
    url = "https://api.github.com/gists"
123
    data = { "public" => !private, "files" => files, "description" => description }
124
    scopes = GitHub::CREATE_GIST_SCOPES
Mike McQuaid's avatar
Mike McQuaid committed
125
    GitHub.open_api(url, data: data, scopes: scopes)["html_url"]
126
127
  end

128
129
  def create_issue(repo, title, body)
    url = "https://api.github.com/repos/#{repo}/issues"
130
    data = { "title" => title, "body" => body }
131
    scopes = GitHub::CREATE_ISSUE_FORK_OR_PR_SCOPES
Mike McQuaid's avatar
Mike McQuaid committed
132
    GitHub.open_api(url, data: data, scopes: scopes)["html_url"]
133
134
135
  end

  def gist_logs
Markus Reiter's avatar
Markus Reiter committed
136
    args = gist_logs_args.parse
137

Markus Reiter's avatar
Markus Reiter committed
138
139
    Install.perform_preinstall_checks(all_fatal: true)
    Install.perform_build_from_source_checks(all_fatal: true)
140
    gistify_logs(args.named.to_resolved_formulae.first, args: args)
141
142
  end
end