gouteur 1.0.0
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.
- checksums.yaml +7 -0
- data/.github/workflows/build.yml +28 -0
- data/.github/workflows/lint.yml +29 -0
- data/.gitignore +17 -0
- data/.gouteur.yml +2 -0
- data/.rspec +4 -0
- data/.rubocop.yml +16 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +94 -0
- data/Rakefile +13 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/gouteur +6 -0
- data/gouteur.gemspec +28 -0
- data/lib/gouteur.rb +16 -0
- data/lib/gouteur/bundle.rb +27 -0
- data/lib/gouteur/checker.rb +127 -0
- data/lib/gouteur/cli.rb +26 -0
- data/lib/gouteur/dotfile.rb +23 -0
- data/lib/gouteur/host.rb +23 -0
- data/lib/gouteur/message.rb +105 -0
- data/lib/gouteur/rake_task.rb +26 -0
- data/lib/gouteur/repo.rb +71 -0
- data/lib/gouteur/shell.rb +50 -0
- data/lib/gouteur/version.rb +3 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 001d0213cebc3a146615388dff657d573cc671395e2e8d62995e11b3b3a84f6e
|
4
|
+
data.tar.gz: 45ed7d535d16effa0c9ad2bd6044e973257d08a111e606beb6b65dd2e7483b83
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2664a0c872fbd8da4b638841454904ae43015652c10e3d2fabbd52f7c5f15fb2f5541eb677954952228e48153ca4ef6f6d85cac5ca3b7ca5abbbac07c11a67a2
|
7
|
+
data.tar.gz: 1befe751c803a11255163d29ef64b37ce143381c11b197c9826ee1f020e249bace18fb597a318840293c7b2d72fac78e3a42b86a99be059db7d8eb5628270a02
|
@@ -0,0 +1,28 @@
|
|
1
|
+
name: build
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
|
9
|
+
strategy:
|
10
|
+
matrix:
|
11
|
+
ruby: [ '2.5', '3.0', 'ruby-head' ]
|
12
|
+
|
13
|
+
steps:
|
14
|
+
- uses: actions/checkout@v2
|
15
|
+
- name: Set up Ruby ${{ matrix.ruby }}
|
16
|
+
uses: ruby/setup-ruby@v1
|
17
|
+
with:
|
18
|
+
ruby-version: ${{ matrix.ruby }}
|
19
|
+
- name: Prepare
|
20
|
+
run: |
|
21
|
+
git config --global init.defaultBranch main
|
22
|
+
git config --global user.email "testuser@example.com"
|
23
|
+
git config --global user.name "Test User"
|
24
|
+
gem install bundler -v 2.2.8
|
25
|
+
bundle install --jobs 4
|
26
|
+
- name: Run the specs
|
27
|
+
run: |
|
28
|
+
bundle exec rspec
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# based on https://github.com/rails/rails/blob/4a78dcb/.github/workflows/rubocop.yml
|
2
|
+
|
3
|
+
name: rubocop linting
|
4
|
+
|
5
|
+
on: [push, pull_request]
|
6
|
+
|
7
|
+
jobs:
|
8
|
+
build:
|
9
|
+
runs-on: ubuntu-latest
|
10
|
+
|
11
|
+
steps:
|
12
|
+
- uses: actions/checkout@v2
|
13
|
+
- name: Set up Ruby
|
14
|
+
uses: ruby/setup-ruby@v1
|
15
|
+
with:
|
16
|
+
ruby-version: 3.0
|
17
|
+
- name: Cache gems
|
18
|
+
uses: actions/cache@v1
|
19
|
+
with:
|
20
|
+
path: vendor/bundle
|
21
|
+
key: ${{ runner.os }}-rubocop-${{ hashFiles('**/Gemfile.lock') }}
|
22
|
+
restore-keys: |
|
23
|
+
${{ runner.os }}-rubocop-
|
24
|
+
- name: Install gems
|
25
|
+
run: |
|
26
|
+
bundle config path vendor/bundle
|
27
|
+
bundle install --jobs 4 --retry 3
|
28
|
+
- name: Run rubocop
|
29
|
+
run: bundle exec rubocop
|
data/.gitignore
ADDED
data/.gouteur.yml
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
inherit_gem:
|
2
|
+
relaxed-rubocop: .rubocop.yml
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
NewCops: enable
|
6
|
+
SuggestExtensions: false
|
7
|
+
TargetRubyVersion: 2.5
|
8
|
+
|
9
|
+
Layout/LineLength:
|
10
|
+
Max: 80
|
11
|
+
|
12
|
+
Lint/AmbiguousBlockAssociation:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Style/FrozenStringLiteralComment:
|
16
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in gouteur.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem 'rake', '~> 13.0'
|
7
|
+
|
8
|
+
gem 'rspec', '~> 3.0'
|
9
|
+
|
10
|
+
gem 'rubocop', '~> 1.7'
|
11
|
+
|
12
|
+
gem 'relaxed-rubocop'
|
13
|
+
|
14
|
+
gem 'example_repo', '0.1.0', path: './spec/gouteur/example_repo'
|
15
|
+
|
16
|
+
gem 'byebug'
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
gouteur (1.0.0)
|
5
|
+
|
6
|
+
PATH
|
7
|
+
remote: spec/gouteur/example_repo
|
8
|
+
specs:
|
9
|
+
example_repo (0.1.0)
|
10
|
+
|
11
|
+
GEM
|
12
|
+
remote: https://rubygems.org/
|
13
|
+
specs:
|
14
|
+
ast (2.4.2)
|
15
|
+
byebug (11.1.3)
|
16
|
+
diff-lcs (1.4.4)
|
17
|
+
parallel (1.20.1)
|
18
|
+
parser (3.0.0.0)
|
19
|
+
ast (~> 2.4.1)
|
20
|
+
rainbow (3.0.0)
|
21
|
+
rake (13.0.3)
|
22
|
+
regexp_parser (2.0.3)
|
23
|
+
relaxed-rubocop (2.5)
|
24
|
+
rexml (3.2.4)
|
25
|
+
rspec (3.10.0)
|
26
|
+
rspec-core (~> 3.10.0)
|
27
|
+
rspec-expectations (~> 3.10.0)
|
28
|
+
rspec-mocks (~> 3.10.0)
|
29
|
+
rspec-core (3.10.1)
|
30
|
+
rspec-support (~> 3.10.0)
|
31
|
+
rspec-expectations (3.10.1)
|
32
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
33
|
+
rspec-support (~> 3.10.0)
|
34
|
+
rspec-mocks (3.10.2)
|
35
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
36
|
+
rspec-support (~> 3.10.0)
|
37
|
+
rspec-support (3.10.2)
|
38
|
+
rubocop (1.9.1)
|
39
|
+
parallel (~> 1.10)
|
40
|
+
parser (>= 3.0.0.0)
|
41
|
+
rainbow (>= 2.2.2, < 4.0)
|
42
|
+
regexp_parser (>= 1.8, < 3.0)
|
43
|
+
rexml
|
44
|
+
rubocop-ast (>= 1.2.0, < 2.0)
|
45
|
+
ruby-progressbar (~> 1.7)
|
46
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
47
|
+
rubocop-ast (1.4.1)
|
48
|
+
parser (>= 2.7.1.5)
|
49
|
+
ruby-progressbar (1.11.0)
|
50
|
+
unicode-display_width (2.0.0)
|
51
|
+
|
52
|
+
PLATFORMS
|
53
|
+
ruby
|
54
|
+
|
55
|
+
DEPENDENCIES
|
56
|
+
byebug
|
57
|
+
example_repo (= 0.1.0)!
|
58
|
+
gouteur!
|
59
|
+
rake (~> 13.0)
|
60
|
+
relaxed-rubocop
|
61
|
+
rspec (~> 3.0)
|
62
|
+
rubocop (~> 1.7)
|
63
|
+
|
64
|
+
BUNDLED WITH
|
65
|
+
2.2.8
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Jannosch Müller
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# 👨🍳 Gouteur
|
2
|
+
|
3
|
+
[](http://badge.fury.io/rb/gouteur)
|
4
|
+
[](https://github.com/jaynetics/gouteur/actions)
|
5
|
+
|
6
|
+
Treat the people that use your gem like royalty! Send for a [gouteur](https://en.wikipedia.org/wiki/Food_taster) before serving them something new!
|
7
|
+
|
8
|
+
## What?
|
9
|
+
|
10
|
+
This gem runs the build tasks of other projects against your unreleased changes.
|
11
|
+
|
12
|
+
## Why?
|
13
|
+
|
14
|
+
Sometimes, other projects start depending on your gem.
|
15
|
+
|
16
|
+
When you release a new version of your gem, these projects might break.
|
17
|
+
|
18
|
+
[Semantic versioning](https://semver.org) obviously helps. People make mistakes, though. The boundary between public and private APIs can also be fuzzy, particularly in an open language like Ruby.
|
19
|
+
|
20
|
+
Thus, when you update your gem, you might feel as if you should check whether things that depend on it will keep working.
|
21
|
+
|
22
|
+
Gouteur automates this step.
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Add `gouteur` to the development dependencies of your gem.
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
### Recommended usage
|
31
|
+
|
32
|
+
Create a `.gouteur.yml` in the root of your project:
|
33
|
+
|
34
|
+
```yml
|
35
|
+
repos:
|
36
|
+
- uri: https://github.com/someone/some_gem
|
37
|
+
ref: some_specific_branch # optional, default is the default branch
|
38
|
+
before: setup_special_dependency # optional, bundle is always installed
|
39
|
+
tasks: ['rspec', 'rake foo'] # optional, default is `rake`
|
40
|
+
locked: true # optional, prevents setting an incompatible VERSION
|
41
|
+
```
|
42
|
+
|
43
|
+
Then simply `bundle exec gouteur` or add the rake task to your Rakefile:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
require 'gouteur/rake_task'
|
47
|
+
Gouteur::RakeTask.new
|
48
|
+
|
49
|
+
# default name is :gouteur, e.g. to include it in the default task:
|
50
|
+
task default: %i[rspec gouteur]
|
51
|
+
```
|
52
|
+
|
53
|
+
Pro tip: for large repos, running only relevant specs can speed up things a lot, e.g.:
|
54
|
+
|
55
|
+
```yml
|
56
|
+
tasks: 'rspec spec/known_relevant_spec.rb'
|
57
|
+
```
|
58
|
+
```yml
|
59
|
+
tasks: 'rspec --pattern "**/{,*}{keyword1,keyword2}{,*,*/**/*}_spec.rb"'`
|
60
|
+
```
|
61
|
+
|
62
|
+
### Manual usage
|
63
|
+
|
64
|
+
From the shell:
|
65
|
+
|
66
|
+
```shell
|
67
|
+
gouteur 'https://github.com/foo/bar'
|
68
|
+
```
|
69
|
+
|
70
|
+
From Ruby:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
repo = Gouteur::Repo.new(uri: 'https://github.com/foo/bar')
|
74
|
+
success, message = Gouteur::Checker.call(repo)
|
75
|
+
```
|
76
|
+
|
77
|
+
## Contributing
|
78
|
+
|
79
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jaynetics/gouteur.
|
80
|
+
|
81
|
+
## Outlook
|
82
|
+
|
83
|
+
Possible future improvements:
|
84
|
+
|
85
|
+
- consider caching of dependent repositories in CI, e.g. in GitHub workflows
|
86
|
+
- support more sources of code, e.g. latest release, private GitHub repositories
|
87
|
+
- improve performance by tracing & rerunning only specs/tests that use the gem
|
88
|
+
- save time in MiniTest by forcing it to run in fail-fast mode like RSpec
|
89
|
+
- add help output and more options to the CLI executable
|
90
|
+
- other ideas? open an issue!
|
91
|
+
|
92
|
+
## License
|
93
|
+
|
94
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
|
6
|
+
require 'rubocop/rake_task'
|
7
|
+
|
8
|
+
RuboCop::RakeTask.new
|
9
|
+
|
10
|
+
task default: %i[spec rubocop]
|
11
|
+
|
12
|
+
require_relative 'lib/gouteur/rake_task'
|
13
|
+
Gouteur::RakeTask.new
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'gouteur'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/exe/gouteur
ADDED
data/gouteur.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'lib/gouteur/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'gouteur'
|
5
|
+
spec.version = Gouteur::VERSION
|
6
|
+
spec.authors = ['Janosch Müller']
|
7
|
+
spec.email = ['janosch84@gmail.com']
|
8
|
+
|
9
|
+
spec.summary = 'See if your lib is still digestible.'
|
10
|
+
spec.description = 'Run tests of dependent gems against your changes.'
|
11
|
+
spec.homepage = 'https://github.com/jaynetics/gouteur'
|
12
|
+
spec.license = 'MIT'
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
|
14
|
+
|
15
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
16
|
+
spec.metadata['source_code_uri'] = 'https://github.com/jaynetics/gouteur'
|
17
|
+
spec.metadata['changelog_uri'] =
|
18
|
+
'https://github.com/jaynetics/gouteur/blob/master/CHANGELOG.md'
|
19
|
+
|
20
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
22
|
+
f.match(%r{\A(?:test|spec|features)/})
|
23
|
+
end
|
24
|
+
end
|
25
|
+
spec.bindir = 'exe'
|
26
|
+
spec.executables = ['gouteur']
|
27
|
+
spec.require_paths = ['lib']
|
28
|
+
end
|
data/lib/gouteur.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
|
3
|
+
require 'gouteur/bundle'
|
4
|
+
require 'gouteur/checker'
|
5
|
+
require 'gouteur/cli'
|
6
|
+
require 'gouteur/dotfile'
|
7
|
+
require 'gouteur/host'
|
8
|
+
require 'gouteur/message'
|
9
|
+
require 'gouteur/repo'
|
10
|
+
require 'gouteur/shell'
|
11
|
+
require 'gouteur/version'
|
12
|
+
|
13
|
+
# :nodoc:
|
14
|
+
module Gouteur
|
15
|
+
class Error < StandardError; end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Gouteur
|
2
|
+
# thin wrapper for bundler calls
|
3
|
+
class Bundle
|
4
|
+
attr_reader :path
|
5
|
+
|
6
|
+
def initialize(path)
|
7
|
+
@path = path
|
8
|
+
end
|
9
|
+
|
10
|
+
def install(env: {})
|
11
|
+
Shell.run(%w[bundle update --quiet --jobs 4], pwd: path, env: env)
|
12
|
+
end
|
13
|
+
|
14
|
+
def depends_on?(gem_name)
|
15
|
+
Shell.run(%W[bundle info #{gem_name}], pwd: path).success?
|
16
|
+
end
|
17
|
+
|
18
|
+
def exec(task, env: {})
|
19
|
+
name = task.sub(/bundle exec +/, '')
|
20
|
+
Shell.run(%W[bundle exec #{name}], pwd: path, env: env)
|
21
|
+
end
|
22
|
+
|
23
|
+
def gemfile_path
|
24
|
+
File.join(path, 'Gemfile')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Gouteur
|
4
|
+
# main process class
|
5
|
+
class Checker
|
6
|
+
attr_reader :repo
|
7
|
+
|
8
|
+
def self.call(repo, silent: false)
|
9
|
+
new(repo, silent: silent).call
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(repo, silent: false)
|
13
|
+
@repo = repo
|
14
|
+
@silent = silent
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
puts "Preparing `#{repo}` for checks..."
|
19
|
+
prepare
|
20
|
+
check_dependence
|
21
|
+
|
22
|
+
run_tasks(adapted: false)
|
23
|
+
|
24
|
+
create_adapted_gemfile
|
25
|
+
install_adapted_bundle or return [
|
26
|
+
true, Message.skipped_incompatible(repo: repo)
|
27
|
+
]
|
28
|
+
|
29
|
+
run_tasks(adapted: true)
|
30
|
+
|
31
|
+
[true, Message.success(repo: repo)]
|
32
|
+
rescue Gouteur::Error => e
|
33
|
+
[false, e.message.chomp]
|
34
|
+
end
|
35
|
+
|
36
|
+
def prepare
|
37
|
+
repo.fetch
|
38
|
+
repo.prepare
|
39
|
+
result = repo.bundle.install
|
40
|
+
result.success? || raise(Error, result.full_error)
|
41
|
+
end
|
42
|
+
|
43
|
+
def check_dependence
|
44
|
+
repo.bundle.depends_on?(Host.name) ||
|
45
|
+
raise(Error, Message.no_dependence(repo: repo))
|
46
|
+
end
|
47
|
+
|
48
|
+
def run_tasks(adapted: false)
|
49
|
+
repo.tasks.empty? && raise(Error, Message.no_tasks(repo: repo))
|
50
|
+
repo.tasks.each { |task| run_task(task, adapted: adapted) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def run_task(task, adapted: false)
|
54
|
+
puts("Running `#{task}` with #{adapted ? :new : :old} `#{Host.name}`...")
|
55
|
+
result = repo.bundle.exec(task, env: adapted ? adaptation_env : {})
|
56
|
+
result.success? or raise Error, Message.send(
|
57
|
+
adapted ? :broken_after_update : :broken,
|
58
|
+
repo: repo, task: task, output: result.stdout, error: result.stderr
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_adapted_gemfile
|
63
|
+
content = File.exist?(gemfile_path) ? File.read(gemfile_path) : ''
|
64
|
+
adapted_content = adapt_gemfile_content(content)
|
65
|
+
File.open(adapted_gemfile_path, 'w') { |f| f.puts(adapted_content) }
|
66
|
+
end
|
67
|
+
|
68
|
+
def adapt_gemfile_content(content)
|
69
|
+
new_entry = "gem '#{Host.name}', path: '#{Host.root}' # set by gouteur "
|
70
|
+
|
71
|
+
existing_ref_pattern =
|
72
|
+
/^ *gem +(?<q>'|")#{Host.name}\k<q>(?<v> *,\s*(?<q2>'|")[^'"]+\k<q2>*)?/
|
73
|
+
|
74
|
+
if content =~ existing_ref_pattern
|
75
|
+
content.gsub(existing_ref_pattern) do
|
76
|
+
# keep version specification if there was one
|
77
|
+
new_entry.sub(/(?=, path:)/, Regexp.last_match[:v].to_s)
|
78
|
+
end
|
79
|
+
else
|
80
|
+
"#{content}\n#{new_entry}\n"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def gemfile_path
|
85
|
+
repo.bundle.gemfile_path
|
86
|
+
end
|
87
|
+
|
88
|
+
def adapted_gemfile_path
|
89
|
+
"#{repo.bundle.gemfile_path}.gouteur"
|
90
|
+
end
|
91
|
+
|
92
|
+
BUNDLER_INCOMPATIBLE_VERSION_CODE = 6
|
93
|
+
BUNDLER_GEM_NOT_FOUND_CODE = 7 # includes some incompatibility cases
|
94
|
+
|
95
|
+
def install_adapted_bundle
|
96
|
+
result = repo.bundle.install(env: adaptation_env)
|
97
|
+
|
98
|
+
if result.success?
|
99
|
+
true
|
100
|
+
elsif result.exitstatus == BUNDLER_INCOMPATIBLE_VERSION_CODE ||
|
101
|
+
result.exitstatus == BUNDLER_GEM_NOT_FOUND_CODE &&
|
102
|
+
result.stderr =~ /following version/
|
103
|
+
if repo.locked?
|
104
|
+
raise Error,
|
105
|
+
Message.incompatible_failure(repo: repo, error: result.stderr)
|
106
|
+
else
|
107
|
+
false
|
108
|
+
end
|
109
|
+
else
|
110
|
+
raise Error, result.full_error
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def adaptation_env
|
115
|
+
{
|
116
|
+
'BUNDLE_GEMFILE' => adapted_gemfile_path,
|
117
|
+
'SPEC_OPTS' => '--fail-fast', # saves time with rspec tasks
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def puts(*args)
|
124
|
+
@silent ? nil : Kernel.puts(*args)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/gouteur/cli.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Gouteur
|
2
|
+
# command line interface - prints to stdout and returns true or false
|
3
|
+
module CLI
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def call(args = ARGV)
|
7
|
+
repos = pick_repos(args)
|
8
|
+
if repos.empty?
|
9
|
+
puts '', Message.no_repos, ''
|
10
|
+
return false
|
11
|
+
end
|
12
|
+
|
13
|
+
repos.all? do |repo|
|
14
|
+
success, message = Gouteur::Checker.call(repo)
|
15
|
+
puts '', message, ''
|
16
|
+
success
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def pick_repos(args)
|
21
|
+
repos = args.map { |arg| Gouteur::Repo.new(uri: arg) }
|
22
|
+
repos = Dotfile.repos if repos.empty?
|
23
|
+
repos
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Gouteur
|
2
|
+
# interface for gouteur's configuration dotfile
|
3
|
+
module Dotfile
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def present?
|
7
|
+
File.exist?(path)
|
8
|
+
end
|
9
|
+
|
10
|
+
def path
|
11
|
+
File.join(Host.root, '.gouteur.yml')
|
12
|
+
end
|
13
|
+
|
14
|
+
def content
|
15
|
+
@content ||=
|
16
|
+
present? ? YAML.safe_load(File.read(path), symbolize_names: true) : {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def repos
|
20
|
+
(content[:repos] || []).map { |attrs| Gouteur::Repo.new(**attrs) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/gouteur/host.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Gouteur
|
2
|
+
# the gem/library/project under test
|
3
|
+
module Host
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def name
|
7
|
+
gemspec.name || raise(Error, "No name set in `#{gemspec.loaded_from}`")
|
8
|
+
end
|
9
|
+
|
10
|
+
def gemspec
|
11
|
+
@gemspec ||= begin
|
12
|
+
gemspecs = Dir[File.join(root, '*.gemspec')]
|
13
|
+
(count = gemspecs.count) == 1 ||
|
14
|
+
raise(Error, "Found #{count} gemspecs, could not determine own name")
|
15
|
+
Bundler.load_gemspec(gemspecs.first)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def root
|
20
|
+
Bundler.root
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Gouteur
|
2
|
+
# user-facing messages are here so they don't clutter the code
|
3
|
+
module Message
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def shell_error(args:, pwd:, stderr:)
|
7
|
+
<<~MSG
|
8
|
+
👨🍳 Oh non!
|
9
|
+
|
10
|
+
The command `$ #{args.join(' ')}` failed in `#{pwd}`.
|
11
|
+
#{original_error_part(stderr)}
|
12
|
+
MSG
|
13
|
+
end
|
14
|
+
|
15
|
+
def success(repo:)
|
16
|
+
<<~MSG
|
17
|
+
👨🍳 Délicieux!
|
18
|
+
|
19
|
+
Your changes to `#{Host.name}` are fine for `#{repo}`. All tasks succeeded.
|
20
|
+
MSG
|
21
|
+
end
|
22
|
+
|
23
|
+
def no_repos
|
24
|
+
<<~MSG
|
25
|
+
👨🍳 Quoi?
|
26
|
+
|
27
|
+
Found no repos to test. Pass repo URIs as command line arguments or list them under `repos:` in `#{Dotfile.path}`.
|
28
|
+
MSG
|
29
|
+
end
|
30
|
+
|
31
|
+
def no_tasks(repo:)
|
32
|
+
<<~MSG
|
33
|
+
👨🍳 Quoi?
|
34
|
+
|
35
|
+
You have defined no tasks to run for `#{repo}`.
|
36
|
+
MSG
|
37
|
+
end
|
38
|
+
|
39
|
+
def no_dependence(repo:)
|
40
|
+
<<~MSG
|
41
|
+
👨🍳 Sacrebleu!
|
42
|
+
|
43
|
+
`#{Host.name}` is not listed in the Gemfile or gemspec of `#{repo}`. Hence it does not make sense to test changes against it.
|
44
|
+
MSG
|
45
|
+
end
|
46
|
+
|
47
|
+
def broken(repo:, task:, error:)
|
48
|
+
<<~MSG
|
49
|
+
👨🍳 Zut alors!
|
50
|
+
|
51
|
+
Task `#{task}` failed for `#{repo}` even before inserting the new code of `#{Host.name}`.
|
52
|
+
|
53
|
+
This likely means the task is broken or does not exist.
|
54
|
+
#{original_error_part(error)}
|
55
|
+
MSG
|
56
|
+
end
|
57
|
+
|
58
|
+
def broken_after_update(repo:, task:, output:, error:)
|
59
|
+
<<~MSG
|
60
|
+
👨🍳 Répugnant!
|
61
|
+
|
62
|
+
Task `#{task}` failed for `#{repo}` after inserting the new code of `#{Host.name}`.
|
63
|
+
|
64
|
+
This likely means you ruined it! (Or the task is not idempotent. Or this is a bug in gouteur.)
|
65
|
+
#{original_output_part(output)}
|
66
|
+
#{original_error_part(error)}
|
67
|
+
MSG
|
68
|
+
end
|
69
|
+
|
70
|
+
def skipped_incompatible(repo:)
|
71
|
+
<<~MSG
|
72
|
+
👨🍳 Attention!
|
73
|
+
|
74
|
+
The new version number of `#{Host.name}` is incompatible with the version requirements specified by `#{repo}`.
|
75
|
+
|
76
|
+
Releasing incompatible versions is considered OK by default, so tasks will be SKIPPED in this case. If you want gouteur to FAIL in this case, set the `locked` flag.
|
77
|
+
MSG
|
78
|
+
end
|
79
|
+
|
80
|
+
def incompatible_failure(repo:, error:)
|
81
|
+
<<~MSG
|
82
|
+
👨🍳 Zut alors!
|
83
|
+
|
84
|
+
The new version number of `#{Host.name}` is incompatible with the version requirements specified by `#{repo}`.
|
85
|
+
|
86
|
+
Incompatible version numbers can be allowed by removing the `locked` flag. This will make gouteur SKIP the tasks in this case.
|
87
|
+
#{original_error_part(error)}
|
88
|
+
MSG
|
89
|
+
end
|
90
|
+
|
91
|
+
def original_error_part(stderr)
|
92
|
+
msg = strip(stderr)
|
93
|
+
msg.empty? ? '' : "\n👇 The original error was:\n\n#{msg}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def original_output_part(stdout)
|
97
|
+
msg = strip(stdout)
|
98
|
+
msg.empty? ? '' : "\n👇 The original output was:\n\n#{msg}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def strip(string)
|
102
|
+
string.to_s.gsub(/\A\s+|\s+\z/, '')
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# this file isn't required by default so rake isn't needed as runtime dependency
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/tasklib'
|
5
|
+
|
6
|
+
module Gouteur
|
7
|
+
# provides a custom rake task
|
8
|
+
class RakeTask < ::Rake::TaskLib
|
9
|
+
attr_accessor :name
|
10
|
+
|
11
|
+
def initialize(name = :gouteur, *args)
|
12
|
+
super()
|
13
|
+
|
14
|
+
self.name = name
|
15
|
+
|
16
|
+
desc 'Run Gouteur' unless ::Rake.application.last_description
|
17
|
+
task(name, *args) do |_, task_args|
|
18
|
+
# lazy-load gouteur so that the task doesn't impact Rakefile load time
|
19
|
+
require 'gouteur'
|
20
|
+
|
21
|
+
success = Gouteur::CLI.call(task_args.extras)
|
22
|
+
success || abort('Gouteur failed!')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/gouteur/repo.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Gouteur
|
4
|
+
# a repository of code that depends on the library under test
|
5
|
+
class Repo
|
6
|
+
attr_reader :uri, :name, :ref, :tasks
|
7
|
+
alias to_s name
|
8
|
+
|
9
|
+
def initialize(uri:, ref: nil, before: [], tasks: 'rake', locked: false)
|
10
|
+
@uri = URI.parse(uri)
|
11
|
+
@name = extract_name_from_uri(uri)
|
12
|
+
@ref = ref
|
13
|
+
@before = Array(before)
|
14
|
+
@tasks = Array(tasks)
|
15
|
+
@locked = !!locked
|
16
|
+
end
|
17
|
+
|
18
|
+
def fetch
|
19
|
+
cloned? ? pull : clone
|
20
|
+
end
|
21
|
+
|
22
|
+
def prepare
|
23
|
+
@before.each { |cmd| Shell.run!(cmd, pwd: clone_path) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def remove
|
27
|
+
cloned? && Shell.run!(%W[rm -rf #{clone_path}])
|
28
|
+
end
|
29
|
+
|
30
|
+
def clone_path
|
31
|
+
File.join(store_dir, name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def bundle
|
35
|
+
@bundle ||= Gouteur::Bundle.new(clone_path)
|
36
|
+
end
|
37
|
+
|
38
|
+
def locked?
|
39
|
+
@locked
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def extract_name_from_uri(uri)
|
45
|
+
uri[%r{git(?:hub|lab)\.com/[^/]+/([^/]+)}, 1] ||
|
46
|
+
uri.split('/').last.to_s[/[a-z0-9\-_]+/i] ||
|
47
|
+
raise(Error, 'could not determine repository name from uri')
|
48
|
+
end
|
49
|
+
|
50
|
+
def cloned?
|
51
|
+
File.exist?(clone_path)
|
52
|
+
end
|
53
|
+
|
54
|
+
def pull
|
55
|
+
Shell.run!(%w[git fetch origin], pwd: clone_path)
|
56
|
+
Shell.run!(%w[git reset --hard --quiet], pwd: clone_path)
|
57
|
+
Shell.run!(%w[git clean -f -d -x], pwd: clone_path)
|
58
|
+
Shell.run!(%w[git pull --ff-only --quiet], pwd: clone_path)
|
59
|
+
end
|
60
|
+
|
61
|
+
def clone
|
62
|
+
Shell.run!(%W[mkdir -p #{store_dir}])
|
63
|
+
Shell.run!(%W[git clone --quiet #{uri} #{clone_path}])
|
64
|
+
Shell.run!(%W[git checkout #{ref}], pwd: clone_path) if ref
|
65
|
+
end
|
66
|
+
|
67
|
+
def store_dir
|
68
|
+
File.join(Host.root, 'tmp', 'gouteur_repos')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Gouteur
|
4
|
+
# thin wrapper for Open3
|
5
|
+
module Shell
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def run(args, pwd: Dir.pwd, env: {})
|
9
|
+
stdout, stderr, status = begin
|
10
|
+
Bundler.with_original_env { Open3.capture3(env, *args, chdir: pwd) }
|
11
|
+
rescue Errno::ENOENT => e
|
12
|
+
# bring errors such as "command not found: bundle" into the same form
|
13
|
+
['', e.message, $?]
|
14
|
+
end
|
15
|
+
|
16
|
+
Result.new(
|
17
|
+
args: args, pwd: pwd, stdout: stdout, stderr: stderr, status: status
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def run!(args, pwd: Dir.pwd, env: {})
|
22
|
+
result = run(args, pwd: pwd, env: env)
|
23
|
+
result.success? || raise(Error, result.full_error)
|
24
|
+
result
|
25
|
+
end
|
26
|
+
|
27
|
+
# return value object of Shell methods
|
28
|
+
class Result
|
29
|
+
attr_reader :stdout, :stderr, :exitstatus
|
30
|
+
|
31
|
+
def initialize(args:, pwd:, stdout:, stderr:, status:)
|
32
|
+
@args = args
|
33
|
+
@pwd = pwd
|
34
|
+
@stdout = stdout
|
35
|
+
@stderr = stderr
|
36
|
+
@exitstatus = status.exitstatus
|
37
|
+
end
|
38
|
+
|
39
|
+
def success?
|
40
|
+
exitstatus.zero?
|
41
|
+
end
|
42
|
+
|
43
|
+
def full_error
|
44
|
+
return nil if success?
|
45
|
+
|
46
|
+
Message.shell_error(args: @args, pwd: @pwd, stderr: stderr)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gouteur
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Janosch Müller
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-22 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Run tests of dependent gems against your changes.
|
14
|
+
email:
|
15
|
+
- janosch84@gmail.com
|
16
|
+
executables:
|
17
|
+
- gouteur
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- ".github/workflows/build.yml"
|
22
|
+
- ".github/workflows/lint.yml"
|
23
|
+
- ".gitignore"
|
24
|
+
- ".gouteur.yml"
|
25
|
+
- ".rspec"
|
26
|
+
- ".rubocop.yml"
|
27
|
+
- CHANGELOG.md
|
28
|
+
- Gemfile
|
29
|
+
- Gemfile.lock
|
30
|
+
- LICENSE.txt
|
31
|
+
- README.md
|
32
|
+
- Rakefile
|
33
|
+
- bin/console
|
34
|
+
- bin/setup
|
35
|
+
- exe/gouteur
|
36
|
+
- gouteur.gemspec
|
37
|
+
- lib/gouteur.rb
|
38
|
+
- lib/gouteur/bundle.rb
|
39
|
+
- lib/gouteur/checker.rb
|
40
|
+
- lib/gouteur/cli.rb
|
41
|
+
- lib/gouteur/dotfile.rb
|
42
|
+
- lib/gouteur/host.rb
|
43
|
+
- lib/gouteur/message.rb
|
44
|
+
- lib/gouteur/rake_task.rb
|
45
|
+
- lib/gouteur/repo.rb
|
46
|
+
- lib/gouteur/shell.rb
|
47
|
+
- lib/gouteur/version.rb
|
48
|
+
homepage: https://github.com/jaynetics/gouteur
|
49
|
+
licenses:
|
50
|
+
- MIT
|
51
|
+
metadata:
|
52
|
+
homepage_uri: https://github.com/jaynetics/gouteur
|
53
|
+
source_code_uri: https://github.com/jaynetics/gouteur
|
54
|
+
changelog_uri: https://github.com/jaynetics/gouteur/blob/master/CHANGELOG.md
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options: []
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: 2.5.0
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
requirements: []
|
70
|
+
rubygems_version: 3.2.3
|
71
|
+
signing_key:
|
72
|
+
specification_version: 4
|
73
|
+
summary: See if your lib is still digestible.
|
74
|
+
test_files: []
|