Merge pull request #15485 from neersighted/github_cli_token
Discover GitHub tokens stored by the GitHub CLI
This commit is contained in:
commit
f088cf88e7
@ -10,9 +10,9 @@ module GitHub
|
||||
def self.pat_blurb(scopes = ALL_SCOPES)
|
||||
<<~EOS
|
||||
Create a GitHub personal access token:
|
||||
#{Formatter.url(
|
||||
"https://github.com/settings/tokens/new?scopes=#{scopes.join(",")}&description=Homebrew",
|
||||
)}
|
||||
#{Formatter.url(
|
||||
"https://github.com/settings/tokens/new?scopes=#{scopes.join(",")}&description=Homebrew",
|
||||
)}
|
||||
#{Utils::Shell.set_variable_in_profile("HOMEBREW_GITHUB_API_TOKEN", "your_token_here")}
|
||||
EOS
|
||||
end
|
||||
@ -62,30 +62,36 @@ module GitHub
|
||||
|
||||
# Error when authentication fails.
|
||||
class AuthenticationFailedError < Error
|
||||
def initialize(github_message)
|
||||
def initialize(credentials_type, github_message)
|
||||
@github_message = github_message
|
||||
message = +"GitHub API Error: #{github_message}\n"
|
||||
message << if Homebrew::EnvConfig.github_api_token
|
||||
message << case credentials_type
|
||||
when :github_cli_token
|
||||
<<~EOS
|
||||
HOMEBREW_GITHUB_API_TOKEN may be invalid or expired; check:
|
||||
#{Formatter.url("https://github.com/settings/tokens")}
|
||||
Your GitHub CLI login session may be invalid.
|
||||
Refresh it with:
|
||||
gh auth login --hostname github.com
|
||||
EOS
|
||||
else
|
||||
when :keychain_username_password
|
||||
<<~EOS
|
||||
The GitHub credentials in the macOS keychain may be invalid.
|
||||
Clear them with:
|
||||
printf "protocol=https\\nhost=github.com\\n" | git credential-osxkeychain erase
|
||||
#{GitHub.pat_blurb}
|
||||
EOS
|
||||
when :env_token
|
||||
<<~EOS
|
||||
HOMEBREW_GITHUB_API_TOKEN may be invalid or expired; check:
|
||||
#{Formatter.url("https://github.com/settings/tokens")}
|
||||
EOS
|
||||
end
|
||||
super message.freeze
|
||||
end
|
||||
end
|
||||
|
||||
# Error when the user has no GitHub API credentials set at all (macOS keychain or envvar).
|
||||
# Error when the user has no GitHub API credentials set at all (macOS keychain, GitHub CLI or envvar).
|
||||
class MissingAuthenticationError < Error
|
||||
def initialize
|
||||
message = +"No GitHub credentials found in macOS Keychain or environment.\n"
|
||||
message = +"No GitHub credentials found in macOS Keychain, GitHub CLI or the environment.\n"
|
||||
message << GitHub.pat_blurb
|
||||
super message
|
||||
end
|
||||
@ -112,15 +118,31 @@ module GitHub
|
||||
JSON::ParserError,
|
||||
].freeze
|
||||
|
||||
# Gets the token from the GitHub CLI for github.com.
|
||||
sig { returns(T.nilable(String)) }
|
||||
def self.github_cli_token
|
||||
env = { "PATH" => PATH.new(Formula["gh"].opt_bin, ENV.fetch("PATH")) }
|
||||
gh_out, _, result = system_command "gh",
|
||||
args: ["auth", "token", "--hostname", "github.com"],
|
||||
env: env,
|
||||
print_stderr: false
|
||||
return unless result.success?
|
||||
|
||||
gh_out.chomp
|
||||
end
|
||||
|
||||
# Gets the password field from `git-credential-osxkeychain` for github.com,
|
||||
# but only if that password looks like a GitHub Personal Access Token.
|
||||
sig { returns(T.nilable(String)) }
|
||||
def self.keychain_username_password
|
||||
github_credentials = Utils.popen_write("git", "credential-osxkeychain", "get") do |pipe|
|
||||
pipe.write "protocol=https\nhost=github.com\n"
|
||||
end
|
||||
github_username = github_credentials[/username=(.+)/, 1]
|
||||
github_password = github_credentials[/password=(.+)/, 1]
|
||||
git_credential_out, _, result = system_command "git",
|
||||
args: ["credential-osxkeychain", "get"],
|
||||
input: ["protocol=https\n", "host=github.com\n"],
|
||||
print_stderr: false
|
||||
return unless result.success?
|
||||
|
||||
github_username = git_credential_out[/username=(.+)/, 1]
|
||||
github_password = git_credential_out[/password=(.+)/, 1]
|
||||
return unless github_username
|
||||
|
||||
# Don't use passwords from the keychain unless they look like
|
||||
@ -129,29 +151,33 @@ module GitHub
|
||||
return unless GITHUB_PERSONAL_ACCESS_TOKEN_REGEX.match?(github_password)
|
||||
|
||||
github_password
|
||||
rescue Errno::EPIPE
|
||||
# The above invocation via `Utils.popen` can fail, causing the pipe to be
|
||||
# prematurely closed (before we can write to it) and thus resulting in a
|
||||
# broken pipe error. The root cause is usually a missing or malfunctioning
|
||||
# `git-credential-osxkeychain` helper.
|
||||
nil
|
||||
end
|
||||
|
||||
# odeprecated: Not really deprecated; change the order to prefer `github_cli_token` over
|
||||
# `keychain_username_password` during the next major/minor release.
|
||||
def self.credentials
|
||||
@credentials ||= Homebrew::EnvConfig.github_api_token || keychain_username_password
|
||||
@credentials ||= Homebrew::EnvConfig.github_api_token || keychain_username_password || github_cli_token
|
||||
end
|
||||
|
||||
sig { returns(Symbol) }
|
||||
def self.credentials_type
|
||||
if Homebrew::EnvConfig.github_api_token
|
||||
if Homebrew::EnvConfig.github_api_token.present?
|
||||
:env_token
|
||||
elsif keychain_username_password
|
||||
elsif keychain_username_password.present?
|
||||
:keychain_username_password
|
||||
elsif github_cli_token.present?
|
||||
:github_cli_token
|
||||
else
|
||||
:none
|
||||
end
|
||||
end
|
||||
|
||||
CREDENTIAL_NAMES = {
|
||||
env_token: "HOMEBREW_GITHUB_API_TOKEN",
|
||||
github_cli_token: "GitHub CLI login",
|
||||
keychain_username_password: "macOS Keychain GitHub",
|
||||
}.freeze
|
||||
|
||||
# Given an API response from GitHub, warn the user if their credentials
|
||||
# have insufficient permissions.
|
||||
def self.credentials_error_message(response_headers, needed_scopes)
|
||||
@ -165,13 +191,7 @@ module GitHub
|
||||
needed_scopes = needed_scopes.to_a.join(", ").presence || "none"
|
||||
credentials_scopes = "none" if credentials_scopes.blank?
|
||||
|
||||
what = case credentials_type
|
||||
when :keychain_username_password
|
||||
"macOS keychain GitHub"
|
||||
when :env_token
|
||||
"HOMEBREW_GITHUB_API_TOKEN"
|
||||
end
|
||||
|
||||
what = CREDENTIAL_NAMES.fetch(credentials_type)
|
||||
@credentials_error_message ||= onoe <<~EOS
|
||||
Your #{what} credentials do not have sufficient scope!
|
||||
Scopes required: #{needed_scopes}
|
||||
@ -296,14 +316,14 @@ module GitHub
|
||||
|
||||
case http_code
|
||||
when "401"
|
||||
raise AuthenticationFailedError, message
|
||||
raise AuthenticationFailedError.new(credentials_type, message)
|
||||
when "403"
|
||||
if meta.fetch("x-ratelimit-remaining", 1).to_i <= 0
|
||||
reset = meta.fetch("x-ratelimit-reset").to_i
|
||||
raise RateLimitExceededError.new(reset, message)
|
||||
end
|
||||
|
||||
raise AuthenticationFailedError, message
|
||||
raise AuthenticationFailedError.new(credentials_type, message)
|
||||
when "404"
|
||||
raise MissingAuthenticationError if credentials_type == :none && scopes.present?
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user