Skip to content
This repository has been archived by the owner on May 16, 2021. It is now read-only.

GitHub Enterprise support #19

Merged
merged 4 commits into from
Nov 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Metrics/ClassLength:
Exclude:
- "test/**/*"

Performance/Casecmp:
Enabled: false

Style/BarePercentLiterals:
EnforcedStyle: percent_q

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ chandler is in a pre-1.0 state. This means that its APIs and behavior are subjec
## [Unreleased][]

* Your contribution here!
* [#19](https://github.com/mattbrictson/chandler/pull/19): Add GitHub Enterprise support - [@mattbrictson](https://github.com/mattbrictson)

## [0.5.0][] (2016-10-07)

Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ gem install chandler

### 2. Configure .netrc or set ENV vars

In order to access the GitHub API on your behalf, you must provide chandler with your GitHub credentials.
In order to access the GitHub API on your behalf, you must provide chandler with your GitHub credentials.

Do this by creating a `~/.netrc` file with your GitHub username and password, like this:

Expand Down Expand Up @@ -80,6 +80,21 @@ Other command-line options:
* `--changelog=History.md` – location of the CHANGELOG (defaults to `CHANGELOG.md`)
* `--tag-prefix=myapp-` – specify Git version tags are in the format `myapp-1.0.0` instead of `1.0.0`

## GitHub Enterprise

Chandler supports GitHub Enterprise as well as public GitHub repositories. It will make an educated guess as to where your GitHub Enterprise installation is located based on the `origin` git remote. You can also specify your GitHub Enterprise repository using the `--github` option like this:

```
[email protected]:organization/project.git
```

Or like this:

```
--github=https://github.mycompany.com/organization/project
```

To authenticate, Chandler relies on your `~/.netrc`, as explained above. Replace `api.github.com` with the hostname of your GitHub Enterprise installation (`github.mycompany.com` in the example).

## Rakefile integration

Expand Down
17 changes: 0 additions & 17 deletions lib/chandler/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,6 @@ def git
@git ||= Chandler::Git.new(:path => git_path, :tag_mapper => tag_mapper)
end

def octokit
@octokit ||= Octokit::Client.new(octokit_options)
end

def octokit_options
chandler_token_key = "CHANDLER_GITHUB_API_TOKEN"
if environment[chandler_token_key]
{ :access_token => environment[chandler_token_key] }
else
{ :netrc => true }
end
end

def environment
@environment ||= ENV
end

def github
@github ||= Chandler::GitHub.new(
:repository => github_repository,
Expand Down
33 changes: 13 additions & 20 deletions lib/chandler/github.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require "octokit"
require "chandler/github/client"
require "chandler/github/errors"
require "chandler/github/remote"

module Chandler
# A facade for performing GitHub API operations on a given GitHub repository
Expand All @@ -7,12 +9,11 @@ module Chandler
# is available in the host environment at "CHANDLER_GITHUB_API_TOKEN""
#
class GitHub
MissingCredentials = Class.new(StandardError)

attr_reader :repository, :config

def initialize(repository:, config:)
@repository = parse_repository(repository)
@repository = repository
@remote = Remote.parse(repository)
@config = config
end

Expand All @@ -23,16 +24,16 @@ def create_or_update_release(tag:, title:, description:)
return update_release(release, title, description) if release

create_release(tag, title, description)
rescue Octokit::NotFound
raise InvalidRepository, repository
end

private

def parse_repository(repo)
repo[%r{([email protected]:|://github.com/)(.*)\.git}, 2] || repo
end
attr_reader :remote

def existing_release(tag)
release = client.release_for_tag(repository, tag)
release = client.release_for_tag(remote.repository, tag)
release.id.nil? ? nil : release
rescue Octokit::NotFound
nil
Expand All @@ -48,21 +49,13 @@ def release_unchanged?(release, title, desc)
end

def create_release(tag, title, desc)
client.create_release(repository, tag, :name => title, :body => desc)
client.create_release(
remote.repository, tag, :name => title, :body => desc
)
end

def client
@client ||= begin
octokit = config.octokit
octokit.login ? octokit : fail_missing_credentials
end
end

def fail_missing_credentials
message = "Couldn’t load GitHub credentials from ~/.netrc.\n"
message << "For .netrc instructions, see: "
message << "https://github.com/octokit/octokit.rb#using-a-netrc-file"
raise MissingCredentials, message
@client ||= Client.new(:host => remote.host).tap(&:login!)
end
end
end
42 changes: 42 additions & 0 deletions lib/chandler/github/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
require "chandler/github/errors"
require "delegate"
require "octokit"
require "uri"

module Chandler
class GitHub
# A thin wrapper around Octokit::Client that adds support for automatic
# GitHub Enterprise, .netrc, and ENV token-based authentication.
#
class Client < SimpleDelegator
def initialize(host: "github.com",
environment: ENV,
octokit_client: Octokit::Client)
options = {}
options.merge!(detect_auth_option(environment))
options.merge!(detect_enterprise_endpoint(host))
super(octokit_client.new(options))
end

def login!
return if login
raise netrc ? NetrcAuthenticationFailure : TokenAuthenticationFailure
end

private

def detect_auth_option(env)
if (token = env["CHANDLER_GITHUB_API_TOKEN"])
{ :access_token => token }
else
{ :netrc => true }
end
end

def detect_enterprise_endpoint(host)
return {} if host.downcase == "github.com"
{ :api_endpoint => "https://#{host}/api/v3/" }
end
end
end
end
34 changes: 34 additions & 0 deletions lib/chandler/github/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module Chandler
class GitHub
Error = Class.new(StandardError)

class InvalidRepository < Error
def initialize(repository)
@repository = repository
end

def message
"Failed to find GitHub repository: #{@repository}.\n"\
"Verify you have permission to access it. Use the --github option to "\
"specify a different repository."
end
end

class NetrcAuthenticationFailure < Error
def message
"GitHub authentication failed.\n"\
"Check that ~/.netrc is properly configured.\n"\
"For instructions, see: "\
"https://github.com/octokit/octokit.rb#using-a-netrc-file"
end
end

class TokenAuthenticationFailure < Error
def message
"GitHub authentication failed.\n"\
"Check that the CHANDLER_GITHUB_API_TOKEN environment variable "\
"is correct."
end
end
end
end
37 changes: 37 additions & 0 deletions lib/chandler/github/remote.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
require "uri"

module Chandler
class GitHub
# Assuming a git remote points to a public GitHub or a GitHub Enterprise
# repository, this class parses the remote to obtain the host and repository
# path. Supports SSH and HTTPS style git remotes.
#
# This class also handles parsing values passed into the `--github` command
# line option, which may be a public GitHub repository name, like
# "mattbrictson/chandler".
#
class Remote
def self.parse(url)
if (match = url.match(/@([^:]+):(.+)$/))
new(match[1], match[2])
else
parsed_uri = URI(url)
host = parsed_uri.host || "github.com"
path = parsed_uri.path.sub(%r{^/+}, "")
new(host, path)
end
end

attr_reader :host, :path

def initialize(host, path)
@host = host.downcase
@path = path
end

def repository
path.sub(/\.git$/, "")
end
end
end
end
1 change: 1 addition & 0 deletions lib/chandler/tasks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require "chandler/configuration"
require "chandler/refinements/version_format"
require "chandler/commands/push"
require "rake"

using Chandler::Refinements::VersionFormat

Expand Down
10 changes: 0 additions & 10 deletions test/chandler/configuration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,4 @@ def test_changelog
assert_instance_of(Chandler::Changelog, changelog)
assert_equal("../test/history.md", changelog.path)
end

def test_octokit_options_as_netrc
@config.environment = { "ANYTHING" => "1234" }
assert_equal({ :netrc => true }, @config.octokit_options)
end

def test_octokit_options_as_access_token
@config.environment = { "CHANDLER_GITHUB_API_TOKEN" => "1234" }
assert_equal({ :access_token => "1234" }, @config.octokit_options)
end
end
72 changes: 72 additions & 0 deletions test/chandler/github/client_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
require "minitest_helper"
require "chandler/github/client"

class Chandler::GitHub::ClientTest < Minitest::Test
class FakeOctokitClient
attr_reader :netrc, :access_token, :api_endpoint

def initialize(options)
@netrc = options.fetch(:netrc, false)
@access_token = options[:access_token]
@api_endpoint = options[:api_endpoint] if options[:api_endpoint]
end

def login
"successful"
end
end

class FakeOctokitClientWithError < FakeOctokitClient
def login
nil
end
end

def test_uses_netrc_by_default
client = Chandler::GitHub::Client.new(:octokit_client => FakeOctokitClient)
assert(client.netrc)
assert_nil(client.access_token)
end

def test_uses_access_token_from_env
client = Chandler::GitHub::Client.new(
:environment => { "CHANDLER_GITHUB_API_TOKEN" => "foo" },
:octokit_client => FakeOctokitClient
)
assert_equal("foo", client.access_token)
refute(client.netrc)
end

def test_doesnt_change_default_endpoint_for_public_github
client = Chandler::GitHub::Client.new(
:host => "github.com",
:octokit_client => FakeOctokitClient
)
assert_nil(client.api_endpoint)
end

def test_assigns_enterprise_endpoint
client = Chandler::GitHub::Client.new(
:host => "github.example.com",
:octokit_client => FakeOctokitClient
)
assert_equal("https://github.example.com/api/v3/", client.api_endpoint)
end

def test_raises_exception_if_netrc_fails
assert_raises(Chandler::GitHub::NetrcAuthenticationFailure) do
Chandler::GitHub::Client.new(
:octokit_client => FakeOctokitClientWithError
).login!
end
end

def test_raises_exception_if_access_token_fails
assert_raises(Chandler::GitHub::TokenAuthenticationFailure) do
Chandler::GitHub::Client.new(
:environment => { "CHANDLER_GITHUB_API_TOKEN" => "foo" },
:octokit_client => FakeOctokitClientWithError
).login!
end
end
end
25 changes: 25 additions & 0 deletions test/chandler/github/errors_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require "minitest_helper"
require "chandler/github/errors"

class Chandler::GitHub::ErrorsTest < Minitest::Test
def test_invalid_repository
invalid_repo = Chandler::GitHub::InvalidRepository.new("user/repo")
assert_kind_of(Chandler::GitHub::Error, invalid_repo)
assert_match(/failed to find/i, invalid_repo.message)
assert_match("user/repo", invalid_repo.message)
end

def test_netrc_authentication_failure
netrc_failure = Chandler::GitHub::NetrcAuthenticationFailure.new
assert_kind_of(Chandler::GitHub::Error, netrc_failure)
assert_match(/authentication failed/i, netrc_failure.message)
assert_match(/netrc/, netrc_failure.message)
end

def test_token_authentication_failure
token_failure = Chandler::GitHub::TokenAuthenticationFailure.new
assert_kind_of(Chandler::GitHub::Error, token_failure)
assert_match(/authentication failed/i, token_failure.message)
assert_match("CHANDLER_GITHUB_API_TOKEN", token_failure.message)
end
end
Loading