Skip to content

Commit

Permalink
✨ Allow behavior to be controlled by ENV variables
Browse files Browse the repository at this point in the history
- GEM_CHECKSUMS_GIT_DRY_RUN : default "false"
- GEM_CHECKSUMS_CHECKSUMS_DIR : default "checksums"
- GEM_CHECKSUMS_PACKAGE_DIR : default "pkg"
  • Loading branch information
pboling committed Feb 23, 2025
1 parent d3c2592 commit 666bcf5
Show file tree
Hide file tree
Showing 14 changed files with 257 additions and 24 deletions.
6 changes: 3 additions & 3 deletions .envrc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
/.idea/

# Packaging Artifacts
*.gem
/pkg/*.gem
gemfiles/*.gemfile.lock
Appraisal.*.gemfile.lock
9 changes: 9 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
inherit_gem:
rubocop-lts: config/rubygem_rspec.yml

RSpec/NestedGroups:
Enabled: false

RSpec/MultipleMemoizedHelpers:
Enabled: false

RSpec/ExampleLength:
Enabled: false
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,29 @@ 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

Generating checksums makes sense when you are building and releasing a gem, so how does it fit into that process?
### 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.

Expand Down
47 changes: 36 additions & 11 deletions lib/gem_checksums.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
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:
#
Expand All @@ -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(
Expand Down Expand Up @@ -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?
Expand All @@ -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} ]
Expand All @@ -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
Expand Down
8 changes: 7 additions & 1 deletion spec/config/byebug.rb
Original file line number Diff line number Diff line change
@@ -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")
if VersionGem::Ruby.gte_minimum_version?("2.7")
require "debug"
else
require "byebug"
end
end
135 changes: 135 additions & 0 deletions spec/gem_checksums/tasks_spec.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions spec/gem_checksums_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 &
Expand Down
11 changes: 10 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Binary file added spec/support/fixtures/gem_checksums-1.0.0.gem
Binary file not shown.
Loading

0 comments on commit 666bcf5

Please sign in to comment.