bundler-age_gate 0.3.0 ā 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +18 -6
- data/bundler-age_gate.gemspec +1 -1
- data/lib/bundler/age_gate/command.rb +104 -3
- data/lib/bundler/age_gate/config.rb +19 -2
- data/lib/bundler/age_gate/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2b415241c77fdb2991dc9f70c5109e91f12ad01591263ab030d82fd51f4a46dc
|
|
4
|
+
data.tar.gz: bfd11605c7d34636c57bf043e3683ad6fef154bc218a0e4c42e6fa4f1bb790ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f46130e0b801d695cb632cb1af3f1f989ff9af32940ed385e9c3bdf1e56add2f939c4082746eebfe018583cd5ccc38dd480ba7c289fea9d5f9908ab3293cacd8
|
|
7
|
+
data.tar.gz: 79a8122be6913d6508a5318f51b2bacbff2185af9e1f10b7947f258547e40f1ff1f90632f7b0a1cc42a42e6d6176aa930a6ac2fe910528b87545f4738dee23c0
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-01-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Parallel gem checking**: Concurrent HTTP requests for faster age verification
|
|
12
|
+
- New configuration option: `max_workers` (range: 1-16, default: 8)
|
|
13
|
+
- Thread-safe data structures with Mutex guards for concurrent access
|
|
14
|
+
- Graceful fallback to sequential processing if parallelisation fails
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Refactored `Command#execute` for parallelisation support
|
|
18
|
+
- Added `check_gems_parallel()` and `check_gems_sequential()` methods
|
|
19
|
+
- HTTP I/O operations now execute concurrently without blocking
|
|
20
|
+
|
|
21
|
+
### Backwards Compatibility
|
|
22
|
+
- Existing configs work unchanged (defaults to 8 workers)
|
|
23
|
+
- Set `max_workers: 1` for sequential processing (debugging or CI constraints)
|
|
24
|
+
|
|
8
25
|
## [0.3.0] - 2026-01-22
|
|
9
26
|
|
|
10
27
|
### Added
|
data/README.md
CHANGED
|
@@ -159,6 +159,17 @@ sources:
|
|
|
159
159
|
- **Audit logging**: Compliance-ready logs for all checks
|
|
160
160
|
- **Enterprise-ready**: Designed for organisation-wide rollout
|
|
161
161
|
|
|
162
|
+
## Performance
|
|
163
|
+
|
|
164
|
+
Parallel gem checking with configurable concurrency (default: 8 workers).
|
|
165
|
+
|
|
166
|
+
Configure via `.bundler-age-gate.yml`:
|
|
167
|
+
```yaml
|
|
168
|
+
max_workers: 8 # Recommended for most projects (range: 1-16)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Set `max_workers: 1` to disable parallelisation if needed.
|
|
172
|
+
|
|
162
173
|
## Example Output
|
|
163
174
|
|
|
164
175
|
```
|
|
@@ -272,15 +283,16 @@ bundle exec rake
|
|
|
272
283
|
|
|
273
284
|
## Roadmap
|
|
274
285
|
|
|
275
|
-
|
|
286
|
+
All planned features have been implemented! š
|
|
276
287
|
|
|
277
288
|
- [x] **Private gem server support**: ā
Implemented in v0.3.0
|
|
278
289
|
- [x] **Multi-source detection**: ā
Implemented in v0.3.0
|
|
279
|
-
- [
|
|
280
|
-
- [
|
|
281
|
-
- [
|
|
282
|
-
- [
|
|
283
|
-
- [
|
|
290
|
+
- [x] **Per-source age requirements**: ā
Implemented in v0.3.0
|
|
291
|
+
- [x] **Authentication for private sources**: ā
Implemented in v0.3.0
|
|
292
|
+
- [x] **Exception handling**: ā
Implemented in v0.2.0
|
|
293
|
+
- [x] **Audit logging**: ā
Implemented in v0.2.0
|
|
294
|
+
- [x] **CI/CD integration examples**: ā
Implemented in v0.2.0
|
|
295
|
+
- [x] **Transitive dependency checking**: ā
Already included (checks entire Gemfile.lock)
|
|
284
296
|
|
|
285
297
|
## Contributing
|
|
286
298
|
|
data/bundler-age_gate.gemspec
CHANGED
|
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
|
|
|
6
6
|
spec.name = "bundler-age_gate"
|
|
7
7
|
spec.version = Bundler::AgeGate::VERSION
|
|
8
8
|
spec.authors = ["Niko Roberts"]
|
|
9
|
-
spec.email = ["niko@
|
|
9
|
+
spec.email = ["niko.roberts@airtasker.com"]
|
|
10
10
|
|
|
11
11
|
spec.summary = "A Bundler plugin to enforce minimum gem age requirements"
|
|
12
12
|
spec.description = "Checks your Gemfile.lock against the RubyGems API to ensure no gems are younger than a specified number of days"
|
|
@@ -19,6 +19,12 @@ module Bundler
|
|
|
19
19
|
@audit_logger = AuditLogger.new(@config.audit_log_path)
|
|
20
20
|
@checked_gems_count = 0
|
|
21
21
|
@source_map = {} # gem_name => source_url
|
|
22
|
+
|
|
23
|
+
# Thread-safety primitives for parallel processing
|
|
24
|
+
@cache_mutex = Mutex.new # Protect @cache hash
|
|
25
|
+
@violations_mutex = Mutex.new # Protect @violations arrays
|
|
26
|
+
@progress_mutex = Mutex.new # Protect stdout
|
|
27
|
+
@checked_count_mutex = Mutex.new # Protect counter
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
def execute
|
|
@@ -50,9 +56,15 @@ module Bundler
|
|
|
50
56
|
puts "Checking #{gems.size} gems..."
|
|
51
57
|
print "Progress: "
|
|
52
58
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
# Determine worker count
|
|
60
|
+
max_workers = @config.max_workers || 8
|
|
61
|
+
worker_count = [[max_workers, gems.size].min, 1].max
|
|
62
|
+
|
|
63
|
+
# Parallel or sequential
|
|
64
|
+
if worker_count > 1
|
|
65
|
+
check_gems_parallel(gems, worker_count)
|
|
66
|
+
else
|
|
67
|
+
check_gems_sequential(gems)
|
|
56
68
|
end
|
|
57
69
|
|
|
58
70
|
puts "\n\n"
|
|
@@ -61,6 +73,95 @@ module Bundler
|
|
|
61
73
|
|
|
62
74
|
private
|
|
63
75
|
|
|
76
|
+
def check_gems_parallel(gems, worker_count)
|
|
77
|
+
work_queue = Queue.new
|
|
78
|
+
gems.each { |spec| work_queue << spec }
|
|
79
|
+
|
|
80
|
+
# Create worker threads
|
|
81
|
+
workers = Array.new(worker_count) do
|
|
82
|
+
Thread.new do
|
|
83
|
+
loop do
|
|
84
|
+
spec = work_queue.pop(true) rescue break # Non-blocking pop
|
|
85
|
+
check_gem_thread_safe(spec)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Wait for all workers to complete
|
|
91
|
+
workers.each(&:join)
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
warn "\nā ļø Parallel processing failed: #{e.message}"
|
|
94
|
+
warn "Falling back to sequential processing..."
|
|
95
|
+
check_gems_sequential(gems)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def check_gems_sequential(gems)
|
|
99
|
+
gems.each do |spec|
|
|
100
|
+
check_gem(spec)
|
|
101
|
+
print "."
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def check_gem_thread_safe(spec)
|
|
106
|
+
gem_name = spec.name
|
|
107
|
+
gem_version = spec.version.to_s
|
|
108
|
+
cache_key = "#{gem_name}@#{gem_version}"
|
|
109
|
+
|
|
110
|
+
# Check cache with lock
|
|
111
|
+
cached = @cache_mutex.synchronize { @cache[cache_key] }
|
|
112
|
+
return if cached
|
|
113
|
+
|
|
114
|
+
# Increment counter with lock
|
|
115
|
+
@checked_count_mutex.synchronize { @checked_gems_count += 1 }
|
|
116
|
+
|
|
117
|
+
# Read-only operations (no locks needed)
|
|
118
|
+
gem_source_url = @source_map[gem_name] || "https://rubygems.org"
|
|
119
|
+
source_config = @config.source_for_url(gem_source_url)
|
|
120
|
+
min_age_days = @cli_override_days || source_config.minimum_age_days
|
|
121
|
+
cutoff_date = Time.now - (min_age_days * 24 * 60 * 60)
|
|
122
|
+
|
|
123
|
+
# HTTP I/O happens here (NO LOCK - this gets parallelized!)
|
|
124
|
+
release_date = fetch_gem_release_date(gem_name, gem_version, source_config)
|
|
125
|
+
|
|
126
|
+
if release_date.nil?
|
|
127
|
+
@cache_mutex.synchronize { @cache[cache_key] = :unknown }
|
|
128
|
+
print_progress_dot
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
@cache_mutex.synchronize { @cache[cache_key] = release_date }
|
|
133
|
+
|
|
134
|
+
# Check violation
|
|
135
|
+
if release_date > cutoff_date
|
|
136
|
+
age_days = ((Time.now - release_date) / (24 * 60 * 60)).round
|
|
137
|
+
violation = {
|
|
138
|
+
name: gem_name,
|
|
139
|
+
version: gem_version,
|
|
140
|
+
release_date: release_date,
|
|
141
|
+
age_days: age_days,
|
|
142
|
+
source: source_config.name,
|
|
143
|
+
required_age: min_age_days
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if @config.gem_excepted?(gem_name, gem_version)
|
|
147
|
+
violation[:excepted] = true
|
|
148
|
+
violation[:exception_reason] = @config.exception_reason(gem_name, gem_version)
|
|
149
|
+
@violations_mutex.synchronize { @excepted_violations << violation }
|
|
150
|
+
else
|
|
151
|
+
@violations_mutex.synchronize { @violations << violation }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
print_progress_dot
|
|
156
|
+
rescue StandardError => e
|
|
157
|
+
@cache_mutex.synchronize { @cache[cache_key] = :error }
|
|
158
|
+
print_progress_dot
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def print_progress_dot
|
|
162
|
+
@progress_mutex.synchronize { print "." }
|
|
163
|
+
end
|
|
164
|
+
|
|
64
165
|
def build_source_map(lockfile)
|
|
65
166
|
# Parse REMOTE sections from lockfile to map gems to sources
|
|
66
167
|
lockfile_content = File.read(File.join(Dir.pwd, "Gemfile.lock"))
|
|
@@ -5,7 +5,7 @@ require "yaml"
|
|
|
5
5
|
module Bundler
|
|
6
6
|
module AgeGate
|
|
7
7
|
class Config
|
|
8
|
-
attr_reader :minimum_age_days, :exceptions, :audit_log_path, :sources
|
|
8
|
+
attr_reader :minimum_age_days, :exceptions, :audit_log_path, :sources, :max_workers
|
|
9
9
|
|
|
10
10
|
DEFAULT_RUBYGEMS_SOURCE = {
|
|
11
11
|
"name" => "rubygems",
|
|
@@ -18,7 +18,8 @@ module Bundler
|
|
|
18
18
|
"minimum_age_days" => 7,
|
|
19
19
|
"exceptions" => [],
|
|
20
20
|
"audit_log_path" => ".bundler-age-gate.log",
|
|
21
|
-
"sources" => [DEFAULT_RUBYGEMS_SOURCE]
|
|
21
|
+
"sources" => [DEFAULT_RUBYGEMS_SOURCE],
|
|
22
|
+
"max_workers" => 8
|
|
22
23
|
}.freeze
|
|
23
24
|
|
|
24
25
|
def initialize(config_path = ".bundler-age-gate.yml")
|
|
@@ -28,6 +29,7 @@ module Bundler
|
|
|
28
29
|
@exceptions = @config["exceptions"] || []
|
|
29
30
|
@audit_log_path = @config["audit_log_path"]
|
|
30
31
|
@sources = (@config["sources"] || [DEFAULT_RUBYGEMS_SOURCE]).map { |s| SourceConfig.new(s, @minimum_age_days) }
|
|
32
|
+
@max_workers = parse_max_workers(@config["max_workers"])
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def source_for_url(source_url)
|
|
@@ -54,6 +56,21 @@ module Bundler
|
|
|
54
56
|
|
|
55
57
|
private
|
|
56
58
|
|
|
59
|
+
def parse_max_workers(value)
|
|
60
|
+
return 8 unless value
|
|
61
|
+
|
|
62
|
+
workers = value.to_i
|
|
63
|
+
if workers < 1
|
|
64
|
+
warn "ā ļø Invalid max_workers: #{value}. Using default: 8"
|
|
65
|
+
8
|
|
66
|
+
elsif workers > 16
|
|
67
|
+
warn "ā ļø max_workers > 16. Using maximum: 16"
|
|
68
|
+
16
|
|
69
|
+
else
|
|
70
|
+
workers
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
57
74
|
def load_config
|
|
58
75
|
if File.exist?(@config_path)
|
|
59
76
|
user_config = YAML.safe_load_file(@config_path, permitted_classes: [Date, Time])
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bundler-age_gate
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Niko Roberts
|
|
@@ -69,7 +69,7 @@ dependencies:
|
|
|
69
69
|
description: Checks your Gemfile.lock against the RubyGems API to ensure no gems are
|
|
70
70
|
younger than a specified number of days
|
|
71
71
|
email:
|
|
72
|
-
- niko@
|
|
72
|
+
- niko.roberts@airtasker.com
|
|
73
73
|
executables: []
|
|
74
74
|
extensions: []
|
|
75
75
|
extra_rdoc_files: []
|