knapsack_pro 8.1.1 → 8.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ad155a65c67bffcb3dad75d114396d2e53cc8896f21a84de7325a56f9a582ba
4
- data.tar.gz: 6a6e0958d5596b150cdc6ee46453d5ff9991255e98113990015e34a8fef8aa1d
3
+ metadata.gz: f23120ba50c0ecf87cb9812a1238debd7d723c59e87fd006b93e1bbed2077cc1
4
+ data.tar.gz: 65758d5c380cb93332756af1ce05c1ad917d59340d5ca0e3c5fb563783b56059
5
5
  SHA512:
6
- metadata.gz: ee9feec10df81e17331b3664af0a30f8d75c4d436bb9c897829588518bb3468e5c00a3729d0e5c9f731ce535ab2b02dc5747a52a743e400bd49ffd046bb7ef51
7
- data.tar.gz: d982189a7880df48104810628f1ff6e416c420325ace9fc2827c988c0289bfe27d2e78ff4bdffd94fe20891e37333805f2d530ab40aa822632847daed5b9596d
6
+ metadata.gz: e54574fe5e0656917394d6246ab61f0b4840262907c35fff7c824f2a8ed854b9b072a30968d82230b4e6454d7683fdb095bba31a21bf4458361ea399e0a93653
7
+ data.tar.gz: 8a5030dd9184aa6d85330f884d849efdc3037e43614736386c718c041170553fdf1aadfe00331f748f2111fec299bd9b8332aa5e14719c241f1761c02c3b8bf4
data/.circleci/config.yml CHANGED
@@ -6,8 +6,8 @@ commands:
6
6
  - checkout
7
7
  - restore_cache:
8
8
  keys:
9
- - v1-bundler-ruby-{{ checksum "knapsack_pro.gemspec" }}
10
- - v1-bundler-ruby-
9
+ - v1-bundler-ruby-{{ checksum "knapsack_pro.gemspec" }}
10
+ - v1-bundler-ruby-
11
11
  - run:
12
12
  command: |
13
13
  bundle config set --local path './vendor/bundle'
@@ -37,10 +37,10 @@ commands:
37
37
  fi
38
38
  - restore_cache:
39
39
  keys:
40
- - v1-bundler-rails-{{ checksum "Gemfile.lock" }}-ruby-<< parameters.ruby >>-rspec-<< parameters.rspec >>
41
- - v1-bundler-rails-{{ checksum "Gemfile.lock" }}-ruby-<< parameters.ruby >>-
42
- - v1-bundler-rails-{{ checksum "Gemfile.lock" }}-
43
- - v1-bundler-rails-
40
+ - v1-bundler-rails-{{ checksum "Gemfile.lock" }}-ruby-<< parameters.ruby >>-rspec-<< parameters.rspec >>
41
+ - v1-bundler-rails-{{ checksum "Gemfile.lock" }}-ruby-<< parameters.ruby >>-
42
+ - v1-bundler-rails-{{ checksum "Gemfile.lock" }}-
43
+ - v1-bundler-rails-
44
44
  - run:
45
45
  working_directory: << parameters.path >>
46
46
  command: |
@@ -57,7 +57,7 @@ jobs:
57
57
  working_directory: ~/knapsack_pro-ruby
58
58
  resource_class: small
59
59
  docker:
60
- - image: cimg/ruby:3.3
60
+ - image: cimg/ruby:3.4
61
61
  steps:
62
62
  - setup_knapsack_pro_ruby
63
63
  - run: gem install rubocop
@@ -88,10 +88,10 @@ jobs:
88
88
  fi
89
89
  - restore_cache:
90
90
  keys:
91
- - v1-bundler-gem-{{ checksum "knapsack_pro.gemspec" }}-ruby-<< parameters.ruby >>-rspec-<< parameters.rspec >>
92
- - v1-bundler-gem-{{ checksum "knapsack_pro.gemspec" }}-ruby-<< parameters.ruby >>-
93
- - v1-bundler-gem-{{ checksum "knapsack_pro.gemspec" }}-
94
- - v1-bundler-gem-
91
+ - v1-bundler-gem-{{ checksum "knapsack_pro.gemspec" }}-ruby-<< parameters.ruby >>-rspec-<< parameters.rspec >>
92
+ - v1-bundler-gem-{{ checksum "knapsack_pro.gemspec" }}-ruby-<< parameters.ruby >>-
93
+ - v1-bundler-gem-{{ checksum "knapsack_pro.gemspec" }}-
94
+ - v1-bundler-gem-
95
95
  - run:
96
96
  command: |
97
97
  bundle config set --local path './vendor/bundle'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ### UNRELEASED
4
+
5
+ ### 8.1.3
6
+
7
+ * Update `changelog_uri` in gemspec
8
+
9
+ ### 8.1.2
10
+
11
+ * Allow running RSpec with `--force-color` (and the default Split by Test Examples)
12
+
13
+ https://github.com/KnapsackPro/knapsack_pro-ruby/pull/298
14
+
15
+ https://github.com/KnapsackPro/knapsack_pro-ruby/compare/v8.1.1...v8.1.2
16
+
3
17
  ### 8.1.1
4
18
 
5
19
  * Do not load Rake tasks on behalf of the user for RSpec and Minitest in Queue Mode
@@ -784,7 +798,7 @@ https://github.com/KnapsackPro/knapsack_pro-ruby/compare/v3.3.0...v3.3.1
784
798
 
785
799
  ### 3.3.0
786
800
 
787
- * Show a JSON report file content when RSpec fails during a dry run
801
+ * Show a JSON report file content when RSpec fails during a dry run
788
802
 
789
803
  https://github.com/KnapsackPro/knapsack_pro-ruby/pull/172
790
804
 
@@ -865,7 +879,7 @@ https://github.com/KnapsackPro/knapsack_pro-ruby/compare/v2.18.0...v2.18.1
865
879
 
866
880
  ### 2.18.0
867
881
 
868
- * Do not allow to use the RSpec tag option together with the RSpec split by test examples feature in knapsack_pro gem in Regular Mode
882
+ * Do not allow to use the RSpec tag option together with the RSpec split by test examples feature in knapsack_pro gem in Regular Mode
869
883
 
870
884
  https://github.com/KnapsackPro/knapsack_pro-ruby/pull/148
871
885
 
data/knapsack_pro.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
15
15
  spec.license = 'MIT'
16
16
  spec.metadata = {
17
17
  'bug_tracker_uri' => 'https://github.com/KnapsackPro/knapsack_pro-ruby/issues',
18
- 'changelog_uri' => 'https://github.com/KnapsackPro/knapsack_pro-ruby/blob/master/CHANGELOG.md',
18
+ 'changelog_uri' => 'https://github.com/KnapsackPro/knapsack_pro-ruby/blob/main/CHANGELOG.md',
19
19
  'documentation_uri' => 'https://docs.knapsackpro.com/knapsack_pro-ruby/guide/',
20
20
  'homepage_uri' => 'https://knapsackpro.com',
21
21
  'source_code_uri' => 'https://github.com/KnapsackPro/knapsack_pro-ruby'
@@ -33,32 +33,22 @@ module KnapsackPro
33
33
  # Apply a --format option which overrides formatters from the RSpec custom option files like `.rspec`.
34
34
  cli_args = cli_args_without_formatters + cli_format + [
35
35
  '--dry-run',
36
- '--no-color',
37
36
  '--out', report_path,
38
- '--default-path', test_dir,
37
+ '--default-path', test_dir
39
38
  ] + KnapsackPro::TestFilePresenter.paths(test_file_entities)
40
- options = ::RSpec::Core::ConfigurationOptions.new(cli_args)
41
- exit_code = ::RSpec::Core::Runner.new(options).run($stderr, $stdout)
42
- if exit_code != 0
43
- debug_cmd = ([
44
- 'bundle exec rspec',
45
- ] + cli_args).join(' ')
46
-
47
- KnapsackPro.logger.error('-'*10 + ' START of actionable error message ' + '-'*50)
48
- KnapsackPro.logger.error('RSpec (with a dry-run option) had a problem generating the report with test examples for the slow test files. Here is what you can do:')
49
-
50
- KnapsackPro.logger.error("a) Please look for an error message from RSpec in the output above or below. If you don't see anything, that is fine. Sometimes RSpec does not produce any errors in the output.")
51
-
52
- KnapsackPro.logger.error("b) Check if RSpec generated the report file #{report_path}. If the report exists, it may contain an error message. Here is a preview of the report file:")
53
- KnapsackPro.logger.error(report_content || 'N/A')
54
-
55
- KnapsackPro.logger.error('c) To reproduce the error manually, please run the following RSpec command. This way, you can find out what is causing the error. Please ensure you run the command in the same environment where the error occurred. For instance, if the error happens on the CI server, you should run the command in the CI environment:')
56
- KnapsackPro.logger.error(debug_cmd)
39
+ exit_code = begin
40
+ options = ::RSpec::Core::ConfigurationOptions.new(cli_args)
41
+ ::RSpec::Core::Runner.new(options).run($stderr, $stdout)
42
+ rescue SystemExit => e
43
+ e.status
44
+ end
57
45
 
58
- KnapsackPro.logger.error('-'*10 + ' END of actionable error message ' + '-'*50)
46
+ return if exit_code.zero?
59
47
 
60
- raise 'There was a problem while generating test examples for the slow test files. Please read the actionable error message above.'
61
- end
48
+ report.fetch('messages', []).each { |message| puts message }
49
+ command = (['bundle exec rspec'] + cli_args).join(' ')
50
+ KnapsackPro.logger.error("Failed to generate the slow test files report: #{command}")
51
+ exit exit_code
62
52
  end
63
53
 
64
54
  def test_file_example_paths
@@ -92,8 +82,9 @@ module KnapsackPro
92
82
  "#{report_dir}/rspec_dry_run_json_report_node_#{KnapsackPro::Config::Env.ci_node_index}.json"
93
83
  end
94
84
 
95
- def report_content
96
- File.read(report_path) if File.exist?(report_path)
85
+ def report
86
+ return {} unless File.exist?(report_path)
87
+ JSON.parse(File.read(report_path))
97
88
  end
98
89
 
99
90
  def adapter_class
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KnapsackPro
4
- VERSION = '8.1.1'
4
+ VERSION = '8.1.3'
5
5
  end
@@ -5,7 +5,7 @@ http_interactions:
5
5
  uri: http://api.knapsackpro.test:3000/v1/build_distributions/subset
6
6
  body:
7
7
  encoding: UTF-8
8
- string: '{"fixed_test_suite_split":true,"cache_read_attempt":true,"commit_hash":"abcdefg","branch":"master","node_total":"2","node_index":"1","ci_build_id":"missing-build-id"}'
8
+ string: '{"fixed_test_suite_split":true,"cache_read_attempt":true,"commit_hash":"abcdefg","branch":"main","node_total":"2","node_index":"1","ci_build_id":"missing-build-id"}'
9
9
  headers:
10
10
  Content-Type:
11
11
  - application/json
@@ -5,7 +5,7 @@ http_interactions:
5
5
  uri: http://api.knapsackpro.test:3000/v1/build_distributions/subset
6
6
  body:
7
7
  encoding: UTF-8
8
- string: '{"fixed_test_suite_split":true,"cache_read_attempt":true,"commit_hash":"abcdefg","branch":"master","node_total":"2","node_index":"1","ci_build_id":"missing-build-id"}'
8
+ string: '{"fixed_test_suite_split":true,"cache_read_attempt":true,"commit_hash":"abcdefg","branch":"main","node_total":"2","node_index":"1","ci_build_id":"missing-build-id"}'
9
9
  headers:
10
10
  Content-Type:
11
11
  - application/json
@@ -5,7 +5,7 @@ http_interactions:
5
5
  uri: http://api.knapsackpro.test:3000/v1/build_subsets
6
6
  body:
7
7
  encoding: UTF-8
8
- string: '{"commit_hash":"abcdefg","branch":"master","node_total":"2","node_index":"1","test_files":[{"path":"a_spec.rb","time_execution":1.2},{"path":"b_spec.rb","time_execution":0.3}],"test_suite_token":"fake"}'
8
+ string: '{"commit_hash":"abcdefg","branch":"main","node_total":"2","node_index":"1","test_files":[{"path":"a_spec.rb","time_execution":1.2},{"path":"b_spec.rb","time_execution":0.3}]}'
9
9
  headers:
10
10
  Content-Type:
11
11
  - application/json
@@ -5,7 +5,7 @@ http_interactions:
5
5
  uri: http://api.knapsackpro.test:3000/v1/build_subsets
6
6
  body:
7
7
  encoding: UTF-8
8
- string: '{"commit_hash":"abcdefg","branch":"master","node_total":"2","node_index":"1","test_files":[{"path":"a_spec.rb","time_execution":1.2},{"path":"b_spec.rb","time_execution":0.3}],"test_suite_token":"3fa64859337f6e56409d49f865d13fd7"}'
8
+ string: '{"commit_hash":"abcdefg","branch":"main","node_total":"2","node_index":"1","test_files":[{"path":"a_spec.rb","time_execution":1.2},{"path":"b_spec.rb","time_execution":0.3}]}'
9
9
  headers:
10
10
  Content-Type:
11
11
  - application/json
@@ -8,7 +8,7 @@ describe 'Request API /v1/build_distributions/subset' do
8
8
  KnapsackPro::Client::API::V1::BuildDistributions.subset(
9
9
  cache_read_attempt: true,
10
10
  commit_hash: 'abcdefg',
11
- branch: 'master',
11
+ branch: 'main',
12
12
  node_total: '2',
13
13
  node_index: '1',
14
14
  test_files: [
@@ -7,7 +7,7 @@ describe 'Request API /v1/build_subsets' do
7
7
  let(:action) do
8
8
  KnapsackPro::Client::API::V1::BuildSubsets.create(
9
9
  commit_hash: 'abcdefg',
10
- branch: 'master',
10
+ branch: 'main',
11
11
  node_total: '2',
12
12
  node_index: '1',
13
13
  test_files: [
@@ -49,8 +49,8 @@ describe KnapsackPro::Config::CI::AppVeyor do
49
49
  subject { described_class.new.branch }
50
50
 
51
51
  context 'when the environment exists' do
52
- let(:env) { { 'APPVEYOR_REPO_BRANCH' => 'master' } }
53
- it { should eql 'master' }
52
+ let(:env) { { 'APPVEYOR_REPO_BRANCH' => 'main' } }
53
+ it { should eql 'main' }
54
54
  end
55
55
 
56
56
  context "when the environment doesn't exist" do
@@ -63,8 +63,8 @@ describe KnapsackPro::Config::CI::CirrusCI do
63
63
  subject { described_class.new.branch }
64
64
 
65
65
  context 'when the environment exists' do
66
- let(:env) { { 'CIRRUS_BRANCH' => 'master' } }
67
- it { should eql 'master' }
66
+ let(:env) { { 'CIRRUS_BRANCH' => 'main' } }
67
+ it { should eql 'main' }
68
68
  end
69
69
 
70
70
  context "when the environment doesn't exist" do
@@ -49,8 +49,8 @@ describe KnapsackPro::Config::CI::Codeship do
49
49
  subject { described_class.new.branch }
50
50
 
51
51
  context 'when the environment exists' do
52
- let(:env) { { 'CI_BRANCH' => 'master' } }
53
- it { should eql 'master' }
52
+ let(:env) { { 'CI_BRANCH' => 'main' } }
53
+ it { should eql 'main' }
54
54
  end
55
55
 
56
56
  context "when the environment doesn't exist" do
@@ -63,8 +63,8 @@ describe KnapsackPro::Config::CI::Heroku do
63
63
  subject { described_class.new.branch }
64
64
 
65
65
  context 'when the environment exists' do
66
- let(:env) { { 'HEROKU_TEST_RUN_BRANCH' => 'master' } }
67
- it { should eql 'master' }
66
+ let(:env) { { 'HEROKU_TEST_RUN_BRANCH' => 'main' } }
67
+ it { should eql 'main' }
68
68
  end
69
69
 
70
70
  context "when the environment doesn't exist" do
@@ -68,13 +68,13 @@ describe KnapsackPro::Config::CI::Semaphore2 do
68
68
  end
69
69
 
70
70
  context 'when both SEMAPHORE_GIT_WORKING_BRANCH and SEMAPHORE_GIT_BRANCH are set' do
71
- let(:env) { { 'SEMAPHORE_GIT_WORKING_BRANCH' => 'feature', 'SEMAPHORE_GIT_BRANCH' => 'master' } }
71
+ let(:env) { { 'SEMAPHORE_GIT_WORKING_BRANCH' => 'feature', 'SEMAPHORE_GIT_BRANCH' => 'main' } }
72
72
  it { should eql 'feature' }
73
73
  end
74
74
 
75
75
  context 'when SEMAPHORE_GIT_BRANCH is set' do
76
- let(:env) { { 'SEMAPHORE_GIT_BRANCH' => 'master' } }
77
- it { should eql 'master' }
76
+ let(:env) { { 'SEMAPHORE_GIT_BRANCH' => 'main' } }
77
+ it { should eql 'main' }
78
78
  end
79
79
 
80
80
  context "when no ENVs are set" do
@@ -63,8 +63,8 @@ describe KnapsackPro::Config::CI::Semaphore do
63
63
  subject { described_class.new.branch }
64
64
 
65
65
  context 'when the environment exists' do
66
- let(:env) { { 'BRANCH_NAME' => 'master' } }
67
- it { should eql 'master' }
66
+ let(:env) { { 'BRANCH_NAME' => 'main' } }
67
+ it { should eql 'main' }
68
68
  end
69
69
 
70
70
  context "when the environment doesn't exist" do
@@ -49,8 +49,8 @@ describe KnapsackPro::Config::CI::Travis do
49
49
  subject { described_class.new.branch }
50
50
 
51
51
  context 'when the environment exists' do
52
- let(:env) { { 'TRAVIS_BRANCH' => 'master' } }
53
- it { should eql 'master' }
52
+ let(:env) { { 'TRAVIS_BRANCH' => 'main' } }
53
+ it { should eql 'main' }
54
54
  end
55
55
 
56
56
  context "when the environment doesn't exist" do
@@ -313,8 +313,8 @@ describe KnapsackPro::Config::Env do
313
313
 
314
314
  context 'when ENV exists' do
315
315
  context 'when KNAPSACK_PRO_BRANCH has value' do
316
- before { stub_const("ENV", { 'KNAPSACK_PRO_BRANCH' => 'master' }) }
317
- it { should eq 'master' }
316
+ before { stub_const("ENV", { 'KNAPSACK_PRO_BRANCH' => 'main' }) }
317
+ it { should eq 'main' }
318
318
  end
319
319
 
320
320
  context 'when CI environment has value' do
@@ -335,22 +335,22 @@ describe KnapsackPro::Config::Env do
335
335
  end
336
336
 
337
337
  context 'when values are different' do
338
- let(:env_value) { 'master' }
338
+ let(:env_value) { 'main' }
339
339
  let(:ci_value) { 'feature-branch' }
340
340
 
341
- it { should eq 'master' }
341
+ it { should eq 'main' }
342
342
 
343
343
  it 'logs a warning' do
344
344
  expect(logger).to receive(:info).with(
345
- 'You have set the environment variable KNAPSACK_PRO_BRANCH to master which could be automatically determined from the CI environment as feature-branch.'
345
+ 'You have set the environment variable KNAPSACK_PRO_BRANCH to main which could be automatically determined from the CI environment as feature-branch.'
346
346
  )
347
347
  subject
348
348
  end
349
349
  end
350
350
 
351
351
  context 'when values are the same' do
352
- let(:env_value) { 'master' }
353
- let(:ci_value) { 'master' }
352
+ let(:env_value) { 'main' }
353
+ let(:ci_value) { 'main' }
354
354
 
355
355
  it 'does not log a warning' do
356
356
  expect(logger).not_to receive(:info)
@@ -28,7 +28,7 @@ describe KnapsackPro::RepositoryAdapters::GitAdapter do
28
28
  describe '#branches' do
29
29
  subject { described_class.new.branches }
30
30
 
31
- it { expect(subject.include?('master')).to be true }
31
+ it { expect(subject.include?('main')).to be true }
32
32
  it { expect(subject.include?(circle_branch)).to be true } if ENV['CIRCLECI']
33
33
  end
34
34
 
@@ -48,7 +48,6 @@ describe KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector do
48
48
  expect(RSpec::Core::ConfigurationOptions).to receive(:new).with(expected_args + [
49
49
  '--format', expected_format,
50
50
  '--dry-run',
51
- '--no-color',
52
51
  '--out', report_path,
53
52
  '--default-path', test_dir,
54
53
  'spec/a_spec.rb', 'spec/b_spec.rb',
@@ -78,7 +77,7 @@ describe KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector do
78
77
  end
79
78
 
80
79
  it do
81
- expect { subject }.to raise_error(RuntimeError, 'There was a problem while generating test examples for the slow test files. Please read the actionable error message above.')
80
+ expect { subject }.to raise_error(SystemExit) { |error| expect(error.status).to eq exit_code }
82
81
  end
83
82
  end
84
83
  end
@@ -123,6 +122,36 @@ describe KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector do
123
122
  expect { subject }.to raise_error("The internal KNAPSACK_PRO_RSPEC_OPTIONS environment variable is unset. Ensure it is not overridden accidentally. Otherwise, please report this as a bug: https://knapsackpro.com/perma/ruby/support")
124
123
  end
125
124
  end
125
+
126
+ context 'with --force-color' do
127
+ let(:rspec_args) { '--force-color' }
128
+ let(:expected_args) { ['--force-color'] }
129
+ let(:expected_format) { 'json' }
130
+
131
+ it_behaves_like 'generate_json_report runs RSpec::Core::Runner'
132
+ end
133
+
134
+ context 'with --no-color and --force-color' do
135
+ let(:rspec_args) { '--no-color --force-color' }
136
+
137
+ after { KnapsackPro.reset_logger! }
138
+
139
+ it do
140
+ subject_class = Class.new(KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector) do
141
+ define_method(:slow_test_files) do
142
+ [{ 'path' => 'spec/a_spec.rb' }]
143
+ end
144
+ end
145
+
146
+ expect do
147
+ KnapsackPro.logger = ::Logger.new($stdout)
148
+ subject_class.new.generate_json_report(rspec_args)
149
+ end
150
+ .to output(/Please only use one of `--force-color` and `--no-color`/).to_stderr
151
+ .and output(%r{ERROR -- : \[knapsack_pro\] Failed to generate the slow test files report: bundle exec rspec --no-color --force-color --format json --dry-run --out .knapsack_pro/test_case_detectors/rspec/rspec_dry_run_json_report_node_0.json --default-path spec spec/a_spec.rb}).to_stdout
152
+ .and raise_error(SystemExit) { |error| expect(error.status).to eq 1 }
153
+ end
154
+ end
126
155
  end
127
156
 
128
157
  describe '#test_file_example_paths' do
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: knapsack_pro
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.1.1
4
+ version: 8.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ArturT
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-10 00:00:00.000000000 Z
10
+ date: 2025-05-09 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rake
@@ -410,7 +410,7 @@ licenses:
410
410
  - MIT
411
411
  metadata:
412
412
  bug_tracker_uri: https://github.com/KnapsackPro/knapsack_pro-ruby/issues
413
- changelog_uri: https://github.com/KnapsackPro/knapsack_pro-ruby/blob/master/CHANGELOG.md
413
+ changelog_uri: https://github.com/KnapsackPro/knapsack_pro-ruby/blob/main/CHANGELOG.md
414
414
  documentation_uri: https://docs.knapsackpro.com/knapsack_pro-ruby/guide/
415
415
  homepage_uri: https://knapsackpro.com
416
416
  source_code_uri: https://github.com/KnapsackPro/knapsack_pro-ruby