Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 84 additions & 17 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ commands:
- run:
name: Install Dependencies
command: |
export RUBYOPT="-r${PWD}/.circleci/global_pinner"
bundle check || bundle install
run_sonarqube:
steps:
Expand Down Expand Up @@ -197,6 +198,55 @@ commands:
command: |
bundle exec rake release[origin]
jobs:
update-currency-versions:
executor:
name: base
ruby_version: "3.3"
steps:
- checkout
- run:
name: Update RubyGems and Bundler
command: |
gem update --system
gem install bundler
bundle config set path './vendor/bundle'
- run:
name: Capture installed gem versions per gemfile
command: |
#!/usr/bin/env bash
set -e
mkdir -p bundle-list

run_bundle_list() {
local label="$1"
local gemfile="$2"
echo "=== Processing $label from $gemfile ==="
BUNDLE_GEMFILE="$gemfile" bundle check 2>/dev/null \
|| BUNDLE_GEMFILE="$gemfile" bundle install --quiet
BUNDLE_GEMFILE="$gemfile" bundle list > "bundle-list/installed_${label}.txt"
echo " Captured $(grep -c '^\s\+\*' bundle-list/installed_${label}.txt) gems"
}

run_bundle_list "rails" "./gemfiles/rails_80.gemfile"
run_bundle_list "sinatra" "./gemfiles/sinatra_40.gemfile"
run_bundle_list "rack" "./gemfiles/rack_30.gemfile"
run_bundle_list "excon" "./gemfiles/excon_100.gemfile"
run_bundle_list "rest-client" "./gemfiles/rest_client_20.gemfile"
run_bundle_list "redis" "./gemfiles/redis_50.gemfile"
run_bundle_list "mongo" "./gemfiles/mongo_219.gemfile"
run_bundle_list "sequel" "./gemfiles/sequel_58.gemfile"
run_bundle_list "bunny" "./gemfiles/bunny_300.gemfile"
run_bundle_list "sidekiq" "./gemfiles/sidekiq_70.gemfile"
run_bundle_list "resque" "./gemfiles/resque_20.gemfile"
run_bundle_list "grpc" "./gemfiles/grpc_10.gemfile"
run_bundle_list "aws" "./gemfiles/aws_60.gemfile"
run_bundle_list "cuba" "./gemfiles/cuba_40.gemfile"
run_bundle_list "dalli" "./gemfiles/dalli_32.gemfile"
run_bundle_list "graphql" "./gemfiles/graphql_20.gemfile"
run_bundle_list "roda" "./gemfiles/roda_30.gemfile"
run_bundle_list "net-http" "./gemfiles/net_http_01.gemfile"
- store_artifacts:
path: bundle-list
test_core:
parameters:
stack:
Expand Down Expand Up @@ -268,6 +318,7 @@ workflows:
tags:
only: /^v.*/
core:
max_auto_reruns: 2
jobs:
- lint
- test_core:
Expand All @@ -281,7 +332,19 @@ workflows:
- "3.3"
- "3.4"
- "4.0"
- update-currency-versions:
filters:
branches:
only:
- master
requires:
- lint
- test_core-ruby-base-3.2
- test_core-ruby-base-3.3
- test_core-ruby-base-3.4
- test_core-ruby-base-4.0
libraries_ruby_32_33:
max_auto_reruns: 2
jobs:
- test_apprisal:
name: "test_apprisal-<<matrix.gemfile>>-ruby-<<matrix.stack>>-<<matrix.ruby_version>>"
Expand All @@ -294,6 +357,7 @@ workflows:
- "3.2"
- "3.3"
libraries_ruby_34_40:
max_auto_reruns: 2
jobs:
- test_apprisal:
name: "test_apprisal-<<matrix.gemfile>>-ruby-<<matrix.stack>>-<<matrix.ruby_version>>"
Expand All @@ -317,6 +381,7 @@ workflows:
ruby_version: "4.0"
gemfile: "./gemfiles/grpc_10.gemfile"
rails_ruby_32_40:
max_auto_reruns: 2
jobs:
- test_apprisal:
name: "test_apprisal-rails-<<matrix.gemfile>>-ruby-<<matrix.stack>>-<<matrix.ruby_version>>"
Expand All @@ -336,6 +401,7 @@ workflows:
- "3.4"
- "4.0"
rails8_ruby_32_40:
max_auto_reruns: 2
jobs:
- test_apprisal:
name: "test_apprisal-rails-8-<<matrix.gemfile>>-ruby-<<matrix.stack>>-<<matrix.ruby_version>>"
Expand All @@ -353,23 +419,24 @@ workflows:
- "3.4"
- "4.0"
sequel:
jobs:
- test_apprisal:
name: "test_apprisal-sequel-<<matrix.gemfile>>-ruby-<<matrix.stack>>-<<matrix.ruby_version>>"
matrix:
parameters:
stack:
- base
- mysql2
gemfile:
- "./gemfiles/sequel_56.gemfile"
- "./gemfiles/sequel_57.gemfile"
- "./gemfiles/sequel_58.gemfile"
ruby_version:
- "3.2"
- "3.3"
- "3.4"
- "4.0"
max_auto_reruns: 2
jobs:
- test_apprisal:
name: "test_apprisal-sequel-<<matrix.gemfile>>-ruby-<<matrix.stack>>-<<matrix.ruby_version>>"
matrix:
parameters:
stack:
- base
- mysql2
gemfile:
- "./gemfiles/sequel_56.gemfile"
- "./gemfiles/sequel_57.gemfile"
- "./gemfiles/sequel_58.gemfile"
ruby_version:
- "3.2"
- "3.3"
- "3.4"
- "4.0"
report_coverage:
jobs:
- report_coverage
168 changes: 168 additions & 0 deletions .circleci/global_pinner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# global_pinner.rb
require 'json'
require 'net/http'
require 'time'
require 'pathname'
require 'rubygems/requirement'

DAYS_BACK = 5
SECONDS_PER_DAY = 24 * 60 * 60
TARGET_DATE = Time.now.utc - (DAYS_BACK * SECONDS_PER_DAY)
RUBYGEMS_HOST = 'https://rubygems.org'
PINNER_DISABLED = ENV['INSTANA_DISABLE_GLOBAL_PINNER'] == 'true'
GLOBAL_PINNER_PATH = File.expand_path(__FILE__)
GLOBAL_PINNER_DIR = File.dirname(GLOBAL_PINNER_PATH)

module GlobalPinner
module_function

def install!
return if PINNER_DISABLED
return if @installed

ensure_rubyopt_uses_absolute_path

@installed = true

# If Bundler is already loaded, patch it immediately
if defined?(Bundler::Dsl)
Bundler::Dsl.prepend(DslPatch)
end

if defined?(Bundler::Injector)
Bundler::Injector.prepend(InjectorPatch)
end

# Set up a hook to patch Bundler when it loads
setup_bundler_hook unless defined?(Bundler)
end

def setup_bundler_hook
trace = TracePoint.new(:class) do |tp|
if tp.self.name == 'Bundler'
# Wait for Bundler::Dsl to be defined
dsl_trace = TracePoint.new(:class) do |dsl_tp|
if dsl_tp.self.name == 'Bundler::Dsl'
Bundler::Dsl.prepend(DslPatch)
dsl_trace.disable
end
end
dsl_trace.enable

# Wait for Bundler::Injector to be defined
injector_trace = TracePoint.new(:class) do |inj_tp|
if inj_tp.self.name == 'Bundler::Injector'
Bundler::Injector.prepend(InjectorPatch)
injector_trace.disable
end
end
injector_trace.enable

trace.disable
end
end
trace.enable
end

def pinned_version_for(name, requirements)
versions = fetch_versions(name)
grace_cutoff = Time.now.utc - (DAYS_BACK * SECONDS_PER_DAY)
current_ruby_version = Gem::Version.new(RUBY_VERSION)

# Filter and sort versions by created_at descending
sorted_versions = versions
.select { |v| v['created_at'] && !v['prerelease'] }
.map { |v| [v, Time.parse(v['created_at'])] }
.sort_by { |_, created_at| created_at }
.reverse

# Find first safe version with grace period reset logic
sorted_versions.each_with_index do |(version, created_at), i|
# Skip if within grace period
next if created_at > grace_cutoff

# Check if superseded by newer version within grace period
grace_end = created_at + (DAYS_BACK * SECONDS_PER_DAY)
superseded = sorted_versions[0...i].any? do |_, newer_date|
newer_date < grace_end
end

next if superseded

# Check if version satisfies requirements
number = version['number']
next unless requirement_for(requirements).satisfied_by?(Gem::Version.new(number))

# Check Ruby version compatibility
ruby_requirement = version['ruby_version']
if ruby_requirement && !Gem::Requirement.new(ruby_requirement).satisfied_by?(current_ruby_version)
next
end

return number
end

nil
rescue StandardError => e
warn "[Date Pinner] Failed to pin #{name}: #{e.class}: #{e.message}"
nil
end

def fetch_versions(name)
uri = URI.parse("#{RUBYGEMS_HOST}/api/v1/versions/#{name}.json")
response = Net::HTTP.get_response(uri)
raise "HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)

JSON.parse(response.body)
end

def requirement_for(requirements)
cleaned = requirements.flatten.compact.reject { |value| value.is_a?(Hash) }
return Gem::Requirement.default if cleaned.empty?

Gem::Requirement.new(*cleaned)
end

module DslPatch
def gem(name, *requirements)
pinned_version = GlobalPinner.pinned_version_for(name, requirements)

if pinned_version
puts " [Date Pinner] #{name} -> #{pinned_version}"
super(name, pinned_version)
else
super(name, *requirements)
end
end
end

module InjectorPatch
def gem(name, *requirements)
pinned_version = GlobalPinner.pinned_version_for(name, requirements)

if pinned_version
puts " [Date Pinner] #{name} -> #{pinned_version}"
super(name, pinned_version)
else
super(name, *requirements)
end
end
end

def ensure_rubyopt_uses_absolute_path
rubyopt = ENV['RUBYOPT'] || ''

relative_flag = '-r./global_pinner'
absolute_flag = "-r#{GLOBAL_PINNER_PATH}"

# Remove any existing references to global_pinner (relative or absolute)
updated_rubyopt = rubyopt.gsub(/#{Regexp.escape(relative_flag)}|#{Regexp.escape(absolute_flag)}/, '').strip

# Add the absolute path reference
updated_rubyopt = "#{updated_rubyopt} #{absolute_flag}".strip

ENV['RUBYOPT'] = updated_rubyopt
end
end

GlobalPinner.install!
Loading