Add cooldown to delay newly published gem#9576
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an opt-in “cooldown” (minimum release age in days) to Bundler installs/updates/resolution, using per-version created_at metadata from the v2 compact index to avoid selecting freshly published versions.
Changes:
- Introduces
cooldownconfiguration/precedence (CLI > config/env > Gemfile per-source) and wires it through sources/remotes to the resolver. - Parses and propagates
created_atfrom compact-index metadata intoEndpointSpecification, and filters resolver candidates within the cooldown window. - Documents
--cooldownacross relevant commands and adds comprehensive specs + artifice support.
Reviewed changes
Copilot reviewed 35 out of 35 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/support/shards.rb | Adds new cooldown specs to CI sharding. |
| spec/support/artifice/helpers/compact_index_cooldown.rb | Adds an artifice compact-index API variant that emits created_at. |
| spec/support/artifice/compact_index_cooldown.rb | Activates the new cooldown-aware artifice endpoint. |
| spec/install/cooldown_spec.rb | End-to-end coverage for cooldown behavior and precedence. |
| spec/bundler/source/rubygems/remote_spec.rb | Tests Remote#cooldown and #effective_cooldown. |
| spec/bundler/source_list_spec.rb | Tests per-remote cooldown storage on sources. |
| spec/bundler/settings_spec.rb | Tests that cooldown is coerced via settings. |
| spec/bundler/resolver/cooldown_spec.rb | Unit tests for resolver cooldown filtering and hinting. |
| spec/bundler/endpoint_specification_spec.rb | Tests robust parsing of created_at metadata shapes. |
| spec/bundler/dsl_spec.rb | Validates Gemfile DSL input validation for cooldown:. |
| spec/bundler/compact_index_client/parser_spec.rb | Ensures compact index parser accepts created_at metadata. |
| lib/rubygems/resolver/api_set/gem_parser.rb | Fixes parsing to avoid splitting ISO8601 values on colons. |
| bundler/lib/bundler/source/rubygems/remote.rb | Adds cooldown state to remotes and computes effective cooldown. |
| bundler/lib/bundler/source/rubygems.rb | Tracks per-remote cooldowns and passes them into Remote objects. |
| bundler/lib/bundler/source_list.rb | Allows storing cooldown when adding global rubygems remotes. |
| bundler/lib/bundler/settings.rb | Treats cooldown as a numeric setting key. |
| bundler/lib/bundler/resolver.rb | Filters candidate specs by cooldown and adds user-facing hints on failure. |
| bundler/lib/bundler/man/bundle-update.1.ronn | Documents --cooldown on bundle update. |
| bundler/lib/bundler/man/bundle-update.1 | Generated manpage update for bundle update. |
| bundler/lib/bundler/man/bundle-outdated.1.ronn | Documents --cooldown behavior/annotation on bundle outdated. |
| bundler/lib/bundler/man/bundle-outdated.1 | Generated manpage update for bundle outdated. |
| bundler/lib/bundler/man/bundle-install.1.ronn | Documents --cooldown on bundle install. |
| bundler/lib/bundler/man/bundle-install.1 | Generated manpage update for bundle install. |
| bundler/lib/bundler/man/bundle-config.1.ronn | Documents cooldown config/env and precedence rules. |
| bundler/lib/bundler/man/bundle-config.1 | Generated manpage update for bundle config. |
| bundler/lib/bundler/man/bundle-add.1.ronn | Documents --cooldown on bundle add. |
| bundler/lib/bundler/man/bundle-add.1 | Generated manpage update for bundle add. |
| bundler/lib/bundler/endpoint_specification.rb | Adds created_at field and parses it from metadata. |
| bundler/lib/bundler/dsl.rb | Adds Gemfile source ..., cooldown: N parsing + validation + wiring. |
| bundler/lib/bundler/cli/update.rb | Validates/applies --cooldown into settings for updates. |
| bundler/lib/bundler/cli/outdated.rb | Validates/applies --cooldown and annotates cooldown remaining. |
| bundler/lib/bundler/cli/install.rb | Validates/applies --cooldown into settings for installs. |
| bundler/lib/bundler/cli/common.rb | Adds shared cooldown validation helper. |
| bundler/lib/bundler/cli/add.rb | Validates/applies --cooldown for bundle add. |
| bundler/lib/bundler/cli.rb | Exposes --cooldown option on install/update/add/outdated commands. |
Comments suppressed due to low confidence (1)
bundler/lib/bundler/settings.rb:53
cooldownis treated as a numeric setting viato_i, which silently accepts malformed/negative values (e.g.BUNDLE_COOLDOWN=7daysbecomes7,-7disables filtering viadays <= 0). Since cooldown is intended as a supply-chain protection feature, this should be validated as a non-negative integer (and reject floats/garbage) when coming from config/env as well, so typos can’t silently change/disable the filter.
NUMBER_KEYS = %w[
cooldown
jobs
redirect
retry
ssl_verify_mode
timeout
].freeze
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| cooldown = options["cooldown"] | ||
| Array(options["remotes"]).reverse_each {|r| add_remote(r, cooldown: cooldown) } | ||
|
|
| when "created_at" | ||
| value = v.is_a?(Array) ? v.last : v | ||
| if value.is_a?(String) | ||
| require "time" | ||
| begin | ||
| @created_at = Time.iso8601(value) |
| # frozen_string_literal: true | ||
|
|
||
| require_relative "compact_index" | ||
|
|
d7711e0 to
0516df9
Compare
|
📝 I should inject https://github.com/ruby/rubygems/blob/master/spec/support/rubygems_ext.rb#L30 |
The upcoming compact index v2 format introduced by rubygems.org appends a `created_at:ISO8601` field to each info line, and the timestamp's internal colons were being treated as additional split delimiters. Splitting only on the first colon keeps existing keys like `ruby:>= 2.7.0` intact while letting multi-colon values pass through untouched. rubygems/rubygems.org#6504 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The cooldown feature needs each gem version's publish timestamp on the client side. Compact index v2 exposes it as a `created_at:ISO8601` entry in the info-line metadata; expose a Time-typed `created_at` attribute on the spec so the resolver can consult it later. Parsing is defensive against older rubygems whose APISet GemParser splits ISO8601 timestamps on every colon, accepting both Array and flat String shapes and silently dropping malformed values. The time stdlib is required lazily inside the case branch so loading the file does not activate the `time` default gem during Bundler.setup. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds `cooldown` to NUMBER_KEYS so that `BUNDLE_COOLDOWN` and `bundle config set cooldown` are parsed as integer days. Reading the value is enough for now; later commits plumb it into the resolver and the Gemfile DSL. #9113 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lets `source "https://rubygems.org", cooldown: 7` attach a per-remote value to the global Rubygems source, which the new `Remote#effective_cooldown` reads with CLI > config > Gemfile per-source precedence. The cooldown is stored on Source::Rubygems keyed by URI so that several top-level `source` lines can carry independent values onto the same global source. Non-negative Integer values are required at parse time; everything else (strings, floats, arrays, negative numbers) raises InvalidOption so a typo can't silently disable the filter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the CLI flag and routes it through `set_command_option_if_given` so the value lands in Bundler.settings' temporary store. This keeps the CLI > config > Gemfile precedence implicit in the existing settings layering. The resolver does not yet consult the value. #9113 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a per-version filter to `Resolver#filter_specs` that drops specs whose `created_at` falls within the effective cooldown window. The filter only runs over `@all_specs`, so lockfile-pinned versions in `@base` survive even if a newer release would now be excluded. Specs without `created_at` are kept so historical versions and indexes that do not yet expose timestamps remain usable. A shared `cooldown_now` memoization ensures every comparison within one resolve sees the same timestamp, stabilizing tests near a threshold boundary. When the resolver fails because every matching version is in cooldown, both `raise_not_found!` and `no_versions_incompatibility_for` surface a hint suggesting `--cooldown 0` to bypass; a small `cooldown_hint` helper keeps the wording in sync between the two error paths and is locked down with unit specs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
bundle outdated still surfaces newer-but-cooldown'd versions instead of hiding them, so the user knows an upgrade is pending rather than silently missing. The prose form gains ", in cooldown for Nd more days" and the table form appends "(cooldown Nd)" to the Latest column. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the `--cooldown=NUMBER` option to the install, update, add, and outdated man pages and describes the `cooldown` / `BUNDLE_COOLDOWN` setting in bundle-config, regenerating the rendered .1 files via `rake man:build`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A `CompactIndexCooldownAPI` subclass overrides `build_gem_version` to emit `CompactIndex::GemVersionV2` with `created_at` sourced from `spec.date`, letting tests carry a deterministic timestamp per built gem. `spec/install/cooldown_spec.rb` exercises the source DSL keyword, `--cooldown` flag, `BUNDLE_COOLDOWN`, `bundle config set cooldown`, the rolling-delay filter, the lockfile bypass, and the CLI vs Gemfile precedence against the new artifice. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Gemfile DSL already rejects `cooldown: -7`, but `--cooldown -7` on install/update/add/outdated slipped through Thor and ended up disabling the filter silently via the `days <= 0` short-circuit in the resolver. Add a shared CLI::Common.validate_cooldown! guard so the CLI surface fails loud with the same message as the DSL. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds three E2E checks against the v2 cooldown artifice: the negative CLI value is rejected at parse time, `bundle outdated --cooldown` tags the latest-but-cooled version in both table and parseable output, and `bundle update --cooldown 99999` surfaces the cooldown hint when every candidate is filtered. These were the remaining coverage gaps called out in the adversarial review. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add `cooldown` to delay newly published gem (cherry picked from commit 88057ed)
|
@hsbt I'm very excited to see you tackle this functionality. Thank you. Please can you confirm how it will behave in the following scenario: Day 0: I have popular_gem v1.0 installed Will I get v1.1 installed and thus suffer from the now known bad code, or will it delay updating until Day 12 when it will give me v1.2? If I get v1.1 then unfortunately I think that negates what feels like the benefits of this mechanism. The goal for me is safety and security, so I'd want it to automatically prevent me getting the bad release. |
Adds the
cooldownsetting discussed at #9113, mirroring npm'smin-release-age, pnpm'sminimumReleaseAge, and Dependabot'scooldownto mitigate freshly published supply-chain attacks.Users opt in through:
--cooldown Noninstall,update,add, andoutdatedbundle config set cooldown NorBUNDLE_COOLDOWNsource "URL", cooldown: Nin the GemfileDesign decisions adopted from #9113 and the follow-up review:
Integerday count. Strings, floats, arrays, and negative numbers raiseInvalidOptionfrom both the Gemfile DSL and the CLI handlers, so a typo can't silently disable the filter.cooldown:value.--cooldown 0is the global escape hatch and applies to the whole resolve including transitive dependencies; combine with--conservativeto minimize churn during urgent updates.source "URL", cooldown: 0in the Gemfile.bundle installagainst an existing lockfile short-circuits the resolver entirely, so a cooldown setting never invalidates a working lock.bundle updatefilters and falls back to the newest version satisfying cooldown;bundle outdatedannotates the latest-but-cooled version ((cooldown Nd)in the table form,in cooldown for N more daysin--parseable) rather than hiding it.--cooldown 0to bypass.created_atis absent are treated as outside the window and remain resolvable.gem install -v Xexact-version cooldown errors (deferred), and a dedicated exit code for cooldownfailures.The
EndpointSpecificationparsescreated_atdefensively against older rubygems whoseAPISet::GemParsersplits ISO8601 on every colon and the flat String shape are accepted, malformed values become nil.Make sure the following tasks are checked