gouteur 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,17 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /example_repo/.git
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
13
+
14
+ Gemfile.gouteur
15
+ Gemfile.gouteur.lock
16
+
17
+ TODO
data/.gouteur.yml ADDED
@@ -0,0 +1,2 @@
1
+ repos:
2
+ - uri: example_uri_in_dotfile
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
4
+ --exclude-pattern "**/example_*/**/*_spec.rb"
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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.0.0] - 2021-02-08
4
+
5
+ - Initial release
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
+ [![Gem Version](https://badge.fury.io/rb/gouteur.svg)](http://badge.fury.io/rb/gouteur)
4
+ [![Build Status](https://github.com/jaynetics/gouteur/workflows/build/badge.svg)](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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/gouteur ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative File.join(__dir__, '..', 'lib', 'gouteur')
4
+
5
+ success = Gouteur::CLI.call
6
+ exit(success ? 0 : 1)
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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Gouteur
2
+ VERSION = '1.0.0'.freeze
3
+ 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: []