cutting_edge 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/LICENSE +674 -0
- data/README.md +95 -0
- data/Rakefile +159 -0
- data/bin/cutting_edge +72 -0
- data/cutting_edge.gemspec +57 -0
- data/lib/cutting_edge.rb +3 -0
- data/lib/cutting_edge/app.rb +77 -0
- data/lib/cutting_edge/badge.rb +46 -0
- data/lib/cutting_edge/langs.rb +123 -0
- data/lib/cutting_edge/langs/python.rb +102 -0
- data/lib/cutting_edge/langs/ruby.rb +47 -0
- data/lib/cutting_edge/langs/rust.rb +68 -0
- data/lib/cutting_edge/repo.rb +67 -0
- data/lib/cutting_edge/versions.rb +46 -0
- data/lib/cutting_edge/workers/badge.rb +22 -0
- data/lib/cutting_edge/workers/dependency.rb +100 -0
- data/lib/cutting_edge/workers/helpers.rb +24 -0
- data/spec/langs/python_spec.rb +89 -0
- data/spec/langs/rust_spec.rb +81 -0
- data/spec/spec_helper.rb +111 -0
- metadata +191 -0
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/repotag/cutting_edge.svg?branch=master)](https://travis-ci.org/repotag/cutting_edge)
|
2
|
+
[![Coverage Status](https://coveralls.io/repos/github/repotag/cutting_edge/badge.svg?branch=master)](https://coveralls.io/github/repotag/cutting_edge?branch=master)
|
3
|
+
|
4
|
+
# CuttingEdge
|
5
|
+
|
6
|
+
## Sinatra App
|
7
|
+
|
8
|
+
* start the app with `bundle exec ruby lib/app.rb`
|
9
|
+
* Runs on http://localhost:4567
|
10
|
+
* Routes under `/source/org/name`
|
11
|
+
* For example: http://localhost:4567/github/gollum/gollum/info
|
12
|
+
|
13
|
+
## DependencyWorker
|
14
|
+
|
15
|
+
The DependencyWorker currently performs the following actions:
|
16
|
+
|
17
|
+
* Fetch gemfile and gemspec for a given Gem
|
18
|
+
* Currently, the `GithubGem` class specifies the needed information for a gem hosted on github, ditto for the `GitlabGem` class for gitlab.
|
19
|
+
* These are subclasses of `RepositoryGem`.
|
20
|
+
* Parse both files for dependency requirements
|
21
|
+
* Does not bork if either of the files could not be retrieved.
|
22
|
+
* Determine the latest version for each required gem
|
23
|
+
* By querying rubygems for a version of the gem `>= 0`.
|
24
|
+
* Generate a Hash of results of the following form:
|
25
|
+
```ruby
|
26
|
+
{:gemspec=>
|
27
|
+
{:outdated_major=>
|
28
|
+
[{:name=>"kramdown",
|
29
|
+
:required=>"~> 1.9.0",
|
30
|
+
:latest=>"2.1.0",
|
31
|
+
:type=>:runtime},
|
32
|
+
{:name=>"sinatra",
|
33
|
+
:required=>"~> 1.4, >= 1.4.4",
|
34
|
+
:latest=>"2.0.8.1",
|
35
|
+
:type=>:runtime},
|
36
|
+
{:name=>"mustache",
|
37
|
+
:required=>">= 0.99.5, < 1.0.0",
|
38
|
+
:latest=>"1.1.1",
|
39
|
+
:type=>:runtime},
|
40
|
+
{:name=>"gemojione",
|
41
|
+
:required=>"~> 3.2",
|
42
|
+
:latest=>"4.3.2",
|
43
|
+
:type=>:runtime}],
|
44
|
+
:outdated_minor=>[],
|
45
|
+
:outdated_bump=>[],
|
46
|
+
:ok=>
|
47
|
+
[{:name=>"gollum-lib",
|
48
|
+
:required=>"~> 4.2, >= 4.2.10",
|
49
|
+
:latest=>"4.2.10",
|
50
|
+
:type=>:runtime},
|
51
|
+
{:name=>"useragent",
|
52
|
+
:required=>"~> 0.16.2",
|
53
|
+
:latest=>"0.16.10",
|
54
|
+
:type=>:runtime}],
|
55
|
+
:unknown=>[]},
|
56
|
+
:gemfile=>
|
57
|
+
{:outdated_major=>
|
58
|
+
[{:name=>"rake",
|
59
|
+
:required=>"~> 10.4",
|
60
|
+
:latest=>"13.0.1",
|
61
|
+
:type=>:runtime}],
|
62
|
+
:outdated_minor=>[],
|
63
|
+
:outdated_bump=>[],
|
64
|
+
:ok=>[],
|
65
|
+
:unknown=>[]}}
|
66
|
+
|
67
|
+
```
|
68
|
+
* These results are stored in a Moneta store under a key generated by `RepositoryGem#identifier` (for e.g. a `GithubGem`, the identifier will be of the form `'github/org/name'`).
|
69
|
+
* The Moneta store used by the Worker is provided by the Sinatra app Class: `::CuttingEdge::App.store`.
|
70
|
+
* So we must do e.g. `CuttingEdge::App.set(:store, Moneta.new(:Memory))` to set the `#store` method on the `CuttingEdge` Class.
|
71
|
+
|
72
|
+
### Example use of the DependencyWorker and Gem API
|
73
|
+
|
74
|
+
See `test.rb` for a basic example (without Sinatra) of how the setup works. Run it with `bundle exec ruby test.rb`.
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
require 'redis-objects'
|
78
|
+
|
79
|
+
gem = GithubGem.new('gollum', 'gollum')
|
80
|
+
|
81
|
+
Redis::Objects.redis = Redis.new
|
82
|
+
gem_dependencies = Redis::Value.new(gem.identifier)
|
83
|
+
|
84
|
+
puts gem_dependencies.value # Currently nil
|
85
|
+
|
86
|
+
DependencyWorker.perform_async(gem.identifier, gem.gemspec_location, gem.gemfile_location) # Fire up a Sidekiq job
|
87
|
+
|
88
|
+
# Sleep 5
|
89
|
+
|
90
|
+
puts gem_dependencies.value # The JSON results hash
|
91
|
+
```
|
92
|
+
|
93
|
+
## License
|
94
|
+
|
95
|
+
This work is licensed under the terms of the [GNU GPLv3.0](LICENSE).
|
data/Rakefile
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'date'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
task :default => :rspec
|
6
|
+
|
7
|
+
desc "Run specs."
|
8
|
+
RSpec::Core::RakeTask.new(:rspec) do |spec|
|
9
|
+
ruby_opts = "-w"
|
10
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
11
|
+
spec.rspec_opts = ['--backtrace --color']
|
12
|
+
end
|
13
|
+
|
14
|
+
#############################################################################
|
15
|
+
#
|
16
|
+
# Helper functions
|
17
|
+
#
|
18
|
+
#############################################################################
|
19
|
+
|
20
|
+
def name
|
21
|
+
@name ||= Dir['*.gemspec'].first.split('.').first
|
22
|
+
end
|
23
|
+
|
24
|
+
def version
|
25
|
+
line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
|
26
|
+
line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
|
27
|
+
end
|
28
|
+
|
29
|
+
# assumes x.y.z all digit version
|
30
|
+
def next_version
|
31
|
+
# x.y.z
|
32
|
+
v = version.split '.'
|
33
|
+
# bump z
|
34
|
+
v[-1] = v[-1].to_i + 1
|
35
|
+
v.join '.'
|
36
|
+
end
|
37
|
+
|
38
|
+
def bump_version
|
39
|
+
old_file = File.read("lib/#{name}.rb")
|
40
|
+
old_version_line = old_file[/^\s*VERSION\s*=\s*.*/]
|
41
|
+
new_version = next_version
|
42
|
+
# replace first match of old vesion with new version
|
43
|
+
old_file.sub!(old_version_line, " VERSION = '#{new_version}'")
|
44
|
+
|
45
|
+
File.write("lib/#{name}.rb", old_file)
|
46
|
+
|
47
|
+
new_version
|
48
|
+
end
|
49
|
+
|
50
|
+
def date
|
51
|
+
Date.today.to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
def gemspec_file
|
55
|
+
"#{name}.gemspec"
|
56
|
+
end
|
57
|
+
|
58
|
+
desc "Build the gem file."
|
59
|
+
task :build do
|
60
|
+
system "gem build gollum_git_adapter_specs.gemspec"
|
61
|
+
end
|
62
|
+
|
63
|
+
def gem_file
|
64
|
+
"#{name}-#{version}.gem"
|
65
|
+
end
|
66
|
+
|
67
|
+
def replace_header(head, header_name)
|
68
|
+
head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
|
69
|
+
end
|
70
|
+
|
71
|
+
#############################################################################
|
72
|
+
#
|
73
|
+
# Custom tasks (add your own tasks here)
|
74
|
+
#
|
75
|
+
#############################################################################
|
76
|
+
|
77
|
+
desc "Update version number and gemspec"
|
78
|
+
task :bump do
|
79
|
+
puts "Updated version to #{bump_version}"
|
80
|
+
# Execute does not invoke dependencies.
|
81
|
+
# Manually invoke gemspec then validate.
|
82
|
+
Rake::Task[:gemspec].execute
|
83
|
+
Rake::Task[:validate].execute
|
84
|
+
end
|
85
|
+
|
86
|
+
#############################################################################
|
87
|
+
#
|
88
|
+
# Packaging tasks
|
89
|
+
#
|
90
|
+
#############################################################################
|
91
|
+
|
92
|
+
desc 'Create a release build and push to rubygems'
|
93
|
+
task :release => :build do
|
94
|
+
unless `git branch` =~ /^\* master$/
|
95
|
+
puts "You must be on the master branch to release!"
|
96
|
+
exit!
|
97
|
+
end
|
98
|
+
sh "git commit --allow-empty -a -m 'Release #{version}'"
|
99
|
+
sh "git pull --rebase origin master"
|
100
|
+
sh "git tag v#{version}"
|
101
|
+
sh "git push origin master"
|
102
|
+
sh "git push origin v#{version}"
|
103
|
+
sh "gem push pkg/#{name}-#{version}.gem"
|
104
|
+
end
|
105
|
+
|
106
|
+
desc 'Publish to rubygems. Same as release'
|
107
|
+
task :publish => :release
|
108
|
+
|
109
|
+
desc 'Build gem'
|
110
|
+
task :build => :gemspec do
|
111
|
+
sh "mkdir -p pkg"
|
112
|
+
sh "gem build #{gemspec_file}"
|
113
|
+
sh "mv #{gem_file} pkg"
|
114
|
+
end
|
115
|
+
|
116
|
+
desc "Build and install"
|
117
|
+
task :install => :build do
|
118
|
+
sh "gem install --local --no-document pkg/#{name}-#{version}.gem"
|
119
|
+
end
|
120
|
+
|
121
|
+
desc 'Update gemspec'
|
122
|
+
task :gemspec => :validate do
|
123
|
+
# read spec file and split out manifest section
|
124
|
+
spec = File.read(gemspec_file)
|
125
|
+
head, manifest, tail = spec.split(" # = MANIFEST =\n")
|
126
|
+
|
127
|
+
# replace name version and date
|
128
|
+
replace_header(head, :name)
|
129
|
+
replace_header(head, :version)
|
130
|
+
replace_header(head, :date)
|
131
|
+
|
132
|
+
# determine file list from git ls-files
|
133
|
+
files = `git ls-files`.
|
134
|
+
split("\n").
|
135
|
+
sort.
|
136
|
+
reject { |file| file =~ /^\./ }.
|
137
|
+
reject { |file| file =~ /^(rdoc|pkg|test|Home\.md|\.gitattributes)/ }.
|
138
|
+
map { |file| " #{file}" }.
|
139
|
+
join("\n")
|
140
|
+
|
141
|
+
# piece file back together and write
|
142
|
+
manifest = " s.files = %w[\n#{files}\n ]\n"
|
143
|
+
spec = [head, manifest, tail].join(" # = MANIFEST =\n")
|
144
|
+
File.open(gemspec_file, 'w') { |io| io.write(spec) }
|
145
|
+
puts "Updated #{gemspec_file}"
|
146
|
+
end
|
147
|
+
|
148
|
+
desc 'Validate lib files and version file'
|
149
|
+
task :validate do
|
150
|
+
libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
|
151
|
+
unless libfiles.empty?
|
152
|
+
puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
|
153
|
+
exit!
|
154
|
+
end
|
155
|
+
unless Dir['VERSION*'].empty?
|
156
|
+
puts "A `VERSION` file at root level violates Gem best practices."
|
157
|
+
exit!
|
158
|
+
end
|
159
|
+
end
|
data/bin/cutting_edge
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.expand_path('../../lib/cutting_edge/app.rb', __FILE__)
|
4
|
+
require 'yaml'
|
5
|
+
require 'rufus-scheduler'
|
6
|
+
|
7
|
+
config = <<YAML
|
8
|
+
github:
|
9
|
+
gollum:
|
10
|
+
gollum:
|
11
|
+
api_token: secret
|
12
|
+
gollum-lib:
|
13
|
+
api_token: secret
|
14
|
+
locations: [gemspec.rb]
|
15
|
+
dependency_types: [runtime, development]
|
16
|
+
singingwolfboy:
|
17
|
+
flask-dance:
|
18
|
+
language: python
|
19
|
+
rust-lang:
|
20
|
+
crates.io:
|
21
|
+
language: rust
|
22
|
+
api_token: secret
|
23
|
+
gitlab:
|
24
|
+
cthowl01:
|
25
|
+
team-chess-ruby:
|
26
|
+
api_token: secret
|
27
|
+
YAML
|
28
|
+
|
29
|
+
options = {
|
30
|
+
:port => 4567,
|
31
|
+
:bind => '0.0.0.0',
|
32
|
+
}
|
33
|
+
|
34
|
+
store = Moneta.new(:Memory)
|
35
|
+
|
36
|
+
repositories = {}
|
37
|
+
YAML.load(config).each do |source, orgs|
|
38
|
+
orgs.each do |org, value|
|
39
|
+
value.each do |name, settings|
|
40
|
+
cfg = settings.is_a?(Hash) ? settings : {}
|
41
|
+
repo = Object.const_get("CuttingEdge::#{source.capitalize}Repository").new(org, name, cfg.fetch('language', nil), cfg.fetch('locations', nil), cfg.fetch('branch', nil), cfg.fetch('api_token', nil))
|
42
|
+
repo.dependency_types = cfg['dependency_types'].map {|dep| dep.to_sym} if cfg['dependency_types'].is_a?(Array)
|
43
|
+
repositories["#{source}/#{org}/#{name}"] = repo
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Need to initialize the log like this once, because otherwise it only becomes available after the Sinatra app has received a request...
|
49
|
+
::SemanticLogger.add_appender(file_name: "#{CuttingEdge::App.environment}.log")
|
50
|
+
|
51
|
+
CuttingEdge::App.set(:repositories, repositories)
|
52
|
+
CuttingEdge::App.set(:store, store)
|
53
|
+
CuttingEdge::App.set(:enable_logging, true)
|
54
|
+
|
55
|
+
puts "Scheduling Jobs..."
|
56
|
+
scheduler = Rufus::Scheduler.new
|
57
|
+
scheduler.every('1h') do
|
58
|
+
worker_fetch_all(repositories.values)
|
59
|
+
end
|
60
|
+
scheduler.every('1h5m') do
|
61
|
+
worker_all_badges(repositories.values)
|
62
|
+
end
|
63
|
+
|
64
|
+
puts "Running Workers a first time..."
|
65
|
+
include CuttingEdgeHelpers
|
66
|
+
worker_fetch_all(repositories.values)
|
67
|
+
|
68
|
+
sleep 5
|
69
|
+
worker_all_badges(repositories.values)
|
70
|
+
|
71
|
+
puts "Starting Sinatra..."
|
72
|
+
CuttingEdge::App.run!(options)
|
@@ -0,0 +1,57 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
3
|
+
s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
|
4
|
+
s.rubygems_version = '1.3.5'
|
5
|
+
s.required_ruby_version = '>= 2.4'
|
6
|
+
|
7
|
+
s.name = 'cutting_edge'
|
8
|
+
s.version = '0.0.1'
|
9
|
+
s.date = '2020-02-19'
|
10
|
+
s.license = 'GPL-3.0-only'
|
11
|
+
|
12
|
+
s.summary = 'Self-hosted dependency monitoring, including shiny badges.'
|
13
|
+
s.description = 'Self-hosted dependency monitoring, including shiny badges.'
|
14
|
+
|
15
|
+
s.authors = ['Dawa Ometto', 'Bart Kamphorst']
|
16
|
+
s.email = 'd.ometto@gmail.com'
|
17
|
+
s.homepage = 'http://github.com/repotag/cutting_edge'
|
18
|
+
|
19
|
+
s.require_paths = %w[lib]
|
20
|
+
|
21
|
+
s.executables = ['cutting_edge']
|
22
|
+
|
23
|
+
s.add_dependency 'gemnasium-parser', '~> 0.1.9'
|
24
|
+
s.add_dependency 'http', '~> 4.3'
|
25
|
+
s.add_dependency 'sucker_punch', '~> 2.1'
|
26
|
+
s.add_dependency 'sinatra', '~> 2.0'
|
27
|
+
s.add_dependency 'moneta', '~> 1.2'
|
28
|
+
s.add_dependency 'victor', '~> 0.2.8'
|
29
|
+
s.add_dependency 'rufus-scheduler', '~> 3.6'
|
30
|
+
s.add_dependency 'sinatra-logger', '~> 0.3'
|
31
|
+
s.add_dependency 'toml-rb', '~> 2.0'
|
32
|
+
# = MANIFEST =
|
33
|
+
s.files = %w[
|
34
|
+
Gemfile
|
35
|
+
LICENSE
|
36
|
+
README.md
|
37
|
+
Rakefile
|
38
|
+
bin/cutting_edge
|
39
|
+
cutting_edge.gemspec
|
40
|
+
lib/cutting_edge.rb
|
41
|
+
lib/cutting_edge/app.rb
|
42
|
+
lib/cutting_edge/badge.rb
|
43
|
+
lib/cutting_edge/langs.rb
|
44
|
+
lib/cutting_edge/langs/python.rb
|
45
|
+
lib/cutting_edge/langs/ruby.rb
|
46
|
+
lib/cutting_edge/langs/rust.rb
|
47
|
+
lib/cutting_edge/repo.rb
|
48
|
+
lib/cutting_edge/versions.rb
|
49
|
+
lib/cutting_edge/workers/badge.rb
|
50
|
+
lib/cutting_edge/workers/dependency.rb
|
51
|
+
lib/cutting_edge/workers/helpers.rb
|
52
|
+
spec/langs/python_spec.rb
|
53
|
+
spec/langs/rust_spec.rb
|
54
|
+
spec/spec_helper.rb
|
55
|
+
]
|
56
|
+
# = MANIFEST =
|
57
|
+
end
|
data/lib/cutting_edge.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sucker_punch'
|
3
|
+
require 'sinatra'
|
4
|
+
require 'sinatra/logger'
|
5
|
+
require 'json'
|
6
|
+
require 'moneta'
|
7
|
+
|
8
|
+
require File.expand_path('../../cutting_edge.rb', __FILE__)
|
9
|
+
require File.expand_path('../repo.rb', __FILE__)
|
10
|
+
require File.expand_path('../workers/dependency.rb', __FILE__)
|
11
|
+
require File.expand_path('../workers/badge.rb', __FILE__)
|
12
|
+
|
13
|
+
module CuttingEdgeHelpers
|
14
|
+
def worker_all_badges(repositories)
|
15
|
+
repositories.each do |repo|
|
16
|
+
worker_generate_badge(repo)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def worker_generate_badge(repo)
|
21
|
+
BadgeWorker.perform_async(repo.identifier)
|
22
|
+
end
|
23
|
+
|
24
|
+
def worker_fetch_all(repositories)
|
25
|
+
repositories.each do |repo|
|
26
|
+
worker_fetch(repo)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def worker_fetch(repo)
|
31
|
+
DependencyWorker.perform_async(repo.identifier, repo.lang, repo.locations, repo.dependency_types)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
module CuttingEdge
|
37
|
+
LAST_VERSION_TIMEOUT = 5
|
38
|
+
|
39
|
+
class App < Sinatra::Base
|
40
|
+
include CuttingEdgeHelpers
|
41
|
+
|
42
|
+
logger filename: "#{settings.environment}.log", level: :trace
|
43
|
+
|
44
|
+
before do
|
45
|
+
@store = settings.store
|
46
|
+
end
|
47
|
+
|
48
|
+
get %r{/(.+)/(.+)/(.+)/info} do |source, org, name|
|
49
|
+
repo_defined?(source, org, name)
|
50
|
+
content_type :json
|
51
|
+
@store[@repo.identifier].merge({:language => @repo.lang}).to_json # Todo: check whether value exists yet? If not, call worker / wait / timeout?
|
52
|
+
end
|
53
|
+
|
54
|
+
get %r{/(.+)/(.+)/(.+)/svg} do |source, org, name|
|
55
|
+
repo_defined?(source, org, name)
|
56
|
+
content_type 'image/svg+xml'
|
57
|
+
@store["svg-#{@repo.identifier}"]
|
58
|
+
end
|
59
|
+
|
60
|
+
post %r{/(.+)/(.+)/(.+)/refresh} do |source, org, name|
|
61
|
+
repo_defined?(source, org, name)
|
62
|
+
if @repo.token && params[:token] == @repo.token
|
63
|
+
worker_fetch(@repo)
|
64
|
+
status 200
|
65
|
+
else
|
66
|
+
status 401
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def repo_defined?(source, org, name)
|
73
|
+
halt 404, '404 Not Found' unless @repo = settings.repositories["#{source}/#{org}/#{name}"]
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|