gemnasium 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +18 -0
- data/.rvmrc +1 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +157 -0
- data/Rakefile +6 -0
- data/bin/gemnasium +43 -0
- data/features/gemnasium.feature +58 -0
- data/features/gemnasium_create.feature +13 -0
- data/features/gemnasium_install.feature +119 -0
- data/features/step_definitions/gemnasium_steps.rb +52 -0
- data/features/support/env.rb +7 -0
- data/gemnasium.gemspec +24 -0
- data/lib/gemnasium.rb +328 -0
- data/lib/gemnasium/configuration.rb +110 -0
- data/lib/gemnasium/connection.rb +42 -0
- data/lib/gemnasium/dependency_files.rb +55 -0
- data/lib/gemnasium/errors.rb +16 -0
- data/lib/gemnasium/options.rb +107 -0
- data/lib/gemnasium/version.rb +3 -0
- data/lib/templates/gemnasium.rake +24 -0
- data/lib/templates/gemnasium.yml +10 -0
- data/lib/templates/post-commit +7 -0
- data/spec/gemnasium/configuration_spec.rb +195 -0
- data/spec/gemnasium/connection_spec.rb +47 -0
- data/spec/gemnasium/dependency_files_spec.rb +80 -0
- data/spec/gemnasium/options_spec.rb +135 -0
- data/spec/gemnasium_spec.rb +520 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/support/gemnasium_helper.rb +86 -0
- metadata +177 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
|
3
|
+
module Gemnasium
|
4
|
+
class Connection
|
5
|
+
def initialize
|
6
|
+
@connection = Net::HTTP.new(Gemnasium.config.site, Gemnasium.config.use_ssl ? 443 : 80)
|
7
|
+
@connection.use_ssl = Gemnasium.config.use_ssl
|
8
|
+
end
|
9
|
+
|
10
|
+
def post(path, body, headers = {})
|
11
|
+
request = Net::HTTP::Post.new(path, headers.merge('Accept' => 'application/json', 'Content-Type' => 'application/json'))
|
12
|
+
request.basic_auth('X', Gemnasium.config.api_key)
|
13
|
+
request.body = body
|
14
|
+
@connection.request(request)
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(path, headers = {})
|
18
|
+
request = Net::HTTP::Get.new(path, headers.merge('Accept' => 'application/json', 'Content-Type' => 'application/json'))
|
19
|
+
request.basic_auth('X', Gemnasium.config.api_key)
|
20
|
+
@connection.request(request)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Set the API path for a specific item
|
24
|
+
#
|
25
|
+
# @param item [String] item the route should point to
|
26
|
+
# @return [String] API path
|
27
|
+
def api_path_for item
|
28
|
+
base = "/api/#{Gemnasium.config.api_version}"
|
29
|
+
|
30
|
+
case item
|
31
|
+
when 'base'
|
32
|
+
base
|
33
|
+
when 'projects'
|
34
|
+
"#{base}/projects"
|
35
|
+
when 'dependency_files'
|
36
|
+
"#{base}/projects/#{Gemnasium.config.project_slug}/dependency_files"
|
37
|
+
else
|
38
|
+
raise "No API path found for #{item}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
module Gemnasium
|
4
|
+
class DependencyFiles
|
5
|
+
|
6
|
+
SUPPORTED_DEPENDENCY_FILES = /(Gemfile|Gemfile\.lock|.*\.gemspec|package\.json|npm-shrinkwrap\.json|setup\.py|requirements\.txt|requires\.txt|composer\.json|composer\.lock)$/
|
7
|
+
|
8
|
+
# Get a Hash of sha1s for each file corresponding to the regex
|
9
|
+
#
|
10
|
+
# @param regexp [Regexp] the regular expression of requested files
|
11
|
+
# @return [Hash] the hash associating each file path with its SHA1 hash
|
12
|
+
def self.get_sha1s_hash(project_path)
|
13
|
+
Dir.chdir(project_path)
|
14
|
+
Dir.glob("**/**").grep(SUPPORTED_DEPENDENCY_FILES).inject({}) do |h, file_path|
|
15
|
+
h[file_path] = calculate_sha1("#{project_path}/#{file_path}") unless is_ignored?(file_path)
|
16
|
+
h
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get the content to upload to Gemnasium.
|
21
|
+
#
|
22
|
+
# @param files_path [Array] an array containing the path of the files
|
23
|
+
# @return [Array] array of hashes containing file name, file sha and file content
|
24
|
+
def self.get_content_to_upload(project_path, files_path)
|
25
|
+
files_path.inject([]) do |arr, file_path|
|
26
|
+
arr << { filename: file_path, sha: calculate_sha1(file_path), content: File.open("#{project_path}/#{file_path}") {|io| io.read} }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Calculate hash of a file in the same way git does
|
33
|
+
#
|
34
|
+
# @param file_path [String] path of the file
|
35
|
+
# @return [String] SHA1 of the file
|
36
|
+
def self.calculate_sha1(file_path)
|
37
|
+
mem_buf = File.open(file_path) {|io| io.read}
|
38
|
+
size = mem_buf.size
|
39
|
+
header = "blob #{size}\0" # type[space]size[null byte]
|
40
|
+
|
41
|
+
Digest::SHA1.hexdigest(header + mem_buf)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Test if the file is ignored by the configuration file
|
45
|
+
#
|
46
|
+
# @param file_path [String] path of the file to test
|
47
|
+
# @return [Boolean] true if the file is ignored
|
48
|
+
def self.is_ignored?(file_path)
|
49
|
+
Gemnasium.config.ignored_paths.each do |ignored_path|
|
50
|
+
return true if file_path =~ ignored_path
|
51
|
+
end
|
52
|
+
false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Gemnasium
|
2
|
+
class InvalidApiKeyError < StandardError
|
3
|
+
def message
|
4
|
+
'Your API key is invalid. Please double check it on https://gemnasium.com/settings/api_access'
|
5
|
+
end
|
6
|
+
end
|
7
|
+
class DeprecatedApiVersionError < StandardError; end
|
8
|
+
class MalformedRequestError < StandardError; end
|
9
|
+
class MissingParamsError < StandardError; end
|
10
|
+
class NonBillableUserError < StandardError; end
|
11
|
+
class NoSlotsAvailableError < StandardError; end
|
12
|
+
class ProjectNotFoundError < StandardError; end
|
13
|
+
class ProjectNotOfflineError < StandardError; end
|
14
|
+
class ProjectParamMissingError < StandardError; end
|
15
|
+
class UnsupportedDependencyFilesError < StandardError; end
|
16
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Gemnasium
|
4
|
+
class Options
|
5
|
+
|
6
|
+
# Parse arguments from command line
|
7
|
+
#
|
8
|
+
# @param args [Array] arguments from command line
|
9
|
+
# @return [Hash, OptionParser] hash of the parsed options & Option parser to get --help message
|
10
|
+
def self.parse args
|
11
|
+
options = {}
|
12
|
+
|
13
|
+
global = OptionParser.new do |opts|
|
14
|
+
opts.banner = 'Usage: gemnasium [options]'
|
15
|
+
|
16
|
+
opts.on '-v', '--version', 'Show Gemnasium version' do
|
17
|
+
options[:show_version] = true
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on '-h', '--help', 'Display this message' do
|
21
|
+
options[:show_help] = true
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.separator ''
|
25
|
+
opts.separator <<-HELP_MESSAGE
|
26
|
+
Available commands are:
|
27
|
+
create : Create or update project on Gemnasium
|
28
|
+
install : Install the necessary config file
|
29
|
+
push : Push your dependency files to Gemnasium
|
30
|
+
migrate : Migrate the configuration file
|
31
|
+
resolve : Resolve project name to an existing project on Gemnasium
|
32
|
+
|
33
|
+
See `gemnasium COMMAND --help` for more information on a specific command.
|
34
|
+
|
35
|
+
WARNING! The gemnasium Rubygem has been deprecated.
|
36
|
+
Please use Gemnasium Toolbelt (https://github.com/gemnasium/toolbelt) instead.
|
37
|
+
HELP_MESSAGE
|
38
|
+
end
|
39
|
+
|
40
|
+
subcommands = {
|
41
|
+
'create' => OptionParser.new do |opts|
|
42
|
+
opts.banner = 'Usage: gemnasium create [options]'
|
43
|
+
|
44
|
+
opts.on '-h', '--help', 'Display this message' do
|
45
|
+
options[:show_help] = true
|
46
|
+
end
|
47
|
+
end,
|
48
|
+
'install' => OptionParser.new do |opts|
|
49
|
+
opts.banner = 'Usage: gemnasium install [options]'
|
50
|
+
|
51
|
+
opts.on '--git', 'Create a post-commit hook to run gemnasium push command if a dependency file has been commited' do
|
52
|
+
options[:install_git_hook] = true
|
53
|
+
end
|
54
|
+
|
55
|
+
opts.on '--rake', 'Create rake task to run Gemnasium' do
|
56
|
+
options[:install_rake_task] = true
|
57
|
+
end
|
58
|
+
|
59
|
+
opts.on '-h', '--help', 'Display this message' do
|
60
|
+
options[:show_help] = true
|
61
|
+
end
|
62
|
+
end,
|
63
|
+
'push' => OptionParser.new do |opts|
|
64
|
+
opts.banner = 'Usage: gemnasium push'
|
65
|
+
|
66
|
+
opts.on '-i', '--ignore-branch', 'Push to gemnasium regardless of branch' do
|
67
|
+
options[:ignore_branch] = true
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on '-s', '--silence-branch', 'Silently ignore untracked branches' do
|
71
|
+
options[:silence_branch] = true
|
72
|
+
end
|
73
|
+
|
74
|
+
opts.on '-h', '--help', 'Display this message' do
|
75
|
+
options[:show_help] = true
|
76
|
+
end
|
77
|
+
end,
|
78
|
+
'migrate' => OptionParser.new do |opts|
|
79
|
+
opts.banner = 'Usage: gemnasium migrate [options]'
|
80
|
+
|
81
|
+
opts.on '-h', '--help', 'Display this message' do
|
82
|
+
options[:show_help] = true
|
83
|
+
end
|
84
|
+
end,
|
85
|
+
'resolve' => OptionParser.new do |opts|
|
86
|
+
opts.banner = 'Usage: gemnasium resolve [options]'
|
87
|
+
|
88
|
+
opts.on '-h', '--help', 'Display this message' do
|
89
|
+
options[:show_help] = true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
}
|
93
|
+
|
94
|
+
global.order! args
|
95
|
+
parser = global
|
96
|
+
|
97
|
+
unless (command = args.shift).nil?
|
98
|
+
raise OptionParser::ParseError unless subcommands.has_key?(command)
|
99
|
+
subcommands[command].order! args
|
100
|
+
options[:command] = command
|
101
|
+
parser = subcommands[command]
|
102
|
+
end
|
103
|
+
|
104
|
+
return options, parser
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
namespace :gemnasium do
|
2
|
+
require 'gemnasium'
|
3
|
+
|
4
|
+
desc "Push dependency files to gemnasium"
|
5
|
+
task :push do
|
6
|
+
Gemnasium.push project_path: File.expand_path(".")
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "Create project on gemnasium"
|
10
|
+
task :create do
|
11
|
+
Gemnasium.create_project project_path: File.expand_path(".")
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "Migrate the configuration file"
|
15
|
+
task :migrate do
|
16
|
+
Gemnasium.migrate project_path: File.expand_path(".")
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "Resolve project name on gemnasium"
|
20
|
+
task :resolve do
|
21
|
+
Gemnasium.resolve_project project_path: File.expand_path(".")
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
api_key: api_key_goes_here # You personal (secret) API key. Get it at https://staging-gemnasium.tech-angels.net/settings#api_key
|
2
|
+
project_name: project_name # A name to remember your project.
|
3
|
+
project_slug: # Unique slug for this project. Get it on the "settings" page of your project or run "gemnasium resolve".
|
4
|
+
project_branch: master # /!\ If you don't use git, remove this line
|
5
|
+
ignored_paths: # Paths you want to ignore when searching for dependency files (from app root)
|
6
|
+
- features/
|
7
|
+
- spec/
|
8
|
+
- test/
|
9
|
+
- tmp/
|
10
|
+
- vendor/
|
@@ -0,0 +1,7 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
SUPPORTED_COMMITED_FILES=$((git diff --name-only HEAD~ HEAD 2>/dev/null || git diff-tree -r --no-commit-id --name-only --root HEAD) | grep '^\(Gemfile\|Gemfile\.lock\|.*\.gemspec\|package\.json\|npm-shrinkwrap\.json\|setup\.py\|requirements\.txt\|requires\.txt\|composer\.json\|composer\.lock\)$')
|
4
|
+
|
5
|
+
if [[ $SUPPORTED_COMMITED_FILES > 0 ]]; then
|
6
|
+
gemnasium push
|
7
|
+
fi
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Gemnasium::Configuration do
|
5
|
+
describe 'default config' do
|
6
|
+
it { expect(Gemnasium::Configuration::DEFAULT_CONFIG['site']).to eql 'gemnasium.com' }
|
7
|
+
it { expect(Gemnasium::Configuration::DEFAULT_CONFIG['use_ssl']).to eql true }
|
8
|
+
it { expect(Gemnasium::Configuration::DEFAULT_CONFIG['api_version']).to eql 'v3' }
|
9
|
+
it { expect(Gemnasium::Configuration::DEFAULT_CONFIG['ignored_paths']).to eql [] }
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:config_file_path) { 'tmp/config.yml' }
|
13
|
+
let(:config) { described_class.new File.expand_path(config_file_path) }
|
14
|
+
|
15
|
+
let(:config_options) do
|
16
|
+
{
|
17
|
+
api_key: 'api_key',
|
18
|
+
project_name: 'gemnasium-gem',
|
19
|
+
project_branch: 'master',
|
20
|
+
ignored_paths: ['spec/','tmp/*.lock', '*.gemspec']
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def write_config_file
|
25
|
+
config_hash = config_options.reduce({}) do |memo, (key,value)|
|
26
|
+
memo ||= {}
|
27
|
+
memo[key.to_s] = value
|
28
|
+
memo
|
29
|
+
end
|
30
|
+
File.open(config_file_path, 'w+') { |f| f.write(config_hash.to_yaml) }
|
31
|
+
end
|
32
|
+
|
33
|
+
after do
|
34
|
+
File.delete(config_file_path) if File.exist?(config_file_path)
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'initialize' do
|
38
|
+
context 'for an inexistant config file' do
|
39
|
+
it { expect { Gemnasium::Configuration.new File.expand_path(config_file_path) }.to raise_error Errno::ENOENT }
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'for a config file that does exist' do
|
43
|
+
before { FileUtils.touch(config_file_path) }
|
44
|
+
|
45
|
+
context 'with missing mandatory values' do
|
46
|
+
let(:config_options) {{ project_name: 'gemnasium-gem' }}
|
47
|
+
before { write_config_file }
|
48
|
+
|
49
|
+
it { expect { Gemnasium::Configuration.new File.expand_path(config_file_path) }.to raise_error('Your configuration file does not contain all mandatory parameters or contain invalid values. Please check the documentation.') }
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'with all mandatory values' do
|
53
|
+
let(:config_options) {{ api_key: 'api_key', project_name: 'gemnasium-gem', project_branch: 'master', ignored_paths: ['spec/','tmp/*.lock', '*.gemspec'] }}
|
54
|
+
before { write_config_file }
|
55
|
+
|
56
|
+
it { expect(config.api_key).to eql config_options[:api_key] }
|
57
|
+
|
58
|
+
# Keep profile name for backward compatibility with version =< 2.0
|
59
|
+
it { expect(config.profile_name).to eql config_options[:profile_name] }
|
60
|
+
|
61
|
+
it { expect(config.project_name).to eql config_options[:project_name] }
|
62
|
+
it { expect(config.project_slug).to eql config_options[:project_slug] }
|
63
|
+
it { expect(config.project_branch).to eql config_options[:project_branch] }
|
64
|
+
it { expect(config.site).to eql Gemnasium::Configuration::DEFAULT_CONFIG['site'] }
|
65
|
+
it { expect(config.use_ssl).to eql Gemnasium::Configuration::DEFAULT_CONFIG['use_ssl'] }
|
66
|
+
it { expect(config.api_version).to eql Gemnasium::Configuration::DEFAULT_CONFIG['api_version'] }
|
67
|
+
it { expect(config.ignored_paths).to include Regexp.new("^spec/") }
|
68
|
+
it { expect(config.ignored_paths).to include Regexp.new("^tmp/[^/]+\\.lock") }
|
69
|
+
it { expect(config.ignored_paths).to include Regexp.new("^[^\/]+\\.gemspec") }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'for a config file with parsable ERB code' do
|
74
|
+
before { write_config_file }
|
75
|
+
before { ENV['GEMNASIUM_API_KEY'] = 'api_key_from_env' }
|
76
|
+
|
77
|
+
let(:config_options) do
|
78
|
+
{
|
79
|
+
api_key: "<%= ENV['GEMNASIUM_API_KEY'] %>",
|
80
|
+
project_name: 'gemnasium-gem',
|
81
|
+
project_branch: 'master',
|
82
|
+
ignored_paths: ['spec/','tmp/*.lock', '*.gemspec']
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'parses ERB code' do
|
87
|
+
expect(config.api_key).to eql 'api_key_from_env'
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'keeps values unchanged if not ERB code' do
|
91
|
+
expect(config.project_name).to eql config_options[:project_name]
|
92
|
+
expect(config.project_branch).to eql config_options[:project_branch]
|
93
|
+
expect(config.ignored_paths).to include Regexp.new("^spec/")
|
94
|
+
expect(config.ignored_paths).to include Regexp.new("^tmp/[^/]+\\.lock")
|
95
|
+
expect(config.ignored_paths).to include Regexp.new("^[^\/]+\\.gemspec")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
pending "writable?"
|
101
|
+
|
102
|
+
describe "#store_value!" do
|
103
|
+
context "with a new value for an existing key" do
|
104
|
+
let(:key) { :project_name }
|
105
|
+
let(:value) { 'new-name' }
|
106
|
+
|
107
|
+
# HACK: fake config reload
|
108
|
+
let(:new_config) { Gemnasium::Configuration.new File.expand_path(config_file_path) }
|
109
|
+
|
110
|
+
before do
|
111
|
+
write_config_file
|
112
|
+
config.store_value! key, value, "my project name"
|
113
|
+
end
|
114
|
+
|
115
|
+
it "updates the given key-value pair" do
|
116
|
+
expect(new_config.project_name).to eql 'new-name'
|
117
|
+
end
|
118
|
+
|
119
|
+
it "keeps other key-value pairs unchanged" do
|
120
|
+
expect(new_config.api_key).to eql config_options[:api_key]
|
121
|
+
expect(new_config.project_branch).to eql config_options[:project_branch]
|
122
|
+
end
|
123
|
+
|
124
|
+
it "stores the given comment" do
|
125
|
+
content = File.read File.expand_path(config_file_path)
|
126
|
+
expect(content).to match /project_name:.*# my project name/
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe "#migrate!" do
|
132
|
+
before { write_config_file }
|
133
|
+
subject { config.needs_to_migrate? }
|
134
|
+
|
135
|
+
context "with NO profile_name key" do
|
136
|
+
let(:config_options) do
|
137
|
+
{
|
138
|
+
api_key: 'api_key',
|
139
|
+
project_name: 'gemnasium-gem',
|
140
|
+
project_branch: 'master'
|
141
|
+
# no profile_name
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
it { should be false }
|
146
|
+
end
|
147
|
+
|
148
|
+
context "with profile_name key" do
|
149
|
+
let(:config_options) do
|
150
|
+
{
|
151
|
+
project_name: 'gemnasium-gem',
|
152
|
+
profile_name: 'tech-angels',
|
153
|
+
api_key: '1337'
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
it { should be true }
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe "#migrate!" do
|
162
|
+
before { write_config_file }
|
163
|
+
|
164
|
+
let(:config_options) do
|
165
|
+
{
|
166
|
+
project_name: 'gemnasium-gem',
|
167
|
+
profile_name: 'tech-angels',
|
168
|
+
api_key: '1337'
|
169
|
+
}
|
170
|
+
end
|
171
|
+
|
172
|
+
# HACK: fake config reload
|
173
|
+
let(:new_config) { Gemnasium::Configuration.new File.expand_path(config_file_path) }
|
174
|
+
|
175
|
+
it "removes the profile_name" do
|
176
|
+
expect(config.profile_name).to eql 'tech-angels'
|
177
|
+
config.migrate!
|
178
|
+
expect(new_config.profile_name).to eql nil
|
179
|
+
end
|
180
|
+
|
181
|
+
it "adds an empty project_slug" do
|
182
|
+
config.migrate!
|
183
|
+
expect(new_config.project_slug).to eql nil
|
184
|
+
|
185
|
+
content = File.read File.expand_path(config_file_path)
|
186
|
+
expect(content).to match /^project_slug:$/
|
187
|
+
end
|
188
|
+
|
189
|
+
it "preserves the other keys" do
|
190
|
+
config.migrate!
|
191
|
+
expect(new_config.project_name).to eql 'gemnasium-gem'
|
192
|
+
expect(new_config.api_key).to eql '1337'
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|