Skip to content

Commit

Permalink
✨ Allow behavior to be controlled by ENV variables (#4)
Browse files Browse the repository at this point in the history
* 📝 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)
  • Loading branch information
pboling authored Feb 23, 2025
1 parent 84c4fab commit 6e48bc6
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 30 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
120 changes: 113 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -88,32 +89,137 @@ 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
```

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<VERSION>"` 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]
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") == 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:
#
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") == 0
if VersionGem::Ruby.gte_minimum_version?("2.7")
require "debug"
else
require "byebug"
end
end
Loading

0 comments on commit 6e48bc6

Please sign in to comment.