brew/Library/Homebrew/cmd/gist-logs.rb
Issy Long 41b143deec
cmd/gist-logs: On 404, the PAT probably needs more permissions
- This 404 error is from GitHub, and GitHub doesn't give us any more
  informations about scopes for the gist endpoint (for some reason). But we can
  safely assume, as it happens a lot, that "Error: Not Found" (404) is because
  the user hasn't granted their `HOMEBREW_GITHUB_API_TOKEN` the `gist` scope.

Before:

```shell
$ HOMEBREW_GITHUB_API_TOKEN=<token_without_gist_scope> brew gist-logs -p logrotate
Error: Not Found
```

After:

```shell
❯ HOMEBREW_GITHUB_API_TOKEN=<token_without_gist_scope> brew gist-logs logrotate
Error: Your GitHub API token likely doesn't have the `gist` scope.
Create a GitHub personal access token:
https://github.com/settings/tokens/new?scopes=gist&description=Homebrew
echo 'export HOMEBREW_GITHUB_API_TOKEN=your_token_here' >> ~/.zshrc
```
2023-02-06 17:00:02 +01:00

131 lines
3.9 KiB
Ruby

# typed: true
# frozen_string_literal: true
require "formula"
require "install"
require "system_config"
require "stringio"
require "socket"
require "cli/parser"
module Homebrew
extend T::Sig
extend Install
module_function
sig { returns(CLI::Parser) }
def gist_logs_args
Homebrew::CLI::Parser.new do
description <<~EOS
Upload logs for a failed build of <formula> to a new Gist. Presents an
error message if no logs are found.
EOS
switch "--with-hostname",
description: "Include the hostname in the Gist."
switch "-n", "--new-issue",
description: "Automatically create a new issue in the appropriate GitHub repository " \
"after creating the Gist."
switch "-p", "--private",
description: "The Gist will be marked private and will not appear in listings but will " \
"be accessible with its link."
named_args :formula, number: 1
end
end
def gistify_logs(f, args:)
files = load_logs(f.logs)
build_time = f.logs.ctime
timestamp = build_time.strftime("%Y-%m-%d_%H-%M-%S")
s = StringIO.new
SystemConfig.dump_verbose_config s
# Dummy summary file, asciibetically first, to control display title of gist
files["# #{f.name} - #{timestamp}.txt"] = { content: brief_build_info(f, with_hostname: args.with_hostname?) }
files["00.config.out"] = { content: s.string }
files["00.doctor.out"] = { content: Utils.popen_read("#{HOMEBREW_PREFIX}/bin/brew", "doctor", err: :out) }
unless f.core_formula?
tap = <<~EOS
Formula: #{f.name}
Tap: #{f.tap}
Path: #{f.path}
EOS
files["00.tap.out"] = { content: tap }
end
odie "`brew gist-logs` requires HOMEBREW_GITHUB_API_TOKEN to be set!" if GitHub::API.credentials_type == :none
# Description formatted to work well as page title when viewing gist
descr = if f.core_formula?
"#{f.name} on #{OS_VERSION} - Homebrew build logs"
else
"#{f.name} (#{f.full_name}) on #{OS_VERSION} - Homebrew build logs"
end
begin
url = GitHub.create_gist(files, descr, private: args.private?)
rescue GitHub::API::HTTPNotFoundError
odie <<~EOS
Your GitHub API token likely doesn't have the `gist` scope.
#{GitHub.pat_blurb(GitHub::CREATE_GIST_SCOPES)}
EOS
end
url = GitHub.create_issue(f.tap, "#{f.name} failed to build on #{MacOS.full_version}", url) if args.new_issue?
puts url if url
end
def brief_build_info(f, with_hostname:)
build_time_str = f.logs.ctime.strftime("%Y-%m-%d %H:%M:%S")
s = +<<~EOS
Homebrew build logs for #{f.full_name} on #{OS_VERSION}
EOS
if with_hostname
hostname = Socket.gethostname
s << "Host: #{hostname}\n"
end
s << "Build date: #{build_time_str}\n"
s.freeze
end
# Causes some terminals to display secure password entry indicators.
def noecho_gets
system "stty", "-echo"
result = $stdin.gets
system "stty", "echo"
puts
result
end
def load_logs(dir, basedir = dir)
logs = {}
if dir.exist?
dir.children.sort.each do |file|
if file.directory?
logs.merge! load_logs(file, basedir)
else
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.relative_path_from(basedir).to_s.tr("/", ":")] = { content: contents }
end
end
end
odie "No logs." if logs.empty?
logs
end
def gist_logs
args = gist_logs_args.parse
Install.perform_preinstall_checks(all_fatal: true)
Install.perform_build_from_source_checks(all_fatal: true)
gistify_logs(args.named.to_resolved_formulae.first, args: args)
end
end