gemnasium 3.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|