Skip to content

Commit

Permalink
Merge pull request #73 from svenfuchs/sf-github-release
Browse files Browse the repository at this point in the history
Add gem release --github
  • Loading branch information
PikachuEXE committed Oct 15, 2019
2 parents b9dff1b + 1924b53 commit 0f5f238
Show file tree
Hide file tree
Showing 13 changed files with 308 additions and 14 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ group :test do
gem 'rspec'
gem 'simplecov'
gem 'coveralls'
gem 'webmock'
end
1 change: 1 addition & 0 deletions lib/gem/release/cmds.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'gem/release/cmds/bootstrap'
require 'gem/release/cmds/bump'
require 'gem/release/cmds/gemspec'
require 'gem/release/cmds/github'
require 'gem/release/cmds/release'
require 'gem/release/cmds/runner'
require 'gem/release/cmds/tag'
Expand Down
79 changes: 79 additions & 0 deletions lib/gem/release/cmds/github.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
require 'gem/release/cmds/base'
require 'gem/release/context/github'

module Gem
module Release
module Cmds
class Github < Base
summary "Creates a GitHub release for the current version."

description <<-str.split("\n").map(&:lstrip).join("\n")
Creates a GitHub release for the current version.
Requires a tag `v[version]` to be present or --tag to be given.
str

MSGS = {
release: 'Creating GitHub release for %s version %s.',
no_tag: 'Tag %s does not exist. Run `gem tag` or pass `--tag`.',
no_repo: 'Could not determine the repository name. Please pass `--repo REPO`, or set homepage or metadata[:github_url] to the GitHub repository URL in the gemspec.',
no_token: 'Could not determine the GitHub OAuth token. Please pass `--token TOKEN`. See https://developer.github.com/v3/#oauth2-token-sent-in-a-header for more details.',
}.freeze

def run
in_gem_dirs do
announce :release, gem.name, tag_name
validate
release
end
end

private

def validate
abort :no_tag, tag_name unless tagged?
abort :no_token unless token
end

def tagged?
git.tags.include?(tag_name)
end

def release
return if pretend?
Context::Github.new(repo, data).release
end

def data
{
version: gem.version,
tag_name: tag_name,
name: "#{gem.name} #{tag_name}",
descr: descr,
token: token
}
end

def tag_name
"v#{gem.version}"
end

def repo
opts[:repo] || repo_from(gem.spec.homepage) || repo_from(gem.spec.metadata[:github_url]) || abort(:no_repo)
end

def repo_from(url)
url && url =~ %r(https://github\.com/(.*/.*)) && $1
end

def token
opts[:token]
end

def descr
opts[:descr]
end
end
end
end
end
49 changes: 42 additions & 7 deletions lib/gem/release/cmds/release.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,17 @@ class Release < Base
host: 'Push to a compatible host other than rubygems.org',
key: 'Use the API key from ~/.gem/credentials',
tag: 'Shortcut for running the `gem tag` command',
push: 'Push tag to the remote git repository',
recurse: 'Recurse into directories that contain gemspec files'
push: 'Push tag to the remote git repository',
github: 'Create a GitHub release',
recurse: 'Recurse into directories that contain gemspec files',

# region github

descr: 'Description of the release',
repo: "Full name of the repository on GitHub, e.g. svenfuchs/gem-release (defaults to the repo name from the gemspec's homepage if this is a GitHub URL)",
token: 'GitHub OAuth token'

# endregion github
}.freeze

opt '-h', '--host HOST', descr(:host) do |value|
Expand All @@ -56,6 +65,26 @@ class Release < Base
opts[:recurse] = value
end

# region github

opt '-g', '--github', descr(:github) do |value|
opts[:github] = value
end

opt '-d', '--description DESCRIPTION', descr(:descr) do |value|
opts[:descr] = value
end

opt '--repo REPO', descr(:repo) do |value|
opts[:repo] = value
end

opt '--token TOKEN', descr(:token) do |value|
opts[:token] = value
end

# endregion github

MSGS = {
release: 'Releasing %s with version %s',
build: 'Building %s',
Expand All @@ -73,7 +102,8 @@ def run
validate
release
end
tag if opts[:tag]
tag if opts[:tag]
github if opts[:github]
end

private
Expand All @@ -84,16 +114,13 @@ def validate

def release
announce :release, gem.name, target_version
return if pretend?
build
push
ensure
cleanup
end

def tag
Tag.new(context, args, opts).run
end

def build
gem_cmd :build, gem.spec_filename
end
Expand All @@ -102,6 +129,14 @@ def push
gem_cmd :push, gem.filename, *push_args
end

def tag
Tag.new(context, args, opts).run
end

def github
Github.new(context, args, opts).run
end

def push_args
args = [:key, :host].map { |opt| ["--#{opt}", opts[opt]] if opts[opt] }
args << "--quiet" if quiet?
Expand Down
12 changes: 10 additions & 2 deletions lib/gem/release/context/gemspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ def initialize(*)
@filename = name && "#{name}.gemspec" || filenames.first
end

def exists?
filename && File.exist?(filename)
end

def gem_name
gemspec.name if gemspec
end
Expand All @@ -21,8 +25,12 @@ def gem_filename
gemspec.file_name if gemspec
end

def exists?
filename && File.exist?(filename)
def metadata
gemspec && gemspec.metadata || {}
end

def homepage
gemspec.homepage if gemspec
end

private
Expand Down
62 changes: 62 additions & 0 deletions lib/gem/release/context/github.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require 'json'
require 'gem/release/helper/http'
require 'gem/release/version'

module Gem
module Release
class Context
class Github
include Helper::Http

URL = 'https://api.github.com/repos/%s/releases'

MSGS = {
error: 'GitHub returned %s (body: %p)'
}.freeze

attr_reader :repo, :data

def initialize(repo, data)
@repo = repo
@data = data
end

def release
# Create a release
# https://developer.github.com/v3/repos/releases/#create-a-release
resp = post(url, body, headers)
status, body = resp
# success status code is 201 (created) not 200 (ok)
raise Abort, MSGS.fetch(:error) % [status, body] unless status == 201
end

private

def url
URL % repo
end

def body
JSON.dump(
tag_name: data[:tag_name],
name: data[:name],
body: data[:descr],
prerelease: pre?(data[:version])
)
end

def headers
{
'User-Agent' => "gem-release/v#{::Gem::Release::VERSION}",
'Content-Type' => 'text/json',
'Authorization' => "token #{data[:token]}",
}
end

def pre?(version)
Version::Number.new(version).pre?
end
end
end
end
end
9 changes: 6 additions & 3 deletions lib/gem/release/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ def gem_cmd(cmd, *args)
end

def abort(msg, *args)
msg = self.class::MSGS[msg] % args if msg.is_a?(Symbol)
msg = "#{msg} Aborting."
context.abort(msg)
processed_msg = if msg.is_a?(Symbol)
self.class::MSGS.fetch(msg) % args
else
msg
end
context.abort("#{processed_msg} Aborting.")
end
end
end
Expand Down
39 changes: 39 additions & 0 deletions lib/gem/release/helper/http.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
require 'net/http'
require 'uri'

module Gem
module Release
module Helper
module Http
class Client < Struct.new(:method, :url, :body, :headers)
def request
req = const.new(uri.request_uri, headers)
req.body = body if body
resp = client.request(req)
[resp.code.to_i, resp.body]
end

private

def uri
@uri ||= URI.parse(url)
end

def client
http_client = Net::HTTP.new(uri.host, uri.port)
http_client.use_ssl = (uri.scheme == 'https')
http_client
end

def const
Net::HTTP.const_get(method.to_s.capitalize)
end
end

def post(url, body = nil, headers = {})
Client.new(:post, url, body, headers).request
end
end
end
end
end
4 changes: 2 additions & 2 deletions lib/gem/release/support/gem_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ def initialize

def execute
Cmds::Runner.new(cmd, opts.delete(:args), opts).run
rescue Abort
abort
rescue Abort => ex
abort(ex.message)
end

def cmd
Expand Down
4 changes: 4 additions & 0 deletions lib/gem/release/version/number.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ def bump
parts.join(stage_delim)
end

def pre?
!!parts[4]
end

private

def specific?
Expand Down
37 changes: 37 additions & 0 deletions spec/gem/release/cmds/github_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
describe Gem::Release::Cmds::Github do
let(:args) { [] }
let(:opts) { { repo: 'foo/bar', token: 'token', descr: 'A new foo bar' } }
let(:body) { '{"tag_name":"v1.0.0","name":"foo-bar v1.0.0","body":"A new foo bar","prerelease":false}' }
# success status code is 201 (created) not 200 (ok)
# https://developer.github.com/v3/repos/releases/#create-a-release
let(:status) { 201 }

cwd 'foo-bar'
gemspec 'foo-bar'

before { context.git.tags << 'v1.0.0' }
before { stub_request(:post, 'https://api.github.com:443/repos/foo/bar/releases').with(body: body).to_return(status: status) }

describe 'by default' do
run_cmd

it { should_not run_cmd 'git push --tags origin' }
it { should output 'Creating GitHub release for foo-bar version v1.0.0' }
it { should output 'All is good, thanks my friend.' }
end

describe 'not tagged' do
before { context.git.tags.clear }
it { expect { run }.to raise_error('Tag v1.0.0 does not exist. Run `gem tag` or pass `--tag`. Aborting.') }
end

describe 'invalid token' do
let(:status) { 401 }
it { expect { run }.to raise_error('GitHub returned 401 (body: "")') }
end

describe 'not found' do
let(:status) { 404 }
it { expect { run }.to raise_error('GitHub returned 404 (body: "")') }
end
end

0 comments on commit 0f5f238

Please sign in to comment.