From 6e48bc6c7c041d00559f56d6add8c4035c506e5f Mon Sep 17 00:00:00 2001 From: Peter Boling Date: Mon, 24 Feb 2025 01:32:21 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Allow=20behavior=20to=20be=20contro?= =?UTF-8?q?lled=20by=20ENV=20variables=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Documentation * 📝 Documentation * 📝 Documentation * 📝 Documentation * ✨ Allow behavior to be controlled by ENV variables - GEM_CHECKSUMS_GIT_DRY_RUN : default "false" - GEM_CHECKSUMS_CHECKSUMS_DIR : default "checksums" - GEM_CHECKSUMS_PACKAGE_DIR : default "pkg" * 🐛 Compatibility with Ruby <= v2.3 (String#casecmp) * 🐛 Compatibility with Ruby <= v2.3 (String#casecmp) --- .envrc | 6 +- .github/workflows/coverage.yml | 4 +- .gitignore | 2 +- .rubocop.yml | 9 ++ CHANGELOG.md | 6 +- Gemfile | 2 +- README.md | 120 +++++++++++++++- lib/gem_checksums.rb | 47 ++++-- spec/config/byebug.rb | 8 +- spec/gem_checksums/tasks_spec.rb | 135 ++++++++++++++++++ spec/gem_checksums_spec.rb | 4 + spec/spec_helper.rb | 11 +- spec/support/fixtures/gem_checksums-1.0.0.gem | Bin 0 -> 15360 bytes spec/support/shared_contexts/with_rake.rb | 26 ++++ 14 files changed, 350 insertions(+), 30 deletions(-) create mode 100644 spec/gem_checksums/tasks_spec.rb create mode 100644 spec/support/fixtures/gem_checksums-1.0.0.gem create mode 100644 spec/support/shared_contexts/with_rake.rb diff --git a/.envrc b/.envrc index dfcfb3b..fc2facb 100644 --- a/.envrc +++ b/.envrc @@ -20,14 +20,14 @@ export K_SOUP_COV_DO=true # Means you want code coverage # Available formats are html, xml, rcov, lcov, json, tty export K_SOUP_COV_COMMAND_NAME="RSpec Coverage" export K_SOUP_COV_FORMATTERS="html,tty" -export K_SOUP_COV_MIN_BRANCH=50 # Means you want to enforce X% branch coverage -export K_SOUP_COV_MIN_LINE=60 # Means you want to enforce X% line coverage +export K_SOUP_COV_MIN_BRANCH=79 # Means you want to enforce X% branch coverage +export K_SOUP_COV_MIN_LINE=98 # Means you want to enforce X% line coverage export K_SOUP_COV_MIN_HARD=true # Means you want the build to fail if the coverage thresholds are not met export K_SOUP_COV_MULTI_FORMATTERS=true export MAX_ROWS=1 # Setting for simplecov-console gem for tty output, limits to the worst N rows of bad coverage # Internal Debugging Controls -export DEBUG=false # do not allow byebug statements (override in .env.local) +export DEBUG=true # do not allow byebug statements (override in .env.local) # .env would override anything in this file, if `dotenv` is uncommented below. # .env is a DOCKER standard, and if we use it, it would be in deployed, or DOCKER, environments, diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 538e679..377a070 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,8 +1,8 @@ name: Test Coverage env: - K_SOUP_COV_MIN_BRANCH: 50 - K_SOUP_COV_MIN_LINE: 60 + K_SOUP_COV_MIN_BRANCH: 79 + K_SOUP_COV_MIN_LINE: 98 K_SOUP_COV_MIN_HARD: true K_SOUP_COV_DO: true K_SOUP_COV_COMMAND_NAME: "RSpec Coverage" diff --git a/.gitignore b/.gitignore index ef31709..9a1c702 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,6 @@ /.idea/ # Packaging Artifacts -*.gem +/pkg/*.gem gemfiles/*.gemfile.lock Appraisal.*.gemfile.lock diff --git a/.rubocop.yml b/.rubocop.yml index 6de99cd..7b09f96 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,2 +1,11 @@ inherit_gem: rubocop-lts: config/rubygem_rspec.yml + +RSpec/NestedGroups: + Enabled: false + +RSpec/MultipleMemoizedHelpers: + Enabled: false + +RSpec/ExampleLength: + Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 32a8a32..97c1d57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,9 +22,9 @@ Please file a bug if you notice a violation of semantic versioning. ### Fixed ### Removed -## [1.0.0] - 2025-01-23 ([tag][1.0.0t]) -- Line Coverage: 60.34% (35 / 58) -- Branch Coverage: 50.0% (6 / 12) +## [1.0.0] - 2025-02-23 ([tag][1.0.0t]) +- COVERAGE: 98.63% -- 72/73 lines in 4 files +- BRANCH COVERAGE: 79.17% -- 19/24 branches in 4 files - 55.56% documented ### Added - Initial release diff --git a/Gemfile b/Gemfile index 1c99398..00bfc49 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ gemspec platform :mri do # Debugging - Ensure ENV["DEBUG"] == "true" to use debuggers within spec suite - if ruby_version < Gem::Version.new("2.7") + if ruby_version < Gem::Version.create("2.7") # Use byebug in code gem "byebug", ">= 11" else diff --git a/README.md b/README.md index e9cbc86..8757959 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,12 @@ [![Donate to my FLOSS or refugee efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS or refugee efforts using Patreon][🖇patreon-img]][🖇patreon] -A ruby script, and rake task, to generate SHA-256 and SHA-512 checksums of RubyGem libraries, shipped as a RubyGem. +A ruby shell script, and rake task, to generate SHA-256 and SHA-512 checksums of RubyGem libraries, +shipped as a RubyGem. You may be familiar with the standard rake task `build:checksum` from RubyGems. This gem ships an improved version as `build:checksums`, based on the -[RubyGems pull request](https://github.com/rubygems/rubygems/pull/6022) I started in October 2022. +[RubyGems pull request][🔒️rubygems-checksums-pr] I started in October 2022. ```shell rake build:checksums @@ -88,11 +89,30 @@ gem install gem_checksums ## Usage -You may be familiar with the standard rake task `build:checksum` from RubyGems. -This gem ships an improved version as `build:checksums`, based on the -[RubyGems pull request and discussion here][🔒️rubygems-checksums-pr]. +Once installed you can use the shell script without any changes to your code. + +```shell +# prepend with `bundle exec` if gem was added to Gemfile instead of installed globally +gem_checksums +``` + +However, if you want to use the bundled rake task you'll need to add it to your Rakefile first. + +```ruby +begin + require "gem_checksums" + GemChecksums.install_tasks +rescue LoadError + task("build:checksums") do + warn("gem_checksums is not available") + end +end +``` + +Then you can do: ```shell +# prepend with `bundle exec` if gem was added to Gemfile instead of installed globally rake build:checksums ``` @@ -100,20 +120,106 @@ It is different from, and improves on, the standard rake task in that it: - does various checks to ensure the generated checksums will be valid - does `git commit` the generated checksums -You can alternatively use the shell script if `rake## 🔐 Security +### ENV variables + +Behavior can be controlled by ENV variables! + +- `GEM_CHECKSUMS_GIT_DRY_RUN` default value is `false` + - when `true` the `git commit` command will run with `--dry-run` flag + - when `true` the checksum files will be unstaged and deleted +- `GEM_CHECKSUMS_CHECKSUMS_DIR` default value is `checksums` (relative path) + - this directory will be created, relative to current working directory, if not present +- `GEM_CHECKSUMS_PACKAGE_DIR` default value is `pkg` (relative path) + - this directory will be searched for the latest gem package to generate checksums for + +### ARGV + +If an argument is provided to the rake task it should be the path to +a specific `.gem` package you want to generate checksums for. + +The script version does not accept arguments, and should be controlled by the ENV variables if needed. + +### How To: Release gem with checksums generated by `gem_checksums` + +Generating checksums makes sense when you are building and releasing a gem, so how does it fit into that process? + +NOTE: This is an example process which assumes your project has bundler binstubs, and a version.rb file, +with notes for `zsh` and `bash` shells. + +1. Run `bin/setup && bin/rake` as a tests, coverage, & linting sanity check +2. Update the version number in `version.rb` +3. Run `bin/setup && bin/rake` again as a secondary check, and to update `Gemfile.lock` +4. Run `git commit -am "🔖 Prepare release v"` to commit the changes +5. Run `git push` to trigger the final CI pipeline before release, & merge PRs + - NOTE: Remember to [check your project's CI][🧪build]! +6. Run `export GIT_TRUNK_BRANCH_NAME="$(git remote show origin | grep 'HEAD branch' | cut -d ' ' -f5)" && echo $GIT_TRUNK_BRANCH_NAME` +7. Run `git checkout $GIT_TRUNK_BRANCH_NAME` +8. Run `git pull origin $GIT_TRUNK_BRANCH_NAME` to ensure you will release the latest trunk code +9. Set `SOURCE_DATE_EPOCH` so `rake build` and `rake release` use same timestamp, and generate same checksums + - Run `export SOURCE_DATE_EPOCH=$EPOCHSECONDS && echo $SOURCE_DATE_EPOCH` + - If the echo above has no output, then it didn't work. + - Note that you'll need the `zsh/datetime` module, if running `zsh`. + - In `bash` you can use `date +%s` instead, i.e. `export SOURCE_DATE_EPOCH=$(date +%s) && echo $SOURCE_DATE_EPOCH` +10. Run `bundle exec rake build` +11. Run `gem_checksums` (from this gem, and added to path in .envrc, + more context [1][🔒️rubygems-checksums-pr] and [2][🔒️rubygems-guides-pr]) to create SHA-256 and SHA-512 checksums + - Checksums will be committed automatically by the script, but not pushed +12. Run `bundle exec rake release` which will create a git tag for the version, + push git commits and tags, and push the `.gem` file to [rubygems.org][💎rubygems] + +[🧪build]: https://github.com/pboling/gem_checksums/actions +[💎rubygems]: https://rubygems.org +[🔒️rubygems-security-guide]: https://guides.rubygems.org/security/#building-gems +[🔒️rubygems-checksums-pr]: https://github.com/rubygems/rubygems/pull/6022 +[🔒️rubygems-guides-pr]: https://github.com/rubygems/guides/pull/325 + +### Too many steps? + +If you don't follow the steps above you'll end up seeing this error: + +``` +WARNING: Build time not provided via environment variable SOURCE_DATE_EPOCH. + To ensure consistent SHA-256 & SHA-512 checksums, + you must set this environment variable *before* building the gem. + +IMPORTANT: After setting the build time, you *must re-build the gem*, i.e. bundle exec rake build, or gem build. + +How to set the build time: + +In zsh shell: + - export SOURCE_DATE_EPOCH=$EPOCHSECONDS && echo $SOURCE_DATE_EPOCH + - If the echo above has no output, then it didn't work. + - Note that you'll need the `zsh/datetime` module enabled. + +In fish shell: + - set -x SOURCE_DATE_EPOCH (date +%s) + - echo $SOURCE_DATE_EPOCH + +In bash shell: + - export SOURCE_DATE_EPOCH=$(date +%s) && echo $SOURCE_DATE_EPOCH` +``` + +Just do what it says! + +## 🔐 Security See [SECURITY.md][🔐security]. ## 🤝 Contributing If you need some ideas of where to help, you could work on adding more code coverage, -or if it is already 💯 (see [below](#code-coverage)) then check [issues][🤝issues], or [PRs][🤝pulls], +or if it is already 💯 (see [below](#code-coverage)) check TODOs (see [below](#todos)), +or check [issues][🤝issues], or [PRs][🤝pulls], or use the gem and think about how it could be better. We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it. See [CONTRIBUTING.md][🤝contributing] for more detailed instructions. +### TODOs + +- [ ] Prepend `rake build` task with check for `SOURCE_DATE_EPOCH` environment variable, and raise error if not set. + ### Code Coverage [![Coverage Graph][🔑codecov-g♻️]][🔑codecov] diff --git a/lib/gem_checksums.rb b/lib/gem_checksums.rb index 7023a12..a546a4e 100644 --- a/lib/gem_checksums.rb +++ b/lib/gem_checksums.rb @@ -22,6 +22,9 @@ class Error < StandardError; end VERSION_REGEX = /((\d+\.\d+\.\d+)([-.][0-9A-Za-z-]+)*)(?=\.gem)/ RUNNING_AS = File.basename($PROGRAM_NAME) BUILD_TIME_ERROR_MESSAGE = "Environment variable SOURCE_DATE_EPOCH must be set. You'll need to rebuild the gem. See gem_checksums/README.md" + GIT_DRY_RUN_ENV = ENV.fetch("GEM_CHECKSUMS_GIT_DRY_RUN", "false").casecmp("true") == 0 + CHECKSUMS_DIR = ENV.fetch("GEM_CHECKSUMS_CHECKSUMS_DIR", "checksums") + PACKAGE_DIR = ENV.fetch("GEM_CHECKSUMS_PACKAGE_DIR", "pkg") # Make this gem's rake tasks available in your Rakefile: # @@ -37,9 +40,11 @@ def install_tasks # NOTE: SOURCE_DATE_EPOCH must be set in your environment prior to building the gem. # This ensures that the gem build, and the gem checksum will use the same timestamp, # and thus will match the SHA-256 checksum generated for every gem on Rubygems.org. - def generate + def generate(git_dry_run: false) build_time = ENV.fetch("SOURCE_DATE_EPOCH", "") build_time_missing = !(build_time =~ /\d{10,}/) + git_dry_run_flag = (git_dry_run || GIT_DRY_RUN_ENV) ? "--dry-run" : nil + warn("Will run git commit with --dry-run") if git_dry_run_flag if build_time_missing warn( @@ -83,9 +88,11 @@ def generate gem_pkg = File.join(gem_path_parts) puts "Looking for: #{gem_pkg.inspect}" gems = Dir[gem_pkg] + raise Error, "Unable to find gem #{gem_pkg}" if gems.empty? + puts "Found: #{gems.inspect}" else - gem_pkgs = File.join("pkg", "*.gem") + gem_pkgs = File.join(PACKAGE_DIR, "*.gem") puts "Looking for: #{gem_pkgs.inspect}" gems = Dir[gem_pkgs] raise Error, "Unable to find gems #{gem_pkgs}" if gems.empty? @@ -103,21 +110,33 @@ def generate # SHA-512 digest is 8 64-bit words digest512_64bit = Digest::SHA512.new.hexdigest(pkg_bits) - digest512_64bit_path = "checksums/#{gem_name}.sha512" + digest512_64bit_path = "#{CHECKSUMS_DIR}/#{gem_name}.sha512" + Dir.mkdir(CHECKSUMS_DIR) unless Dir.exist?(CHECKSUMS_DIR) File.write(digest512_64bit_path, digest512_64bit) # SHA-256 digest is 8 32-bit words digest256_32bit = Digest::SHA256.new.hexdigest(pkg_bits) - digest256_32bit_path = "checksums/#{gem_name}.sha256" + digest256_32bit_path = "#{CHECKSUMS_DIR}/#{gem_name}.sha256" File.write(digest256_32bit_path, digest256_32bit) version = gem_name[VERSION_REGEX] - git_cmd = <<-GIT_MSG -git add checksums/* && \ -git commit -m "🔒️ Checksums for v#{version}" + git_cmd = <<-GIT_MSG.rstrip +git add #{CHECKSUMS_DIR}/* && \ +git commit #{git_dry_run_flag} -m "🔒️ Checksums for v#{version}" GIT_MSG + if git_dry_run_flag + git_cmd += <<-CLEANUP_MSG + && \ +echo "Cleaning up in dry run mode" && \ +git reset #{digest512_64bit_path} && \ +git reset #{digest256_32bit_path} && \ +rm -f #{digest512_64bit_path} && \ +rm -f #{digest256_32bit_path} + CLEANUP_MSG + end + puts <<-RESULTS [ GEM: #{gem_name} ] [ VERSION: #{version} ] @@ -132,10 +151,16 @@ def generate #{git_cmd} RESULTS - # This will replace the current process with the git process, and exit. - # Within the generate method, Ruby code placed after the `exec` *will not be run*: - # See: https://www.akshaykhot.com/call-shell-commands-in-ruby - exec(git_cmd) + if git_dry_run_flag + %x{#{git_cmd}} + else + # `exec` will replace the current process with the git process, and exit. + # Within the generate method, Ruby code placed after the `exec` *will not be run*: + # See: https://www.akshaykhot.com/call-shell-commands-in-ruby + # But we can't exit the process when testing from RSpec, + # since that would exit the parent RSpec process + exec(git_cmd) + end end module_function :generate end diff --git a/spec/config/byebug.rb b/spec/config/byebug.rb index 8acba80..ae3fbaf 100644 --- a/spec/config/byebug.rb +++ b/spec/config/byebug.rb @@ -1 +1,7 @@ -require "byebug" if VersionGem::Ruby.gte_minimum_version?("2.7") && ENV.fetch("DEBUG", "false").casecmp?("true") +if ENV.fetch("DEBUG", "false").casecmp("true") == 0 + if VersionGem::Ruby.gte_minimum_version?("2.7") + require "debug" + else + require "byebug" + end +end diff --git a/spec/gem_checksums/tasks_spec.rb b/spec/gem_checksums/tasks_spec.rb new file mode 100644 index 0000000..7b96067 --- /dev/null +++ b/spec/gem_checksums/tasks_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require "rake" + +RSpec.describe "rake build:checksums" do # rubocop:disable RSpec/DescribeClass + subject(:build_checksums) { invoke } + + include_context "with rake", "gem_checksums" + + context "with SOURCE_DATE_EPOCH set" do + before do + stub_env("SOURCE_DATE_EPOCH" => "1738472935") + end + + context "when running as Rake without arguments" do + before do + stub_const("GemChecksums::RUNNING_AS", "rake") + stub_const("ARGV", []) + end + + it "raises an error" do + block_is_expected.to raise_error(GemChecksums::Error, "Unable to find gems pkg/*.gem") + end + + context "when good package directory" do + let(:pkg_dir) { "spec/support/fixtures" } + + before do + stub_const("GemChecksums::RUNNING_AS", "rake") + stub_const("GemChecksums::PACKAGE_DIR", pkg_dir) + end + + it "does not raise an error" do + block_is_expected.to not_raise_error + end + + context "with output" do + it "prints information" do + block_is_expected.to output(<<-CHECKSUMS_OUTPUT).to_stdout +Looking for: "spec/support/fixtures/*.gem" +Found: 1 gems; latest is gem_checksums-1.0.0.gem +[ GEM: gem_checksums-1.0.0.gem ] +[ VERSION: 1.0.0 ] +[ GEM PKG LOCATION: spec/support/fixtures/gem_checksums-1.0.0.gem ] +[ CHECKSUM SHA-256: 8e35e0a7cae7fab47c0b7e3d3e81d38294151138bcfcfe8d4aa2e971ff302339 ] +[ CHECKSUM SHA-512: 8b18f8422f22a5a3b301deeaeb8b4e669dfc8033a63c1b5fbfc787af59f60e9df7f920ae2c48ede27ef7d09f0b025f63fc2474a842d54d8f148b266eac4bfa94 ] +[ CHECKSUM SHA-256 PATH: checksums/gem_checksums-1.0.0.gem.sha256 ] +[ CHECKSUM SHA-512 PATH: checksums/gem_checksums-1.0.0.gem.sha512 ] + +... Running ... + +git add checksums/* && git commit --dry-run -m "🔒️ Checksums for v1.0.0" && echo "Cleaning up in dry run mode" && git reset checksums/gem_checksums-1.0.0.gem.sha512 && git reset checksums/gem_checksums-1.0.0.gem.sha256 && rm -f checksums/gem_checksums-1.0.0.gem.sha512 && rm -f checksums/gem_checksums-1.0.0.gem.sha256 + + CHECKSUMS_OUTPUT + end + end + end + end + + context "when running as Rake with good arguments" do + let(:gem_fixture) { "spec/support/fixtures/gem_checksums-1.0.0.gem" } + + before do + stub_const("GemChecksums::RUNNING_AS", "rake") + stub_const("ARGV", [gem_fixture]) + end + + it "does not raise an error" do + block_is_expected.to not_raise_error + end + + context "with output" do + it "prints information" do + block_is_expected.to output(<<-CHECKSUMS_OUTPUT).to_stdout +Looking for: "spec/support/fixtures/gem_checksums-1.0.0.gem" +Found: ["spec/support/fixtures/gem_checksums-1.0.0.gem"] +[ GEM: gem_checksums-1.0.0.gem ] +[ VERSION: 1.0.0 ] +[ GEM PKG LOCATION: spec/support/fixtures/gem_checksums-1.0.0.gem ] +[ CHECKSUM SHA-256: 8e35e0a7cae7fab47c0b7e3d3e81d38294151138bcfcfe8d4aa2e971ff302339 ] +[ CHECKSUM SHA-512: 8b18f8422f22a5a3b301deeaeb8b4e669dfc8033a63c1b5fbfc787af59f60e9df7f920ae2c48ede27ef7d09f0b025f63fc2474a842d54d8f148b266eac4bfa94 ] +[ CHECKSUM SHA-256 PATH: checksums/gem_checksums-1.0.0.gem.sha256 ] +[ CHECKSUM SHA-512 PATH: checksums/gem_checksums-1.0.0.gem.sha512 ] + +... Running ... + +git add checksums/* && git commit --dry-run -m "🔒️ Checksums for v1.0.0" && echo "Cleaning up in dry run mode" && git reset checksums/gem_checksums-1.0.0.gem.sha512 && git reset checksums/gem_checksums-1.0.0.gem.sha256 && rm -f checksums/gem_checksums-1.0.0.gem.sha512 && rm -f checksums/gem_checksums-1.0.0.gem.sha256 + + CHECKSUMS_OUTPUT + end + end + + context "with no checksums directory" do + let(:checksums_dir) { "snooty_checksums" } + + before do + stub_const("GemChecksums::CHECKSUMS_DIR", checksums_dir) + Dir.rmdir(checksums_dir) if Dir.exist?(checksums_dir) + end + + after do + Dir.rmdir(checksums_dir) if Dir.exist?(checksums_dir) + end + + it "creates the checksums directory" do + build_checksums + expect(Dir.exist?(checksums_dir)).to be true + end + end + end + + context "when running as Rake with bad arguments" do + let(:gem_fixture) { "spec/support/fixtures/unreal-1.0.0.gem" } + + before do + stub_const("GemChecksums::RUNNING_AS", "rake") + stub_const("ARGV", [gem_fixture]) + end + + it "raises an error" do + block_is_expected.to raise_error(GemChecksums::Error, "Unable to find gem #{gem_fixture}") + end + end + end + + context "without SOURCE_DATE_EPOCH set" do + before do + stub_env("SOURCE_DATE_EPOCH" => "") + end + + it "raises an error" do + block_is_expected.to raise_error(GemChecksums::Error, "Environment variable SOURCE_DATE_EPOCH must be set. You'll need to rebuild the gem. See gem_checksums/README.md") + end + end +end diff --git a/spec/gem_checksums_spec.rb b/spec/gem_checksums_spec.rb index 0936504..9727cda 100644 --- a/spec/gem_checksums_spec.rb +++ b/spec/gem_checksums_spec.rb @@ -36,6 +36,10 @@ describe "::install_tasks" do subject(:install_tasks) { described_class.install_tasks } + before do + Rake.application = Rake::Application.new + end + it "loads gem_checksums/tasks.rb" do # The order is important, spec will fail if wrong order block_is_expected.to not_raise_error & diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 27b635b..e1a1c2b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -11,15 +11,24 @@ require_relative "config/rspec/rspec_core" require_relative "config/rspec/version_gem" +# Support +require_relative "support/shared_contexts/with_rake" + # Last thing before loading this gem is to set up code coverage begin # This does not require "simplecov", but require "kettle-soup-cover" # this next line has a side effect of running `.simplecov` - require "simplecov" if Kettle::Soup::Cover::DO_COV + require "simplecov" if defined?(Kettle) && Kettle::Soup::Cover::DO_COV rescue LoadError nil end # This gem require "gem_checksums" + +RSpec.configure do |config| + config.before do + stub_const("GemChecksums::GIT_DRY_RUN_ENV", true) + end +end diff --git a/spec/support/fixtures/gem_checksums-1.0.0.gem b/spec/support/fixtures/gem_checksums-1.0.0.gem new file mode 100644 index 0000000000000000000000000000000000000000..9e2bebc1d8ccb0290b4c37c934bfa0bf89c2805b GIT binary patch literal 15360 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