Bundler::AgeGate
A Bundler plugin that enforces minimum gem age requirements by checking your Gemfile.lock against the RubyGems API.
Why?
Freshly released gems may contain bugs or security vulnerabilities that haven't been discovered yet. This plugin helps you implement a "waiting period" policy before adopting new gem versions in production environments.
Installation
Install the plugin:
bundle plugin install bundler-age_gateOr add it to your project's plugin list:
bundle plugin install bundler-age_gate --local_git=/path/to/bundler-age_gateUsage
Check that all gems in your Gemfile.lock are at least 7 days old:
bundle age_checkSpecify a custom minimum age (in days):
bundle age_check 14Check for 30-day minimum:
bundle age_check 30Clean up exceptions that are no longer needed:
bundle age_gate_cleanupThis command removes exceptions for gems that are now old enough to pass the age gate without an exception.
Example output:
🔍 Checking 3 exception(s) for cleanup...
✅ rails (7.1.3.1) - Released 15 days ago (7 days required) - Removing
⏳ activerecord (7.1.4) - Released 3 days ago (7 days required) - Keeping
⚠️ custom-gem (1.0.0) - Not in Gemfile.lock (keeping exception)
✅ Removed 1 exception(s) from .bundler-age-gate.yml
📝 2 exception(s) remaining
Configuration
Create a .bundler-age-gate.yml file in your project root to customise behaviour:
# Minimum age in days (default: 7)
minimum_age_days: 7
# Audit log path (default: .bundler-age-gate.log)
audit_log_path: .bundler-age-gate.log
# Approved exceptions
exceptions:
- gem: rails
version: 7.1.3.1
reason: "Critical security patch for CVE-2024-12345"
approved_by: security-team
expires: 2026-02-15Exception Workflow
- Developer encounters age gate violation
- Request exception approval (security team, staff engineer, etc.)
- Add approved exception to
.bundler-age-gate.yml - Include reason, approver, and expiry date
- Commit configuration with approval documented
Exception fields:
-
gem(required): Gem name -
version(optional): Specific version, omit for all versions -
reason(required): Explanation for exception -
approved_by(required): Who approved this -
expires(optional): Expiry date (YYYY-MM-DD)
Audit Logging
All checks are logged to .bundler-age-gate.log in JSON format:
{
"timestamp": "2026-01-22T13:45:00Z",
"result": "pass",
"violations_count": 0,
"checked_gems_count": 80,
"exceptions_used": 1,
"violations": []
}Compliance benefits:
- Track all security checks
- Audit exception usage
- Demonstrate policy compliance
- Investigate historical violations
Multi-Source Support
Configure different age requirements for different gem sources (public vs private):
minimum_age_days: 7 # Global default
sources:
# Public RubyGems - strict
- name: rubygems
url: https://rubygems.org
api_endpoint: https://rubygems.org/api/v1/versions/%s.json
minimum_age_days: 7
# Internal GitHub Packages - less strict
- name: github-internal
url: https://rubygems.pkg.github.com/your-org
api_endpoint: https://rubygems.pkg.github.com/your-org/api/v1/versions/%s.json
minimum_age_days: 3
auth_token: ${GITHUB_TOKEN} # Environment variable
# Internal Artifactory - very permissive
- name: artifactory
url: https://artifactory.company.com/api/gems
api_endpoint: https://artifactory.company.com/api/gems/api/v1/versions/%s.json
minimum_age_days: 1
auth_token: ${ARTIFACTORY_API_KEY}Benefits:
- Stricter requirements for public gems (supply chain risk)
- Permissive for internal gems (trusted sources)
- Per-source API endpoints for private registries
- Authentication support via environment variables
- CLI override still applies globally:
bundle age_check 14enforces 14 days for ALL sources
How it works:
- Plugin reads
Gemfile.lockto determine each gem's source - Applies per-source minimum age requirements
- Queries appropriate API endpoint with authentication
- Reports violations with source context
How It Works
- Parses your
Gemfile.lockusing Bundler's built-in parser - Queries the RubyGems API for each gem's release date
- Compares the release date against your specified minimum age
- Reports any violations and exits with status code 1 if violations are found
Note: Currently only supports rubygems.org. Private gem servers or GitHub packages are not yet supported. See Roadmap for planned features.
Features
- Efficient API usage: Caches API responses to avoid duplicate requests
- Progress indicator: Prints a dot for each gem checked
- Clear output: Shows violating gems with release dates and age
- Graceful error handling: Skips gems that can't be checked (API errors, network issues)
- CI-friendly: Returns appropriate exit codes (0 for pass, 1 for fail)
-
Configuration file: Customise settings via
.bundler-age-gate.yml - Exception handling: Approve specific gems with documented reasons
- Audit logging: Compliance-ready logs for all checks
- Enterprise-ready: Designed for organisation-wide rollout
Performance
Parallel gem checking with configurable concurrency (default: 8 workers).
Configure via .bundler-age-gate.yml:
max_workers: 8 # Recommended for most projects (range: 1-16)Set max_workers: 1 to disable parallelisation if needed.
Example Output
🔍 Checking gem ages (minimum: 7 days)...
📅 Cutoff date: 2026-01-15
Checking 143 gems...
Progress: ...............................................
⚠️ Found 2 gem(s) younger than 7 days:
❌ rails (7.1.3)
Released: 2026-01-20 (2 days ago)
❌ activerecord (7.1.3)
Released: 2026-01-20 (2 days ago)
⛔ Age gate check FAILED
Use Cases
- Production deployments: Ensure stability by waiting for community feedback
- Security compliance: Enforce policies requiring "battle-tested" versions
- CI pipelines: Add as a check in your continuous integration workflow
- Risk management: Reduce exposure to zero-day vulnerabilities in new releases
CI/CD Integration
GitHub Actions
# .github/workflows/gem-security-check.yml
- name: Install bundler-age_gate
run: |
gem install bundler-age_gate
bundle plugin install bundler-age_gate
- name: Check gem ages
run: bundle age_check 7Full example: See examples/github-actions.yml for complete workflow with PR comments and artefact upload.
GitLab CI
# .gitlab-ci.yml
gem-age-gate-check:
script:
- bundle plugin install bundler-age_gate
- bundle age_check 7Full example: See examples/gitlab-ci.yml
CircleCI
# .circleci/config.yml
- run:
name: Check gem ages
command: bundle age_check 7Full example: See examples/circleci-config.yml
Pre-commit Hooks
Shell Script
# .git/hooks/pre-commit
#!/bin/bash
if git diff --cached --name-only | grep -q "^Gemfile.lock$"; then
bundle age_check 7
fiInstallation: See examples/pre-commit
Pre-commit Framework
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: bundler-age-gate
name: Check gem ages
entry: bundle age_check 7
language: system
files: ^Gemfile\.lock$Full example: See examples/.pre-commit-config.yaml
Development
After checking out the repo:
bundle installTo test locally:
bundle exec rakeRoadmap
All planned features have been implemented! 🎉
- Private gem server support: ✅ Implemented in v0.3.0
- Multi-source detection: ✅ Implemented in v0.3.0
- Per-source age requirements: ✅ Implemented in v0.3.0
- Authentication for private sources: ✅ Implemented in v0.3.0
- Exception handling: ✅ Implemented in v0.2.0
- Audit logging: ✅ Implemented in v0.2.0
- CI/CD integration examples: ✅ Implemented in v0.2.0
- Transitive dependency checking: ✅ Already included (checks entire Gemfile.lock)
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/NikoRoberts/bundler-age_gate.
License
The gem is available as open source under the terms of the MIT License.